@embedpdf/core-stage 3.0.0-next.6 → 3.0.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -9,7 +9,9 @@ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
9
9
  * (a focal point relative to a page — what survives layout/zoom changes & reloads).
10
10
  *
11
11
  * Navigation is pure camera math. Bounds/home/margin are three separate functions.
12
- * No DOM, no framework, fully serializable — this is the v4 Rust core, verbatim.
12
+ * No DOM, no framework; plain-data model plus derived spatial queries (the DATA
13
+ * is serializable — `Scene` also carries query methods derived from it). This
14
+ * is the v4 Rust core, verbatim.
13
15
  */
14
16
  /** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared
15
17
  * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["NO_FRAME"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @embedpdf/core-stage — the pure spatial model.\n *\n * Scene (a layout strategy turns pages into positioned items + a spatial index),\n * Camera ({x,y,zoom}: the world point at the viewport origin + scale), and Anchor\n * (a focal point relative to a page — what survives layout/zoom changes & reloads).\n *\n * Navigation is pure camera math. Bounds/home/margin are three separate functions.\n * No DOM, no framework, fully serializable — this is the v4 Rust core, verbatim.\n */\n\n// The pure coordinate primitives live in the dependency-free geometry base\n// (shared with the framework adapters); stage-core re-exports them so existing\n// `from '@embedpdf/core-stage'` imports are unaffected.\nimport {\n displaySize,\n NO_FRAME,\n type PageFrame,\n type PageRotation,\n type Point,\n type Size,\n} from '@embedpdf/core-geometry';\nexport type { Point, Size, PageRotation, PageFrame } from '@embedpdf/core-geometry';\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\nexport interface Camera {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface PageGeom {\n /** UN-rotated (intrinsic) page size — the content's own dimensions before rotation. */\n size: Size;\n /** Total display rotation. Layout swaps w↔h for 90/270 so the box is the\n * on-screen footprint; the renderer rotates the content into it. */\n rotation?: PageRotation;\n /**\n * PDF `/UserUnit` (§14.11.6): how many 1/72\" units one point of THIS page\n * spans — the page is physically `userUnit ×` larger than its point size\n * says. Folded into the layout (a userUnit-5 page lays out 5× bigger, like\n * Acrobat) AND into its `contentScale`, so \"world per content point\" stays\n * true per page. Default 1 (virtually every document).\n */\n userUnit?: number;\n}\nexport interface PageBox {\n pageIndex: number;\n x: number;\n y: number;\n /** DISPLAY width — swapped to the page's height for 90/270 rotations. */\n width: number;\n /** DISPLAY height — swapped to the page's width for 90/270 rotations. */\n height: number;\n /** The page's total display rotation; the renderer rotates the (normalized,\n * un-rotated) content bitmap by this to fill the display box. */\n rotation: PageRotation;\n /**\n * World size ÷ intrinsic size for this page (1 for `intrinsic` sizing). The shell\n * multiplies it by the camera zoom to get device-px-per-PDF-point, so render\n * resolution and point↔screen mapping stay correct under `uniform` sizing.\n * Isotropic, so it is unaffected by rotation.\n */\n contentScale: number;\n}\n\n/** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared\n * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a\n * userUnit-5 page measures 5× its point size). Everything the layout packs uses\n * these display dims; the content scale (isotropic) and the renderer's\n * transform recover the content. */\nfunction displayDims(pg: PageGeom): Size {\n const u = pg.userUnit ?? 1;\n const d = displaySize(pg.size, pg.rotation ?? 0);\n return u === 1 ? d : { width: d.width * u, height: d.height * u };\n}\nexport interface SceneItem {\n index: number;\n x: number;\n y: number;\n width: number;\n height: number;\n pageIndexes: number[];\n pages: PageBox[];\n}\nexport type Axis = 'x' | 'y' | 'grid';\nexport interface Scene {\n size: Size;\n items: SceneItem[];\n itemCount: number;\n axis: Axis;\n /**\n * The largest item width & height in the document. Fit-modes resolve against this\n * (not the current page) so the zoom is STABLE across the whole document and no\n * page ever overflows — matching a conventional PDF viewer's \"fit width\".\n */\n maxItemSize: Size;\n query(rect: Rect): SceneItem[];\n nearestItem(pt: Point): SceneItem;\n itemOfPage(pageIndex: number): number;\n}\nexport interface CameraConstraint {\n bounded: boolean;\n /**\n * Breathing room (screen px) around the content — the ONE spacing concept.\n * Fit-modes inset by it, placement leaves it as gutter, and the clamp lets the\n * camera reveal exactly this much beyond each content edge.\n */\n padding: number;\n /** Where content RESTS on an axis it FITS (locked — there is nowhere else to\n * be). Default center/center. x is logical against `direction`. */\n fitAlign?: Alignment;\n direction?: Direction;\n}\n/**\n * The zoom intent — an equation about the fit-box, each resolved by `resolveZoom`:\n * { mode } — viewport-relative: fit-width / fit-page / automatic / fit-all\n * { level } — document-relative: a fixed scale factor\n * { pageWidth } — absolute: the page unit renders N **screen px** wide. The only\n * intent stable across BOTH viewport and document differences —\n * e.g. thumbnails at 200px for any document. Targets the page\n * UNIT (a spread counts as one, exactly as in fit-page/fit-width);\n * combine with sizing 'uniform' to make every page exactly N px.\n * { pageHeight } — absolute, vertical twin (horizontal filmstrip thumbnails).\n */\nexport type ZoomSpec =\n | { mode: ZoomModeValue }\n | { level: number }\n | { pageWidth: number }\n | { pageHeight: number };\nexport interface Anchor {\n pageIndex: number;\n fx: number;\n fy: number;\n}\nexport type SpreadMode = 'none' | 'odd' | 'even';\n/**\n * Reading direction — a LAYOUT property, not a navigation one. Navigation steps by\n * index (reading order); direction only decides where reading-order puts pages in\n * space: horizontal items advance leftward, spreads bind on the right, grid rows\n * fill right→left. The camera, cursor, fit and clamp never learn about it.\n */\nexport type Direction = 'ltr' | 'rtl';\n/**\n * Page sizing policy (per-item world scale, set in the layout):\n * 'intrinsic' — true PDF sizes; relative proportions preserved.\n * 'uniform' — every item scaled to the same CROSS-axis size (vertical → equal\n * widths, horizontal → equal heights, grid → equal widths), so pages\n * sit flush with no left/right gaps. Camera/zoom are untouched.\n */\nexport type SizingMode = 'intrinsic' | 'uniform';\n\nexport const ZOOM_MIN = 0.05;\nexport const ZOOM_MAX = 64;\nexport const ZoomMode = {\n Automatic: 'automatic',\n FitPage: 'fit-page',\n FitWidth: 'fit-width',\n /** Fit the WHOLE scene (every page) in view — the construction overview. */\n FitAll: 'fit-all',\n} as const;\nexport type ZoomModeValue = (typeof ZoomMode)[keyof typeof ZoomMode];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\n\n// ── Projection ──────────────────────────────────────────────────────────────\nexport const toScreen = (c: Camera, w: Point): Point => ({\n x: (w.x - c.x) * c.zoom,\n y: (w.y - c.y) * c.zoom,\n});\nexport const toWorld = (c: Camera, s: Point): Point => ({\n x: c.x + s.x / c.zoom,\n y: c.y + s.y / c.zoom,\n});\nexport const cameraWorldRect = (c: Camera, vp: Size): Rect => ({\n x: c.x,\n y: c.y,\n width: vp.width / c.zoom,\n height: vp.height / c.zoom,\n});\n\n// ── Camera operations ────────────────────────────────────────────────────────\n/** Focal zoom: keep the world point under `screenPt` fixed (no bounce). */\nexport const zoomAround = (c: Camera, screenPt: Point, factor: number): Camera => {\n const zoom = clamp(c.zoom * factor, ZOOM_MIN, ZOOM_MAX);\n return {\n x: c.x + screenPt.x / c.zoom - screenPt.x / zoom,\n y: c.y + screenPt.y / c.zoom - screenPt.y / zoom,\n zoom,\n };\n};\nexport const panByScreen = (c: Camera, dxScreen: number, dyScreen: number): Camera => ({\n x: c.x - dxScreen / c.zoom,\n y: c.y - dyScreen / c.zoom,\n zoom: c.zoom,\n});\nexport const centerOnWorld = (worldPt: Point, vp: Size, zoom: number): Camera => ({\n zoom,\n x: worldPt.x - vp.width / 2 / zoom,\n y: worldPt.y - vp.height / 2 / zoom,\n});\n\n/**\n * One axis of camera travel against content bounds — THE scroll geometry.\n * `near`/`far` are the two flush camera positions (content start edge at the\n * padded viewport start / content end edge at the padded viewport end); the\n * camera travels in [near, far] while the padded content overflows the view.\n * When it FITS the interval inverts and `fits` is true — there is nowhere to\n * travel, and the clamp locks the camera to its fitAlign rest instead. Shared\n * by `clampCamera` and `scrollMetrics`, so the pan clamp and a scrollbar can\n * never disagree about where travel ends.\n */\nexport interface AxisTravel {\n near: number;\n far: number;\n fits: boolean;\n}\nexport const travelRange = (\n origin: number,\n content: number,\n view: number,\n zoom: number,\n padding: number,\n): AxisTravel => ({\n near: origin - padding / zoom,\n far: origin + content - (view - padding) / zoom,\n fits: content * zoom <= view - 2 * padding,\n});\n\n/**\n * Bounds — the one place travel limits live. Clamps the camera into an arbitrary\n * world `Rect` (the whole document in continuous flow, one item in paged flow),\n * with a `padding` gutter the camera may reveal beyond each content edge.\n *\n * Per axis the travel interval is `travelRange`'s [near, far]. If the padded\n * content OVERFLOWS the viewport, the camera travels freely in it. If it FITS,\n * the camera is LOCKED to the `fitAlign` rest point — start = near, end = far,\n * center = midpoint (x resolved logically against `direction`).\n */\nexport const clampCamera = (c: Camera, bounds: Rect, vp: Size, k: CameraConstraint): Camera => {\n if (!k.bounded) return c;\n const p = k.padding;\n const fit = k.fitAlign ?? { x: 'center', y: 'center' };\n const axis = (pos: number, origin: number, content: number, view: number, a: Align): number => {\n const t = travelRange(origin, content, view, c.zoom, p);\n if (t.fits) return a === 'start' ? t.near : a === 'end' ? t.far : (t.near + t.far) / 2; // fits: rest & lock\n return clamp(pos, t.near, t.far);\n };\n // logical x: under RTL the reading start is the RIGHT edge\n const ax =\n k.direction === 'rtl' && fit.x !== 'center' ? (fit.x === 'start' ? 'end' : 'start') : fit.x;\n return {\n zoom: c.zoom,\n x: axis(c.x, bounds.x, bounds.width, vp.width, ax),\n y: axis(c.y, bounds.y, bounds.height, vp.height, fit.y),\n };\n};\n\n/**\n * The camera as a NATIVE SCROLLER — the DOM scroll vocabulary, in screen px:\n * every field means exactly what it means on a DOM element. Per axis the scroll\n * range is the UNION of the padded content extent and the current camera window:\n * • bounded — the clamp already keeps the window inside the padded content\n * (or rests it when the axis fits), so the union IS the padded content and\n * the numbers degenerate to the classic scroller (scrollLeft ∈\n * [0, scrollWidth − clientWidth], aligned with `travelRange`'s [near, far]).\n * • unbounded — pan beyond the content and the union grows (the Figma\n * scrollbar): the thumb shrinks toward the edge but always remains a road\n * back over the content.\n * `scrollableX/Y` false ⇔ the window covers the whole range (native: no bar).\n * One formula, no mode branching — the clamp did the branching already.\n * Coordinates are PHYSICAL on both axes (RTL included), deliberately sidestepping\n * the DOM's negative-scrollLeft-under-RTL behavior.\n */\nexport interface ScrollMetrics {\n scrollLeft: number;\n scrollTop: number;\n scrollWidth: number;\n scrollHeight: number;\n clientWidth: number;\n clientHeight: number;\n scrollableX: boolean;\n scrollableY: boolean;\n}\nexport const scrollMetrics = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n): ScrollMetrics => {\n const axis = (pos: number, origin: number, content: number, view: number) => {\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const total = (hi - lo) * c.zoom;\n // half-px slack, like the fits-predicate: sub-pixel overflow must not flash a bar\n return { offset: (pos - lo) * c.zoom, total, scrollable: total > view + 0.5 };\n };\n const x = axis(c.x, bounds.x, bounds.width, vp.width);\n const y = axis(c.y, bounds.y, bounds.height, vp.height);\n return {\n scrollLeft: x.offset,\n scrollTop: y.offset,\n scrollWidth: x.total,\n scrollHeight: y.total,\n clientWidth: vp.width,\n clientHeight: vp.height,\n scrollableX: x.scrollable,\n scrollableY: y.scrollable,\n };\n};\n\n/**\n * The inverse write — `Element.scrollTo` semantics: absolute offsets into the\n * CURRENT range (the same union `scrollMetrics` reports), clamped into\n * [0, total − view]; an omitted axis does not move. Zoom is untouched: scrolling\n * is a pan in scroller clothing.\n */\nexport const cameraFromScroll = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n target: { left?: number; top?: number },\n): Camera => {\n const axis = (\n want: number | undefined,\n pos: number,\n origin: number,\n content: number,\n view: number,\n ): number => {\n if (want === undefined) return pos;\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const max = Math.max(0, (hi - lo) * c.zoom - view);\n return lo + clamp(want, 0, max) / c.zoom;\n };\n return {\n zoom: c.zoom,\n x: axis(target.left, c.x, bounds.x, bounds.width, vp.width),\n y: axis(target.top, c.y, bounds.y, bounds.height, vp.height),\n };\n};\n\n/**\n * Per-axis alignment value. Used by the rest constraint (fitAlign — where\n * content settles on an axis the camera cannot travel) and, extended with\n * fractions (see {@link AlignValue}), by the arrival/zoom/anchor policies.\n * On the x-axis the NAMED values are LOGICAL (CSS-style): 'start' = where\n * reading begins (left in LTR, RIGHT in RTL), 'end' = where it ends. On the\n * y-axis they are physical (start = top). So a 'start' default is\n * automatically correct in both directions — no 'auto' value needed.\n */\nexport type Align = 'start' | 'center' | 'end';\nexport interface Alignment {\n x: Align;\n y: Align;\n}\n/**\n * One axis of an alignment POLICY: a named stop, or a viewport fraction 0–1\n * that positions the subject's CENTER at that fraction of the viewport\n * ('center' ≡ 0.5; 0.35 = the browser find-bar line). Named stops are logical\n * on x; fractions are physical, like every screen coordinate.\n */\nexport type AlignValue = Align | number;\nexport interface AlignmentValue {\n x: AlignValue;\n y: AlignValue;\n}\n\n/**\n * THE placement algorithm — every arrival (goToPage, next/prev, reset) lands\n * through it. Pure alignment: put the subject at `align` in the viewport —\n * per axis 'start' (reading edge at the padded viewport edge), 'end' (far\n * edge), 'center', or a fraction — with x resolved logically against the\n * reading direction. The SAME rule at every zoom: whether the subject fits or\n * overflows never changes where it lands. Landing is policy, not a side\n * effect of magnification.\n *\n * Deliberately clamp-free: the caller clamps the result against the TRUE\n * travel bounds (the scene in continuous flow, the item slice in paged). On\n * an axis with no real freedom that clamp collapses the landing to the\n * `fitAlign` rest point — \"rests where it must\" comes out of the clamp's\n * geometry, never out of a branch here.\n */\nexport function placeCamera(\n subject: Rect,\n vp: Size,\n zoom: number,\n padding = 0,\n align: AlignmentValue = { x: 'start', y: 'start' },\n direction: Direction = 'ltr',\n): Camera {\n const axis = (a: AlignValue, pos: number, extent: number, view: number): number => {\n if (a === 'start') return pos - padding / zoom;\n if (a === 'end') return pos + extent - (view - padding) / zoom;\n const f = a === 'center' ? 0.5 : clamp(a, 0, 1);\n return pos + extent / 2 - (view * f) / zoom;\n };\n const ax =\n direction === 'rtl' && align.x === 'start'\n ? 'end'\n : direction === 'rtl' && align.x === 'end'\n ? 'start'\n : align.x;\n return {\n zoom,\n x: axis(ax, subject.x, subject.width, vp.width),\n y: axis(align.y, subject.y, subject.height, vp.height),\n };\n}\n\n/**\n * The MINIMAL camera move that makes `rect` fully visible (with a `padding` gutter)\n * — DOM scrollIntoView({ block: 'nearest' }) as camera math. Per axis, the cameras\n * that show the rect form an interval; the answer is the current camera CLAMPED\n * into it: already inside → unchanged (the no-op case, by construction, not by\n * condition). An oversized rect (interval inverted) aligns to its start.\n *\n * This is deliberately NOT placement: `placeCamera` answers \"I am navigating here\"\n * (canonical); `revealCamera` answers \"make sure this is seeable\" (minimal).\n */\nexport function revealCamera(cam: Camera, rect: Rect, vp: Size, padding = 0): Camera {\n const axis = (pos: number, start: number, size: number, view: number): number => {\n const lo = start + size - (view - padding) / cam.zoom; // far edge just inside\n const hi = start - padding / cam.zoom; // near edge just inside\n if (lo > hi) return hi; // rect larger than the viewport: align its start\n return clamp(pos, lo, hi);\n };\n return {\n zoom: cam.zoom,\n x: axis(cam.x, rect.x, rect.width, vp.width),\n y: axis(cam.y, rect.y, rect.height, vp.height),\n };\n}\n\n// ── Zoom intent — resolved against a fit-box chosen by the caller: the document's\n// max item (continuous), the current item (paged), or the whole scene (fit-all). ─\nexport function resolveZoom(spec: ZoomSpec, box: Size, vp: Size, padding = 0): number {\n if ('level' in spec) return clamp(spec.level, ZOOM_MIN, ZOOM_MAX);\n // absolute pixel targets: box dimension = N screen px (document-independent)\n if ('pageWidth' in spec) return clamp(spec.pageWidth / box.width, ZOOM_MIN, ZOOM_MAX);\n if ('pageHeight' in spec) return clamp(spec.pageHeight / box.height, ZOOM_MIN, ZOOM_MAX);\n const fitW = Math.max(1, vp.width - 2 * padding) / box.width;\n const fitH = Math.max(1, vp.height - 2 * padding) / box.height;\n switch (spec.mode) {\n case ZoomMode.FitWidth:\n return clamp(fitW, ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.FitPage:\n case ZoomMode.FitAll: // same fit math; the caller passes the whole-scene box\n return clamp(Math.min(fitW, fitH), ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.Automatic:\n default:\n // fit width, but never upscale past 100% (Adobe's \"Automatic\"). Height-\n // independent: a portrait page fills the width and you scroll within it.\n return clamp(Math.min(fitW, 1), ZOOM_MIN, ZOOM_MAX);\n }\n}\n\n// ── Anchor — a PAGE-RELATIVE point, the durable \"what am I looking at\". The world\n// point under a screen position is meaningless across a re-layout (pages move);\n// the page-point survives. Point-generalized: WHICH viewport point the anchor\n// lives at is the caller's policy (anchorAlign for reframes, zoomAlign for\n// zoom-intent changes); the wrappers default to the classic center. ──────────\n/** The anchor at an arbitrary world point: its nearest page + the point, page-relative. */\nexport function anchorAtPoint(scene: Scene, worldPt: Point): Anchor {\n const item = scene.nearestItem(worldPt);\n let page = item.pages[0];\n let best = Infinity;\n for (const p of item.pages) {\n const d = Math.abs(p.x + p.width / 2 - worldPt.x);\n if (d < best) {\n best = d;\n page = p;\n }\n }\n return {\n pageIndex: page.pageIndex,\n fx: (worldPt.x - page.x) / page.width,\n fy: (worldPt.y - page.y) / page.height,\n };\n}\n\n/** The camera that puts the anchor's world point at a given SCREEN point. */\nexport function cameraForAnchorAtScreen(\n anchor: Anchor,\n scene: Scene,\n screenPt: Point,\n zoom: number,\n): Camera {\n const item = scene.items[scene.itemOfPage(anchor.pageIndex)];\n let page = item.pages[0];\n for (const p of item.pages) if (p.pageIndex === anchor.pageIndex) page = p;\n const world = { x: page.x + anchor.fx * page.width, y: page.y + anchor.fy * page.height };\n return { zoom, x: world.x - screenPt.x / zoom, y: world.y - screenPt.y / zoom };\n}\n\n/** The anchor under a viewport point — `at` is the policy point (anchorAlign /\n * zoomAlign resolved to px); it defaults to the classic viewport center. */\nexport function anchorFromCamera(cam: Camera, scene: Scene, vp: Size, at?: Point): Anchor {\n return anchorAtPoint(scene, toWorld(cam, at ?? { x: vp.width / 2, y: vp.height / 2 }));\n}\n/** The camera that restores an anchor to the same viewport point it was\n * captured at — capture and restore MUST agree on `at` for exact round-trips. */\nexport function cameraFromAnchor(\n anchor: Anchor,\n scene: Scene,\n vp: Size,\n zoom: number,\n at?: Point,\n): Camera {\n return cameraForAnchorAtScreen(anchor, scene, at ?? { x: vp.width / 2, y: vp.height / 2 }, zoom);\n}\n\n// ── Spatial-index helpers ──────────────────────────────────────────────────────\nfunction lowerBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] < t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nfunction upperBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] <= t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nconst OVERSCAN = 2;\n\n/**\n * Reserved chrome space around each PAGE — the `PageFrame` primitive (world\n * units in this pure layer; the shell derives them from screen px). The frame\n * belongs to PAGES, not items — in a spread every page keeps its own flanks,\n * so left/right chrome works everywhere.\n */\n\ninterface LocalBox {\n pageIndex: number;\n lx: number;\n ly: number;\n w: number;\n h: number;\n rotation: PageRotation;\n /** The page's `/UserUnit` — folded into ITS `contentScale` at placement, so\n * \"world units per content point\" stays true for every page of a spread. */\n userUnit: number;\n}\n\n/** Intrinsic item dimensions (pages only, no margins) — the `uniform` reference.\n * Uses DISPLAY dims, so `uniform` equalizes the rotated footprint and a rotated\n * page sizes to match its neighbours as it actually appears. */\nfunction measureItem(\n pages: readonly PageGeom[],\n group: number[],\n gap: number,\n): { width: number; height: number } {\n let w = 0;\n let h = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n w += d.width + (j > 0 ? gap : 0);\n if (d.height > h) h = d.height;\n }\n return { width: w, height: h };\n}\n\n/**\n * Pack one item from SCALED pages plus constant per-page margins. The page boxes\n * (and the spread's inner gap) scale with the sizing factor; the margins do NOT —\n * they are screen-px-derived chrome bands and stay constant in world space.\n */\nfunction packScaledItem(\n pages: readonly PageGeom[],\n group: number[],\n scale: number,\n gap: number,\n frame: PageFrame,\n direction: Direction,\n): { local: LocalBox[]; width: number; height: number } {\n let maxH = 0;\n for (let j = 0; j < group.length; j++)\n maxH = Math.max(maxH, displayDims(pages[group[j]]).height * scale);\n const local: LocalBox[] = [];\n let lx = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n const w = d.width * scale;\n const h = d.height * scale;\n local.push({\n pageIndex: group[j],\n lx: lx + frame.left,\n ly: frame.top + (maxH - h) / 2, // page centers within the inner band\n w,\n h,\n rotation: pages[group[j]].rotation ?? 0,\n userUnit: pages[group[j]].userUnit ?? 1,\n });\n lx += frame.left + w + frame.right + gap * scale;\n }\n const width = Math.max(0, lx - gap * scale);\n const height = frame.top + maxH + frame.bottom;\n if (direction === 'rtl') {\n // reading-first page takes the RIGHTMOST slot (the spread binds on the right);\n // slots mirror, but each page keeps its PHYSICAL margins (left room stays left)\n for (const b of local) b.lx = width - b.lx - b.w + (frame.left - frame.right);\n }\n return { local, width, height };\n}\n\nfunction placePages(item: SceneItem, local: LocalBox[], contentScale: number): PageBox[] {\n return local.map((b) => ({\n pageIndex: b.pageIndex,\n x: item.x + b.lx,\n y: item.y + b.ly,\n width: b.w,\n height: b.h,\n rotation: b.rotation,\n // Per PAGE, not per item: the box was measured at `size × userUnit`, so\n // the world-per-content-point factor must carry the same userUnit — every\n // content→world mapping downstream divides/multiplies by exactly this.\n contentScale: contentScale * b.userUnit,\n }));\n}\n\n/** Per-item world scale = sizing factor (1 intrinsic, else cross-equalize for\n * uniform) × `viewUnitsPerPoint` (the platform's points→view-px factor). The\n * result becomes the page's `contentScale`, so world units are view px and 100%\n * is physically accurate. `viewUnitsPerPoint` defaults to 1 (neutral). */\nconst itemScale = (\n crossIntrinsic: number,\n refCross: number,\n sizing: SizingMode,\n viewUnitsPerPoint = 1,\n): number =>\n (sizing === 'uniform' && crossIntrinsic > 0 ? refCross / crossIntrinsic : 1) * viewUnitsPerPoint;\n\n// ── Layout strategies ─────────────────────────────────────────────────────────\nexport interface LinearOptions {\n gap?: number;\n axis?: 'x' | 'y';\n align?: 'center' | 'start';\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function linearLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: LinearOptions = {},\n): Scene {\n const gap = opts.gap ?? 16;\n const vertical = (opts.axis ?? 'y') === 'y';\n const align = opts.align ?? 'center';\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n // RTL mirrors the main axis only when it's horizontal; vertical scroll is\n // direction-agnostic (RTL books still scroll down) — only spreads swap.\n const mirrored = !vertical && direction === 'rtl';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure every item at intrinsic PAGE size (no margins) — the cross axis\n // (width when vertical) gives the reference for `uniform` sizing, so uniform\n // equalizes the pages themselves; constant margins then keep outer edges flush.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const crossOf = (p: { width: number; height: number }) => (vertical ? p.width : p.height);\n const refCross = measures.reduce((m, p) => Math.max(m, crossOf(p)), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins, lay along the axis.\n const items: SceneItem[] = new Array(grouping.length);\n const locals: LocalBox[][] = new Array(grouping.length);\n const scales: number[] = new Array(grouping.length);\n let main = 0;\n let crossMax = 0;\n let maxW = 0;\n let maxH = 0;\n\n for (let i = 0; i < grouping.length; i++) {\n const s = itemScale(crossOf(measures[i]), refCross, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n const { width, height } = packed;\n scales[i] = s;\n locals[i] = packed.local;\n const it: SceneItem = {\n index: i,\n x: 0,\n y: 0,\n width,\n height,\n pageIndexes: grouping[i],\n pages: [],\n };\n if (vertical) {\n it.y = main;\n main += height + gap;\n } else {\n it.x = main;\n main += width + gap;\n }\n crossMax = Math.max(crossMax, vertical ? width : height);\n maxW = Math.max(maxW, width);\n maxH = Math.max(maxH, height);\n items[i] = it;\n }\n\n const sceneMain = Math.max(0, main - gap);\n const size: Size = vertical\n ? { width: crossMax, height: sceneMain }\n : { width: sceneMain, height: crossMax };\n\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (mirrored) it.x = size.width - it.x - it.width; // reading order advances leftward\n if (vertical) it.x = align === 'center' ? (size.width - it.width) / 2 : 0;\n else it.y = align === 'center' ? (size.height - it.height) / 2 : 0;\n it.pages = placePages(it, locals[i], scales[i]);\n }\n\n // Spatial index in POSITION order (ascending coordinates for binary search).\n // In a mirrored layout position order is reverse index order — `posIdx` maps back.\n const posIdx = (k: number): number => (mirrored ? items.length - 1 - k : k);\n const starts = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y : it.x;\n });\n const ends = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y + it.height : it.x + it.width;\n });\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: items.length,\n axis: vertical ? 'y' : 'x',\n maxItemSize: { width: maxW, height: maxH },\n query(r) {\n const a0 = vertical ? r.y : r.x;\n const a1 = vertical ? r.y + r.height : r.x + r.width;\n const lo = Math.max(0, lowerBound(ends, a0) - OVERSCAN);\n const hi = Math.min(items.length, upperBound(starts, a1) + OVERSCAN);\n const out: SceneItem[] = [];\n for (let k = lo; k < hi; k++) out.push(items[posIdx(k)]);\n return out;\n },\n nearestItem(pt) {\n const k = clamp(upperBound(starts, vertical ? pt.y : pt.x) - 1, 0, items.length - 1);\n return items[posIdx(k)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, items.length - 1);\n },\n };\n}\n\nexport interface GridOptions {\n gap?: number;\n columns?: number;\n /**\n * WRAPPED mode: instead of declaring `columns`, give the available line width\n * (world units) and the grid derives how many cells fit — the responsive\n * thumbnail-sidebar behavior. Takes precedence over `columns`.\n */\n lineWidth?: number;\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function gridLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: GridOptions = {},\n): Scene {\n const gap = opts.gap ?? 48;\n const n = grouping.length;\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure at intrinsic PAGE size; the widest item is the `uniform` reference.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const refW = measures.reduce((m, p) => Math.max(m, p.width), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins (uniform → equal\n // page widths; equal margins keep the OUTER boxes equal too, so columns line up).\n const locals: LocalBox[][] = new Array(n);\n const sizes: Array<{ width: number; height: number }> = new Array(n);\n const scales: number[] = new Array(n);\n let cellW = 1;\n let cellH = 1;\n for (let i = 0; i < n; i++) {\n const s = itemScale(measures[i].width, refW, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n scales[i] = s;\n locals[i] = packed.local;\n sizes[i] = { width: packed.width, height: packed.height };\n cellW = Math.max(cellW, sizes[i].width);\n cellH = Math.max(cellH, sizes[i].height);\n }\n\n // Column count — declared, or DERIVED from the available line width (wrapped):\n // how many cells fit the line. Computed here because it needs cellW. Clamped to\n // the item count so a short document doesn't occupy (or mirror across) a wider\n // line than it fills.\n const wanted =\n opts.lineWidth !== undefined\n ? Math.floor((opts.lineWidth + gap) / (cellW + gap))\n : (opts.columns ?? Math.ceil(Math.sqrt(n)));\n const columns = Math.max(1, Math.min(wanted, Math.max(1, n)));\n // RTL fills each row right→left (like RTL text wrap); rows stay top→bottom.\n const colAt = (col: number): number => (direction === 'rtl' ? columns - 1 - col : col);\n\n // Rows are as tall as THEIR tallest item — a wrapped grid is text-wrap for pages,\n // and a wrapped line is as tall as its own tallest glyph, not the document's\n // tallest. (A single global cell height left short pages floating in huge voids\n // with mixed page sizes.) Columns keep a uniform width: that's what makes a\n // column grid a grid — and `sizing: 'uniform'` removes the horizontal voids.\n const stepX = cellW + gap;\n const rows = Math.max(1, Math.ceil(n / columns));\n const rowHeight: number[] = new Array(rows).fill(1);\n for (let i = 0; i < n; i++) {\n const row = Math.floor(i / columns);\n rowHeight[row] = Math.max(rowHeight[row], sizes[i].height);\n }\n const rowTop: number[] = new Array(rows);\n let yCursor = 0;\n for (let r = 0; r < rows; r++) {\n rowTop[r] = yCursor;\n yCursor += rowHeight[r] + gap;\n }\n const rowEnd = rowTop.map((top, r) => top + rowHeight[r]);\n\n const items: SceneItem[] = new Array(n);\n for (let i = 0; i < n; i++) {\n const col = colAt(i % columns);\n const row = Math.floor(i / columns);\n const it: SceneItem = {\n index: i,\n x: col * stepX + (cellW - sizes[i].width) / 2,\n y: rowTop[row] + (rowHeight[row] - sizes[i].height) / 2,\n width: sizes[i].width,\n height: sizes[i].height,\n pageIndexes: grouping[i],\n pages: [],\n };\n it.pages = placePages(it, locals[i], scales[i]);\n items[i] = it;\n }\n\n const size: Size = { width: columns * stepX - gap, height: Math.max(1, yCursor - gap) };\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: n,\n axis: 'grid',\n maxItemSize: { width: cellW, height: cellH },\n query(r) {\n const c0 = Math.max(0, Math.floor(r.x / stepX));\n const c1 = Math.min(columns - 1, Math.floor((r.x + r.width) / stepX));\n // rows by binary search over the prefix-summed row extents (O(log rows))\n const r0 = Math.max(0, lowerBound(rowEnd, r.y));\n const r1 = Math.min(rows - 1, upperBound(rowTop, r.y + r.height) - 1);\n const out: SceneItem[] = [];\n for (let row = r0; row <= r1; row++)\n for (let col = c0; col <= c1; col++) {\n // colAt is its own inverse: spatial column → reading-order column\n const idx = row * columns + colAt(col);\n if (idx < n) out.push(items[idx]);\n }\n return out;\n },\n nearestItem(pt) {\n const col = clamp(Math.floor(pt.x / stepX), 0, columns - 1);\n const row = clamp(upperBound(rowTop, pt.y) - 1, 0, rows - 1);\n return items[clamp(row * columns + colAt(col), 0, n - 1)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, n - 1);\n },\n };\n}\n\n/** Spread grouping — a pure input to layout (all `plugin-spread` needs to be). */\nexport function groupPages(pageCount: number, mode: SpreadMode = 'none'): number[][] {\n const out: number[][] = [];\n if (mode === 'none') {\n for (let i = 0; i < pageCount; i++) out.push([i]);\n return out;\n }\n let i = 0;\n if (mode === 'even' && pageCount > 0) out.push([i++]);\n for (; i < pageCount; i += 2) out.push(i + 1 < pageCount ? [i, i + 1] : [i]);\n return out;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["NO_FRAME"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @embedpdf/core-stage — the pure spatial model.\n *\n * Scene (a layout strategy turns pages into positioned items + a spatial index),\n * Camera ({x,y,zoom}: the world point at the viewport origin + scale), and Anchor\n * (a focal point relative to a page — what survives layout/zoom changes & reloads).\n *\n * Navigation is pure camera math. Bounds/home/margin are three separate functions.\n * No DOM, no framework; plain-data model plus derived spatial queries (the DATA\n * is serializable — `Scene` also carries query methods derived from it). This\n * is the v4 Rust core, verbatim.\n */\n\n// The pure coordinate primitives live in the dependency-free geometry base\n// (shared with the framework adapters); stage-core re-exports them so existing\n// `from '@embedpdf/core-stage'` imports are unaffected.\nimport {\n displaySize,\n NO_FRAME,\n type PageFrame,\n type PageRotation,\n type Point,\n type Size,\n} from '@embedpdf/core-geometry';\nexport type { Point, Size, PageRotation, PageFrame } from '@embedpdf/core-geometry';\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\nexport interface Camera {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface PageGeom {\n /** UN-rotated (intrinsic) page size — the content's own dimensions before rotation. */\n size: Size;\n /** Total display rotation. Layout swaps w↔h for 90/270 so the box is the\n * on-screen footprint; the renderer rotates the content into it. */\n rotation?: PageRotation;\n /**\n * PDF `/UserUnit` (§14.11.6): how many 1/72\" units one point of THIS page\n * spans — the page is physically `userUnit ×` larger than its point size\n * says. Folded into the layout (a userUnit-5 page lays out 5× bigger, like\n * Acrobat) AND into its `contentScale`, so \"world per content point\" stays\n * true per page. Default 1 (virtually every document).\n */\n userUnit?: number;\n}\nexport interface PageBox {\n pageIndex: number;\n x: number;\n y: number;\n /** DISPLAY width — swapped to the page's height for 90/270 rotations. */\n width: number;\n /** DISPLAY height — swapped to the page's width for 90/270 rotations. */\n height: number;\n /** The page's total display rotation; the renderer rotates the (normalized,\n * un-rotated) content bitmap by this to fill the display box. */\n rotation: PageRotation;\n /**\n * World size ÷ intrinsic size for this page (1 for `intrinsic` sizing). The shell\n * multiplies it by the camera zoom to get device-px-per-PDF-point, so render\n * resolution and point↔screen mapping stay correct under `uniform` sizing.\n * Isotropic, so it is unaffected by rotation.\n */\n contentScale: number;\n}\n\n/** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared\n * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a\n * userUnit-5 page measures 5× its point size). Everything the layout packs uses\n * these display dims; the content scale (isotropic) and the renderer's\n * transform recover the content. */\nfunction displayDims(pg: PageGeom): Size {\n const u = pg.userUnit ?? 1;\n const d = displaySize(pg.size, pg.rotation ?? 0);\n return u === 1 ? d : { width: d.width * u, height: d.height * u };\n}\nexport interface SceneItem {\n index: number;\n x: number;\n y: number;\n width: number;\n height: number;\n pageIndexes: number[];\n pages: PageBox[];\n}\nexport type Axis = 'x' | 'y' | 'grid';\nexport interface Scene {\n size: Size;\n items: SceneItem[];\n itemCount: number;\n axis: Axis;\n /**\n * The largest item width & height in the document. Fit-modes resolve against this\n * (not the current page) so the zoom is STABLE across the whole document and no\n * page ever overflows — matching a conventional PDF viewer's \"fit width\".\n */\n maxItemSize: Size;\n query(rect: Rect): SceneItem[];\n nearestItem(pt: Point): SceneItem;\n itemOfPage(pageIndex: number): number;\n}\nexport interface CameraConstraint {\n bounded: boolean;\n /**\n * Breathing room (screen px) around the content — the ONE spacing concept.\n * Fit-modes inset by it, placement leaves it as gutter, and the clamp lets the\n * camera reveal exactly this much beyond each content edge.\n */\n padding: number;\n /** Where content RESTS on an axis it FITS (locked — there is nowhere else to\n * be). Default center/center. x is logical against `direction`. */\n fitAlign?: Alignment;\n direction?: Direction;\n}\n/**\n * The zoom intent — an equation about the fit-box, each resolved by `resolveZoom`:\n * { mode } — viewport-relative: fit-width / fit-page / automatic / fit-all\n * { level } — document-relative: a fixed scale factor\n * { pageWidth } — absolute: the page unit renders N **screen px** wide. The only\n * intent stable across BOTH viewport and document differences —\n * e.g. thumbnails at 200px for any document. Targets the page\n * UNIT (a spread counts as one, exactly as in fit-page/fit-width);\n * combine with sizing 'uniform' to make every page exactly N px.\n * { pageHeight } — absolute, vertical twin (horizontal filmstrip thumbnails).\n */\nexport type ZoomSpec =\n | { mode: ZoomModeValue }\n | { level: number }\n | { pageWidth: number }\n | { pageHeight: number };\nexport interface Anchor {\n pageIndex: number;\n fx: number;\n fy: number;\n}\nexport type SpreadMode = 'none' | 'odd' | 'even';\n/**\n * Reading direction — a LAYOUT property, not a navigation one. Navigation steps by\n * index (reading order); direction only decides where reading-order puts pages in\n * space: horizontal items advance leftward, spreads bind on the right, grid rows\n * fill right→left. The camera, cursor, fit and clamp never learn about it.\n */\nexport type Direction = 'ltr' | 'rtl';\n/**\n * Page sizing policy (per-item world scale, set in the layout):\n * 'intrinsic' — true PDF sizes; relative proportions preserved.\n * 'uniform' — every item scaled to the same CROSS-axis size (vertical → equal\n * widths, horizontal → equal heights, grid → equal widths), so pages\n * sit flush with no left/right gaps. Camera/zoom are untouched.\n */\nexport type SizingMode = 'intrinsic' | 'uniform';\n\nexport const ZOOM_MIN = 0.05;\nexport const ZOOM_MAX = 64;\nexport const ZoomMode = {\n Automatic: 'automatic',\n FitPage: 'fit-page',\n FitWidth: 'fit-width',\n /** Fit the WHOLE scene (every page) in view — the construction overview. */\n FitAll: 'fit-all',\n} as const;\nexport type ZoomModeValue = (typeof ZoomMode)[keyof typeof ZoomMode];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\n\n// ── Projection ──────────────────────────────────────────────────────────────\nexport const toScreen = (c: Camera, w: Point): Point => ({\n x: (w.x - c.x) * c.zoom,\n y: (w.y - c.y) * c.zoom,\n});\nexport const toWorld = (c: Camera, s: Point): Point => ({\n x: c.x + s.x / c.zoom,\n y: c.y + s.y / c.zoom,\n});\nexport const cameraWorldRect = (c: Camera, vp: Size): Rect => ({\n x: c.x,\n y: c.y,\n width: vp.width / c.zoom,\n height: vp.height / c.zoom,\n});\n\n// ── Camera operations ────────────────────────────────────────────────────────\n/** Focal zoom: keep the world point under `screenPt` fixed (no bounce). */\nexport const zoomAround = (c: Camera, screenPt: Point, factor: number): Camera => {\n const zoom = clamp(c.zoom * factor, ZOOM_MIN, ZOOM_MAX);\n return {\n x: c.x + screenPt.x / c.zoom - screenPt.x / zoom,\n y: c.y + screenPt.y / c.zoom - screenPt.y / zoom,\n zoom,\n };\n};\nexport const panByScreen = (c: Camera, dxScreen: number, dyScreen: number): Camera => ({\n x: c.x - dxScreen / c.zoom,\n y: c.y - dyScreen / c.zoom,\n zoom: c.zoom,\n});\nexport const centerOnWorld = (worldPt: Point, vp: Size, zoom: number): Camera => ({\n zoom,\n x: worldPt.x - vp.width / 2 / zoom,\n y: worldPt.y - vp.height / 2 / zoom,\n});\n\n/**\n * One axis of camera travel against content bounds — THE scroll geometry.\n * `near`/`far` are the two flush camera positions (content start edge at the\n * padded viewport start / content end edge at the padded viewport end); the\n * camera travels in [near, far] while the padded content overflows the view.\n * When it FITS the interval inverts and `fits` is true — there is nowhere to\n * travel, and the clamp locks the camera to its fitAlign rest instead. Shared\n * by `clampCamera` and `scrollMetrics`, so the pan clamp and a scrollbar can\n * never disagree about where travel ends.\n */\nexport interface AxisTravel {\n near: number;\n far: number;\n fits: boolean;\n}\nexport const travelRange = (\n origin: number,\n content: number,\n view: number,\n zoom: number,\n padding: number,\n): AxisTravel => ({\n near: origin - padding / zoom,\n far: origin + content - (view - padding) / zoom,\n fits: content * zoom <= view - 2 * padding,\n});\n\n/**\n * Bounds — the one place travel limits live. Clamps the camera into an arbitrary\n * world `Rect` (the whole document in continuous flow, one item in paged flow),\n * with a `padding` gutter the camera may reveal beyond each content edge.\n *\n * Per axis the travel interval is `travelRange`'s [near, far]. If the padded\n * content OVERFLOWS the viewport, the camera travels freely in it. If it FITS,\n * the camera is LOCKED to the `fitAlign` rest point — start = near, end = far,\n * center = midpoint (x resolved logically against `direction`).\n */\nexport const clampCamera = (c: Camera, bounds: Rect, vp: Size, k: CameraConstraint): Camera => {\n if (!k.bounded) return c;\n const p = k.padding;\n const fit = k.fitAlign ?? { x: 'center', y: 'center' };\n const axis = (pos: number, origin: number, content: number, view: number, a: Align): number => {\n const t = travelRange(origin, content, view, c.zoom, p);\n if (t.fits) return a === 'start' ? t.near : a === 'end' ? t.far : (t.near + t.far) / 2; // fits: rest & lock\n return clamp(pos, t.near, t.far);\n };\n // logical x: under RTL the reading start is the RIGHT edge\n const ax =\n k.direction === 'rtl' && fit.x !== 'center' ? (fit.x === 'start' ? 'end' : 'start') : fit.x;\n return {\n zoom: c.zoom,\n x: axis(c.x, bounds.x, bounds.width, vp.width, ax),\n y: axis(c.y, bounds.y, bounds.height, vp.height, fit.y),\n };\n};\n\n/**\n * The camera as a NATIVE SCROLLER — the DOM scroll vocabulary, in screen px:\n * every field means exactly what it means on a DOM element. Per axis the scroll\n * range is the UNION of the padded content extent and the current camera window:\n * • bounded — the clamp already keeps the window inside the padded content\n * (or rests it when the axis fits), so the union IS the padded content and\n * the numbers degenerate to the classic scroller (scrollLeft ∈\n * [0, scrollWidth − clientWidth], aligned with `travelRange`'s [near, far]).\n * • unbounded — pan beyond the content and the union grows (the Figma\n * scrollbar): the thumb shrinks toward the edge but always remains a road\n * back over the content.\n * `scrollableX/Y` false ⇔ the window covers the whole range (native: no bar).\n * One formula, no mode branching — the clamp did the branching already.\n * Coordinates are PHYSICAL on both axes (RTL included), deliberately sidestepping\n * the DOM's negative-scrollLeft-under-RTL behavior.\n */\nexport interface ScrollMetrics {\n scrollLeft: number;\n scrollTop: number;\n scrollWidth: number;\n scrollHeight: number;\n clientWidth: number;\n clientHeight: number;\n scrollableX: boolean;\n scrollableY: boolean;\n}\nexport const scrollMetrics = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n): ScrollMetrics => {\n const axis = (pos: number, origin: number, content: number, view: number) => {\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const total = (hi - lo) * c.zoom;\n // half-px slack, like the fits-predicate: sub-pixel overflow must not flash a bar\n return { offset: (pos - lo) * c.zoom, total, scrollable: total > view + 0.5 };\n };\n const x = axis(c.x, bounds.x, bounds.width, vp.width);\n const y = axis(c.y, bounds.y, bounds.height, vp.height);\n return {\n scrollLeft: x.offset,\n scrollTop: y.offset,\n scrollWidth: x.total,\n scrollHeight: y.total,\n clientWidth: vp.width,\n clientHeight: vp.height,\n scrollableX: x.scrollable,\n scrollableY: y.scrollable,\n };\n};\n\n/**\n * The inverse write — `Element.scrollTo` semantics: absolute offsets into the\n * CURRENT range (the same union `scrollMetrics` reports), clamped into\n * [0, total − view]; an omitted axis does not move. Zoom is untouched: scrolling\n * is a pan in scroller clothing.\n */\nexport const cameraFromScroll = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n target: { left?: number; top?: number },\n): Camera => {\n const axis = (\n want: number | undefined,\n pos: number,\n origin: number,\n content: number,\n view: number,\n ): number => {\n if (want === undefined) return pos;\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const max = Math.max(0, (hi - lo) * c.zoom - view);\n return lo + clamp(want, 0, max) / c.zoom;\n };\n return {\n zoom: c.zoom,\n x: axis(target.left, c.x, bounds.x, bounds.width, vp.width),\n y: axis(target.top, c.y, bounds.y, bounds.height, vp.height),\n };\n};\n\n/**\n * Per-axis alignment value. Used by the rest constraint (fitAlign — where\n * content settles on an axis the camera cannot travel) and, extended with\n * fractions (see {@link AlignValue}), by the arrival/zoom/anchor policies.\n * On the x-axis the NAMED values are LOGICAL (CSS-style): 'start' = where\n * reading begins (left in LTR, RIGHT in RTL), 'end' = where it ends. On the\n * y-axis they are physical (start = top). So a 'start' default is\n * automatically correct in both directions — no 'auto' value needed.\n */\nexport type Align = 'start' | 'center' | 'end';\nexport interface Alignment {\n x: Align;\n y: Align;\n}\n/**\n * One axis of an alignment POLICY: a named stop, or a viewport fraction 0–1\n * that positions the subject's CENTER at that fraction of the viewport\n * ('center' ≡ 0.5; 0.35 = the browser find-bar line). Named stops are logical\n * on x; fractions are physical, like every screen coordinate.\n */\nexport type AlignValue = Align | number;\nexport interface AlignmentValue {\n x: AlignValue;\n y: AlignValue;\n}\n\n/**\n * THE placement algorithm — every arrival (goToPage, next/prev, reset) lands\n * through it. Pure alignment: put the subject at `align` in the viewport —\n * per axis 'start' (reading edge at the padded viewport edge), 'end' (far\n * edge), 'center', or a fraction — with x resolved logically against the\n * reading direction. The SAME rule at every zoom: whether the subject fits or\n * overflows never changes where it lands. Landing is policy, not a side\n * effect of magnification.\n *\n * Deliberately clamp-free: the caller clamps the result against the TRUE\n * travel bounds (the scene in continuous flow, the item slice in paged). On\n * an axis with no real freedom that clamp collapses the landing to the\n * `fitAlign` rest point — \"rests where it must\" comes out of the clamp's\n * geometry, never out of a branch here.\n */\nexport function placeCamera(\n subject: Rect,\n vp: Size,\n zoom: number,\n padding = 0,\n align: AlignmentValue = { x: 'start', y: 'start' },\n direction: Direction = 'ltr',\n): Camera {\n const axis = (a: AlignValue, pos: number, extent: number, view: number): number => {\n if (a === 'start') return pos - padding / zoom;\n if (a === 'end') return pos + extent - (view - padding) / zoom;\n const f = a === 'center' ? 0.5 : clamp(a, 0, 1);\n return pos + extent / 2 - (view * f) / zoom;\n };\n const ax =\n direction === 'rtl' && align.x === 'start'\n ? 'end'\n : direction === 'rtl' && align.x === 'end'\n ? 'start'\n : align.x;\n return {\n zoom,\n x: axis(ax, subject.x, subject.width, vp.width),\n y: axis(align.y, subject.y, subject.height, vp.height),\n };\n}\n\n/**\n * The MINIMAL camera move that makes `rect` fully visible (with a `padding` gutter)\n * — DOM scrollIntoView({ block: 'nearest' }) as camera math. Per axis, the cameras\n * that show the rect form an interval; the answer is the current camera CLAMPED\n * into it: already inside → unchanged (the no-op case, by construction, not by\n * condition). An oversized rect (interval inverted) aligns to its start.\n *\n * This is deliberately NOT placement: `placeCamera` answers \"I am navigating here\"\n * (canonical); `revealCamera` answers \"make sure this is seeable\" (minimal).\n */\nexport function revealCamera(cam: Camera, rect: Rect, vp: Size, padding = 0): Camera {\n const axis = (pos: number, start: number, size: number, view: number): number => {\n const lo = start + size - (view - padding) / cam.zoom; // far edge just inside\n const hi = start - padding / cam.zoom; // near edge just inside\n if (lo > hi) return hi; // rect larger than the viewport: align its start\n return clamp(pos, lo, hi);\n };\n return {\n zoom: cam.zoom,\n x: axis(cam.x, rect.x, rect.width, vp.width),\n y: axis(cam.y, rect.y, rect.height, vp.height),\n };\n}\n\n// ── Zoom intent — resolved against a fit-box chosen by the caller: the document's\n// max item (continuous), the current item (paged), or the whole scene (fit-all). ─\nexport function resolveZoom(spec: ZoomSpec, box: Size, vp: Size, padding = 0): number {\n if ('level' in spec) return clamp(spec.level, ZOOM_MIN, ZOOM_MAX);\n // absolute pixel targets: box dimension = N screen px (document-independent)\n if ('pageWidth' in spec) return clamp(spec.pageWidth / box.width, ZOOM_MIN, ZOOM_MAX);\n if ('pageHeight' in spec) return clamp(spec.pageHeight / box.height, ZOOM_MIN, ZOOM_MAX);\n const fitW = Math.max(1, vp.width - 2 * padding) / box.width;\n const fitH = Math.max(1, vp.height - 2 * padding) / box.height;\n switch (spec.mode) {\n case ZoomMode.FitWidth:\n return clamp(fitW, ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.FitPage:\n case ZoomMode.FitAll: // same fit math; the caller passes the whole-scene box\n return clamp(Math.min(fitW, fitH), ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.Automatic:\n default:\n // fit width, but never upscale past 100% (Adobe's \"Automatic\"). Height-\n // independent: a portrait page fills the width and you scroll within it.\n return clamp(Math.min(fitW, 1), ZOOM_MIN, ZOOM_MAX);\n }\n}\n\n// ── Anchor — a PAGE-RELATIVE point, the durable \"what am I looking at\". The world\n// point under a screen position is meaningless across a re-layout (pages move);\n// the page-point survives. Point-generalized: WHICH viewport point the anchor\n// lives at is the caller's policy (anchorAlign for reframes, zoomAlign for\n// zoom-intent changes); the wrappers default to the classic center. ──────────\n/** The anchor at an arbitrary world point: its nearest page + the point, page-relative. */\nexport function anchorAtPoint(scene: Scene, worldPt: Point): Anchor {\n const item = scene.nearestItem(worldPt);\n let page = item.pages[0];\n let best = Infinity;\n for (const p of item.pages) {\n const d = Math.abs(p.x + p.width / 2 - worldPt.x);\n if (d < best) {\n best = d;\n page = p;\n }\n }\n return {\n pageIndex: page.pageIndex,\n fx: (worldPt.x - page.x) / page.width,\n fy: (worldPt.y - page.y) / page.height,\n };\n}\n\n/** The camera that puts the anchor's world point at a given SCREEN point. */\nexport function cameraForAnchorAtScreen(\n anchor: Anchor,\n scene: Scene,\n screenPt: Point,\n zoom: number,\n): Camera {\n const item = scene.items[scene.itemOfPage(anchor.pageIndex)];\n let page = item.pages[0];\n for (const p of item.pages) if (p.pageIndex === anchor.pageIndex) page = p;\n const world = { x: page.x + anchor.fx * page.width, y: page.y + anchor.fy * page.height };\n return { zoom, x: world.x - screenPt.x / zoom, y: world.y - screenPt.y / zoom };\n}\n\n/** The anchor under a viewport point — `at` is the policy point (anchorAlign /\n * zoomAlign resolved to px); it defaults to the classic viewport center. */\nexport function anchorFromCamera(cam: Camera, scene: Scene, vp: Size, at?: Point): Anchor {\n return anchorAtPoint(scene, toWorld(cam, at ?? { x: vp.width / 2, y: vp.height / 2 }));\n}\n/** The camera that restores an anchor to the same viewport point it was\n * captured at — capture and restore MUST agree on `at` for exact round-trips. */\nexport function cameraFromAnchor(\n anchor: Anchor,\n scene: Scene,\n vp: Size,\n zoom: number,\n at?: Point,\n): Camera {\n return cameraForAnchorAtScreen(anchor, scene, at ?? { x: vp.width / 2, y: vp.height / 2 }, zoom);\n}\n\n// ── Spatial-index helpers ──────────────────────────────────────────────────────\nfunction lowerBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] < t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nfunction upperBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] <= t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nconst OVERSCAN = 2;\n\n/**\n * Reserved chrome space around each PAGE — the `PageFrame` primitive (world\n * units in this pure layer; the shell derives them from screen px). The frame\n * belongs to PAGES, not items — in a spread every page keeps its own flanks,\n * so left/right chrome works everywhere.\n */\n\ninterface LocalBox {\n pageIndex: number;\n lx: number;\n ly: number;\n w: number;\n h: number;\n rotation: PageRotation;\n /** The page's `/UserUnit` — folded into ITS `contentScale` at placement, so\n * \"world units per content point\" stays true for every page of a spread. */\n userUnit: number;\n}\n\n/** Intrinsic item dimensions (pages only, no margins) — the `uniform` reference.\n * Uses DISPLAY dims, so `uniform` equalizes the rotated footprint and a rotated\n * page sizes to match its neighbours as it actually appears. */\nfunction measureItem(\n pages: readonly PageGeom[],\n group: number[],\n gap: number,\n): { width: number; height: number } {\n let w = 0;\n let h = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n w += d.width + (j > 0 ? gap : 0);\n if (d.height > h) h = d.height;\n }\n return { width: w, height: h };\n}\n\n/**\n * Pack one item from SCALED pages plus constant per-page margins. The page boxes\n * (and the spread's inner gap) scale with the sizing factor; the margins do NOT —\n * they are screen-px-derived chrome bands and stay constant in world space.\n */\nfunction packScaledItem(\n pages: readonly PageGeom[],\n group: number[],\n scale: number,\n gap: number,\n frame: PageFrame,\n direction: Direction,\n): { local: LocalBox[]; width: number; height: number } {\n let maxH = 0;\n for (let j = 0; j < group.length; j++)\n maxH = Math.max(maxH, displayDims(pages[group[j]]).height * scale);\n const local: LocalBox[] = [];\n let lx = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n const w = d.width * scale;\n const h = d.height * scale;\n local.push({\n pageIndex: group[j],\n lx: lx + frame.left,\n ly: frame.top + (maxH - h) / 2, // page centers within the inner band\n w,\n h,\n rotation: pages[group[j]].rotation ?? 0,\n userUnit: pages[group[j]].userUnit ?? 1,\n });\n lx += frame.left + w + frame.right + gap * scale;\n }\n const width = Math.max(0, lx - gap * scale);\n const height = frame.top + maxH + frame.bottom;\n if (direction === 'rtl') {\n // reading-first page takes the RIGHTMOST slot (the spread binds on the right);\n // slots mirror, but each page keeps its PHYSICAL margins (left room stays left)\n for (const b of local) b.lx = width - b.lx - b.w + (frame.left - frame.right);\n }\n return { local, width, height };\n}\n\nfunction placePages(item: SceneItem, local: LocalBox[], contentScale: number): PageBox[] {\n return local.map((b) => ({\n pageIndex: b.pageIndex,\n x: item.x + b.lx,\n y: item.y + b.ly,\n width: b.w,\n height: b.h,\n rotation: b.rotation,\n // Per PAGE, not per item: the box was measured at `size × userUnit`, so\n // the world-per-content-point factor must carry the same userUnit — every\n // content→world mapping downstream divides/multiplies by exactly this.\n contentScale: contentScale * b.userUnit,\n }));\n}\n\n/** Per-item world scale = sizing factor (1 intrinsic, else cross-equalize for\n * uniform) × `viewUnitsPerPoint` (the platform's points→view-px factor). The\n * result becomes the page's `contentScale`, so world units are view px and 100%\n * is physically accurate. `viewUnitsPerPoint` defaults to 1 (neutral). */\nconst itemScale = (\n crossIntrinsic: number,\n refCross: number,\n sizing: SizingMode,\n viewUnitsPerPoint = 1,\n): number =>\n (sizing === 'uniform' && crossIntrinsic > 0 ? refCross / crossIntrinsic : 1) * viewUnitsPerPoint;\n\n// ── Layout strategies ─────────────────────────────────────────────────────────\nexport interface LinearOptions {\n gap?: number;\n axis?: 'x' | 'y';\n align?: 'center' | 'start';\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function linearLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: LinearOptions = {},\n): Scene {\n const gap = opts.gap ?? 16;\n const vertical = (opts.axis ?? 'y') === 'y';\n const align = opts.align ?? 'center';\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n // RTL mirrors the main axis only when it's horizontal; vertical scroll is\n // direction-agnostic (RTL books still scroll down) — only spreads swap.\n const mirrored = !vertical && direction === 'rtl';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure every item at intrinsic PAGE size (no margins) — the cross axis\n // (width when vertical) gives the reference for `uniform` sizing, so uniform\n // equalizes the pages themselves; constant margins then keep outer edges flush.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const crossOf = (p: { width: number; height: number }) => (vertical ? p.width : p.height);\n const refCross = measures.reduce((m, p) => Math.max(m, crossOf(p)), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins, lay along the axis.\n const items: SceneItem[] = new Array(grouping.length);\n const locals: LocalBox[][] = new Array(grouping.length);\n const scales: number[] = new Array(grouping.length);\n let main = 0;\n let crossMax = 0;\n let maxW = 0;\n let maxH = 0;\n\n for (let i = 0; i < grouping.length; i++) {\n const s = itemScale(crossOf(measures[i]), refCross, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n const { width, height } = packed;\n scales[i] = s;\n locals[i] = packed.local;\n const it: SceneItem = {\n index: i,\n x: 0,\n y: 0,\n width,\n height,\n pageIndexes: grouping[i],\n pages: [],\n };\n if (vertical) {\n it.y = main;\n main += height + gap;\n } else {\n it.x = main;\n main += width + gap;\n }\n crossMax = Math.max(crossMax, vertical ? width : height);\n maxW = Math.max(maxW, width);\n maxH = Math.max(maxH, height);\n items[i] = it;\n }\n\n const sceneMain = Math.max(0, main - gap);\n const size: Size = vertical\n ? { width: crossMax, height: sceneMain }\n : { width: sceneMain, height: crossMax };\n\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (mirrored) it.x = size.width - it.x - it.width; // reading order advances leftward\n if (vertical) it.x = align === 'center' ? (size.width - it.width) / 2 : 0;\n else it.y = align === 'center' ? (size.height - it.height) / 2 : 0;\n it.pages = placePages(it, locals[i], scales[i]);\n }\n\n // Spatial index in POSITION order (ascending coordinates for binary search).\n // In a mirrored layout position order is reverse index order — `posIdx` maps back.\n const posIdx = (k: number): number => (mirrored ? items.length - 1 - k : k);\n const starts = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y : it.x;\n });\n const ends = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y + it.height : it.x + it.width;\n });\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: items.length,\n axis: vertical ? 'y' : 'x',\n maxItemSize: { width: maxW, height: maxH },\n query(r) {\n const a0 = vertical ? r.y : r.x;\n const a1 = vertical ? r.y + r.height : r.x + r.width;\n const lo = Math.max(0, lowerBound(ends, a0) - OVERSCAN);\n const hi = Math.min(items.length, upperBound(starts, a1) + OVERSCAN);\n const out: SceneItem[] = [];\n for (let k = lo; k < hi; k++) out.push(items[posIdx(k)]);\n return out;\n },\n nearestItem(pt) {\n const k = clamp(upperBound(starts, vertical ? pt.y : pt.x) - 1, 0, items.length - 1);\n return items[posIdx(k)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, items.length - 1);\n },\n };\n}\n\nexport interface GridOptions {\n gap?: number;\n columns?: number;\n /**\n * WRAPPED mode: instead of declaring `columns`, give the available line width\n * (world units) and the grid derives how many cells fit — the responsive\n * thumbnail-sidebar behavior. Takes precedence over `columns`.\n */\n lineWidth?: number;\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function gridLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: GridOptions = {},\n): Scene {\n const gap = opts.gap ?? 48;\n const n = grouping.length;\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure at intrinsic PAGE size; the widest item is the `uniform` reference.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const refW = measures.reduce((m, p) => Math.max(m, p.width), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins (uniform → equal\n // page widths; equal margins keep the OUTER boxes equal too, so columns line up).\n const locals: LocalBox[][] = new Array(n);\n const sizes: Array<{ width: number; height: number }> = new Array(n);\n const scales: number[] = new Array(n);\n let cellW = 1;\n let cellH = 1;\n for (let i = 0; i < n; i++) {\n const s = itemScale(measures[i].width, refW, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n scales[i] = s;\n locals[i] = packed.local;\n sizes[i] = { width: packed.width, height: packed.height };\n cellW = Math.max(cellW, sizes[i].width);\n cellH = Math.max(cellH, sizes[i].height);\n }\n\n // Column count — declared, or DERIVED from the available line width (wrapped):\n // how many cells fit the line. Computed here because it needs cellW. Clamped to\n // the item count so a short document doesn't occupy (or mirror across) a wider\n // line than it fills.\n const wanted =\n opts.lineWidth !== undefined\n ? Math.floor((opts.lineWidth + gap) / (cellW + gap))\n : (opts.columns ?? Math.ceil(Math.sqrt(n)));\n const columns = Math.max(1, Math.min(wanted, Math.max(1, n)));\n // RTL fills each row right→left (like RTL text wrap); rows stay top→bottom.\n const colAt = (col: number): number => (direction === 'rtl' ? columns - 1 - col : col);\n\n // Rows are as tall as THEIR tallest item — a wrapped grid is text-wrap for pages,\n // and a wrapped line is as tall as its own tallest glyph, not the document's\n // tallest. (A single global cell height left short pages floating in huge voids\n // with mixed page sizes.) Columns keep a uniform width: that's what makes a\n // column grid a grid — and `sizing: 'uniform'` removes the horizontal voids.\n const stepX = cellW + gap;\n const rows = Math.max(1, Math.ceil(n / columns));\n const rowHeight: number[] = new Array(rows).fill(1);\n for (let i = 0; i < n; i++) {\n const row = Math.floor(i / columns);\n rowHeight[row] = Math.max(rowHeight[row], sizes[i].height);\n }\n const rowTop: number[] = new Array(rows);\n let yCursor = 0;\n for (let r = 0; r < rows; r++) {\n rowTop[r] = yCursor;\n yCursor += rowHeight[r] + gap;\n }\n const rowEnd = rowTop.map((top, r) => top + rowHeight[r]);\n\n const items: SceneItem[] = new Array(n);\n for (let i = 0; i < n; i++) {\n const col = colAt(i % columns);\n const row = Math.floor(i / columns);\n const it: SceneItem = {\n index: i,\n x: col * stepX + (cellW - sizes[i].width) / 2,\n y: rowTop[row] + (rowHeight[row] - sizes[i].height) / 2,\n width: sizes[i].width,\n height: sizes[i].height,\n pageIndexes: grouping[i],\n pages: [],\n };\n it.pages = placePages(it, locals[i], scales[i]);\n items[i] = it;\n }\n\n const size: Size = { width: columns * stepX - gap, height: Math.max(1, yCursor - gap) };\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: n,\n axis: 'grid',\n maxItemSize: { width: cellW, height: cellH },\n query(r) {\n const c0 = Math.max(0, Math.floor(r.x / stepX));\n const c1 = Math.min(columns - 1, Math.floor((r.x + r.width) / stepX));\n // rows by binary search over the prefix-summed row extents (O(log rows))\n const r0 = Math.max(0, lowerBound(rowEnd, r.y));\n const r1 = Math.min(rows - 1, upperBound(rowTop, r.y + r.height) - 1);\n const out: SceneItem[] = [];\n for (let row = r0; row <= r1; row++)\n for (let col = c0; col <= c1; col++) {\n // colAt is its own inverse: spatial column → reading-order column\n const idx = row * columns + colAt(col);\n if (idx < n) out.push(items[idx]);\n }\n return out;\n },\n nearestItem(pt) {\n const col = clamp(Math.floor(pt.x / stepX), 0, columns - 1);\n const row = clamp(upperBound(rowTop, pt.y) - 1, 0, rows - 1);\n return items[clamp(row * columns + colAt(col), 0, n - 1)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, n - 1);\n },\n };\n}\n\n/** Spread grouping — a pure input to layout (all `plugin-spread` needs to be). */\nexport function groupPages(pageCount: number, mode: SpreadMode = 'none'): number[][] {\n const out: number[][] = [];\n if (mode === 'none') {\n for (let i = 0; i < pageCount; i++) out.push([i]);\n return out;\n }\n let i = 0;\n if (mode === 'even' && pageCount > 0) out.push([i++]);\n for (; i < pageCount; i += 2) out.push(i + 1 < pageCount ? [i, i + 1] : [i]);\n return out;\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ import { NO_FRAME, displaySize } from "@embedpdf/core-geometry";
