190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import type { CSSProperties } from "react"
|
|
import { mmToPx } from "./doc-page-setup.ts"
|
|
import { pageTopPx, usesPageLayer } from "./docs-graphic-position.ts"
|
|
import {
|
|
type DocsGraphicAttrs,
|
|
parseGraphicAttrs,
|
|
} from "./docs-graphic-types.ts"
|
|
|
|
export type DocsGraphicLayoutStyle = {
|
|
wrapper: CSSProperties
|
|
inner: CSSProperties
|
|
content: CSSProperties
|
|
behindText: boolean
|
|
inFrontText: boolean
|
|
usePageLayer: boolean
|
|
}
|
|
|
|
export type DocsGraphicLayoutOptions = {
|
|
pageHeight?: number
|
|
}
|
|
|
|
export function computeGraphicLayoutStyle(
|
|
raw: Record<string, unknown> | DocsGraphicAttrs,
|
|
options: DocsGraphicLayoutOptions = {}
|
|
): DocsGraphicLayoutStyle {
|
|
const attrs = "graphicType" in raw && typeof raw.graphicType === "string"
|
|
? (raw as DocsGraphicAttrs)
|
|
: parseGraphicAttrs(raw)
|
|
|
|
const width = attrs.width
|
|
const height = attrs.height
|
|
const rotation = attrs.rotationDeg
|
|
|
|
const behindText = attrs.wrap === "behind"
|
|
const inFrontText = attrs.wrap === "in-front"
|
|
const onPageLayer = usesPageLayer(attrs)
|
|
const isAbsolute = onPageLayer || attrs.placement === "absolute"
|
|
|
|
const wrapMarginPx = mmToPx(attrs.wrapMarginMm)
|
|
const blockMarginPx = Math.max(4, Math.round(wrapMarginPx / 2))
|
|
|
|
const wrapper: CSSProperties = {}
|
|
const inner: CSSProperties = {
|
|
width,
|
|
height,
|
|
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
|
transformOrigin: "center center",
|
|
position: isAbsolute ? "absolute" : "relative",
|
|
zIndex: behindText ? 1 : inFrontText ? 2 : attrs.zIndex > 0 ? attrs.zIndex : undefined,
|
|
pointerEvents: "auto",
|
|
opacity: attrs.opacity < 1 ? attrs.opacity : undefined,
|
|
boxShadow: attrs.shadow || undefined,
|
|
}
|
|
|
|
if (onPageLayer) {
|
|
if (options.pageHeight != null && options.pageHeight > 0) {
|
|
inner.left = attrs.pageX
|
|
inner.top = pageTopPx(attrs.pageIndex, options.pageHeight) + attrs.pageY
|
|
} else {
|
|
inner.left = attrs.pageX
|
|
inner.top = attrs.pageY
|
|
}
|
|
} else if (isAbsolute) {
|
|
// Legacy anchored-absolute coords (kept for raw/unmigrated attrs).
|
|
inner.left = attrs.x
|
|
inner.top = attrs.y
|
|
} else {
|
|
// In-flow: layout (float, margins, display) must live on the wrapper so
|
|
// surrounding text actually wraps around the node.
|
|
wrapper.width = width
|
|
wrapper.height = height
|
|
wrapper.maxWidth = "100%"
|
|
|
|
if (attrs.placement === "inline" || attrs.wrap === "inline") {
|
|
wrapper.display = "inline-block"
|
|
wrapper.verticalAlign = "baseline"
|
|
} else if (attrs.wrap === "top-bottom") {
|
|
wrapper.display = "block"
|
|
wrapper.clear = "both"
|
|
wrapper.marginBlock = `${wrapMarginPx}px`
|
|
if (attrs.floatSide === "center") {
|
|
wrapper.marginInline = "auto"
|
|
}
|
|
} else if (attrs.wrap === "square" || attrs.wrap === "tight" || attrs.wrap === "through") {
|
|
wrapper.display = "block"
|
|
wrapper.marginBlock = `${blockMarginPx}px`
|
|
if (attrs.floatSide === "center") {
|
|
wrapper.float = "none"
|
|
wrapper.marginInline = "auto"
|
|
} else {
|
|
const sideMargin = attrs.wrap === "tight" ? Math.max(2, blockMarginPx / 2) : wrapMarginPx
|
|
wrapper.float = attrs.floatSide === "right" ? "right" : "left"
|
|
// Gap goes on the text side of the float.
|
|
wrapper.marginInlineStart = attrs.floatSide === "right" ? `${sideMargin}px` : undefined
|
|
wrapper.marginInlineEnd = attrs.floatSide === "right" ? undefined : `${sideMargin}px`
|
|
if (attrs.wrap === "tight") {
|
|
wrapper.shapeOutside = "margin-box"
|
|
}
|
|
}
|
|
if (attrs.wrap === "through") {
|
|
inner.opacity = 0.85
|
|
inner.mixBlendMode = "multiply"
|
|
}
|
|
} else {
|
|
wrapper.display = "block"
|
|
wrapper.marginBlock = `${wrapMarginPx}px`
|
|
if (attrs.floatSide === "center") {
|
|
wrapper.marginInline = "auto"
|
|
}
|
|
}
|
|
|
|
// Resize-from-left/top keeps the opposite edge fixed via x/y offset.
|
|
if (attrs.x || attrs.y) {
|
|
inner.left = attrs.x
|
|
inner.top = attrs.y
|
|
}
|
|
}
|
|
|
|
const content: CSSProperties = {
|
|
width: "100%",
|
|
height: "100%",
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
}
|
|
|
|
return { wrapper, inner, content, behindText, inFrontText, usePageLayer: onPageLayer }
|
|
}
|
|
|
|
export const RESIZE_HANDLES = [
|
|
"nw",
|
|
"n",
|
|
"ne",
|
|
"e",
|
|
"se",
|
|
"s",
|
|
"sw",
|
|
"w",
|
|
] as const
|
|
|
|
export type ResizeHandle = (typeof RESIZE_HANDLES)[number]
|
|
|
|
export function resizeWithHandle(
|
|
handle: ResizeHandle,
|
|
startWidth: number,
|
|
startHeight: number,
|
|
dx: number,
|
|
dy: number,
|
|
minSize = 24,
|
|
lockAspect = false
|
|
): { width: number; height: number; xOffset: number; yOffset: number } {
|
|
let width = startWidth
|
|
let height = startHeight
|
|
let xOffset = 0
|
|
let yOffset = 0
|
|
|
|
if (lockAspect) {
|
|
const aspect = startWidth / Math.max(startHeight, 1)
|
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
width = startWidth + (handle.includes("w") ? -dx : dx)
|
|
height = width / aspect
|
|
} else {
|
|
height = startHeight + (handle.includes("n") ? -dy : dy)
|
|
width = height * aspect
|
|
}
|
|
if (handle.includes("w")) xOffset = startWidth - width
|
|
if (handle.includes("n")) yOffset = startHeight - height
|
|
} else {
|
|
if (handle.includes("e")) width = startWidth + dx
|
|
if (handle.includes("w")) {
|
|
width = startWidth - dx
|
|
xOffset = dx
|
|
}
|
|
if (handle.includes("s")) height = startHeight + dy
|
|
if (handle.includes("n")) {
|
|
height = startHeight - dy
|
|
yOffset = dy
|
|
}
|
|
}
|
|
|
|
width = Math.max(minSize, width)
|
|
height = Math.max(minSize, height)
|
|
|
|
if (handle.includes("w") && width === minSize) xOffset = startWidth - minSize
|
|
if (handle.includes("n") && height === minSize) yOffset = startHeight - minSize
|
|
|
|
return { width: Math.round(width), height: Math.round(height), xOffset, yOffset }
|
|
}
|
|
|
|
export const DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM = [0, 3, 6, 12] as const
|