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 | 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