8
8
  * (a focal point relative to a page — what survives layout/zoom changes & reloads).
9
9
  *
10
10
  * Navigation is pure camera math. Bounds/home/margin are three separate functions.
11
- * No DOM, no framework, fully serializable — this is the v4 Rust core, verbatim.
11
+ * No DOM, no framework; plain-data model plus derived spatial queries (the DATA
12
+ * is serializable — `Scene` also carries query methods derived from it). This
13
+ * is the v4 Rust core, verbatim.
12
14
  */
13
15
  /** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared
14
16
  * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @embedpdf/core-stage — the pure spatial model.\n *\n * Scene (a layout strategy turns pages into positioned items + a spatial index),\n * Camera ({x,y,zoom}: the world point at the viewport origin + scale), and Anchor\n * (a focal point relative to a page — what survives layout/zoom changes & reloads).\n *\n * Navigation is pure camera math. Bounds/home/margin are three separate functions.\n * No DOM, no framework, fully serializable — this is the v4 Rust core, verbatim.\n */\n\n// The pure coordinate primitives live in the dependency-free geometry base\n// (shared with the framework adapters); stage-core re-exports them so existing\n// `from '@embedpdf/core-stage'` imports are unaffected.\nimport {\n displaySize,\n NO_FRAME,\n type PageFrame,\n type PageRotation,\n type Point,\n type Size,\n} from '@embedpdf/core-geometry';\nexport type { Point, Size, PageRotation, PageFrame } from '@embedpdf/core-geometry';\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\nexport interface Camera {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface PageGeom {\n /** UN-rotated (intrinsic) page size — the content's own dimensions before rotation. */\n size: Size;\n /** Total display rotation. Layout swaps w↔h for 90/270 so the box is the\n * on-screen footprint; the renderer rotates the content into it. */\n rotation?: PageRotation;\n /**\n * PDF `/UserUnit` (§14.11.6): how many 1/72\" units one point of THIS page\n * spans — the page is physically `userUnit ×` larger than its point size\n * says. Folded into the layout (a userUnit-5 page lays out 5× bigger, like\n * Acrobat) AND into its `contentScale`, so \"world per content point\" stays\n * true per page. Default 1 (virtually every document).\n */\n userUnit?: number;\n}\nexport interface PageBox {\n pageIndex: number;\n x: number;\n y: number;\n /** DISPLAY width — swapped to the page's height for 90/270 rotations. */\n width: number;\n /** DISPLAY height — swapped to the page's width for 90/270 rotations. */\n height: number;\n /** The page's total display rotation; the renderer rotates the (normalized,\n * un-rotated) content bitmap by this to fill the display box. */\n rotation: PageRotation;\n /**\n * World size ÷ intrinsic size for this page (1 for `intrinsic` sizing). The shell\n * multiplies it by the camera zoom to get device-px-per-PDF-point, so render\n * resolution and point↔screen mapping stay correct under `uniform` sizing.\n * Isotropic, so it is unaffected by rotation.\n */\n contentScale: number;\n}\n\n/** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared\n * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a\n * userUnit-5 page measures 5× its point size). Everything the layout packs uses\n * these display dims; the content scale (isotropic) and the renderer's\n * transform recover the content. */\nfunction displayDims(pg: PageGeom): Size {\n const u = pg.userUnit ?? 1;\n const d = displaySize(pg.size, pg.rotation ?? 0);\n return u === 1 ? d : { width: d.width * u, height: d.height * u };\n}\nexport interface SceneItem {\n index: number;\n x: number;\n y: number;\n width: number;\n height: number;\n pageIndexes: number[];\n pages: PageBox[];\n}\nexport type Axis = 'x' | 'y' | 'grid';\nexport interface Scene {\n size: Size;\n items: SceneItem[];\n itemCount: number;\n axis: Axis;\n /**\n * The largest item width & height in the document. Fit-modes resolve against this\n * (not the current page) so the zoom is STABLE across the whole document and no\n * page ever overflows — matching a conventional PDF viewer's \"fit width\".\n */\n maxItemSize: Size;\n query(rect: Rect): SceneItem[];\n nearestItem(pt: Point): SceneItem;\n itemOfPage(pageIndex: number): number;\n}\nexport interface CameraConstraint {\n bounded: boolean;\n /**\n * Breathing room (screen px) around the content — the ONE spacing concept.\n * Fit-modes inset by it, placement leaves it as gutter, and the clamp lets the\n * camera reveal exactly this much beyond each content edge.\n */\n padding: number;\n /** Where content RESTS on an axis it FITS (locked — there is nowhere else to\n * be). Default center/center. x is logical against `direction`. */\n fitAlign?: Alignment;\n direction?: Direction;\n}\n/**\n * The zoom intent — an equation about the fit-box, each resolved by `resolveZoom`:\n * { mode } — viewport-relative: fit-width / fit-page / automatic / fit-all\n * { level } — document-relative: a fixed scale factor\n * { pageWidth } — absolute: the page unit renders N **screen px** wide. The only\n * intent stable across BOTH viewport and document differences —\n * e.g. thumbnails at 200px for any document. Targets the page\n * UNIT (a spread counts as one, exactly as in fit-page/fit-width);\n * combine with sizing 'uniform' to make every page exactly N px.\n * { pageHeight } — absolute, vertical twin (horizontal filmstrip thumbnails).\n */\nexport type ZoomSpec =\n | { mode: ZoomModeValue }\n | { level: number }\n | { pageWidth: number }\n | { pageHeight: number };\nexport interface Anchor {\n pageIndex: number;\n fx: number;\n fy: number;\n}\nexport type SpreadMode = 'none' | 'odd' | 'even';\n/**\n * Reading direction — a LAYOUT property, not a navigation one. Navigation steps by\n * index (reading order); direction only decides where reading-order puts pages in\n * space: horizontal items advance leftward, spreads bind on the right, grid rows\n * fill right→left. The camera, cursor, fit and clamp never learn about it.\n */\nexport type Direction = 'ltr' | 'rtl';\n/**\n * Page sizing policy (per-item world scale, set in the layout):\n * 'intrinsic' — true PDF sizes; relative proportions preserved.\n * 'uniform' — every item scaled to the same CROSS-axis size (vertical → equal\n * widths, horizontal → equal heights, grid → equal widths), so pages\n * sit flush with no left/right gaps. Camera/zoom are untouched.\n */\nexport type SizingMode = 'intrinsic' | 'uniform';\n\nexport const ZOOM_MIN = 0.05;\nexport const ZOOM_MAX = 64;\nexport const ZoomMode = {\n Automatic: 'automatic',\n FitPage: 'fit-page',\n FitWidth: 'fit-width',\n /** Fit the WHOLE scene (every page) in view — the construction overview. */\n FitAll: 'fit-all',\n} as const;\nexport type ZoomModeValue = (typeof ZoomMode)[keyof typeof ZoomMode];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\n\n// ── Projection ──────────────────────────────────────────────────────────────\nexport const toScreen = (c: Camera, w: Point): Point => ({\n x: (w.x - c.x) * c.zoom,\n y: (w.y - c.y) * c.zoom,\n});\nexport const toWorld = (c: Camera, s: Point): Point => ({\n x: c.x + s.x / c.zoom,\n y: c.y + s.y / c.zoom,\n});\nexport const cameraWorldRect = (c: Camera, vp: Size): Rect => ({\n x: c.x,\n y: c.y,\n width: vp.width / c.zoom,\n height: vp.height / c.zoom,\n});\n\n// ── Camera operations ────────────────────────────────────────────────────────\n/** Focal zoom: keep the world point under `screenPt` fixed (no bounce). */\nexport const zoomAround = (c: Camera, screenPt: Point, factor: number): Camera => {\n const zoom = clamp(c.zoom * factor, ZOOM_MIN, ZOOM_MAX);\n return {\n x: c.x + screenPt.x / c.zoom - screenPt.x / zoom,\n y: c.y + screenPt.y / c.zoom - screenPt.y / zoom,\n zoom,\n };\n};\nexport const panByScreen = (c: Camera, dxScreen: number, dyScreen: number): Camera => ({\n x: c.x - dxScreen / c.zoom,\n y: c.y - dyScreen / c.zoom,\n zoom: c.zoom,\n});\nexport const centerOnWorld = (worldPt: Point, vp: Size, zoom: number): Camera => ({\n zoom,\n x: worldPt.x - vp.width / 2 / zoom,\n y: worldPt.y - vp.height / 2 / zoom,\n});\n\n/**\n * One axis of camera travel against content bounds — THE scroll geometry.\n * `near`/`far` are the two flush camera positions (content start edge at the\n * padded viewport start / content end edge at the padded viewport end); the\n * camera travels in [near, far] while the padded content overflows the view.\n * When it FITS the interval inverts and `fits` is true — there is nowhere to\n * travel, and the clamp locks the camera to its fitAlign rest instead. Shared\n * by `clampCamera` and `scrollMetrics`, so the pan clamp and a scrollbar can\n * never disagree about where travel ends.\n */\nexport interface AxisTravel {\n near: number;\n far: number;\n fits: boolean;\n}\nexport const travelRange = (\n origin: number,\n content: number,\n view: number,\n zoom: number,\n padding: number,\n): AxisTravel => ({\n near: origin - padding / zoom,\n far: origin + content - (view - padding) / zoom,\n fits: content * zoom <= view - 2 * padding,\n});\n\n/**\n * Bounds — the one place travel limits live. Clamps the camera into an arbitrary\n * world `Rect` (the whole document in continuous flow, one item in paged flow),\n * with a `padding` gutter the camera may reveal beyond each content edge.\n *\n * Per axis the travel interval is `travelRange`'s [near, far]. If the padded\n * content OVERFLOWS the viewport, the camera travels freely in it. If it FITS,\n * the camera is LOCKED to the `fitAlign` rest point — start = near, end = far,\n * center = midpoint (x resolved logically against `direction`).\n */\nexport const clampCamera = (c: Camera, bounds: Rect, vp: Size, k: CameraConstraint): Camera => {\n if (!k.bounded) return c;\n const p = k.padding;\n const fit = k.fitAlign ?? { x: 'center', y: 'center' };\n const axis = (pos: number, origin: number, content: number, view: number, a: Align): number => {\n const t = travelRange(origin, content, view, c.zoom, p);\n if (t.fits) return a === 'start' ? t.near : a === 'end' ? t.far : (t.near + t.far) / 2; // fits: rest & lock\n return clamp(pos, t.near, t.far);\n };\n // logical x: under RTL the reading start is the RIGHT edge\n const ax =\n k.direction === 'rtl' && fit.x !== 'center' ? (fit.x === 'start' ? 'end' : 'start') : fit.x;\n return {\n zoom: c.zoom,\n x: axis(c.x, bounds.x, bounds.width, vp.width, ax),\n y: axis(c.y, bounds.y, bounds.height, vp.height, fit.y),\n };\n};\n\n/**\n * The camera as a NATIVE SCROLLER — the DOM scroll vocabulary, in screen px:\n * every field means exactly what it means on a DOM element. Per axis the scroll\n * range is the UNION of the padded content extent and the current camera window:\n * • bounded — the clamp already keeps the window inside the padded content\n * (or rests it when the axis fits), so the union IS the padded content and\n * the numbers degenerate to the classic scroller (scrollLeft ∈\n * [0, scrollWidth − clientWidth], aligned with `travelRange`'s [near, far]).\n * • unbounded — pan beyond the content and the union grows (the Figma\n * scrollbar): the thumb shrinks toward the edge but always remains a road\n * back over the content.\n * `scrollableX/Y` false ⇔ the window covers the whole range (native: no bar).\n * One formula, no mode branching — the clamp did the branching already.\n * Coordinates are PHYSICAL on both axes (RTL included), deliberately sidestepping\n * the DOM's negative-scrollLeft-under-RTL behavior.\n */\nexport interface ScrollMetrics {\n scrollLeft: number;\n scrollTop: number;\n scrollWidth: number;\n scrollHeight: number;\n clientWidth: number;\n clientHeight: number;\n scrollableX: boolean;\n scrollableY: boolean;\n}\nexport const scrollMetrics = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n): ScrollMetrics => {\n const axis = (pos: number, origin: number, content: number, view: number) => {\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const total = (hi - lo) * c.zoom;\n // half-px slack, like the fits-predicate: sub-pixel overflow must not flash a bar\n return { offset: (pos - lo) * c.zoom, total, scrollable: total > view + 0.5 };\n };\n const x = axis(c.x, bounds.x, bounds.width, vp.width);\n const y = axis(c.y, bounds.y, bounds.height, vp.height);\n return {\n scrollLeft: x.offset,\n scrollTop: y.offset,\n scrollWidth: x.total,\n scrollHeight: y.total,\n clientWidth: vp.width,\n clientHeight: vp.height,\n scrollableX: x.scrollable,\n scrollableY: y.scrollable,\n };\n};\n\n/**\n * The inverse write — `Element.scrollTo` semantics: absolute offsets into the\n * CURRENT range (the same union `scrollMetrics` reports), clamped into\n * [0, total − view]; an omitted axis does not move. Zoom is untouched: scrolling\n * is a pan in scroller clothing.\n */\nexport const cameraFromScroll = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n target: { left?: number; top?: number },\n): Camera => {\n const axis = (\n want: number | undefined,\n pos: number,\n origin: number,\n content: number,\n view: number,\n ): number => {\n if (want === undefined) return pos;\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const max = Math.max(0, (hi - lo) * c.zoom - view);\n return lo + clamp(want, 0, max) / c.zoom;\n };\n return {\n zoom: c.zoom,\n x: axis(target.left, c.x, bounds.x, bounds.width, vp.width),\n y: axis(target.top, c.y, bounds.y, bounds.height, vp.height),\n };\n};\n\n/**\n * Per-axis alignment value. Used by the rest constraint (fitAlign — where\n * content settles on an axis the camera cannot travel) and, extended with\n * fractions (see {@link AlignValue}), by the arrival/zoom/anchor policies.\n * On the x-axis the NAMED values are LOGICAL (CSS-style): 'start' = where\n * reading begins (left in LTR, RIGHT in RTL), 'end' = where it ends. On the\n * y-axis they are physical (start = top). So a 'start' default is\n * automatically correct in both directions — no 'auto' value needed.\n */\nexport type Align = 'start' | 'center' | 'end';\nexport interface Alignment {\n x: Align;\n y: Align;\n}\n/**\n * One axis of an alignment POLICY: a named stop, or a viewport fraction 0–1\n * that positions the subject's CENTER at that fraction of the viewport\n * ('center' ≡ 0.5; 0.35 = the browser find-bar line). Named stops are logical\n * on x; fractions are physical, like every screen coordinate.\n */\nexport type AlignValue = Align | number;\nexport interface AlignmentValue {\n x: AlignValue;\n y: AlignValue;\n}\n\n/**\n * THE placement algorithm — every arrival (goToPage, next/prev, reset) lands\n * through it. Pure alignment: put the subject at `align` in the viewport —\n * per axis 'start' (reading edge at the padded viewport edge), 'end' (far\n * edge), 'center', or a fraction — with x resolved logically against the\n * reading direction. The SAME rule at every zoom: whether the subject fits or\n * overflows never changes where it lands. Landing is policy, not a side\n * effect of magnification.\n *\n * Deliberately clamp-free: the caller clamps the result against the TRUE\n * travel bounds (the scene in continuous flow, the item slice in paged). On\n * an axis with no real freedom that clamp collapses the landing to the\n * `fitAlign` rest point — \"rests where it must\" comes out of the clamp's\n * geometry, never out of a branch here.\n */\nexport function placeCamera(\n subject: Rect,\n vp: Size,\n zoom: number,\n padding = 0,\n align: AlignmentValue = { x: 'start', y: 'start' },\n direction: Direction = 'ltr',\n): Camera {\n const axis = (a: AlignValue, pos: number, extent: number, view: number): number => {\n if (a === 'start') return pos - padding / zoom;\n if (a === 'end') return pos + extent - (view - padding) / zoom;\n const f = a === 'center' ? 0.5 : clamp(a, 0, 1);\n return pos + extent / 2 - (view * f) / zoom;\n };\n const ax =\n direction === 'rtl' && align.x === 'start'\n ? 'end'\n : direction === 'rtl' && align.x === 'end'\n ? 'start'\n : align.x;\n return {\n zoom,\n x: axis(ax, subject.x, subject.width, vp.width),\n y: axis(align.y, subject.y, subject.height, vp.height),\n };\n}\n\n/**\n * The MINIMAL camera move that makes `rect` fully visible (with a `padding` gutter)\n * — DOM scrollIntoView({ block: 'nearest' }) as camera math. Per axis, the cameras\n * that show the rect form an interval; the answer is the current camera CLAMPED\n * into it: already inside → unchanged (the no-op case, by construction, not by\n * condition). An oversized rect (interval inverted) aligns to its start.\n *\n * This is deliberately NOT placement: `placeCamera` answers \"I am navigating here\"\n * (canonical); `revealCamera` answers \"make sure this is seeable\" (minimal).\n */\nexport function revealCamera(cam: Camera, rect: Rect, vp: Size, padding = 0): Camera {\n const axis = (pos: number, start: number, size: number, view: number): number => {\n const lo = start + size - (view - padding) / cam.zoom; // far edge just inside\n const hi = start - padding / cam.zoom; // near edge just inside\n if (lo > hi) return hi; // rect larger than the viewport: align its start\n return clamp(pos, lo, hi);\n };\n return {\n zoom: cam.zoom,\n x: axis(cam.x, rect.x, rect.width, vp.width),\n y: axis(cam.y, rect.y, rect.height, vp.height),\n };\n}\n\n// ── Zoom intent — resolved against a fit-box chosen by the caller: the document's\n// max item (continuous), the current item (paged), or the whole scene (fit-all). ─\nexport function resolveZoom(spec: ZoomSpec, box: Size, vp: Size, padding = 0): number {\n if ('level' in spec) return clamp(spec.level, ZOOM_MIN, ZOOM_MAX);\n // absolute pixel targets: box dimension = N screen px (document-independent)\n if ('pageWidth' in spec) return clamp(spec.pageWidth / box.width, ZOOM_MIN, ZOOM_MAX);\n if ('pageHeight' in spec) return clamp(spec.pageHeight / box.height, ZOOM_MIN, ZOOM_MAX);\n const fitW = Math.max(1, vp.width - 2 * padding) / box.width;\n const fitH = Math.max(1, vp.height - 2 * padding) / box.height;\n switch (spec.mode) {\n case ZoomMode.FitWidth:\n return clamp(fitW, ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.FitPage:\n case ZoomMode.FitAll: // same fit math; the caller passes the whole-scene box\n return clamp(Math.min(fitW, fitH), ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.Automatic:\n default:\n // fit width, but never upscale past 100% (Adobe's \"Automatic\"). Height-\n // independent: a portrait page fills the width and you scroll within it.\n return clamp(Math.min(fitW, 1), ZOOM_MIN, ZOOM_MAX);\n }\n}\n\n// ── Anchor — a PAGE-RELATIVE point, the durable \"what am I looking at\". The world\n// point under a screen position is meaningless across a re-layout (pages move);\n// the page-point survives. Point-generalized: WHICH viewport point the anchor\n// lives at is the caller's policy (anchorAlign for reframes, zoomAlign for\n// zoom-intent changes); the wrappers default to the classic center. ──────────\n/** The anchor at an arbitrary world point: its nearest page + the point, page-relative. */\nexport function anchorAtPoint(scene: Scene, worldPt: Point): Anchor {\n const item = scene.nearestItem(worldPt);\n let page = item.pages[0];\n let best = Infinity;\n for (const p of item.pages) {\n const d = Math.abs(p.x + p.width / 2 - worldPt.x);\n if (d < best) {\n best = d;\n page = p;\n }\n }\n return {\n pageIndex: page.pageIndex,\n fx: (worldPt.x - page.x) / page.width,\n fy: (worldPt.y - page.y) / page.height,\n };\n}\n\n/** The camera that puts the anchor's world point at a given SCREEN point. */\nexport function cameraForAnchorAtScreen(\n anchor: Anchor,\n scene: Scene,\n screenPt: Point,\n zoom: number,\n): Camera {\n const item = scene.items[scene.itemOfPage(anchor.pageIndex)];\n let page = item.pages[0];\n for (const p of item.pages) if (p.pageIndex === anchor.pageIndex) page = p;\n const world = { x: page.x + anchor.fx * page.width, y: page.y + anchor.fy * page.height };\n return { zoom, x: world.x - screenPt.x / zoom, y: world.y - screenPt.y / zoom };\n}\n\n/** The anchor under a viewport point — `at` is the policy point (anchorAlign /\n * zoomAlign resolved to px); it defaults to the classic viewport center. */\nexport function anchorFromCamera(cam: Camera, scene: Scene, vp: Size, at?: Point): Anchor {\n return anchorAtPoint(scene, toWorld(cam, at ?? { x: vp.width / 2, y: vp.height / 2 }));\n}\n/** The camera that restores an anchor to the same viewport point it was\n * captured at — capture and restore MUST agree on `at` for exact round-trips. */\nexport function cameraFromAnchor(\n anchor: Anchor,\n scene: Scene,\n vp: Size,\n zoom: number,\n at?: Point,\n): Camera {\n return cameraForAnchorAtScreen(anchor, scene, at ?? { x: vp.width / 2, y: vp.height / 2 }, zoom);\n}\n\n// ── Spatial-index helpers ──────────────────────────────────────────────────────\nfunction lowerBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] < t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nfunction upperBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] <= t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nconst OVERSCAN = 2;\n\n/**\n * Reserved chrome space around each PAGE — the `PageFrame` primitive (world\n * units in this pure layer; the shell derives them from screen px). The frame\n * belongs to PAGES, not items — in a spread every page keeps its own flanks,\n * so left/right chrome works everywhere.\n */\n\ninterface LocalBox {\n pageIndex: number;\n lx: number;\n ly: number;\n w: number;\n h: number;\n rotation: PageRotation;\n /** The page's `/UserUnit` — folded into ITS `contentScale` at placement, so\n * \"world units per content point\" stays true for every page of a spread. */\n userUnit: number;\n}\n\n/** Intrinsic item dimensions (pages only, no margins) — the `uniform` reference.\n * Uses DISPLAY dims, so `uniform` equalizes the rotated footprint and a rotated\n * page sizes to match its neighbours as it actually appears. */\nfunction measureItem(\n pages: readonly PageGeom[],\n group: number[],\n gap: number,\n): { width: number; height: number } {\n let w = 0;\n let h = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n w += d.width + (j > 0 ? gap : 0);\n if (d.height > h) h = d.height;\n }\n return { width: w, height: h };\n}\n\n/**\n * Pack one item from SCALED pages plus constant per-page margins. The page boxes\n * (and the spread's inner gap) scale with the sizing factor; the margins do NOT —\n * they are screen-px-derived chrome bands and stay constant in world space.\n */\nfunction packScaledItem(\n pages: readonly PageGeom[],\n group: number[],\n scale: number,\n gap: number,\n frame: PageFrame,\n direction: Direction,\n): { local: LocalBox[]; width: number; height: number } {\n let maxH = 0;\n for (let j = 0; j < group.length; j++)\n maxH = Math.max(maxH, displayDims(pages[group[j]]).height * scale);\n const local: LocalBox[] = [];\n let lx = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n const w = d.width * scale;\n const h = d.height * scale;\n local.push({\n pageIndex: group[j],\n lx: lx + frame.left,\n ly: frame.top + (maxH - h) / 2, // page centers within the inner band\n w,\n h,\n rotation: pages[group[j]].rotation ?? 0,\n userUnit: pages[group[j]].userUnit ?? 1,\n });\n lx += frame.left + w + frame.right + gap * scale;\n }\n const width = Math.max(0, lx - gap * scale);\n const height = frame.top + maxH + frame.bottom;\n if (direction === 'rtl') {\n // reading-first page takes the RIGHTMOST slot (the spread binds on the right);\n // slots mirror, but each page keeps its PHYSICAL margins (left room stays left)\n for (const b of local) b.lx = width - b.lx - b.w + (frame.left - frame.right);\n }\n return { local, width, height };\n}\n\nfunction placePages(item: SceneItem, local: LocalBox[], contentScale: number): PageBox[] {\n return local.map((b) => ({\n pageIndex: b.pageIndex,\n x: item.x + b.lx,\n y: item.y + b.ly,\n width: b.w,\n height: b.h,\n rotation: b.rotation,\n // Per PAGE, not per item: the box was measured at `size × userUnit`, so\n // the world-per-content-point factor must carry the same userUnit — every\n // content→world mapping downstream divides/multiplies by exactly this.\n contentScale: contentScale * b.userUnit,\n }));\n}\n\n/** Per-item world scale = sizing factor (1 intrinsic, else cross-equalize for\n * uniform) × `viewUnitsPerPoint` (the platform's points→view-px factor). The\n * result becomes the page's `contentScale`, so world units are view px and 100%\n * is physically accurate. `viewUnitsPerPoint` defaults to 1 (neutral). */\nconst itemScale = (\n crossIntrinsic: number,\n refCross: number,\n sizing: SizingMode,\n viewUnitsPerPoint = 1,\n): number =>\n (sizing === 'uniform' && crossIntrinsic > 0 ? refCross / crossIntrinsic : 1) * viewUnitsPerPoint;\n\n// ── Layout strategies ─────────────────────────────────────────────────────────\nexport interface LinearOptions {\n gap?: number;\n axis?: 'x' | 'y';\n align?: 'center' | 'start';\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function linearLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: LinearOptions = {},\n): Scene {\n const gap = opts.gap ?? 16;\n const vertical = (opts.axis ?? 'y') === 'y';\n const align = opts.align ?? 'center';\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n // RTL mirrors the main axis only when it's horizontal; vertical scroll is\n // direction-agnostic (RTL books still scroll down) — only spreads swap.\n const mirrored = !vertical && direction === 'rtl';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure every item at intrinsic PAGE size (no margins) — the cross axis\n // (width when vertical) gives the reference for `uniform` sizing, so uniform\n // equalizes the pages themselves; constant margins then keep outer edges flush.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const crossOf = (p: { width: number; height: number }) => (vertical ? p.width : p.height);\n const refCross = measures.reduce((m, p) => Math.max(m, crossOf(p)), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins, lay along the axis.\n const items: SceneItem[] = new Array(grouping.length);\n const locals: LocalBox[][] = new Array(grouping.length);\n const scales: number[] = new Array(grouping.length);\n let main = 0;\n let crossMax = 0;\n let maxW = 0;\n let maxH = 0;\n\n for (let i = 0; i < grouping.length; i++) {\n const s = itemScale(crossOf(measures[i]), refCross, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n const { width, height } = packed;\n scales[i] = s;\n locals[i] = packed.local;\n const it: SceneItem = {\n index: i,\n x: 0,\n y: 0,\n width,\n height,\n pageIndexes: grouping[i],\n pages: [],\n };\n if (vertical) {\n it.y = main;\n main += height + gap;\n } else {\n it.x = main;\n main += width + gap;\n }\n crossMax = Math.max(crossMax, vertical ? width : height);\n maxW = Math.max(maxW, width);\n maxH = Math.max(maxH, height);\n items[i] = it;\n }\n\n const sceneMain = Math.max(0, main - gap);\n const size: Size = vertical\n ? { width: crossMax, height: sceneMain }\n : { width: sceneMain, height: crossMax };\n\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (mirrored) it.x = size.width - it.x - it.width; // reading order advances leftward\n if (vertical) it.x = align === 'center' ? (size.width - it.width) / 2 : 0;\n else it.y = align === 'center' ? (size.height - it.height) / 2 : 0;\n it.pages = placePages(it, locals[i], scales[i]);\n }\n\n // Spatial index in POSITION order (ascending coordinates for binary search).\n // In a mirrored layout position order is reverse index order — `posIdx` maps back.\n const posIdx = (k: number): number => (mirrored ? items.length - 1 - k : k);\n const starts = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y : it.x;\n });\n const ends = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y + it.height : it.x + it.width;\n });\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: items.length,\n axis: vertical ? 'y' : 'x',\n maxItemSize: { width: maxW, height: maxH },\n query(r) {\n const a0 = vertical ? r.y : r.x;\n const a1 = vertical ? r.y + r.height : r.x + r.width;\n const lo = Math.max(0, lowerBound(ends, a0) - OVERSCAN);\n const hi = Math.min(items.length, upperBound(starts, a1) + OVERSCAN);\n const out: SceneItem[] = [];\n for (let k = lo; k < hi; k++) out.push(items[posIdx(k)]);\n return out;\n },\n nearestItem(pt) {\n const k = clamp(upperBound(starts, vertical ? pt.y : pt.x) - 1, 0, items.length - 1);\n return items[posIdx(k)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, items.length - 1);\n },\n };\n}\n\nexport interface GridOptions {\n gap?: number;\n columns?: number;\n /**\n * WRAPPED mode: instead of declaring `columns`, give the available line width\n * (world units) and the grid derives how many cells fit — the responsive\n * thumbnail-sidebar behavior. Takes precedence over `columns`.\n */\n lineWidth?: number;\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function gridLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: GridOptions = {},\n): Scene {\n const gap = opts.gap ?? 48;\n const n = grouping.length;\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure at intrinsic PAGE size; the widest item is the `uniform` reference.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const refW = measures.reduce((m, p) => Math.max(m, p.width), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins (uniform → equal\n // page widths; equal margins keep the OUTER boxes equal too, so columns line up).\n const locals: LocalBox[][] = new Array(n);\n const sizes: Array<{ width: number; height: number }> = new Array(n);\n const scales: number[] = new Array(n);\n let cellW = 1;\n let cellH = 1;\n for (let i = 0; i < n; i++) {\n const s = itemScale(measures[i].width, refW, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n scales[i] = s;\n locals[i] = packed.local;\n sizes[i] = { width: packed.width, height: packed.height };\n cellW = Math.max(cellW, sizes[i].width);\n cellH = Math.max(cellH, sizes[i].height);\n }\n\n // Column count — declared, or DERIVED from the available line width (wrapped):\n // how many cells fit the line. Computed here because it needs cellW. Clamped to\n // the item count so a short document doesn't occupy (or mirror across) a wider\n // line than it fills.\n const wanted =\n opts.lineWidth !== undefined\n ? Math.floor((opts.lineWidth + gap) / (cellW + gap))\n : (opts.columns ?? Math.ceil(Math.sqrt(n)));\n const columns = Math.max(1, Math.min(wanted, Math.max(1, n)));\n // RTL fills each row right→left (like RTL text wrap); rows stay top→bottom.\n const colAt = (col: number): number => (direction === 'rtl' ? columns - 1 - col : col);\n\n // Rows are as tall as THEIR tallest item — a wrapped grid is text-wrap for pages,\n // and a wrapped line is as tall as its own tallest glyph, not the document's\n // tallest. (A single global cell height left short pages floating in huge voids\n // with mixed page sizes.) Columns keep a uniform width: that's what makes a\n // column grid a grid — and `sizing: 'uniform'` removes the horizontal voids.\n const stepX = cellW + gap;\n const rows = Math.max(1, Math.ceil(n / columns));\n const rowHeight: number[] = new Array(rows).fill(1);\n for (let i = 0; i < n; i++) {\n const row = Math.floor(i / columns);\n rowHeight[row] = Math.max(rowHeight[row], sizes[i].height);\n }\n const rowTop: number[] = new Array(rows);\n let yCursor = 0;\n for (let r = 0; r < rows; r++) {\n rowTop[r] = yCursor;\n yCursor += rowHeight[r] + gap;\n }\n const rowEnd = rowTop.map((top, r) => top + rowHeight[r]);\n\n const items: SceneItem[] = new Array(n);\n for (let i = 0; i < n; i++) {\n const col = colAt(i % columns);\n const row = Math.floor(i / columns);\n const it: SceneItem = {\n index: i,\n x: col * stepX + (cellW - sizes[i].width) / 2,\n y: rowTop[row] + (rowHeight[row] - sizes[i].height) / 2,\n width: sizes[i].width,\n height: sizes[i].height,\n pageIndexes: grouping[i],\n pages: [],\n };\n it.pages = placePages(it, locals[i], scales[i]);\n items[i] = it;\n }\n\n const size: Size = { width: columns * stepX - gap, height: Math.max(1, yCursor - gap) };\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: n,\n axis: 'grid',\n maxItemSize: { width: cellW, height: cellH },\n query(r) {\n const c0 = Math.max(0, Math.floor(r.x / stepX));\n const c1 = Math.min(columns - 1, Math.floor((r.x + r.width) / stepX));\n // rows by binary search over the prefix-summed row extents (O(log rows))\n const r0 = Math.max(0, lowerBound(rowEnd, r.y));\n const r1 = Math.min(rows - 1, upperBound(rowTop, r.y + r.height) - 1);\n const out: SceneItem[] = [];\n for (let row = r0; row <= r1; row++)\n for (let col = c0; col <= c1; col++) {\n // colAt is its own inverse: spatial column → reading-order column\n const idx = row * columns + colAt(col);\n if (idx < n) out.push(items[idx]);\n }\n return out;\n },\n nearestItem(pt) {\n const col = clamp(Math.floor(pt.x / stepX), 0, columns - 1);\n const row = clamp(upperBound(rowTop, pt.y) - 1, 0, rows - 1);\n return items[clamp(row * columns + colAt(col), 0, n - 1)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, n - 1);\n },\n };\n}\n\n/** Spread grouping — a pure input to layout (all `plugin-spread` needs to be). */\nexport function groupPages(pageCount: number, mode: SpreadMode = 'none'): number[][] {\n const out: number[][] = [];\n if (mode === 'none') {\n for (let i = 0; i < pageCount; i++) out.push([i]);\n return out;\n }\n let i = 0;\n if (mode === 'even' && pageCount > 0) out.push([i++]);\n for (; i < pageCount; i += 2) out.push(i + 1 < pageCount ? [i, i + 1] : [i]);\n return out;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @embedpdf/core-stage — the pure spatial model.\n *\n * Scene (a layout strategy turns pages into positioned items + a spatial index),\n * Camera ({x,y,zoom}: the world point at the viewport origin + scale), and Anchor\n * (a focal point relative to a page — what survives layout/zoom changes & reloads).\n *\n * Navigation is pure camera math. Bounds/home/margin are three separate functions.\n * No DOM, no framework; plain-data model plus derived spatial queries (the DATA\n * is serializable — `Scene` also carries query methods derived from it). This\n * is the v4 Rust core, verbatim.\n */\n\n// The pure coordinate primitives live in the dependency-free geometry base\n// (shared with the framework adapters); stage-core re-exports them so existing\n// `from '@embedpdf/core-stage'` imports are unaffected.\nimport {\n displaySize,\n NO_FRAME,\n type PageFrame,\n type PageRotation,\n type Point,\n type Size,\n} from '@embedpdf/core-geometry';\nexport type { Point, Size, PageRotation, PageFrame } from '@embedpdf/core-geometry';\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\nexport interface Camera {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface PageGeom {\n /** UN-rotated (intrinsic) page size — the content's own dimensions before rotation. */\n size: Size;\n /** Total display rotation. Layout swaps w↔h for 90/270 so the box is the\n * on-screen footprint; the renderer rotates the content into it. */\n rotation?: PageRotation;\n /**\n * PDF `/UserUnit` (§14.11.6): how many 1/72\" units one point of THIS page\n * spans — the page is physically `userUnit ×` larger than its point size\n * says. Folded into the layout (a userUnit-5 page lays out 5× bigger, like\n * Acrobat) AND into its `contentScale`, so \"world per content point\" stays\n * true per page. Default 1 (virtually every document).\n */\n userUnit?: number;\n}\nexport interface PageBox {\n pageIndex: number;\n x: number;\n y: number;\n /** DISPLAY width — swapped to the page's height for 90/270 rotations. */\n width: number;\n /** DISPLAY height — swapped to the page's width for 90/270 rotations. */\n height: number;\n /** The page's total display rotation; the renderer rotates the (normalized,\n * un-rotated) content bitmap by this to fill the display box. */\n rotation: PageRotation;\n /**\n * World size ÷ intrinsic size for this page (1 for `intrinsic` sizing). The shell\n * multiplies it by the camera zoom to get device-px-per-PDF-point, so render\n * resolution and point↔screen mapping stay correct under `uniform` sizing.\n * Isotropic, so it is unaffected by rotation.\n */\n contentScale: number;\n}\n\n/** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared\n * geometry primitive), scaled by its `/UserUnit` (the page's PHYSICAL size — a\n * userUnit-5 page measures 5× its point size). Everything the layout packs uses\n * these display dims; the content scale (isotropic) and the renderer's\n * transform recover the content. */\nfunction displayDims(pg: PageGeom): Size {\n const u = pg.userUnit ?? 1;\n const d = displaySize(pg.size, pg.rotation ?? 0);\n return u === 1 ? d : { width: d.width * u, height: d.height * u };\n}\nexport interface SceneItem {\n index: number;\n x: number;\n y: number;\n width: number;\n height: number;\n pageIndexes: number[];\n pages: PageBox[];\n}\nexport type Axis = 'x' | 'y' | 'grid';\nexport interface Scene {\n size: Size;\n items: SceneItem[];\n itemCount: number;\n axis: Axis;\n /**\n * The largest item width & height in the document. Fit-modes resolve against this\n * (not the current page) so the zoom is STABLE across the whole document and no\n * page ever overflows — matching a conventional PDF viewer's \"fit width\".\n */\n maxItemSize: Size;\n query(rect: Rect): SceneItem[];\n nearestItem(pt: Point): SceneItem;\n itemOfPage(pageIndex: number): number;\n}\nexport interface CameraConstraint {\n bounded: boolean;\n /**\n * Breathing room (screen px) around the content — the ONE spacing concept.\n * Fit-modes inset by it, placement leaves it as gutter, and the clamp lets the\n * camera reveal exactly this much beyond each content edge.\n */\n padding: number;\n /** Where content RESTS on an axis it FITS (locked — there is nowhere else to\n * be). Default center/center. x is logical against `direction`. */\n fitAlign?: Alignment;\n direction?: Direction;\n}\n/**\n * The zoom intent — an equation about the fit-box, each resolved by `resolveZoom`:\n * { mode } — viewport-relative: fit-width / fit-page / automatic / fit-all\n * { level } — document-relative: a fixed scale factor\n * { pageWidth } — absolute: the page unit renders N **screen px** wide. The only\n * intent stable across BOTH viewport and document differences —\n * e.g. thumbnails at 200px for any document. Targets the page\n * UNIT (a spread counts as one, exactly as in fit-page/fit-width);\n * combine with sizing 'uniform' to make every page exactly N px.\n * { pageHeight } — absolute, vertical twin (horizontal filmstrip thumbnails).\n */\nexport type ZoomSpec =\n | { mode: ZoomModeValue }\n | { level: number }\n | { pageWidth: number }\n | { pageHeight: number };\nexport interface Anchor {\n pageIndex: number;\n fx: number;\n fy: number;\n}\nexport type SpreadMode = 'none' | 'odd' | 'even';\n/**\n * Reading direction — a LAYOUT property, not a navigation one. Navigation steps by\n * index (reading order); direction only decides where reading-order puts pages in\n * space: horizontal items advance leftward, spreads bind on the right, grid rows\n * fill right→left. The camera, cursor, fit and clamp never learn about it.\n */\nexport type Direction = 'ltr' | 'rtl';\n/**\n * Page sizing policy (per-item world scale, set in the layout):\n * 'intrinsic' — true PDF sizes; relative proportions preserved.\n * 'uniform' — every item scaled to the same CROSS-axis size (vertical → equal\n * widths, horizontal → equal heights, grid → equal widths), so pages\n * sit flush with no left/right gaps. Camera/zoom are untouched.\n */\nexport type SizingMode = 'intrinsic' | 'uniform';\n\nexport const ZOOM_MIN = 0.05;\nexport const ZOOM_MAX = 64;\nexport const ZoomMode = {\n Automatic: 'automatic',\n FitPage: 'fit-page',\n FitWidth: 'fit-width',\n /** Fit the WHOLE scene (every page) in view — the construction overview. */\n FitAll: 'fit-all',\n} as const;\nexport type ZoomModeValue = (typeof ZoomMode)[keyof typeof ZoomMode];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\n\n// ── Projection ──────────────────────────────────────────────────────────────\nexport const toScreen = (c: Camera, w: Point): Point => ({\n x: (w.x - c.x) * c.zoom,\n y: (w.y - c.y) * c.zoom,\n});\nexport const toWorld = (c: Camera, s: Point): Point => ({\n x: c.x + s.x / c.zoom,\n y: c.y + s.y / c.zoom,\n});\nexport const cameraWorldRect = (c: Camera, vp: Size): Rect => ({\n x: c.x,\n y: c.y,\n width: vp.width / c.zoom,\n height: vp.height / c.zoom,\n});\n\n// ── Camera operations ────────────────────────────────────────────────────────\n/** Focal zoom: keep the world point under `screenPt` fixed (no bounce). */\nexport const zoomAround = (c: Camera, screenPt: Point, factor: number): Camera => {\n const zoom = clamp(c.zoom * factor, ZOOM_MIN, ZOOM_MAX);\n return {\n x: c.x + screenPt.x / c.zoom - screenPt.x / zoom,\n y: c.y + screenPt.y / c.zoom - screenPt.y / zoom,\n zoom,\n };\n};\nexport const panByScreen = (c: Camera, dxScreen: number, dyScreen: number): Camera => ({\n x: c.x - dxScreen / c.zoom,\n y: c.y - dyScreen / c.zoom,\n zoom: c.zoom,\n});\nexport const centerOnWorld = (worldPt: Point, vp: Size, zoom: number): Camera => ({\n zoom,\n x: worldPt.x - vp.width / 2 / zoom,\n y: worldPt.y - vp.height / 2 / zoom,\n});\n\n/**\n * One axis of camera travel against content bounds — THE scroll geometry.\n * `near`/`far` are the two flush camera positions (content start edge at the\n * padded viewport start / content end edge at the padded viewport end); the\n * camera travels in [near, far] while the padded content overflows the view.\n * When it FITS the interval inverts and `fits` is true — there is nowhere to\n * travel, and the clamp locks the camera to its fitAlign rest instead. Shared\n * by `clampCamera` and `scrollMetrics`, so the pan clamp and a scrollbar can\n * never disagree about where travel ends.\n */\nexport interface AxisTravel {\n near: number;\n far: number;\n fits: boolean;\n}\nexport const travelRange = (\n origin: number,\n content: number,\n view: number,\n zoom: number,\n padding: number,\n): AxisTravel => ({\n near: origin - padding / zoom,\n far: origin + content - (view - padding) / zoom,\n fits: content * zoom <= view - 2 * padding,\n});\n\n/**\n * Bounds — the one place travel limits live. Clamps the camera into an arbitrary\n * world `Rect` (the whole document in continuous flow, one item in paged flow),\n * with a `padding` gutter the camera may reveal beyond each content edge.\n *\n * Per axis the travel interval is `travelRange`'s [near, far]. If the padded\n * content OVERFLOWS the viewport, the camera travels freely in it. If it FITS,\n * the camera is LOCKED to the `fitAlign` rest point — start = near, end = far,\n * center = midpoint (x resolved logically against `direction`).\n */\nexport const clampCamera = (c: Camera, bounds: Rect, vp: Size, k: CameraConstraint): Camera => {\n if (!k.bounded) return c;\n const p = k.padding;\n const fit = k.fitAlign ?? { x: 'center', y: 'center' };\n const axis = (pos: number, origin: number, content: number, view: number, a: Align): number => {\n const t = travelRange(origin, content, view, c.zoom, p);\n if (t.fits) return a === 'start' ? t.near : a === 'end' ? t.far : (t.near + t.far) / 2; // fits: rest & lock\n return clamp(pos, t.near, t.far);\n };\n // logical x: under RTL the reading start is the RIGHT edge\n const ax =\n k.direction === 'rtl' && fit.x !== 'center' ? (fit.x === 'start' ? 'end' : 'start') : fit.x;\n return {\n zoom: c.zoom,\n x: axis(c.x, bounds.x, bounds.width, vp.width, ax),\n y: axis(c.y, bounds.y, bounds.height, vp.height, fit.y),\n };\n};\n\n/**\n * The camera as a NATIVE SCROLLER — the DOM scroll vocabulary, in screen px:\n * every field means exactly what it means on a DOM element. Per axis the scroll\n * range is the UNION of the padded content extent and the current camera window:\n * • bounded — the clamp already keeps the window inside the padded content\n * (or rests it when the axis fits), so the union IS the padded content and\n * the numbers degenerate to the classic scroller (scrollLeft ∈\n * [0, scrollWidth − clientWidth], aligned with `travelRange`'s [near, far]).\n * • unbounded — pan beyond the content and the union grows (the Figma\n * scrollbar): the thumb shrinks toward the edge but always remains a road\n * back over the content.\n * `scrollableX/Y` false ⇔ the window covers the whole range (native: no bar).\n * One formula, no mode branching — the clamp did the branching already.\n * Coordinates are PHYSICAL on both axes (RTL included), deliberately sidestepping\n * the DOM's negative-scrollLeft-under-RTL behavior.\n */\nexport interface ScrollMetrics {\n scrollLeft: number;\n scrollTop: number;\n scrollWidth: number;\n scrollHeight: number;\n clientWidth: number;\n clientHeight: number;\n scrollableX: boolean;\n scrollableY: boolean;\n}\nexport const scrollMetrics = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n): ScrollMetrics => {\n const axis = (pos: number, origin: number, content: number, view: number) => {\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const total = (hi - lo) * c.zoom;\n // half-px slack, like the fits-predicate: sub-pixel overflow must not flash a bar\n return { offset: (pos - lo) * c.zoom, total, scrollable: total > view + 0.5 };\n };\n const x = axis(c.x, bounds.x, bounds.width, vp.width);\n const y = axis(c.y, bounds.y, bounds.height, vp.height);\n return {\n scrollLeft: x.offset,\n scrollTop: y.offset,\n scrollWidth: x.total,\n scrollHeight: y.total,\n clientWidth: vp.width,\n clientHeight: vp.height,\n scrollableX: x.scrollable,\n scrollableY: y.scrollable,\n };\n};\n\n/**\n * The inverse write — `Element.scrollTo` semantics: absolute offsets into the\n * CURRENT range (the same union `scrollMetrics` reports), clamped into\n * [0, total − view]; an omitted axis does not move. Zoom is untouched: scrolling\n * is a pan in scroller clothing.\n */\nexport const cameraFromScroll = (\n c: Camera,\n bounds: Rect,\n vp: Size,\n padding: number,\n target: { left?: number; top?: number },\n): Camera => {\n const axis = (\n want: number | undefined,\n pos: number,\n origin: number,\n content: number,\n view: number,\n ): number => {\n if (want === undefined) return pos;\n const lo = Math.min(origin - padding / c.zoom, pos);\n const hi = Math.max(origin + content + padding / c.zoom, pos + view / c.zoom);\n const max = Math.max(0, (hi - lo) * c.zoom - view);\n return lo + clamp(want, 0, max) / c.zoom;\n };\n return {\n zoom: c.zoom,\n x: axis(target.left, c.x, bounds.x, bounds.width, vp.width),\n y: axis(target.top, c.y, bounds.y, bounds.height, vp.height),\n };\n};\n\n/**\n * Per-axis alignment value. Used by the rest constraint (fitAlign — where\n * content settles on an axis the camera cannot travel) and, extended with\n * fractions (see {@link AlignValue}), by the arrival/zoom/anchor policies.\n * On the x-axis the NAMED values are LOGICAL (CSS-style): 'start' = where\n * reading begins (left in LTR, RIGHT in RTL), 'end' = where it ends. On the\n * y-axis they are physical (start = top). So a 'start' default is\n * automatically correct in both directions — no 'auto' value needed.\n */\nexport type Align = 'start' | 'center' | 'end';\nexport interface Alignment {\n x: Align;\n y: Align;\n}\n/**\n * One axis of an alignment POLICY: a named stop, or a viewport fraction 0–1\n * that positions the subject's CENTER at that fraction of the viewport\n * ('center' ≡ 0.5; 0.35 = the browser find-bar line). Named stops are logical\n * on x; fractions are physical, like every screen coordinate.\n */\nexport type AlignValue = Align | number;\nexport interface AlignmentValue {\n x: AlignValue;\n y: AlignValue;\n}\n\n/**\n * THE placement algorithm — every arrival (goToPage, next/prev, reset) lands\n * through it. Pure alignment: put the subject at `align` in the viewport —\n * per axis 'start' (reading edge at the padded viewport edge), 'end' (far\n * edge), 'center', or a fraction — with x resolved logically against the\n * reading direction. The SAME rule at every zoom: whether the subject fits or\n * overflows never changes where it lands. Landing is policy, not a side\n * effect of magnification.\n *\n * Deliberately clamp-free: the caller clamps the result against the TRUE\n * travel bounds (the scene in continuous flow, the item slice in paged). On\n * an axis with no real freedom that clamp collapses the landing to the\n * `fitAlign` rest point — \"rests where it must\" comes out of the clamp's\n * geometry, never out of a branch here.\n */\nexport function placeCamera(\n subject: Rect,\n vp: Size,\n zoom: number,\n padding = 0,\n align: AlignmentValue = { x: 'start', y: 'start' },\n direction: Direction = 'ltr',\n): Camera {\n const axis = (a: AlignValue, pos: number, extent: number, view: number): number => {\n if (a === 'start') return pos - padding / zoom;\n if (a === 'end') return pos + extent - (view - padding) / zoom;\n const f = a === 'center' ? 0.5 : clamp(a, 0, 1);\n return pos + extent / 2 - (view * f) / zoom;\n };\n const ax =\n direction === 'rtl' && align.x === 'start'\n ? 'end'\n : direction === 'rtl' && align.x === 'end'\n ? 'start'\n : align.x;\n return {\n zoom,\n x: axis(ax, subject.x, subject.width, vp.width),\n y: axis(align.y, subject.y, subject.height, vp.height),\n };\n}\n\n/**\n * The MINIMAL camera move that makes `rect` fully visible (with a `padding` gutter)\n * — DOM scrollIntoView({ block: 'nearest' }) as camera math. Per axis, the cameras\n * that show the rect form an interval; the answer is the current camera CLAMPED\n * into it: already inside → unchanged (the no-op case, by construction, not by\n * condition). An oversized rect (interval inverted) aligns to its start.\n *\n * This is deliberately NOT placement: `placeCamera` answers \"I am navigating here\"\n * (canonical); `revealCamera` answers \"make sure this is seeable\" (minimal).\n */\nexport function revealCamera(cam: Camera, rect: Rect, vp: Size, padding = 0): Camera {\n const axis = (pos: number, start: number, size: number, view: number): number => {\n const lo = start + size - (view - padding) / cam.zoom; // far edge just inside\n const hi = start - padding / cam.zoom; // near edge just inside\n if (lo > hi) return hi; // rect larger than the viewport: align its start\n return clamp(pos, lo, hi);\n };\n return {\n zoom: cam.zoom,\n x: axis(cam.x, rect.x, rect.width, vp.width),\n y: axis(cam.y, rect.y, rect.height, vp.height),\n };\n}\n\n// ── Zoom intent — resolved against a fit-box chosen by the caller: the document's\n// max item (continuous), the current item (paged), or the whole scene (fit-all). ─\nexport function resolveZoom(spec: ZoomSpec, box: Size, vp: Size, padding = 0): number {\n if ('level' in spec) return clamp(spec.level, ZOOM_MIN, ZOOM_MAX);\n // absolute pixel targets: box dimension = N screen px (document-independent)\n if ('pageWidth' in spec) return clamp(spec.pageWidth / box.width, ZOOM_MIN, ZOOM_MAX);\n if ('pageHeight' in spec) return clamp(spec.pageHeight / box.height, ZOOM_MIN, ZOOM_MAX);\n const fitW = Math.max(1, vp.width - 2 * padding) / box.width;\n const fitH = Math.max(1, vp.height - 2 * padding) / box.height;\n switch (spec.mode) {\n case ZoomMode.FitWidth:\n return clamp(fitW, ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.FitPage:\n case ZoomMode.FitAll: // same fit math; the caller passes the whole-scene box\n return clamp(Math.min(fitW, fitH), ZOOM_MIN, ZOOM_MAX);\n case ZoomMode.Automatic:\n default:\n // fit width, but never upscale past 100% (Adobe's \"Automatic\"). Height-\n // independent: a portrait page fills the width and you scroll within it.\n return clamp(Math.min(fitW, 1), ZOOM_MIN, ZOOM_MAX);\n }\n}\n\n// ── Anchor — a PAGE-RELATIVE point, the durable \"what am I looking at\". The world\n// point under a screen position is meaningless across a re-layout (pages move);\n// the page-point survives. Point-generalized: WHICH viewport point the anchor\n// lives at is the caller's policy (anchorAlign for reframes, zoomAlign for\n// zoom-intent changes); the wrappers default to the classic center. ──────────\n/** The anchor at an arbitrary world point: its nearest page + the point, page-relative. */\nexport function anchorAtPoint(scene: Scene, worldPt: Point): Anchor {\n const item = scene.nearestItem(worldPt);\n let page = item.pages[0];\n let best = Infinity;\n for (const p of item.pages) {\n const d = Math.abs(p.x + p.width / 2 - worldPt.x);\n if (d < best) {\n best = d;\n page = p;\n }\n }\n return {\n pageIndex: page.pageIndex,\n fx: (worldPt.x - page.x) / page.width,\n fy: (worldPt.y - page.y) / page.height,\n };\n}\n\n/** The camera that puts the anchor's world point at a given SCREEN point. */\nexport function cameraForAnchorAtScreen(\n anchor: Anchor,\n scene: Scene,\n screenPt: Point,\n zoom: number,\n): Camera {\n const item = scene.items[scene.itemOfPage(anchor.pageIndex)];\n let page = item.pages[0];\n for (const p of item.pages) if (p.pageIndex === anchor.pageIndex) page = p;\n const world = { x: page.x + anchor.fx * page.width, y: page.y + anchor.fy * page.height };\n return { zoom, x: world.x - screenPt.x / zoom, y: world.y - screenPt.y / zoom };\n}\n\n/** The anchor under a viewport point — `at` is the policy point (anchorAlign /\n * zoomAlign resolved to px); it defaults to the classic viewport center. */\nexport function anchorFromCamera(cam: Camera, scene: Scene, vp: Size, at?: Point): Anchor {\n return anchorAtPoint(scene, toWorld(cam, at ?? { x: vp.width / 2, y: vp.height / 2 }));\n}\n/** The camera that restores an anchor to the same viewport point it was\n * captured at — capture and restore MUST agree on `at` for exact round-trips. */\nexport function cameraFromAnchor(\n anchor: Anchor,\n scene: Scene,\n vp: Size,\n zoom: number,\n at?: Point,\n): Camera {\n return cameraForAnchorAtScreen(anchor, scene, at ?? { x: vp.width / 2, y: vp.height / 2 }, zoom);\n}\n\n// ── Spatial-index helpers ──────────────────────────────────────────────────────\nfunction lowerBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] < t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nfunction upperBound(arr: number[], t: number): number {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (arr[mid] <= t) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\nconst OVERSCAN = 2;\n\n/**\n * Reserved chrome space around each PAGE — the `PageFrame` primitive (world\n * units in this pure layer; the shell derives them from screen px). The frame\n * belongs to PAGES, not items — in a spread every page keeps its own flanks,\n * so left/right chrome works everywhere.\n */\n\ninterface LocalBox {\n pageIndex: number;\n lx: number;\n ly: number;\n w: number;\n h: number;\n rotation: PageRotation;\n /** The page's `/UserUnit` — folded into ITS `contentScale` at placement, so\n * \"world units per content point\" stays true for every page of a spread. */\n userUnit: number;\n}\n\n/** Intrinsic item dimensions (pages only, no margins) — the `uniform` reference.\n * Uses DISPLAY dims, so `uniform` equalizes the rotated footprint and a rotated\n * page sizes to match its neighbours as it actually appears. */\nfunction measureItem(\n pages: readonly PageGeom[],\n group: number[],\n gap: number,\n): { width: number; height: number } {\n let w = 0;\n let h = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n w += d.width + (j > 0 ? gap : 0);\n if (d.height > h) h = d.height;\n }\n return { width: w, height: h };\n}\n\n/**\n * Pack one item from SCALED pages plus constant per-page margins. The page boxes\n * (and the spread's inner gap) scale with the sizing factor; the margins do NOT —\n * they are screen-px-derived chrome bands and stay constant in world space.\n */\nfunction packScaledItem(\n pages: readonly PageGeom[],\n group: number[],\n scale: number,\n gap: number,\n frame: PageFrame,\n direction: Direction,\n): { local: LocalBox[]; width: number; height: number } {\n let maxH = 0;\n for (let j = 0; j < group.length; j++)\n maxH = Math.max(maxH, displayDims(pages[group[j]]).height * scale);\n const local: LocalBox[] = [];\n let lx = 0;\n for (let j = 0; j < group.length; j++) {\n const d = displayDims(pages[group[j]]);\n const w = d.width * scale;\n const h = d.height * scale;\n local.push({\n pageIndex: group[j],\n lx: lx + frame.left,\n ly: frame.top + (maxH - h) / 2, // page centers within the inner band\n w,\n h,\n rotation: pages[group[j]].rotation ?? 0,\n userUnit: pages[group[j]].userUnit ?? 1,\n });\n lx += frame.left + w + frame.right + gap * scale;\n }\n const width = Math.max(0, lx - gap * scale);\n const height = frame.top + maxH + frame.bottom;\n if (direction === 'rtl') {\n // reading-first page takes the RIGHTMOST slot (the spread binds on the right);\n // slots mirror, but each page keeps its PHYSICAL margins (left room stays left)\n for (const b of local) b.lx = width - b.lx - b.w + (frame.left - frame.right);\n }\n return { local, width, height };\n}\n\nfunction placePages(item: SceneItem, local: LocalBox[], contentScale: number): PageBox[] {\n return local.map((b) => ({\n pageIndex: b.pageIndex,\n x: item.x + b.lx,\n y: item.y + b.ly,\n width: b.w,\n height: b.h,\n rotation: b.rotation,\n // Per PAGE, not per item: the box was measured at `size × userUnit`, so\n // the world-per-content-point factor must carry the same userUnit — every\n // content→world mapping downstream divides/multiplies by exactly this.\n contentScale: contentScale * b.userUnit,\n }));\n}\n\n/** Per-item world scale = sizing factor (1 intrinsic, else cross-equalize for\n * uniform) × `viewUnitsPerPoint` (the platform's points→view-px factor). The\n * result becomes the page's `contentScale`, so world units are view px and 100%\n * is physically accurate. `viewUnitsPerPoint` defaults to 1 (neutral). */\nconst itemScale = (\n crossIntrinsic: number,\n refCross: number,\n sizing: SizingMode,\n viewUnitsPerPoint = 1,\n): number =>\n (sizing === 'uniform' && crossIntrinsic > 0 ? refCross / crossIntrinsic : 1) * viewUnitsPerPoint;\n\n// ── Layout strategies ─────────────────────────────────────────────────────────\nexport interface LinearOptions {\n gap?: number;\n axis?: 'x' | 'y';\n align?: 'center' | 'start';\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function linearLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: LinearOptions = {},\n): Scene {\n const gap = opts.gap ?? 16;\n const vertical = (opts.axis ?? 'y') === 'y';\n const align = opts.align ?? 'center';\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n // RTL mirrors the main axis only when it's horizontal; vertical scroll is\n // direction-agnostic (RTL books still scroll down) — only spreads swap.\n const mirrored = !vertical && direction === 'rtl';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure every item at intrinsic PAGE size (no margins) — the cross axis\n // (width when vertical) gives the reference for `uniform` sizing, so uniform\n // equalizes the pages themselves; constant margins then keep outer edges flush.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const crossOf = (p: { width: number; height: number }) => (vertical ? p.width : p.height);\n const refCross = measures.reduce((m, p) => Math.max(m, crossOf(p)), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins, lay along the axis.\n const items: SceneItem[] = new Array(grouping.length);\n const locals: LocalBox[][] = new Array(grouping.length);\n const scales: number[] = new Array(grouping.length);\n let main = 0;\n let crossMax = 0;\n let maxW = 0;\n let maxH = 0;\n\n for (let i = 0; i < grouping.length; i++) {\n const s = itemScale(crossOf(measures[i]), refCross, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n const { width, height } = packed;\n scales[i] = s;\n locals[i] = packed.local;\n const it: SceneItem = {\n index: i,\n x: 0,\n y: 0,\n width,\n height,\n pageIndexes: grouping[i],\n pages: [],\n };\n if (vertical) {\n it.y = main;\n main += height + gap;\n } else {\n it.x = main;\n main += width + gap;\n }\n crossMax = Math.max(crossMax, vertical ? width : height);\n maxW = Math.max(maxW, width);\n maxH = Math.max(maxH, height);\n items[i] = it;\n }\n\n const sceneMain = Math.max(0, main - gap);\n const size: Size = vertical\n ? { width: crossMax, height: sceneMain }\n : { width: sceneMain, height: crossMax };\n\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (mirrored) it.x = size.width - it.x - it.width; // reading order advances leftward\n if (vertical) it.x = align === 'center' ? (size.width - it.width) / 2 : 0;\n else it.y = align === 'center' ? (size.height - it.height) / 2 : 0;\n it.pages = placePages(it, locals[i], scales[i]);\n }\n\n // Spatial index in POSITION order (ascending coordinates for binary search).\n // In a mirrored layout position order is reverse index order — `posIdx` maps back.\n const posIdx = (k: number): number => (mirrored ? items.length - 1 - k : k);\n const starts = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y : it.x;\n });\n const ends = items.map((_, k) => {\n const it = items[posIdx(k)];\n return vertical ? it.y + it.height : it.x + it.width;\n });\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: items.length,\n axis: vertical ? 'y' : 'x',\n maxItemSize: { width: maxW, height: maxH },\n query(r) {\n const a0 = vertical ? r.y : r.x;\n const a1 = vertical ? r.y + r.height : r.x + r.width;\n const lo = Math.max(0, lowerBound(ends, a0) - OVERSCAN);\n const hi = Math.min(items.length, upperBound(starts, a1) + OVERSCAN);\n const out: SceneItem[] = [];\n for (let k = lo; k < hi; k++) out.push(items[posIdx(k)]);\n return out;\n },\n nearestItem(pt) {\n const k = clamp(upperBound(starts, vertical ? pt.y : pt.x) - 1, 0, items.length - 1);\n return items[posIdx(k)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, items.length - 1);\n },\n };\n}\n\nexport interface GridOptions {\n gap?: number;\n columns?: number;\n /**\n * WRAPPED mode: instead of declaring `columns`, give the available line width\n * (world units) and the grid derives how many cells fit — the responsive\n * thumbnail-sidebar behavior. Takes precedence over `columns`.\n */\n lineWidth?: number;\n sizing?: SizingMode;\n direction?: Direction;\n /** Reserved chrome space around each PAGE (world units; constant, never scaled). */\n pageFrame?: PageFrame;\n /** Points→view-px factor folded into each page's scale (default 1). */\n viewUnitsPerPoint?: number;\n}\nexport function gridLayout(\n pages: readonly PageGeom[],\n grouping: number[][],\n opts: GridOptions = {},\n): Scene {\n const gap = opts.gap ?? 48;\n const n = grouping.length;\n const sizing = opts.sizing ?? 'intrinsic';\n const direction = opts.direction ?? 'ltr';\n\n const frame = opts.pageFrame ?? NO_FRAME;\n\n // Pass 1: measure at intrinsic PAGE size; the widest item is the `uniform` reference.\n const measures = grouping.map((group) => measureItem(pages, group, gap));\n const refW = measures.reduce((m, p) => Math.max(m, p.width), 0);\n\n // Pass 2: pack each item from scaled pages + constant margins (uniform → equal\n // page widths; equal margins keep the OUTER boxes equal too, so columns line up).\n const locals: LocalBox[][] = new Array(n);\n const sizes: Array<{ width: number; height: number }> = new Array(n);\n const scales: number[] = new Array(n);\n let cellW = 1;\n let cellH = 1;\n for (let i = 0; i < n; i++) {\n const s = itemScale(measures[i].width, refW, sizing, opts.viewUnitsPerPoint);\n const packed = packScaledItem(pages, grouping[i], s, gap, frame, direction);\n scales[i] = s;\n locals[i] = packed.local;\n sizes[i] = { width: packed.width, height: packed.height };\n cellW = Math.max(cellW, sizes[i].width);\n cellH = Math.max(cellH, sizes[i].height);\n }\n\n // Column count — declared, or DERIVED from the available line width (wrapped):\n // how many cells fit the line. Computed here because it needs cellW. Clamped to\n // the item count so a short document doesn't occupy (or mirror across) a wider\n // line than it fills.\n const wanted =\n opts.lineWidth !== undefined\n ? Math.floor((opts.lineWidth + gap) / (cellW + gap))\n : (opts.columns ?? Math.ceil(Math.sqrt(n)));\n const columns = Math.max(1, Math.min(wanted, Math.max(1, n)));\n // RTL fills each row right→left (like RTL text wrap); rows stay top→bottom.\n const colAt = (col: number): number => (direction === 'rtl' ? columns - 1 - col : col);\n\n // Rows are as tall as THEIR tallest item — a wrapped grid is text-wrap for pages,\n // and a wrapped line is as tall as its own tallest glyph, not the document's\n // tallest. (A single global cell height left short pages floating in huge voids\n // with mixed page sizes.) Columns keep a uniform width: that's what makes a\n // column grid a grid — and `sizing: 'uniform'` removes the horizontal voids.\n const stepX = cellW + gap;\n const rows = Math.max(1, Math.ceil(n / columns));\n const rowHeight: number[] = new Array(rows).fill(1);\n for (let i = 0; i < n; i++) {\n const row = Math.floor(i / columns);\n rowHeight[row] = Math.max(rowHeight[row], sizes[i].height);\n }\n const rowTop: number[] = new Array(rows);\n let yCursor = 0;\n for (let r = 0; r < rows; r++) {\n rowTop[r] = yCursor;\n yCursor += rowHeight[r] + gap;\n }\n const rowEnd = rowTop.map((top, r) => top + rowHeight[r]);\n\n const items: SceneItem[] = new Array(n);\n for (let i = 0; i < n; i++) {\n const col = colAt(i % columns);\n const row = Math.floor(i / columns);\n const it: SceneItem = {\n index: i,\n x: col * stepX + (cellW - sizes[i].width) / 2,\n y: rowTop[row] + (rowHeight[row] - sizes[i].height) / 2,\n width: sizes[i].width,\n height: sizes[i].height,\n pageIndexes: grouping[i],\n pages: [],\n };\n it.pages = placePages(it, locals[i], scales[i]);\n items[i] = it;\n }\n\n const size: Size = { width: columns * stepX - gap, height: Math.max(1, yCursor - gap) };\n const firstPage = items.map((it) => it.pageIndexes[0]);\n\n return {\n size,\n items,\n itemCount: n,\n axis: 'grid',\n maxItemSize: { width: cellW, height: cellH },\n query(r) {\n const c0 = Math.max(0, Math.floor(r.x / stepX));\n const c1 = Math.min(columns - 1, Math.floor((r.x + r.width) / stepX));\n // rows by binary search over the prefix-summed row extents (O(log rows))\n const r0 = Math.max(0, lowerBound(rowEnd, r.y));\n const r1 = Math.min(rows - 1, upperBound(rowTop, r.y + r.height) - 1);\n const out: SceneItem[] = [];\n for (let row = r0; row <= r1; row++)\n for (let col = c0; col <= c1; col++) {\n // colAt is its own inverse: spatial column → reading-order column\n const idx = row * columns + colAt(col);\n if (idx < n) out.push(items[idx]);\n }\n return out;\n },\n nearestItem(pt) {\n const col = clamp(Math.floor(pt.x / stepX), 0, columns - 1);\n const row = clamp(upperBound(rowTop, pt.y) - 1, 0, rows - 1);\n return items[clamp(row * columns + colAt(col), 0, n - 1)];\n },\n itemOfPage(pi) {\n return clamp(upperBound(firstPage, pi) - 1, 0, n - 1);\n },\n };\n}\n\n/** Spread grouping — a pure input to layout (all `plugin-spread` needs to be). */\nexport function groupPages(pageCount: number, mode: SpreadMode = 'none'): number[][] {\n const out: number[][] = [];\n if (mode === 'none') {\n for (let i = 0; i < pageCount; i++) out.push([i]);\n return out;\n }\n let i = 0;\n if (mode === 'even' && pageCount > 0) out.push([i++]);\n for (; i < pageCount; i += 2) out.push(i + 1 < pageCount ? [i, i + 1] : [i]);\n return out;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embedpdf/core-stage",
3
- "version": "3.0.0-next.6",
3
+ "version": "3.0.0-next.7",
4
4
  "private": false,
5
5
  "description": "Pure spatial model for the EmbedPDF viewer: Scene (layout strategies), Camera, Anchor, and framing. DOM-free, framework-free, and serializable.",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@embedpdf/core-geometry": "3.0.0-next.6"
21
+ "@embedpdf/core-geometry": "3.0.0-next.7"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^5.0.0",