citation-evidence/src/anchor/pdf-selector-math.ts

80 lines
2.5 KiB
TypeScript
Raw Normal View History

/**
* 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 };
}