import { DOCS_PAGE_GAP_PX } from "./docs-page-layout-constants.ts" import { type DocsGraphicAttrs, parseGraphicAttrs, } from "./docs-graphic-types.ts" export type PageCoords = { pageIndex: number pageX: number pageY: number } /** Graphics rendered on the page overlay layer (not clipped by prose). */ export function usesPageLayer(attrs: DocsGraphicAttrs | Record): boolean { const parsed = "graphicType" in attrs ? (attrs as DocsGraphicAttrs) : parseGraphicAttrs(attrs) return ( parsed.wrap === "behind" || parsed.wrap === "in-front" || parsed.positionMode === "fixed-on-page" ) } export type PageLayerSlot = "behind" | "front" /** Which page overlay slot should host this graphic. */ export function pageLayerSlot(attrs: DocsGraphicAttrs): PageLayerSlot | null { if (!usesPageLayer(attrs)) return null if (attrs.wrap === "behind") return "behind" return "front" } export function pageLayerElementId(slot: PageLayerSlot): string { return slot === "behind" ? "docs-page-graphic-layer-behind" : "docs-page-graphic-layer-front" } export function pageTopPx(pageIndex: number, pageHeight: number): number { return pageIndex * (pageHeight + DOCS_PAGE_GAP_PX) } export function pageIndexFromStackY(stackY: number, pageHeight: number): number { if (stackY < 0) return 0 const stride = pageHeight + DOCS_PAGE_GAP_PX return Math.max(0, Math.floor(stackY / stride)) } export function stackYFromPageCoords(pageIndex: number, pageY: number, pageHeight: number): number { return pageTopPx(pageIndex, pageHeight) + pageY } /** Convert DOM bbox (viewport px) to page-stack coordinates. */ export function bboxToPageCoords( rect: DOMRect, stackRect: DOMRect, scale: number, pageHeight: number ): PageCoords { const stackX = (rect.left - stackRect.left) / scale const stackY = (rect.top - stackRect.top) / scale const pageIndex = pageIndexFromStackY(stackY, pageHeight) const pageY = stackY - pageTopPx(pageIndex, pageHeight) return { pageIndex, pageX: Math.round(stackX), pageY: Math.round(Math.max(0, pageY)), } } /** * Re-resolve page index after a drag so pageY stays within the page the * graphic visually sits on (a drag may cross page boundaries). */ export function normalizePageCoords( pageIndex: number, pageY: number, pageHeight: number ): { pageIndex: number; pageY: number } { if (pageHeight <= 0) { return { pageIndex: Math.max(0, pageIndex), pageY: Math.round(pageY) } } const stackY = stackYFromPageCoords(Math.max(0, pageIndex), pageY, pageHeight) const nextIndex = pageIndexFromStackY(stackY, pageHeight) const rawY = stackY - pageTopPx(nextIndex, pageHeight) // Clamp into the page band (a drop in the inter-page gap snaps to the page bottom). const nextY = Math.min(Math.max(0, Math.round(rawY)), pageHeight) return { pageIndex: nextIndex, pageY: nextY } } export function pageCoordsToStackStyle( pageIndex: number, pageX: number, pageY: number, pageHeight: number ): { left: number; top: number } { return { left: pageX, top: stackYFromPageCoords(pageIndex, pageY, pageHeight), } }