/** * Pure, library-free transformations between the adapter's * `PdfSelectionCapture` and the shared `Selector[]` shapes. * * Extracted from `pdf-viewer-adapter-spike.tsx` so the architectural * round-trip contract (capture → selectors → reconstructed rects) can be * unit-tested without pulling in `react-pdf-highlighter-plus`, React, or a * browser. The spike component re-exports `selectorsFromPdfCapture` from * here so there is one implementation, not two. * * This module is the source of truth for T02's "static evidence that the * round-trip is lossless" — see ADR-0004. */ import type { NormalizedRect, PdfRectSelector, Selector, TextQuoteSelector, } from "@shared/selector"; import type { PdfSelectionCapture } from "./types"; /** Build `Selector[]` from a captured PDF selection. */ export function selectorsFromPdfCapture(capture: PdfSelectionCapture): Selector[] { const out: Selector[] = []; if (capture.text.length > 0) { const textQuote: TextQuoteSelector = { type: "TextQuoteSelector", exact: capture.text, }; out.push(textQuote); } if (capture.rects.length > 0) { const rect: PdfRectSelector = { type: "PdfRectSelector", page: capture.page, rects: capture.rects, }; out.push(rect); } return out; } /** Find the `PdfRectSelector` in a selector list, if any. */ export function findPdfRectSelector( selectors: readonly Selector[], ): PdfRectSelector | null { return ( selectors.find((s): s is PdfRectSelector => s.type === "PdfRectSelector") ?? null ); } /** Find the `TextQuoteSelector` in a selector list, if any. */ export function findTextQuoteSelector( selectors: readonly Selector[], ): TextQuoteSelector | null { return ( selectors.find((s): s is TextQuoteSelector => s.type === "TextQuoteSelector") ?? null ); } /** Bounding rectangle of a non-empty list of normalized rects. */ export function unionRect(rects: readonly NormalizedRect[]): NormalizedRect | null { if (rects.length === 0) return null; const first = rects[0]!; let minX = first.x; let minY = first.y; let maxX = first.x + first.width; let maxY = first.y + first.height; for (let i = 1; i < rects.length; i++) { const r = rects[i]!; if (r.x < minX) minX = r.x; if (r.y < minY) minY = r.y; if (r.x + r.width > maxX) maxX = r.x + r.width; if (r.y + r.height > maxY) maxY = r.y + r.height; } return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }