@embedpdf/plugin-selection 3.0.0-next.1 → 3.0.0-next.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["InteractionToken"],"sources":["../src/geometry.ts","../src/capability.ts","../src/handler.ts","../src/reducer.ts","../src/types.ts","../src/selection.plugin.ts"],"sourcesContent":["/**\n * The pure selection geometry: engine text geometry (PDF user space, y-up) →\n * viewer content space (y-down), glyph hit-testing, word/line expansion, and the\n * line-merge. No DOM, no engine, no React.\n *\n * The non-trivial bits are ports of battle-tested algorithms, adapted to v3's\n * content-space `Rect {x,y,width,height}`:\n * - `glyphAt` — PDFium `GetIndexAtPos` (exact tight box, then a tolerance pass).\n * - `rectsForRange` — Chromium `pdfium_range.cc` `MergeAdjacentRects` (one rect per line).\n * - `expandToWord/Line` — Chromium `OnMultipleClick` (double = word, triple = line).\n * The PDF↔content y-flip is the geometry package's `pageGeometry` (the one place\n * that math lives), never hand-rolled here.\n */\nimport {\n applyRect,\n boundsOfRects,\n pageGeometry,\n type Point,\n type Rect,\n type RectIn,\n} from '@embedpdf/core-geometry';\nimport type { PageGeometrySnapshot, PdfRect } from '@embedpdf/engine-core/runtime';\n\nconst FLAG_SPACE = 1;\nconst FLAG_EMPTY = 2;\nconst isBoundary = (flags: number): boolean => (flags & (FLAG_SPACE | FLAG_EMPTY)) !== 0;\nconst isEmpty = (flags: number): boolean => (flags & FLAG_EMPTY) !== 0;\n\n/** One glyph in content space. `loose` builds selection rects; `tight` hit-tests. */\nexport interface GlyphInfo {\n loose: Rect;\n tight?: Rect;\n flags: number;\n}\n\n/** A contiguous run of glyphs (a text object). `rect` is its loose enclosing box. */\nexport interface RunInfo {\n start: number; // index into PageText.glyphs (the selection coordinate)\n count: number;\n rect: Rect;\n fontSize?: number;\n}\n\n/** A page's text laid out in content space: a flat glyph list + the run structure. */\nexport interface PageText {\n glyphs: GlyphInfo[];\n runs: RunInfo[];\n}\n\nconst toContent = (m: Parameters<typeof applyRect>[0], b: PdfRect): Rect =>\n applyRect(\n m as never,\n {\n x: b.left,\n y: b.bottom,\n width: b.right - b.left,\n height: b.top - b.bottom,\n } as RectIn<'pdf'>,\n ) as Rect;\n\n/**\n * Flatten a page's text geometry into content-space glyphs + runs. Rotation does\n * NOT enter here — the overlay rides the page's CSS rotation, so glyphs stay in\n * un-rotated content space (the crop-relative y-flip is the only conversion).\n */\nexport function buildPageText(\n snapshot: PageGeometrySnapshot,\n crop: PdfRect,\n rotation: 0 | 90 | 180 | 270,\n userUnit: number,\n): PageText {\n // zoom = 1: pdfToContent is scale-free (just the y-flip); the viewer's zoom is\n // applied later by PageTransform.pageToContent, not baked in.\n const { pdfToContent } = pageGeometry({ crop, rotation, userUnit }, 1);\n const glyphs: GlyphInfo[] = [];\n const runs: RunInfo[] = [];\n for (const run of snapshot.runs) {\n const start = glyphs.length;\n for (const g of run.glyphs) {\n glyphs.push({\n loose: toContent(pdfToContent, g.looseBox),\n tight: g.tightBox ? toContent(pdfToContent, g.tightBox) : undefined,\n flags: g.flags,\n });\n }\n runs.push({\n start,\n count: run.glyphs.length,\n rect: toContent(pdfToContent, run.rect),\n fontSize: run.fontSize,\n });\n }\n return { glyphs, runs };\n}\n\nfunction avgGlyphHeight(text: PageText): number {\n let total = 0;\n let count = 0;\n for (const g of text.glyphs) {\n if (isEmpty(g.flags)) continue;\n total += g.loose.height;\n count++;\n }\n return count === 0 ? 0 : total / count;\n}\n\n/**\n * The glyph index at a content-space point, or null when nothing is near (so the\n * caller can show the pointer cursor off-text). PDFium `GetIndexAtPos`: exact\n * tight-box containment first, then a tolerance pass (closest by Manhattan\n * distance within `toleranceFactor × average glyph height`).\n */\nexport function glyphAt(text: PageText, p: Point, toleranceFactor = 1.5): number | null {\n for (const run of text.runs) {\n const r = run.rect;\n if (p.x < r.x || p.x > r.x + r.width || p.y < r.y || p.y > r.y + r.height) continue;\n for (let i = 0; i < run.count; i++) {\n const b = text.glyphs[run.start + i].tight ?? text.glyphs[run.start + i].loose;\n if (p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height) {\n return run.start + i;\n }\n }\n }\n if (toleranceFactor <= 0) return null;\n\n const half = (avgGlyphHeight(text) * toleranceFactor) / 2;\n let best = -1;\n let bestDist = Infinity;\n for (const run of text.runs) {\n const r = run.rect;\n if (\n p.y < r.y - half ||\n p.y > r.y + r.height + half ||\n p.x < r.x - half ||\n p.x > r.x + r.width + half\n ) {\n continue;\n }\n for (let i = 0; i < run.count; i++) {\n const g = text.glyphs[run.start + i];\n if (isEmpty(g.flags)) continue;\n const b = g.tight ?? g.loose;\n if (\n p.x < b.x - half ||\n p.x > b.x + b.width + half ||\n p.y < b.y - half ||\n p.y > b.y + b.height + half\n ) {\n continue;\n }\n const dx = Math.min(Math.abs(p.x - b.x), Math.abs(p.x - (b.x + b.width)));\n const dy = Math.min(Math.abs(p.y - b.y), Math.abs(p.y - (b.y + b.height)));\n if (dx + dy < bestDist) {\n bestDist = dx + dy;\n best = run.start + i;\n }\n }\n }\n return best >= 0 ? best : null;\n}\n\n/** Double-click: the word around `i` (walk to space/empty glyphs both ways). */\nexport function expandToWord(text: PageText, i: number): [number, number] {\n const n = text.glyphs.length;\n if (i < 0 || i >= n) return [i, i];\n let from = i;\n while (from > 0 && !isBoundary(text.glyphs[from - 1].flags)) from--;\n let to = i;\n while (to < n - 1 && !isBoundary(text.glyphs[to + 1].flags)) to++;\n return [from, to];\n}\n\n/** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */\nexport function expandToLine(text: PageText, i: number): [number, number] {\n const ri = text.runs.findIndex((r) => i >= r.start && i < r.start + r.count);\n if (ri < 0) return [i, i];\n const anchor = text.runs[ri];\n const top = anchor.rect.y;\n const bottom = anchor.rect.y + anchor.rect.height;\n let from = anchor.start;\n let to = anchor.start + anchor.count - 1;\n for (let r = ri - 1; r >= 0; r--) {\n const run = text.runs[r];\n if (isZero(run.rect)) continue;\n if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;\n from = run.start;\n }\n for (let r = ri + 1; r < text.runs.length; r++) {\n const run = text.runs[r];\n if (isZero(run.rect)) continue;\n if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;\n to = run.start + run.count - 1;\n }\n return [from, to];\n}\n\n/* ── line-merge ────────────────────────────────────────────────────────────\n * Adapted from Chromium's pdf/pdfium/pdfium_range.cc (BSD-licensed,\n * Copyright 2010 The Chromium Authors). Glyphs → sub-runs (split on big intra-run\n * gaps) → merged line rects. */\n\nconst CHAR_DISTANCE_FACTOR = 2.5;\nconst FONT_SIZE_RATIO_THRESHOLD = 1.5;\nconst VERTICAL_OVERLAP_THRESHOLD = 0.8;\nconst LINE_OVERLAP_THRESHOLD = 0.5;\n\ninterface SubRun {\n rect: Rect;\n charCount: number;\n fontSize?: number;\n}\n\nexport function rectsForRange(text: PageText, from: number, to: number): Rect[] {\n const lo = Math.max(0, Math.min(from, to));\n const hi = Math.min(text.glyphs.length - 1, Math.max(from, to));\n const subRuns: SubRun[] = [];\n\n for (const run of text.runs) {\n const runEnd = run.start + run.count - 1;\n if (runEnd < lo || run.start > hi) continue;\n const s = Math.max(lo, run.start);\n const e = Math.min(hi, runEnd);\n\n let minX = Infinity;\n let maxX = -Infinity;\n let minY = Infinity;\n let maxY = -Infinity;\n let charCount = 0;\n let widthSum = 0;\n let prevRight = -Infinity;\n const flush = () => {\n if (minX !== Infinity && charCount > 0) {\n subRuns.push({\n rect: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },\n charCount,\n fontSize: run.fontSize,\n });\n }\n minX = Infinity;\n maxX = -Infinity;\n minY = Infinity;\n maxY = -Infinity;\n charCount = 0;\n widthSum = 0;\n prevRight = -Infinity;\n };\n\n for (let gi = s; gi <= e; gi++) {\n const g = text.glyphs[gi];\n if (isEmpty(g.flags)) continue;\n const b = g.loose;\n if (charCount > 0 && prevRight > -Infinity) {\n const avg = widthSum / charCount;\n if (avg > 0 && Math.abs(b.x - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();\n }\n minX = Math.min(minX, b.x);\n maxX = Math.max(maxX, b.x + b.width);\n minY = Math.min(minY, b.y);\n maxY = Math.max(maxY, b.y + b.height);\n charCount++;\n widthSum += b.width;\n prevRight = b.x + b.width;\n }\n flush();\n }\n\n return mergeAdjacentRects(subRuns);\n}\n\nfunction mergeAdjacentRects(runs: SubRun[]): Rect[] {\n const out: Rect[] = [];\n let prev: SubRun | null = null;\n let cur: Rect | null = null;\n for (const run of runs) {\n if (prev && cur && shouldMerge(prev, run)) {\n cur = boundsOfRects([cur, run.rect])!;\n } else {\n if (cur) out.push(cur);\n cur = run.rect;\n }\n prev = run;\n }\n if (cur && cur.width > 0 && cur.height > 0) out.push(cur);\n return out;\n}\n\nfunction shouldMerge(a: SubRun, b: SubRun): boolean {\n if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {\n const ratio = Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize);\n if (ratio > FONT_SIZE_RATIO_THRESHOLD) return false;\n }\n if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;\n const aw = a.rect.width / a.charCount;\n const bw = b.rect.width / b.charCount;\n const aL = a.rect.x - aw;\n const aR = a.rect.x + a.rect.width + aw;\n const bL = b.rect.x - bw;\n const bR = b.rect.x + b.rect.width + bw;\n return aL < bR && aR > bL;\n}\n\nfunction verticalOverlap(a: Rect, b: Rect): number {\n if (a.height <= 0 || b.height <= 0) return 0;\n const u = Math.max(a.y + a.height, b.y + b.height) - Math.min(a.y, b.y);\n if (u === a.height || u === b.height) return 1;\n const i = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));\n return i / u;\n}\n\nconst isZero = (r: Rect): boolean => r.width === 0 && r.height === 0;\n\nfunction overlapV(top1: number, bottom1: number, top2: number, bottom2: number): boolean {\n const u = Math.max(bottom1, bottom2) - Math.min(top1, top2);\n if (u === 0) return false;\n const i = Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2));\n return i / u >= LINE_OVERLAP_THRESHOLD;\n}\n","import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport type { Point, Rect } from '@embedpdf/core-geometry';\nimport {\n buildPageText,\n expandToLine,\n expandToWord,\n glyphAt,\n rectsForRange,\n type PageText,\n} from './geometry';\nimport type {\n GlyphPointer,\n SelectionAction,\n SelectionCapability,\n SelectionEndpoint,\n SelectionRange,\n SelectionSnapshot,\n SelectionState,\n} from './types';\n\nconst EMPTY: Rect[] = [];\n\n/**\n * The selection capability. The reducer state holds the selection range, the\n * derived content-space rects (per page), and per-page loaded flags; the (large,\n * non-serializable) per-page text geometry is cached HERE in the closure.\n *\n * Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph), so\n * a drag from page 2 into page 4 selects the tail of 2, all of 3, and the head of\n * 4. `recompute` rebuilds the merged line rects for every loaded page in the span\n * and re-runs whenever a mid-span page finishes loading.\n */\nexport function createSelectionCapability(\n ctx: PluginContext<SelectionState, SelectionAction>,\n): SelectionCapability {\n const cache = new Map<number, PageText>();\n const pending = new Set<number>();\n // Consumers (e.g. text-markup) observe the selection without selection knowing\n // about them: `change` fires whenever the rects change, `commit` when a gesture\n // ends (pointer-up). One typed callback each — not an event bus.\n const changeCbs = new Set<() => void>();\n const commitCbs = new Set<() => void>();\n const fireChange = (): void => changeCbs.forEach((cb) => cb());\n\n const pageIndexOf = (pon: PageObjectNumber): number =>\n ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;\n const ponAtIndex = (i: number): PageObjectNumber | undefined =>\n ctx.document()?.pages[i]?.pageObjectNumber;\n\n // Order the two ends of a selection by document position (page, then glyph).\n function orderedEnds(sel: SelectionRange): {\n start: GlyphPointer;\n end: GlyphPointer;\n direction: 'forward' | 'backward';\n } {\n const ai = pageIndexOf(sel.anchor.pon);\n const fi = pageIndexOf(sel.focus.pon);\n const anchorFirst = ai < fi || (ai === fi && sel.anchor.glyph <= sel.focus.glyph);\n return anchorFirst\n ? { start: sel.anchor, end: sel.focus, direction: 'forward' }\n : { start: sel.focus, end: sel.anchor, direction: 'backward' };\n }\n\n function endpointFor(ptr: GlyphPointer, which: 'start' | 'end'): SelectionEndpoint | null {\n const rects = ctx.getState().rects[ptr.pon] ?? EMPTY;\n if (!rects.length) return null;\n return { pon: ptr.pon, rect: which === 'start' ? rects[0] : rects[rects.length - 1] };\n }\n\n function snapshot(): SelectionSnapshot {\n const { selection, rects } = ctx.getState();\n const pages = Object.keys(rects)\n .map(Number)\n .filter((pon) => (rects[pon]?.length ?? 0) > 0)\n .map((pon) => ({ pon: pon as PageObjectNumber, rects: rects[pon] }));\n if (!selection) return { pages, start: null, end: null, direction: 'forward' };\n const { start, end, direction } = orderedEnds(selection);\n return {\n pages,\n start: endpointFor(start, 'start'),\n end: endpointFor(end, 'end'),\n direction,\n };\n }\n\n function ensurePage(pon: PageObjectNumber): void {\n if (cache.has(pon) || pending.has(pon)) return;\n const doc = ctx.doc;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!doc || !layout) return;\n pending.add(pon);\n doc\n .page(pon)\n .geometry.read()\n .then(\n (snapshot) => {\n pending.delete(pon);\n cache.set(\n pon,\n buildPageText(snapshot, layout.boxes.crop, layout.rotation, layout.userUnit),\n );\n ctx.dispatch({ type: 'PAGE_LOADED', pon });\n if (ctx.getState().selection) recompute(); // a mid-span page arrived → fill its rects\n },\n () => {\n pending.delete(pon); // doc closed / read aborted — ignore\n },\n );\n }\n\n // Rebuild merged line rects for every loaded page in the span; ensure the rest.\n function recompute(sel: SelectionRange | null = ctx.getState().selection): void {\n if (!sel) return;\n const { start, end } = orderedEnds(sel);\n const si = pageIndexOf(start.pon);\n const ei = pageIndexOf(end.pon);\n if (si < 0 || ei < 0) return;\n const rects: Record<number, Rect[]> = {};\n for (let i = si; i <= ei; i++) {\n const pon = ponAtIndex(i);\n if (pon == null) continue;\n const text = cache.get(pon);\n if (!text) {\n ensurePage(pon); // not loaded yet — it'll recompute when ready\n continue;\n }\n const from = i === si ? start.glyph : 0;\n const to = i === ei ? end.glyph : text.glyphs.length - 1;\n rects[pon] = rectsForRange(text, from, to);\n }\n ctx.dispatch({ type: 'SET', selection: sel, rects });\n fireChange();\n }\n\n // Set the selection to a flat [from,to] glyph span on one page (word/line).\n function selectSpan(pon: PageObjectNumber, point: Point, expand: 'word' | 'line'): void {\n const text = cache.get(pon);\n if (!text) return;\n const i = glyphAt(text, point);\n if (i == null) return;\n const [from, to] = expand === 'word' ? expandToWord(text, i) : expandToLine(text, i);\n recompute({ anchor: { pon, glyph: from }, focus: { pon, glyph: to } });\n }\n\n return {\n ensurePage,\n\n isLoaded: (pon) => !!ctx.getState().loaded[pon],\n\n isOverText: (pon, point: Point) => {\n const text = cache.get(pon);\n return text ? glyphAt(text, point) != null : false;\n },\n\n beginAt: (pon, point: Point) => {\n const text = cache.get(pon);\n if (!text) return false;\n const i = glyphAt(text, point);\n if (i == null) return false; // not near text — caller deselects, doesn't capture\n recompute({ anchor: { pon, glyph: i }, focus: { pon, glyph: i } });\n return true;\n },\n\n selectWord: (pon, point: Point) => selectSpan(pon, point, 'word'),\n selectLine: (pon, point: Point) => selectSpan(pon, point, 'line'),\n\n extendTo: (pon, point: Point) => {\n const cur = ctx.getState().selection;\n if (!cur) return;\n const text = cache.get(pon);\n if (!text) {\n ensurePage(pon); // dragged onto a not-yet-loaded page — warm it, recompute on load\n return;\n }\n const i = glyphAt(text, point);\n if (i == null) return; // off-text — keep the last focus\n recompute({ anchor: cur.anchor, focus: { pon, glyph: i } });\n },\n\n end: () => commitCbs.forEach((cb) => cb()), // the gesture ended (pointer-up) → notify consumers\n\n clear: () => {\n ctx.dispatch({ type: 'CLEAR' });\n fireChange();\n },\n\n snapshot,\n\n rectsForPage: (pon) => ctx.getState().rects[pon] ?? EMPTY,\n\n hasSelection: () => ctx.getState().selection != null,\n\n selectedPages: () => {\n const { rects } = ctx.getState();\n return Object.keys(rects)\n .map(Number)\n .filter((pon) => (rects[pon]?.length ?? 0) > 0) as PageObjectNumber[];\n },\n\n onChange: (cb) => {\n changeCbs.add(cb);\n return () => changeCbs.delete(cb);\n },\n onCommit: (cb) => {\n commitCbs.add(cb);\n return () => commitCbs.delete(cb);\n },\n\n setHighlightVisible: (visible) =>\n ctx.dispatch({ type: 'SET_HIGHLIGHT_HIDDEN', hidden: !visible }),\n highlightVisible: () => !ctx.getState().highlightHidden,\n };\n}\n","import type { InteractionCapability, InteractionHandler } from '@embedpdf/plugin-interaction';\nimport type { SelectionCapability } from './types';\n\nconst CURSOR_TOKEN = 'selection-text';\nconst DRAG_THRESHOLD_PX = 4; // viewport px the pointer must move before a drag-select begins\n\n/**\n * The text-selection pointer handler. It is live only under tools that enable the\n * `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so\n * switching to pan disables text selection with zero special-casing here).\n *\n * - single click → deselect; records an anchor but selects NOTHING until\n * the pointer moves past the drag threshold (Chrome/Acrobat feel)\n * - single-click + drag → caret selection from the anchor (extends across pages)\n * - double-click → select the word\n * - triple-click → select the visual line\n * - down off-text → deselect, and DON'T capture (so it can't block)\n * - hover → I-beam only when over text, else the pointer cursor\n */\nexport function createTextSelectHandler(\n selection: SelectionCapability,\n interaction: InteractionCapability,\n): InteractionHandler {\n // Per-gesture drag-threshold state (one active gesture at a time — the hub owner).\n let anchor: { pon: number; point: { x: number; y: number }; vx: number; vy: number } | null =\n null;\n let dragging = false;\n\n return {\n id: 'text-select',\n priority: 60,\n enabledFor: (tool) => tool.enables.has('text-select'),\n onDown: (s) => {\n anchor = null;\n dragging = false;\n if (!s.page) return false; // over a gap — let a lower handler (e.g. scroll) try\n const { pon, point } = s.page;\n const clicks = s.clickCount ?? 1;\n if (clicks >= 3) {\n selection.selectLine(pon, point);\n return true;\n }\n if (clicks === 2) {\n selection.selectWord(pon, point);\n return true;\n }\n // Single click: clear immediately (clicking deselects), then record an anchor\n // ONLY if over text — but begin no selection until the drag threshold is met.\n selection.clear();\n if (!selection.isOverText(pon, point)) return false; // empty space → don't capture\n anchor = { pon, point, vx: s.viewport.x, vy: s.viewport.y };\n return true;\n },\n onMove: (s) => {\n if (!s.page || !anchor) return;\n if (!dragging) {\n if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) {\n return; // still a click, not a drag — select nothing yet\n }\n dragging = true;\n selection.beginAt(anchor.pon, anchor.point); // open the selection at the anchor\n }\n selection.extendTo(s.page.pon, s.page.point);\n },\n onUp: () => {\n anchor = null;\n dragging = false;\n selection.end();\n },\n onHover: (s) => {\n if (!s.page) {\n interaction.setCursor(CURSOR_TOKEN, null); // off the page → pointer\n return;\n }\n // Warm geometry on first hover; show the I-beam ONLY when actually over text.\n selection.ensurePage(s.page.pon);\n const overText = selection.isOverText(s.page.pon, s.page.point);\n interaction.setCursor(CURSOR_TOKEN, overText ? 'text' : null, 10);\n },\n };\n}\n","import type { SelectionAction, SelectionState } from './types';\n\nexport const initialSelectionState: SelectionState = {\n selection: null,\n rects: {},\n loaded: {},\n highlightHidden: false,\n};\n\nexport const selectionReducer = (\n state: SelectionState,\n action: SelectionAction,\n): SelectionState => {\n switch (action.type) {\n case 'PAGE_LOADED':\n return state.loaded[action.pon]\n ? state\n : { ...state, loaded: { ...state.loaded, [action.pon]: true } };\n case 'SET':\n return { ...state, selection: action.selection, rects: action.rects };\n case 'CLEAR':\n return state.selection === null && Object.keys(state.rects).length === 0\n ? state\n : { ...state, selection: null, rects: {} };\n case 'SET_HIGHLIGHT_HIDDEN':\n return state.highlightHidden === action.hidden\n ? state\n : { ...state, highlightHidden: action.hidden };\n default:\n return state;\n }\n};\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Point, Rect } from '@embedpdf/core-geometry';\n\n/** A glyph address: a page + a flat glyph index within that page's geometry. */\nexport interface GlyphPointer {\n pon: PageObjectNumber;\n glyph: number;\n}\n\n/** Anchor = where the drag began, focus = the current end. Inclusive. */\nexport interface SelectionRange {\n anchor: GlyphPointer;\n focus: GlyphPointer;\n}\n\nexport interface SelectionEndpoint {\n pon: PageObjectNumber;\n rect: Rect;\n}\n\nexport interface SelectionSnapshot {\n pages: Array<{ pon: PageObjectNumber; rects: Rect[] }>;\n start: SelectionEndpoint | null;\n end: SelectionEndpoint | null;\n direction: 'forward' | 'backward';\n}\n\nexport interface SelectionState {\n selection: SelectionRange | null;\n /** Derived highlight rects per page, in CONTENT space (y-down, PDF units). */\n rects: Record<number, Rect[]>;\n /** Pages whose text geometry has loaded (so the layer re-renders when ready). */\n loaded: Record<number, boolean>;\n /** When a consumer owns the selection visual (e.g. a markup tool draws its own\n * preview), the default highlight rects are suppressed. */\n highlightHidden: boolean;\n}\n\nexport type SelectionAction =\n | { type: 'PAGE_LOADED'; pon: PageObjectNumber }\n | { type: 'SET'; selection: SelectionRange; rects: Record<number, Rect[]> }\n | { type: 'CLEAR' }\n | { type: 'SET_HIGHLIGHT_HIDDEN'; hidden: boolean };\n\nexport interface SelectionCapability {\n /** Warm a page's text geometry (idempotent). Layers call this when a page mounts. */\n ensurePage(pon: PageObjectNumber): void;\n isLoaded(pon: PageObjectNumber): boolean;\n /** Is a content-space point on (or near) text? Drives the I-beam vs pointer cursor. */\n isOverText(pon: PageObjectNumber, point: Point): boolean;\n /** Begin a caret selection at a page point. Returns false if not near any text. */\n beginAt(pon: PageObjectNumber, point: Point): boolean;\n /** Double-click: select the word around the point. */\n selectWord(pon: PageObjectNumber, point: Point): void;\n /** Triple-click: select the whole visual line around the point. */\n selectLine(pon: PageObjectNumber, point: Point): void;\n /** Extend the current selection to a page point (drag). */\n extendTo(pon: PageObjectNumber, point: Point): void;\n end(): void;\n clear(): void;\n /** Coherent read-model for consumers that create annotations or selection UI. */\n snapshot(): SelectionSnapshot;\n /** Highlight rects for a page, in content space — the layer's only input. */\n rectsForPage(pon: PageObjectNumber): Rect[];\n hasSelection(): boolean;\n /** The pages the current selection covers (those with at least one rect) — so a\n * cross-page action (e.g. text-markup creation) can fan out per page. */\n selectedPages(): PageObjectNumber[];\n /** Fires whenever the selection rects change (e.g. drag-extend) — for live preview. */\n onChange(cb: () => void): () => void;\n /** Fires when a selection gesture ends (pointer-up) — the commit point. */\n onCommit(cb: () => void): () => void;\n /** Suppress / restore the default highlight visual (a consumer drawing its own). */\n setHighlightVisible(visible: boolean): void;\n highlightVisible(): boolean;\n}\n\nexport const SelectionToken = createCapabilityToken<SelectionCapability>('selection');\n","import { definePlugin } from '@embedpdf/core';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n interaction.registerHandler(createTextSelectHandler(selection, interaction));\n },\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["SelectionToken","InteractionToken","FeedbackToken","initialSelectionState","selectionReducer","createSelectionCapability","createTextSelectHandler"],"sources":["../src/selection.plugin.ts","../src/handles.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { FeedbackToken, InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n // Platform feedback (haptics) is OPTIONAL: absent in headless setups, the\n // handler simply never buzzes.\n optional: [FeedbackToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n const feedback = ctx.tryGet(FeedbackToken);\n interaction.registerHandler(\n createTextSelectHandler(selection, interaction, feedback ?? undefined),\n );\n },\n });\n","/**\n * Selection handles — the pure policy half (the `wheel.ts` convention).\n *\n * A handle IS the boundary glyph's leading (or trailing) edge: a segment\n * between two corners of the glyph's ORIENTED cell, with the grab head just\n * beyond the ascent at the start / past the baseline at the end. Bar length,\n * angle, and head all derive from that one segment, so rotated text and\n * rotated pages are carried by construction — the AABB never enters.\n *\n * Everything here is DOM-free and framework-free: geometry is pure given a\n * projector, and the drag session speaks the selection's own gesture verbs\n * (`beginAt`/`extendTo`/`end` — the same ones the pointer handler uses; a\n * handle drag IS a selection gesture, re-anchored). The framework adapters\n * keep only subscriptions and markup; the DOM listener mechanics live in\n * `@embedpdf/web`'s `attachSelectionHandle`.\n *\n * The view dependency is STRUCTURAL (satisfied by five lines over\n * `StageCapability`) so this plugin stays stage-free — selection also runs\n * in stage-less hosts (`PageView`) — and the math tests run against a fake.\n */\nimport { textQuadEdge } from '@embedpdf/core-geometry';\nimport type { Point, TextQuad } from '@embedpdf/core-geometry';\nimport type { PageObjectNumber } from '@embedpdf/core';\n\n/** What handle geometry & drags need from the hosting view. */\nexport interface SelectionHandleView {\n /** Page content point → overlay px. Must be POINT-exact (compose\n * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */\n toOverlay(pon: PageObjectNumber, pt: Point): Point | null;\n /** Overlay px → the page under it, or null over a gap. */\n pageAt(overlay: Point): { pon: PageObjectNumber; point: Point } | null;\n /** Overlay px → a SPECIFIC page's content space, unclamped. */\n pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;\n}\n\n/** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */\nexport interface SelectionHandleEndpoint {\n pon: PageObjectNumber;\n /** The boundary glyph's own oriented cell, page content space. */\n glyphQuad: TextQuad;\n /** Reading direction of its segment (+1 = the frame's +x) — decides which\n * side of the cell is the selection's leading edge. */\n advance: 1 | -1;\n}\n\n/** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */\nexport interface SelectionHandleTarget {\n beginAt(pon: PageObjectNumber, point: Point): boolean;\n extendTo(pon: PageObjectNumber, point: Point): void;\n end(): void;\n}\n\n// The iOS caret-handle design, one source for every adapter: a thin BAR that\n// is the selection's edge, capped by a screen-constant circle; PAD is the\n// invisible finger padding around the visual.\nexport const HANDLE_HEAD = 12; // px — the circle\nexport const HANDLE_BAR = 2; // px — the caret bar\nexport const HANDLE_PAD = 14; // px — grab padding\n\n/** Tilts within ~0.05° of upright render untransformed (float-noise guard —\n * and the dominant case stays pixel-identical to an axis-aligned box). */\nconst HANDLE_ROT_EPSILON = 0.05;\n\nexport interface SelectionHandleGeom {\n /** The projected edge, ascent corner first, overlay px. */\n bar: { from: Point; to: Point };\n /** The glyph's ink height on screen — zoom scaling comes free. */\n length: number;\n /** Degrees clockwise; 0 when the text is upright on screen. */\n rotation: number;\n /** True within the epsilon of upright: render with NO transform. */\n upright: boolean;\n /** Centre of the grab head, overlay px. */\n head: Point;\n}\n\nconst midpoint = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });\n\n/**\n * The handle's geometry for one endpoint, in overlay space. Null when the\n * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.\n */\nexport function selectionHandleGeom(\n view: SelectionHandleView,\n ep: SelectionHandleEndpoint,\n role: 'start' | 'end',\n): SelectionHandleGeom | null {\n // Which SIDE of the cell is this selection's edge is a reading-order\n // question (`advance`), never a geometric one.\n const leading = role === 'start' ? ep.advance > 0 : ep.advance < 0;\n const [upperPage, lowerPage] = textQuadEdge(ep.glyphQuad, leading ? 'start' : 'end');\n const upper = view.toOverlay(ep.pon, upperPage);\n const lower = view.toOverlay(ep.pon, lowerPage);\n if (!upper || !lower) return null;\n const dx = lower.x - upper.x;\n const dy = lower.y - upper.y;\n const length = Math.hypot(dx, dy);\n if (length === 0) return null;\n // 0° when the text is upright: the bar runs straight down the screen.\n const rotation = (Math.atan2(dy, dx) * 180) / Math.PI - 90;\n const upright = Math.abs(rotation) < HANDLE_ROT_EPSILON;\n const ux = dx / length;\n const uy = dy / length;\n const head =\n role === 'start'\n ? { x: upper.x - (ux * HANDLE_HEAD) / 2, y: upper.y - (uy * HANDLE_HEAD) / 2 }\n : { x: lower.x + (ux * HANDLE_HEAD) / 2, y: lower.y + (uy * HANDLE_HEAD) / 2 };\n return { bar: { from: upper, to: lower }, length, rotation, upright, head };\n}\n\nexport interface SelectionHandleDragSession {\n /** Feed the pointer's current overlay position. */\n move(overlay: Point): void;\n /** The pointer released (or was cancelled): settle the gesture. */\n end(): void;\n}\n\n/**\n * One armed handle drag. Dragging a handle extends from the OPPOSITE\n * endpoint: the first movement re-roots the selection gesture at that\n * endpoint's cell centre, and every position then extends toward the pointer\n * — snapping to glyphs and crossing pages exactly like a pointer drag,\n * firing the same change/commit signals. Over a gap the point projects onto\n * the last page hit (unclamped), so the selection keeps tracking instead of\n * freezing at a page edge. `end()` commits only if the drag actually\n * re-rooted; an untouched press settles nothing.\n */\nexport function createSelectionHandleDrag(\n selection: SelectionHandleTarget,\n view: SelectionHandleView,\n opposite: SelectionHandleEndpoint,\n draggedPon: PageObjectNumber,\n): SelectionHandleDragSession {\n // The fixed anchor: the opposite endpoint's cell CENTRE — orientation-free\n // (a parallelogram's diagonal midpoints coincide), so it lands inside the\n // glyph for rotated text too.\n const q = opposite.glyphQuad;\n const anchorPoint = midpoint(midpoint(q.upperStart, q.lowerEnd), midpoint(q.upperEnd, q.lowerStart));\n let begun = false;\n let lastPon = draggedPon;\n return {\n move: (overlay) => {\n if (!begun) {\n if (!selection.beginAt(opposite.pon, anchorPoint)) return;\n begun = true;\n }\n const hit = view.pageAt(overlay);\n if (hit) {\n lastPon = hit.pon;\n selection.extendTo(hit.pon, hit.point);\n } else {\n const p = view.pointOnPage(lastPon, overlay);\n if (p) selection.extendTo(lastPon, p);\n }\n },\n end: () => {\n if (begun) selection.end(); // settle → menu reappears, onCommit fires\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,MAAa,yBAAA,GAAA,eAAA,aAAA,CAC4D;CACrE,IAAI;CACJ,OAAOA,cAAAA;CACP,OAAO;CACP,UAAU,CAACC,sCAAAA,gBAAgB;CAG3B,UAAU,CAACC,sCAAAA,aAAa;CACxB,cAAcC,gBAAAA;CACd,QAAQC,gBAAAA;CACR,YAAYC,gBAAAA;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAIJ,sCAAAA,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAID,cAAAA,cAAc;EACxC,MAAM,WAAW,IAAI,OAAOE,sCAAAA,aAAa;EACzC,YAAY,gBACVI,gBAAAA,wBAAwB,WAAW,aAAa,YAAY,KAAA,CAAS,CACvE;CACF;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;ACiBH,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,aAAa;;;AAI1B,MAAM,qBAAqB;AAe3B,MAAM,YAAY,GAAU,OAAqB;CAAE,IAAI,EAAE,IAAI,EAAE,KAAK;CAAG,IAAI,EAAE,IAAI,EAAE,KAAK;AAAE;;;;;AAM1F,SAAgB,oBACd,MACA,IACA,MAC4B;CAG5B,MAAM,UAAU,SAAS,UAAU,GAAG,UAAU,IAAI,GAAG,UAAU;CACjE,MAAM,CAAC,WAAW,cAAA,GAAA,wBAAA,aAAA,CAA0B,GAAG,WAAW,UAAU,UAAU,KAAK;CACnF,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,IAAI,CAAC,SAAS,CAAC,OAAO,OAAO;CAC7B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,SAAS,KAAK,MAAM,IAAI,EAAE;CAChC,IAAI,WAAW,GAAG,OAAO;CAEzB,MAAM,WAAY,KAAK,MAAM,IAAI,EAAE,IAAI,MAAO,KAAK,KAAK;CACxD,MAAM,UAAU,KAAK,IAAI,QAAQ,IAAI;CACrC,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,KAAK;CAChB,MAAM,OACJ,SAAS,UACL;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE,IAC3E;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE;CACjF,OAAO;EAAE,KAAK;GAAE,MAAM;GAAO,IAAI;EAAM;EAAG;EAAQ;EAAU;EAAS;CAAK;AAC5E;;;;;;;;;;;AAmBA,SAAgB,0BACd,WACA,MACA,UACA,YAC4B;CAI5B,MAAM,IAAI,SAAS;CACnB,MAAM,cAAc,SAAS,SAAS,EAAE,YAAY,EAAE,QAAQ,GAAG,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC;CACnG,IAAI,QAAQ;CACZ,IAAI,UAAU;CACd,OAAO;EACL,OAAO,YAAY;GACjB,IAAI,CAAC,OAAO;IACV,IAAI,CAAC,UAAU,QAAQ,SAAS,KAAK,WAAW,GAAG;IACnD,QAAQ;GACV;GACA,MAAM,MAAM,KAAK,OAAO,OAAO;GAC/B,IAAI,KAAK;IACP,UAAU,IAAI;IACd,UAAU,SAAS,IAAI,KAAK,IAAI,KAAK;GACvC,OAAO;IACL,MAAM,IAAI,KAAK,YAAY,SAAS,OAAO;IAC3C,IAAI,GAAG,UAAU,SAAS,SAAS,CAAC;GACtC;EACF;EACA,WAAW;GACT,IAAI,OAAO,UAAU,IAAI;EAC3B;CACF;AACF"}
package/dist/index.d.cts CHANGED
@@ -1,167 +1,89 @@
1
- import { PageObjectNumber, PluginContext } from "@embedpdf/core";
2
- import { Point, Rect } from "@embedpdf/core-geometry";
3
- import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
4
- import { PageGeometrySnapshot, PdfRect } from "@embedpdf/engine-core/runtime";
5
- //#region src/types.d.ts
6
- /** A glyph address: a page + a flat glyph index within that page's geometry. */
7
- interface GlyphPointer {
8
- pon: PageObjectNumber;
9
- glyph: number;
10
- }
11
- /** Anchor = where the drag began, focus = the current end. Inclusive. */
12
- interface SelectionRange {
13
- anchor: GlyphPointer;
14
- focus: GlyphPointer;
15
- }
16
- interface SelectionEndpoint {
17
- pon: PageObjectNumber;
18
- rect: Rect;
19
- }
20
- interface SelectionSnapshot {
21
- pages: Array<{
22
- pon: PageObjectNumber;
23
- rects: Rect[];
24
- }>;
25
- start: SelectionEndpoint | null;
26
- end: SelectionEndpoint | null;
27
- direction: 'forward' | 'backward';
28
- }
29
- interface SelectionState {
30
- selection: SelectionRange | null;
31
- /** Derived highlight rects per page, in CONTENT space (y-down, PDF units). */
32
- rects: Record<number, Rect[]>;
33
- /** Pages whose text geometry has loaded (so the layer re-renders when ready). */
34
- loaded: Record<number, boolean>;
35
- /** When a consumer owns the selection visual (e.g. a markup tool draws its own
36
- * preview), the default highlight rects are suppressed. */
37
- highlightHidden: boolean;
38
- }
39
- type SelectionAction = {
40
- type: 'PAGE_LOADED';
41
- pon: PageObjectNumber;
42
- } | {
43
- type: 'SET';
44
- selection: SelectionRange;
45
- rects: Record<number, Rect[]>;
46
- } | {
47
- type: 'CLEAR';
48
- } | {
49
- type: 'SET_HIGHLIGHT_HIDDEN';
50
- hidden: boolean;
51
- };
52
- interface SelectionCapability {
53
- /** Warm a page's text geometry (idempotent). Layers call this when a page mounts. */
54
- ensurePage(pon: PageObjectNumber): void;
55
- isLoaded(pon: PageObjectNumber): boolean;
56
- /** Is a content-space point on (or near) text? Drives the I-beam vs pointer cursor. */
57
- isOverText(pon: PageObjectNumber, point: Point): boolean;
58
- /** Begin a caret selection at a page point. Returns false if not near any text. */
59
- beginAt(pon: PageObjectNumber, point: Point): boolean;
60
- /** Double-click: select the word around the point. */
61
- selectWord(pon: PageObjectNumber, point: Point): void;
62
- /** Triple-click: select the whole visual line around the point. */
63
- selectLine(pon: PageObjectNumber, point: Point): void;
64
- /** Extend the current selection to a page point (drag). */
65
- extendTo(pon: PageObjectNumber, point: Point): void;
66
- end(): void;
67
- clear(): void;
68
- /** Coherent read-model for consumers that create annotations or selection UI. */
69
- snapshot(): SelectionSnapshot;
70
- /** Highlight rects for a page, in content space — the layer's only input. */
71
- rectsForPage(pon: PageObjectNumber): Rect[];
72
- hasSelection(): boolean;
73
- /** The pages the current selection covers (those with at least one rect) — so a
74
- * cross-page action (e.g. text-markup creation) can fan out per page. */
75
- selectedPages(): PageObjectNumber[];
76
- /** Fires whenever the selection rects change (e.g. drag-extend) — for live preview. */
77
- onChange(cb: () => void): () => void;
78
- /** Fires when a selection gesture ends (pointer-up) — the commit point. */
79
- onCommit(cb: () => void): () => void;
80
- /** Suppress / restore the default highlight visual (a consumer drawing its own). */
81
- setHighlightVisible(visible: boolean): void;
82
- highlightVisible(): boolean;
83
- }
84
- declare const SelectionToken: import("@embedpdf/core").CapabilityToken<SelectionCapability>;
85
- //#endregion
1
+ import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-CUrUVl6K.cjs";
2
+ import { SelectionToken } from "./contract.cjs";
3
+ import { PageObjectNumber } from "@embedpdf/core";
4
+ import { Point, TextQuad } from "@embedpdf/core-geometry";
86
5
  //#region src/selection.plugin.d.ts
87
6
  /**
88
7
  * Text selection — document-scoped, requires the interaction hub. In `init` it
89
8
  * registers ITS pointer handler with the hub; the hub owns the pointer stream
90
9
  * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection
91
10
  * only needs the page coordinate context + the engine's text geometry.
92
- */
93
- declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionCapability>;
94
- //#endregion
95
- //#region src/capability.d.ts
96
- /**
97
- * The selection capability. The reducer state holds the selection range, the
98
- * derived content-space rects (per page), and per-page loaded flags; the (large,
99
- * non-serializable) per-page text geometry is cached HERE in the closure.
100
- *
101
- * Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph), so
102
- * a drag from page 2 into page 4 selects the tail of 2, all of 3, and the head of
103
- * 4. `recompute` rebuilds the merged line rects for every loaded page in the span
104
- * and re-runs whenever a mid-span page finishes loading.
105
- */
106
- declare function createSelectionCapability(ctx: PluginContext<SelectionState, SelectionAction>): SelectionCapability;
107
- //#endregion
108
- //#region src/handler.d.ts
109
- /**
110
- * The text-selection pointer handler. It is live only under tools that enable the
111
- * `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
112
- * switching to pan disables text selection with zero special-casing here).
113
11
  *
114
- * - single click → deselect; records an anchor but selects NOTHING until
115
- * the pointer moves past the drag threshold (Chrome/Acrobat feel)
116
- * - single-click + drag → caret selection from the anchor (extends across pages)
117
- * - double-click → select the word
118
- * - triple-click → select the visual line
119
- * - down off-text → deselect, and DON'T capture (so it can't block)
120
- * - hover → I-beam only when over text, else the pointer cursor
12
+ * Registry/content invalidation is wired inside the capability itself
13
+ * (revision watch + document event subscription), so headless/programmatic
14
+ * use gets the same reconciliation as the full viewer.
121
15
  */
122
- declare function createTextSelectHandler(selection: SelectionCapability, interaction: InteractionCapability): InteractionHandler;
16
+ declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionHostCapability>;
123
17
  //#endregion
124
- //#region src/reducer.d.ts
125
- declare const initialSelectionState: SelectionState;
126
- declare const selectionReducer: (state: SelectionState, action: SelectionAction) => SelectionState;
127
- //#endregion
128
- //#region src/geometry.d.ts
129
- /** One glyph in content space. `loose` builds selection rects; `tight` hit-tests. */
130
- interface GlyphInfo {
131
- loose: Rect;
132
- tight?: Rect;
133
- flags: number;
18
+ //#region src/handles.d.ts
19
+ /** What handle geometry & drags need from the hosting view. */
20
+ interface SelectionHandleView {
21
+ /** Page content point → overlay px. Must be POINT-exact (compose
22
+ * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */
23
+ toOverlay(pon: PageObjectNumber, pt: Point): Point | null;
24
+ /** Overlay px → the page under it, or null over a gap. */
25
+ pageAt(overlay: Point): {
26
+ pon: PageObjectNumber;
27
+ point: Point;
28
+ } | null;
29
+ /** Overlay px → a SPECIFIC page's content space, unclamped. */
30
+ pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;
134
31
  }
135
- /** A contiguous run of glyphs (a text object). `rect` is its loose enclosing box. */
136
- interface RunInfo {
137
- start: number;
138
- count: number;
139
- rect: Rect;
140
- fontSize?: number;
32
+ /** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */
33
+ interface SelectionHandleEndpoint {
34
+ pon: PageObjectNumber;
35
+ /** The boundary glyph's own oriented cell, page content space. */
36
+ glyphQuad: TextQuad;
37
+ /** Reading direction of its segment (+1 = the frame's +x) — decides which
38
+ * side of the cell is the selection's leading edge. */
39
+ advance: 1 | -1;
141
40
  }
142
- /** A page's text laid out in content space: a flat glyph list + the run structure. */
143
- interface PageText {
144
- glyphs: GlyphInfo[];
145
- runs: RunInfo[];
41
+ /** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */
42
+ interface SelectionHandleTarget {
43
+ beginAt(pon: PageObjectNumber, point: Point): boolean;
44
+ extendTo(pon: PageObjectNumber, point: Point): void;
45
+ end(): void;
46
+ }
47
+ declare const HANDLE_HEAD = 12;
48
+ declare const HANDLE_BAR = 2;
49
+ declare const HANDLE_PAD = 14;
50
+ interface SelectionHandleGeom {
51
+ /** The projected edge, ascent corner first, overlay px. */
52
+ bar: {
53
+ from: Point;
54
+ to: Point;
55
+ };
56
+ /** The glyph's ink height on screen — zoom scaling comes free. */
57
+ length: number;
58
+ /** Degrees clockwise; 0 when the text is upright on screen. */
59
+ rotation: number;
60
+ /** True within the epsilon of upright: render with NO transform. */
61
+ upright: boolean;
62
+ /** Centre of the grab head, overlay px. */
63
+ head: Point;
146
64
  }
147
65
  /**
148
- * Flatten a page's text geometry into content-space glyphs + runs. Rotation does
149
- * NOT enter here — the overlay rides the page's CSS rotation, so glyphs stay in
150
- * un-rotated content space (the crop-relative y-flip is the only conversion).
66
+ * The handle's geometry for one endpoint, in overlay space. Null when the
67
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
151
68
  */
152
- declare function buildPageText(snapshot: PageGeometrySnapshot, crop: PdfRect, rotation: 0 | 90 | 180 | 270, userUnit: number): PageText;
69
+ declare function selectionHandleGeom(view: SelectionHandleView, ep: SelectionHandleEndpoint, role: 'start' | 'end'): SelectionHandleGeom | null;
70
+ interface SelectionHandleDragSession {
71
+ /** Feed the pointer's current overlay position. */
72
+ move(overlay: Point): void;
73
+ /** The pointer released (or was cancelled): settle the gesture. */
74
+ end(): void;
75
+ }
153
76
  /**
154
- * The glyph index at a content-space point, or null when nothing is near (so the
155
- * caller can show the pointer cursor off-text). PDFium `GetIndexAtPos`: exact
156
- * tight-box containment first, then a tolerance pass (closest by Manhattan
157
- * distance within `toleranceFactor × average glyph height`).
77
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
78
+ * endpoint: the first movement re-roots the selection gesture at that
79
+ * endpoint's cell centre, and every position then extends toward the pointer
80
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
81
+ * firing the same change/commit signals. Over a gap the point projects onto
82
+ * the last page hit (unclamped), so the selection keeps tracking instead of
83
+ * freezing at a page edge. `end()` commits only if the drag actually
84
+ * re-rooted; an untouched press settles nothing.
158
85
  */
159
- declare function glyphAt(text: PageText, p: Point, toleranceFactor?: number): number | null;
160
- /** Double-click: the word around `i` (walk to space/empty glyphs both ways). */
161
- declare function expandToWord(text: PageText, i: number): [number, number];
162
- /** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */
163
- declare function expandToLine(text: PageText, i: number): [number, number];
164
- declare function rectsForRange(text: PageText, from: number, to: number): Rect[];
86
+ declare function createSelectionHandleDrag(selection: SelectionHandleTarget, view: SelectionHandleView, opposite: SelectionHandleEndpoint, draggedPon: PageObjectNumber): SelectionHandleDragSession;
165
87
  //#endregion
166
- export { type GlyphInfo, type GlyphPointer, type PageText, type RunInfo, type SelectionAction, type SelectionCapability, type SelectionEndpoint, type SelectionRange, type SelectionSnapshot, type SelectionState, SelectionToken, buildPageText, createSelectionCapability, createTextSelectHandler, expandToLine, expandToWord, glyphAt, initialSelectionState, rectsForRange, selectionPlugin, selectionReducer };
88
+ export { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, type SelectionCapability, type SelectionEndpoint, type SelectionHandleDragSession, type SelectionHandleEndpoint, type SelectionHandleGeom, type SelectionHandleTarget, type SelectionHandleView, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, createSelectionHandleDrag, selectionHandleGeom, selectionPlugin };
167
89
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,167 +1,89 @@
1
- import { PageObjectNumber, PluginContext } from "@embedpdf/core";
2
- import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
3
- import { Point, Rect } from "@embedpdf/core-geometry";
4
- import { PageGeometrySnapshot, PdfRect } from "@embedpdf/engine-core/runtime";
5
- //#region src/types.d.ts
6
- /** A glyph address: a page + a flat glyph index within that page's geometry. */
7
- interface GlyphPointer {
8
- pon: PageObjectNumber;
9
- glyph: number;
10
- }
11
- /** Anchor = where the drag began, focus = the current end. Inclusive. */
12
- interface SelectionRange {
13
- anchor: GlyphPointer;
14
- focus: GlyphPointer;
15
- }
16
- interface SelectionEndpoint {
17
- pon: PageObjectNumber;
18
- rect: Rect;
19
- }
20
- interface SelectionSnapshot {
21
- pages: Array<{
22
- pon: PageObjectNumber;
23
- rects: Rect[];
24
- }>;
25
- start: SelectionEndpoint | null;
26
- end: SelectionEndpoint | null;
27
- direction: 'forward' | 'backward';
28
- }
29
- interface SelectionState {
30
- selection: SelectionRange | null;
31
- /** Derived highlight rects per page, in CONTENT space (y-down, PDF units). */
32
- rects: Record<number, Rect[]>;
33
- /** Pages whose text geometry has loaded (so the layer re-renders when ready). */
34
- loaded: Record<number, boolean>;
35
- /** When a consumer owns the selection visual (e.g. a markup tool draws its own
36
- * preview), the default highlight rects are suppressed. */
37
- highlightHidden: boolean;
38
- }
39
- type SelectionAction = {
40
- type: 'PAGE_LOADED';
41
- pon: PageObjectNumber;
42
- } | {
43
- type: 'SET';
44
- selection: SelectionRange;
45
- rects: Record<number, Rect[]>;
46
- } | {
47
- type: 'CLEAR';
48
- } | {
49
- type: 'SET_HIGHLIGHT_HIDDEN';
50
- hidden: boolean;
51
- };
52
- interface SelectionCapability {
53
- /** Warm a page's text geometry (idempotent). Layers call this when a page mounts. */
54
- ensurePage(pon: PageObjectNumber): void;
55
- isLoaded(pon: PageObjectNumber): boolean;
56
- /** Is a content-space point on (or near) text? Drives the I-beam vs pointer cursor. */
57
- isOverText(pon: PageObjectNumber, point: Point): boolean;
58
- /** Begin a caret selection at a page point. Returns false if not near any text. */
59
- beginAt(pon: PageObjectNumber, point: Point): boolean;
60
- /** Double-click: select the word around the point. */
61
- selectWord(pon: PageObjectNumber, point: Point): void;
62
- /** Triple-click: select the whole visual line around the point. */
63
- selectLine(pon: PageObjectNumber, point: Point): void;
64
- /** Extend the current selection to a page point (drag). */
65
- extendTo(pon: PageObjectNumber, point: Point): void;
66
- end(): void;
67
- clear(): void;
68
- /** Coherent read-model for consumers that create annotations or selection UI. */
69
- snapshot(): SelectionSnapshot;
70
- /** Highlight rects for a page, in content space — the layer's only input. */
71
- rectsForPage(pon: PageObjectNumber): Rect[];
72
- hasSelection(): boolean;
73
- /** The pages the current selection covers (those with at least one rect) — so a
74
- * cross-page action (e.g. text-markup creation) can fan out per page. */
75
- selectedPages(): PageObjectNumber[];
76
- /** Fires whenever the selection rects change (e.g. drag-extend) — for live preview. */
77
- onChange(cb: () => void): () => void;
78
- /** Fires when a selection gesture ends (pointer-up) — the commit point. */
79
- onCommit(cb: () => void): () => void;
80
- /** Suppress / restore the default highlight visual (a consumer drawing its own). */
81
- setHighlightVisible(visible: boolean): void;
82
- highlightVisible(): boolean;
83
- }
84
- declare const SelectionToken: import("@embedpdf/core").CapabilityToken<SelectionCapability>;
85
- //#endregion
1
+ import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-CUrUVl6K.js";
2
+ import { SelectionToken } from "./contract.js";
3
+ import { PageObjectNumber } from "@embedpdf/core";
4
+ import { Point, TextQuad } from "@embedpdf/core-geometry";
86
5
  //#region src/selection.plugin.d.ts
87
6
  /**
88
7
  * Text selection — document-scoped, requires the interaction hub. In `init` it
89
8
  * registers ITS pointer handler with the hub; the hub owns the pointer stream
90
9
  * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection
91
10
  * only needs the page coordinate context + the engine's text geometry.
92
- */
93
- declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionCapability>;
94
- //#endregion
95
- //#region src/capability.d.ts
96
- /**
97
- * The selection capability. The reducer state holds the selection range, the
98
- * derived content-space rects (per page), and per-page loaded flags; the (large,
99
- * non-serializable) per-page text geometry is cached HERE in the closure.
100
- *
101
- * Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph), so
102
- * a drag from page 2 into page 4 selects the tail of 2, all of 3, and the head of
103
- * 4. `recompute` rebuilds the merged line rects for every loaded page in the span
104
- * and re-runs whenever a mid-span page finishes loading.
105
- */
106
- declare function createSelectionCapability(ctx: PluginContext<SelectionState, SelectionAction>): SelectionCapability;
107
- //#endregion
108
- //#region src/handler.d.ts
109
- /**
110
- * The text-selection pointer handler. It is live only under tools that enable the
111
- * `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
112
- * switching to pan disables text selection with zero special-casing here).
113
11
  *
114
- * - single click → deselect; records an anchor but selects NOTHING until
115
- * the pointer moves past the drag threshold (Chrome/Acrobat feel)
116
- * - single-click + drag → caret selection from the anchor (extends across pages)
117
- * - double-click → select the word
118
- * - triple-click → select the visual line
119
- * - down off-text → deselect, and DON'T capture (so it can't block)
120
- * - hover → I-beam only when over text, else the pointer cursor
12
+ * Registry/content invalidation is wired inside the capability itself
13
+ * (revision watch + document event subscription), so headless/programmatic
14
+ * use gets the same reconciliation as the full viewer.
121
15
  */
122
- declare function createTextSelectHandler(selection: SelectionCapability, interaction: InteractionCapability): InteractionHandler;
16
+ declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionHostCapability>;
123
17
  //#endregion
124
- //#region src/reducer.d.ts
125
- declare const initialSelectionState: SelectionState;
126
- declare const selectionReducer: (state: SelectionState, action: SelectionAction) => SelectionState;
127
- //#endregion
128
- //#region src/geometry.d.ts
129
- /** One glyph in content space. `loose` builds selection rects; `tight` hit-tests. */
130
- interface GlyphInfo {
131
- loose: Rect;
132
- tight?: Rect;
133
- flags: number;
18
+ //#region src/handles.d.ts
19
+ /** What handle geometry & drags need from the hosting view. */
20
+ interface SelectionHandleView {
21
+ /** Page content point → overlay px. Must be POINT-exact (compose
22
+ * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */
23
+ toOverlay(pon: PageObjectNumber, pt: Point): Point | null;
24
+ /** Overlay px → the page under it, or null over a gap. */
25
+ pageAt(overlay: Point): {
26
+ pon: PageObjectNumber;
27
+ point: Point;
28
+ } | null;
29
+ /** Overlay px → a SPECIFIC page's content space, unclamped. */
30
+ pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;
134
31
  }
135
- /** A contiguous run of glyphs (a text object). `rect` is its loose enclosing box. */
136
- interface RunInfo {
137
- start: number;
138
- count: number;
139
- rect: Rect;
140
- fontSize?: number;
32
+ /** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */
33
+ interface SelectionHandleEndpoint {
34
+ pon: PageObjectNumber;
35
+ /** The boundary glyph's own oriented cell, page content space. */
36
+ glyphQuad: TextQuad;
37
+ /** Reading direction of its segment (+1 = the frame's +x) — decides which
38
+ * side of the cell is the selection's leading edge. */
39
+ advance: 1 | -1;
141
40
  }
142
- /** A page's text laid out in content space: a flat glyph list + the run structure. */
143
- interface PageText {
144
- glyphs: GlyphInfo[];
145
- runs: RunInfo[];
41
+ /** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */
42
+ interface SelectionHandleTarget {
43
+ beginAt(pon: PageObjectNumber, point: Point): boolean;
44
+ extendTo(pon: PageObjectNumber, point: Point): void;
45
+ end(): void;
46
+ }
47
+ declare const HANDLE_HEAD = 12;
48
+ declare const HANDLE_BAR = 2;
49
+ declare const HANDLE_PAD = 14;
50
+ interface SelectionHandleGeom {
51
+ /** The projected edge, ascent corner first, overlay px. */
52
+ bar: {
53
+ from: Point;
54
+ to: Point;
55
+ };
56
+ /** The glyph's ink height on screen — zoom scaling comes free. */
57
+ length: number;
58
+ /** Degrees clockwise; 0 when the text is upright on screen. */
59
+ rotation: number;
60
+ /** True within the epsilon of upright: render with NO transform. */
61
+ upright: boolean;
62
+ /** Centre of the grab head, overlay px. */
63
+ head: Point;
146
64
  }
147
65
  /**
148
- * Flatten a page's text geometry into content-space glyphs + runs. Rotation does
149
- * NOT enter here — the overlay rides the page's CSS rotation, so glyphs stay in
150
- * un-rotated content space (the crop-relative y-flip is the only conversion).
66
+ * The handle's geometry for one endpoint, in overlay space. Null when the
67
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
151
68
  */
152
- declare function buildPageText(snapshot: PageGeometrySnapshot, crop: PdfRect, rotation: 0 | 90 | 180 | 270, userUnit: number): PageText;
69
+ declare function selectionHandleGeom(view: SelectionHandleView, ep: SelectionHandleEndpoint, role: 'start' | 'end'): SelectionHandleGeom | null;
70
+ interface SelectionHandleDragSession {
71
+ /** Feed the pointer's current overlay position. */
72
+ move(overlay: Point): void;
73
+ /** The pointer released (or was cancelled): settle the gesture. */
74
+ end(): void;
75
+ }
153
76
  /**
154
- * The glyph index at a content-space point, or null when nothing is near (so the
155
- * caller can show the pointer cursor off-text). PDFium `GetIndexAtPos`: exact
156
- * tight-box containment first, then a tolerance pass (closest by Manhattan
157
- * distance within `toleranceFactor × average glyph height`).
77
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
78
+ * endpoint: the first movement re-roots the selection gesture at that
79
+ * endpoint's cell centre, and every position then extends toward the pointer
80
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
81
+ * firing the same change/commit signals. Over a gap the point projects onto
82
+ * the last page hit (unclamped), so the selection keeps tracking instead of
83
+ * freezing at a page edge. `end()` commits only if the drag actually
84
+ * re-rooted; an untouched press settles nothing.
158
85
  */
159
- declare function glyphAt(text: PageText, p: Point, toleranceFactor?: number): number | null;
160
- /** Double-click: the word around `i` (walk to space/empty glyphs both ways). */
161
- declare function expandToWord(text: PageText, i: number): [number, number];
162
- /** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */
163
- declare function expandToLine(text: PageText, i: number): [number, number];
164
- declare function rectsForRange(text: PageText, from: number, to: number): Rect[];
86
+ declare function createSelectionHandleDrag(selection: SelectionHandleTarget, view: SelectionHandleView, opposite: SelectionHandleEndpoint, draggedPon: PageObjectNumber): SelectionHandleDragSession;
165
87
  //#endregion
166
- export { type GlyphInfo, type GlyphPointer, type PageText, type RunInfo, type SelectionAction, type SelectionCapability, type SelectionEndpoint, type SelectionRange, type SelectionSnapshot, type SelectionState, SelectionToken, buildPageText, createSelectionCapability, createTextSelectHandler, expandToLine, expandToWord, glyphAt, initialSelectionState, rectsForRange, selectionPlugin, selectionReducer };
88
+ export { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, type SelectionCapability, type SelectionEndpoint, type SelectionHandleDragSession, type SelectionHandleEndpoint, type SelectionHandleGeom, type SelectionHandleTarget, type SelectionHandleView, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, createSelectionHandleDrag, selectionHandleGeom, selectionPlugin };
167
89
  //# sourceMappingURL=index.d.ts.map