101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
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<string, unknown>): 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),
|
|
}
|
|
}
|