@reekon-tools/boldr-utils 1.18.2 → 1.19.0

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.
Files changed (80) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +2 -0
  2. package/dist/annotation/canvas/AnnotationCanvasInner.js +26 -5
  3. package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +2 -0
  4. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +29 -3
  5. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -1
  6. package/dist/annotation/canvas/AnnotationCanvasSkia.js +2 -2
  7. package/dist/annotation/canvas/backgroundSlot.d.ts +11 -0
  8. package/dist/annotation/canvas/pdfDocument.d.ts +25 -0
  9. package/dist/annotation/canvas/pdfDocument.js +83 -0
  10. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +26 -0
  11. package/dist/annotation/canvas/useAnnotationCanvasState.js +35 -5
  12. package/dist/annotation/canvas/viewport.d.ts +9 -1
  13. package/dist/annotation/canvas/viewport.js +7 -2
  14. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +5 -1
  15. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +26 -0
  16. package/dist/exports.d.ts +3 -1
  17. package/dist/exports.js +2 -1
  18. package/dist/types/annotation.d.ts +18 -0
  19. package/dist/types/annotation.js +18 -0
  20. package/package.json +2 -1
  21. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  22. package/dist/canvas/AnnotationCanvas.js +0 -10
  23. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  24. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  25. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  26. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  27. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  28. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  29. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  30. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  31. package/dist/canvas/Tool.d.ts +0 -38
  32. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  33. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  34. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  35. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  36. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  37. package/dist/canvas/elements/ShapeElement.js +0 -62
  38. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  39. package/dist/canvas/elements/StrokeElement.js +0 -18
  40. package/dist/canvas/measurementPicker.d.ts +0 -10
  41. package/dist/canvas/measurementPicker.js +0 -1
  42. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  43. package/dist/canvas/measurementStampOverlay.js +0 -1
  44. package/dist/canvas/pointerAdapter.d.ts +0 -3
  45. package/dist/canvas/pointerAdapter.js +0 -19
  46. package/dist/canvas/stampLayout.d.ts +0 -5
  47. package/dist/canvas/stampLayout.js +0 -14
  48. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  49. package/dist/canvas/tools/measurementStampTool.js +0 -37
  50. package/dist/canvas/tools/panTool.d.ts +0 -5
  51. package/dist/canvas/tools/panTool.js +0 -25
  52. package/dist/canvas/tools/penTool.d.ts +0 -13
  53. package/dist/canvas/tools/penTool.js +0 -68
  54. package/dist/canvas/tools/selectTool.d.ts +0 -2
  55. package/dist/canvas/tools/selectTool.js +0 -182
  56. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  57. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  58. package/dist/canvas/viewport.d.ts +0 -16
  59. package/dist/canvas/viewport.js +0 -54
  60. package/dist/data/AnnotationDataContext.d.ts +0 -8
  61. package/dist/data/AnnotationDataContext.js +0 -11
  62. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  63. package/dist/data/AnnotationDataProvider.js +0 -4
  64. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  65. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  66. package/dist/data/canvasPersistence.d.ts +0 -3
  67. package/dist/data/canvasPersistence.js +0 -26
  68. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  69. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  70. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  71. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  72. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  73. package/dist/data/hooks/useAnnotationList.js +0 -26
  74. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  75. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  76. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  77. package/dist/hooks/useParseMeasurement.js +0 -14
  78. package/dist/utils/evaluateFormula.d.ts +0 -20
  79. package/dist/utils/evaluateFormula.js +0 -31
  80. /package/dist/{canvas/Tool.js → annotation/canvas/backgroundSlot.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { type CSSProperties, type MutableRefObject } from 'react';
2
2
  import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../../types/firestore.js';
3
3
  import type { AnnotationCanvasState, AnnotationDocumentPatch, PlacedMeasurementRef, Selection } from '../../types/annotation.js';
4
+ import type { RenderCanvasBackground } from './backgroundSlot.js';
4
5
  import type { MeasurementRef } from './measurementPicker.js';
5
6
  import type { RequestTextInput, Tool } from './Tool.js';
6
7
  import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
@@ -21,6 +22,7 @@ export interface AnnotationCanvasInnerProps {
21
22
  resolveImageUrl?: (storagePath: string) => Promise<string>;
22
23
  pickMeasurement?: () => Promise<MeasurementRef | null>;
23
24
  requestTextInput?: RequestTextInput;
25
+ renderBackground?: RenderCanvasBackground;
24
26
  renderMeasurementStamp?: RenderMeasurementStamp;
25
27
  onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
26
28
  onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
@@ -20,8 +20,14 @@ const HANDLE_RING_PX = 2;
20
20
  const LONG_PRESS_MS = 500;
21
21
  const LONG_PRESS_SLOP_PX = 10;
22
22
  const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
23
+ // Device pixel ratio handed to the background slot (a host rasterizing its own
24
+ // doc content needs device px per doc point = zoom × pixelRatio). Read per
25
+ // render rather than tracked: it only changes with browser zoom or a monitor
26
+ // switch, both of which also resize the canvas. Guarded for non-DOM renders
27
+ // (SSR / the vitest environment), where 1 is the right answer.
28
+ const devicePixelRatioOf = () => typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1;
23
29
  export const AnnotationCanvasInner = (props) => {
24
- const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, } = props;
30
+ const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, renderBackground, } = props;
25
31
  // Snapshot plumbing for thumbnail capture: hold the <Canvas> ref and publish
26
32
  // a capture fn on the consumer's snapshotRef. encodeToBytes() yields PNG.
27
33
  const canvasRef = useCanvasRef();
@@ -200,14 +206,19 @@ export const AnnotationCanvasInner = (props) => {
200
206
  x: event.clientX - (rect?.left ?? 0),
201
207
  y: event.clientY - (rect?.top ?? 0),
202
208
  };
209
+ // `zoomBy`, not `zoom`: the factor has to compose against the viewport
210
+ // React is about to commit. Wheel events arrive in bursts and React
211
+ // batches them, so computing an absolute target here from
212
+ // `state.ctx.viewport.state.zoom` — a render-stale read — made every
213
+ // event in a batch aim at the same zoom and `zoomAt` no-op for all but
214
+ // the last. Measured: ten same-task wheel events moved the view x1.49
215
+ // where their deltas asked for x54.6.
203
216
  if (event.ctrlKey || event.metaKey) {
204
- const factor = Math.exp(-event.deltaY / 200);
205
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
217
+ state.zoomBy(focal, Math.exp(-event.deltaY / 200));
206
218
  return;
207
219
  }
208
220
  if (wheelMode === 'zoom') {
209
- const factor = Math.exp(-event.deltaY / 300);
210
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
221
+ state.zoomBy(focal, Math.exp(-event.deltaY / 300));
211
222
  return;
212
223
  }
213
224
  if (event.shiftKey) {
@@ -267,6 +278,15 @@ export const AnnotationCanvasInner = (props) => {
267
278
  ...style,
268
279
  };
269
280
  const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
281
+ // Host substrate (see renderBackground). `state.viewport` IS the committed
282
+ // viewport on web — the whole viewport lives in React state here, so every
283
+ // wheel/pan step is already a commit and there is no per-frame channel to
284
+ // accidentally hand over.
285
+ const backgroundContent = useMemo(() => renderBackground?.({
286
+ viewport: state.viewport,
287
+ screen: { width, height },
288
+ pixelRatio: devicePixelRatioOf(),
289
+ }), [renderBackground, state.viewport, width, height]);
270
290
  const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, onMeasurementStampDoubleTap, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
271
291
  // Double-click a stamp → fire the consumer callback (e.g. open its value
272
292
  // editor). Runs the SAME hit-test the select tool uses (top-of-z-order), on
@@ -295,6 +315,7 @@ export const AnnotationCanvasInner = (props) => {
295
315
  worldTransform: state.worldTransform,
296
316
  viewport: state.viewport,
297
317
  resolveImageUrl,
318
+ backgroundContent,
298
319
  canvasRef,
299
320
  valueFont,
300
321
  textFontMgr,
@@ -3,6 +3,7 @@ import { type ViewStyle } from 'react-native';
3
3
  import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
4
4
  import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../../types/firestore.js';
5
5
  import { type AnnotationCanvasState, type AnnotationDocumentPatch, type PlacedMeasurementRef, type Selection } from '../../types/annotation.js';
6
+ import type { RenderCanvasBackground } from './backgroundSlot.js';
6
7
  import type { MeasurementRef } from './measurementPicker.js';
7
8
  import type { RequestTextInput, Tool } from './Tool.js';
8
9
  import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
@@ -22,6 +23,7 @@ export interface AnnotationCanvasInnerProps {
22
23
  resolveImageUrl?: (storagePath: string) => Promise<string>;
23
24
  pickMeasurement?: () => Promise<MeasurementRef | null>;
24
25
  requestTextInput?: RequestTextInput;
26
+ renderBackground?: RenderCanvasBackground;
25
27
  renderMeasurementStamp?: RenderMeasurementStamp;
26
28
  onMeasurementStampPress?: (placed: PlacedMeasurementRef) => void;
27
29
  onMeasurementStampLongPress?: (placed: PlacedMeasurementRef) => void;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Group, Path, Rect, RoundedRect, Skia, rect, rrect, useFont, } from '@shopify/react-native-skia';
3
3
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
- import { StyleSheet, TouchableOpacity, View, } from 'react-native';
4
+ import { PixelRatio, StyleSheet, TouchableOpacity, View, } from 'react-native';
5
5
  import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
6
6
  import Animated, { runOnJS, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
7
7
  import { stampTileDims } from './stampLayout.js';
@@ -64,7 +64,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
64
64
  // is only re-synced once each gesture ends. The tool/tap path still hops to
65
65
  // JS (`runOnJS(true)`) since drawing/selection are React-state operations.
66
66
  export const AnnotationCanvasInner = (props) => {
67
- const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, } = props;
67
+ const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, renderBackground, } = props;
68
68
  const valueFont = useFont(stampFontSource, stampValueFontSize);
69
69
  // The animated-viewport implementation needs the Reanimated shared values,
70
70
  // which are created below AFTER the state hook — so the hook gets a stable
@@ -128,6 +128,14 @@ export const AnnotationCanvasInner = (props) => {
128
128
  const zoom = useSharedValue(state.viewport.zoom);
129
129
  const panX = useSharedValue(state.viewport.pan.x);
130
130
  const panY = useSharedValue(state.viewport.pan.y);
131
+ // Document-derived pinch floor (see computeZoomFloor). A shared value rather
132
+ // than a captured number so the gesture memo below needn't rebuild — and
133
+ // can't be caught mid-pinch holding a stale floor — when the document's
134
+ // extent changes.
135
+ const zoomMin = useSharedValue(state.zoomMin);
136
+ useEffect(() => {
137
+ zoomMin.value = state.zoomMin;
138
+ }, [zoomMin, state.zoomMin]);
131
139
  // Set by the viewport gestures' onChange, consumed by their onFinalize: the
132
140
  // post-gesture sync to React state happens iff a gesture actually moved the
133
141
  // viewport, and in onFinalize rather than onEnd because on Android the
@@ -1135,7 +1143,7 @@ export const AnnotationCanvasInner = (props) => {
1135
1143
  const ratio = e.scaleChange;
1136
1144
  if (!Number.isFinite(ratio) || ratio <= 0)
1137
1145
  return;
1138
- const next = Math.min(50, Math.max(0.05, zoom.value * ratio));
1146
+ const next = Math.min(50, Math.max(zoomMin.value, zoom.value * ratio));
1139
1147
  if (next === zoom.value)
1140
1148
  return; // clamped at a bound, or ratio 1
1141
1149
  // World point currently under the focal screen point (using the live
@@ -1863,6 +1871,19 @@ export const AnnotationCanvasInner = (props) => {
1863
1871
  ]);
1864
1872
  const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
1865
1873
  const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
1874
+ // Host substrate (see renderBackground). It is handed `state.viewport` — the
1875
+ // JS/React snapshot the state hook owns — NOT `zoom`/`panX`/`panY`. Those
1876
+ // shared values move every frame of a pan/pinch and never touch React; the
1877
+ // snapshot is written only by setViewport, i.e. at the end of a viewport
1878
+ // gesture (syncViewport), at the end of an animated glide, on the initial
1879
+ // content fit, and from the imperative view methods. So this memo re-runs
1880
+ // strictly on commit — there is no useAnimatedReaction/runOnJS bridging the
1881
+ // live values into it, by design.
1882
+ const backgroundContent = useMemo(() => renderBackground?.({
1883
+ viewport: state.viewport,
1884
+ screen: { width, height },
1885
+ pixelRatio: PixelRatio.get(),
1886
+ }), [renderBackground, state.viewport, width, height]);
1866
1887
  const { renderMeasurementStamp, onMeasurementStampPress, onMeasurementStampLongPress, resolveStampMeasurement, onMeasurementStampRemove, selection, } = props;
1867
1888
  // The Skia scene props, shared by the main canvas and the loupe overlay. The
1868
1889
  // loupe re-runs the SAME scene with a magnifying world transform + a lens
@@ -1875,6 +1896,11 @@ export const AnnotationCanvasInner = (props) => {
1875
1896
  worldTransform,
1876
1897
  resolveImageUrl,
1877
1898
  backgroundSkImages,
1899
+ // Shared with the loupe overlay below (it re-runs this same scene through a
1900
+ // magnifying transform), so the lens shows the substrate magnified instead
1901
+ // of a hole where the page should be. The node is rendered in both trees —
1902
+ // fine for a draw-only slot, which is what the doc-space contract implies.
1903
+ backgroundContent,
1878
1904
  valueFont,
1879
1905
  // Freehand drawing runs on the UI thread via `livePreview`, so the
1880
1906
  // JS-state pen preview is unused on native.
@@ -24,6 +24,7 @@ export interface AnnotationCanvasSkiaProps {
24
24
  viewport?: ViewportState;
25
25
  resolveImageUrl?: (storagePath: string) => Promise<string>;
26
26
  backgroundSkImages?: Record<string, SkImage | null>;
27
+ backgroundContent?: ReactNode;
27
28
  valueFont: SkFont | null;
28
29
  textFontMgr?: SkTypefaceFontProvider | null;
29
30
  penDrawingStroke: AnnotationStroke | null;
@@ -104,5 +105,5 @@ export interface AnnotationCanvasSkiaProps {
104
105
  wrapContent?: (content: ReactNode) => ReactNode;
105
106
  canvasRef?: RefObject<CanvasRef | null>;
106
107
  }
107
- export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
108
+ export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
108
109
  export {};
@@ -80,7 +80,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
80
80
  // since the function-call pattern works identically on native we use it
81
81
  // in both Inners for consistency. Don't add hooks here; this is a plain
82
82
  // JSX-returning helper, not a component.
83
- export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }) => {
83
+ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, viewport, resolveImageUrl, backgroundSkImages, backgroundContent, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, showPlaneHandles, planeDragId, planeDragIndex, planeDragPath, planeDragGridPath, planeDragHandle, planeOutlineScale = 1, planeDragHandleHidden, customPreview, wrapContent, canvasRef, }) => {
84
84
  // Doc rect a SCALE-mode plane grid covers: the first background layer (the
85
85
  // photo being calibrated), else the document rect. Pinned to layer 0 like
86
86
  // the tile-sizing reference — adding a second image must not re-span the
@@ -94,7 +94,7 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
94
94
  width: effectiveCanvas.viewport.width,
95
95
  height: effectiveCanvas.viewport.height,
96
96
  };
97
- const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundLayersOf(effectiveCanvas.viewport).map((layer) => (_jsx(DraggableElement, { isDragging: layer.id === draggingId || layer.id === resizingId, transform: layer.id === resizingId ? resizeTransform : dragTransform, children: _jsx(BackgroundLayerElement, { layer: layer, resolveUrl: resolveImageUrl, skImage: backgroundSkImages?.[layer.id], viewport: viewport, screenWidth: width, screenHeight: height }) }, layer.id))), (effectiveCanvas.planes ?? []).map((plane) => {
97
+ const world = (_jsxs(Group, { transform: worldTransform, children: [backgroundContent, backgroundLayersOf(effectiveCanvas.viewport).map((layer) => (_jsx(DraggableElement, { isDragging: layer.id === draggingId || layer.id === resizingId, transform: layer.id === resizingId ? resizeTransform : dragTransform, children: _jsx(BackgroundLayerElement, { layer: layer, resolveUrl: resolveImageUrl, skImage: backgroundSkImages?.[layer.id], viewport: viewport, screenWidth: width, screenHeight: height }) }, layer.id))), (effectiveCanvas.planes ?? []).map((plane) => {
98
98
  if (plane.id === planeDragId && planeDragPath) {
99
99
  const statics = plane.mode === 'perspective'
100
100
  ? plane.corners
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ViewportState } from './viewport.js';
3
+ export interface CanvasBackgroundSlotArgs {
4
+ viewport: ViewportState;
5
+ screen: {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ pixelRatio: number;
10
+ }
11
+ export type RenderCanvasBackground = (args: CanvasBackgroundSlotArgs) => ReactNode;
@@ -0,0 +1,25 @@
1
+ import type { AnnotationCanvasState, AnnotationPdfDocument, AnnotationPdfPageSize } from '../../types/annotation.js';
2
+ export interface PdfDocRect {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export declare const DEFAULT_PDF_PAGE_GAP = 12;
9
+ export declare const makePdfDocumentId: () => string;
10
+ export declare const pdfPageDocRects: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => PdfDocRect[];
11
+ export declare const pdfDocumentExtent: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ export declare const pdfFirstPageDocRect: (doc: Pick<AnnotationPdfDocument, "pages" | "layout" | "gap">) => PdfDocRect | null;
16
+ export declare const createPdfDocument: (args: {
17
+ id: string;
18
+ storagePath: string;
19
+ pages: AnnotationPdfPageSize[];
20
+ fileName?: string;
21
+ gap?: number;
22
+ }) => AnnotationPdfDocument;
23
+ export declare const DEFAULT_MIN_ZOOM = 0.05;
24
+ export declare const DEFAULT_MAX_ZOOM = 50;
25
+ export declare const computeZoomFloor: (canvas: Pick<AnnotationCanvasState, "viewport">, width: number, height: number) => number;
@@ -0,0 +1,83 @@
1
+ import { fitRectToScreen } from './viewport.js';
2
+ // Doc-space geometry of a PDF substrate, derived from the persisted descriptor
3
+ // alone — no rasterizer, no file. The rules here MUST match the layout the
4
+ // host hands its PDF rasterizer for the same `layout` value (the package's
5
+ // `continuousVertical(gap)`): pages stacked top to bottom, horizontally centred
6
+ // on the widest page, `gap` between consecutive pages, no trailing gap. If the
7
+ // two ever disagree the pages draw offset from where the fit, the thumbnail
8
+ // crop and the tile-scale reference think they are.
9
+ // Default doc-space (PDF point) gap between pages. Same as the package's.
10
+ export const DEFAULT_PDF_PAGE_GAP = 12;
11
+ // Unique within a document across the clients that touch it — same contract
12
+ // as background layer ids (timestamp + module counter). The `pdf-` prefix
13
+ // keeps it distinguishable from `background-*` in the shared backgrounds slot.
14
+ let pdfIdCounter = 0;
15
+ export const makePdfDocumentId = () => `pdf-${Date.now().toString(36)}-${(pdfIdCounter++).toString(36)}`;
16
+ export const pdfPageDocRects = (doc) => {
17
+ // Only one layout exists; a switch keeps the exhaustiveness check honest for
18
+ // the day a second one lands.
19
+ switch (doc.layout) {
20
+ case 'continuousVertical':
21
+ default: {
22
+ let maxWidth = 0;
23
+ for (const p of doc.pages)
24
+ maxWidth = Math.max(maxWidth, p.width);
25
+ const rects = [];
26
+ let y = 0;
27
+ for (const p of doc.pages) {
28
+ rects.push({
29
+ x: (maxWidth - p.width) / 2,
30
+ y,
31
+ width: p.width,
32
+ height: p.height,
33
+ });
34
+ y += p.height + doc.gap;
35
+ }
36
+ return rects;
37
+ }
38
+ }
39
+ };
40
+ // The union of the page rects — what `viewport.width/height` is set to when
41
+ // the document is attached. Never degenerate: a zero-page document would
42
+ // otherwise hand fitRectToScreen a divide-by-zero.
43
+ export const pdfDocumentExtent = (doc) => {
44
+ let width = 0;
45
+ let height = 0;
46
+ for (const r of pdfPageDocRects(doc)) {
47
+ width = Math.max(width, r.x + r.width);
48
+ height = Math.max(height, r.y + r.height);
49
+ }
50
+ return { width: Math.max(1, width), height: Math.max(1, height) };
51
+ };
52
+ // The first page's rect: the tile-scale reference for a PDF-backed canvas
53
+ // (see computeBaseFitZoom). Fitting the WHOLE filmstrip instead would make a
54
+ // tile's footprint ~N× smaller on an N-page document at the same visual zoom
55
+ // — and would grow every existing tile the moment a page is appended.
56
+ export const pdfFirstPageDocRect = (doc) => pdfPageDocRects(doc)[0] ?? null;
57
+ // Build the descriptor from what a host learns by opening the file.
58
+ export const createPdfDocument = (args) => ({
59
+ id: args.id,
60
+ storagePath: args.storagePath,
61
+ ...(args.fileName ? { fileName: args.fileName } : {}),
62
+ pageCount: args.pages.length,
63
+ pages: args.pages.map((p) => ({ width: p.width, height: p.height })),
64
+ layout: 'continuousVertical',
65
+ gap: args.gap ?? DEFAULT_PDF_PAGE_GAP,
66
+ });
67
+ // The zoom floor for a canvas. The historical floor (0.05) predates multi-page
68
+ // documents: a 40-page letter filmstrip is ~32,000 points tall and fits an
69
+ // 800pt screen at zoom ~0.025, so with a fixed floor the opening fit is legal
70
+ // but the first pinch snaps 2× in. The floor therefore follows the document:
71
+ // half the content-fit zoom of the doc rect, never above the historical value
72
+ // (so single-image canvases behave exactly as before).
73
+ export const DEFAULT_MIN_ZOOM = 0.05;
74
+ export const DEFAULT_MAX_ZOOM = 50;
75
+ export const computeZoomFloor = (canvas, width, height) => {
76
+ if (!(width > 0) || !(height > 0))
77
+ return DEFAULT_MIN_ZOOM;
78
+ const { viewport } = canvas;
79
+ const fit = fitRectToScreen({ x: 0, y: 0, width: viewport.width, height: viewport.height }, width, height);
80
+ return fit.zoom > 0
81
+ ? Math.min(DEFAULT_MIN_ZOOM, fit.zoom / 2)
82
+ : DEFAULT_MIN_ZOOM;
83
+ };
@@ -83,6 +83,7 @@ export interface AnnotationCanvasStateApi {
83
83
  translateY: number;
84
84
  }>;
85
85
  viewport: ViewportState;
86
+ zoomMin: number;
86
87
  measurementsById: Map<string, Measurement>;
87
88
  activeTool: Tool | null;
88
89
  toolState: ToolState;
@@ -100,6 +101,31 @@ export interface AnnotationCanvasStateApi {
100
101
  dispatchLongPress(event: CanvasPointerEvent): void;
101
102
  pan(deltaScreen: Vec2): void;
102
103
  zoom(focalScreen: Vec2, nextZoom: number): void;
104
+ /**
105
+ * Zoom by a MULTIPLICATIVE FACTOR about a focal screen point.
106
+ *
107
+ * Same anchoring as `zoom`, but the target zoom is derived INSIDE the
108
+ * functional update, so the factor composes against the viewport React is
109
+ * about to commit rather than against whatever was on screen when the event
110
+ * handler ran.
111
+ *
112
+ * WHY THIS EXISTS. `zoom` takes an absolute target, so a caller has to read
113
+ * the current zoom to compute one — and a read during an event handler is a
114
+ * RENDER-STALE read. Wheel and pinch events arrive in bursts that React
115
+ * batches into a single commit, so every event in the batch computes its
116
+ * target from the SAME committed zoom; `zoomAt` is then handed a zoom it has
117
+ * already reached and no-ops, silently discarding the earlier events' steps.
118
+ * Measured on the web PDF probe: ten wheel events dispatched in one task
119
+ * produced x1.49 where the deltas asked for x54.6 — nine of ten steps lost.
120
+ * (The ANCHOR was never the problem; it is exact to 5e-4 CSS px for whatever
121
+ * pair `zoomAt` is handed.)
122
+ *
123
+ * `zoom` is deliberately left alone: an absolute target is the right shape for
124
+ * a slider, a zoom-to-fit or a native gesture that already tracks its own
125
+ * cumulative scale, and the seven production canvas screens keep their
126
+ * behaviour untouched.
127
+ */
128
+ zoomBy(focalScreen: Vec2, factor: number): void;
103
129
  setViewport(next: ViewportState): void;
104
130
  }
105
131
  export declare const useAnnotationCanvasState: (props: UseAnnotationCanvasStateProps) => AnnotationCanvasStateApi;
@@ -1,7 +1,8 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
3
3
  import { resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
4
- import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
4
+ import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
5
+ import { computeZoomFloor, pdfFirstPageDocRect } from './pdfDocument.js';
5
6
  import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
6
7
  import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
7
8
  // The viewport that frames the document's content inside a screen rect of the
@@ -85,9 +86,19 @@ const computeBaseFitZoom = (canvas, width, height) => {
85
86
  return 1;
86
87
  const { viewport } = canvas;
87
88
  const first = backgroundLayersOf(viewport)[0];
89
+ // A PDF-backed canvas with no image layers references its FIRST PAGE, not
90
+ // the whole filmstrip — see pdfFirstPageDocRect for why.
91
+ const pdfFirstPage = viewport.pdfDocument
92
+ ? pdfFirstPageDocRect(viewport.pdfDocument)
93
+ : null;
88
94
  const baseRect = first
89
95
  ? backgroundLayerDocRect(first)
90
- : { x: 0, y: 0, width: viewport.width, height: viewport.height };
96
+ : (pdfFirstPage ?? {
97
+ x: 0,
98
+ y: 0,
99
+ width: viewport.width,
100
+ height: viewport.height,
101
+ });
91
102
  return fitRectToScreen(baseRect, width, height).zoom;
92
103
  };
93
104
  // Default leader length (doc units) for a placed measurement when no explicit
@@ -129,6 +140,13 @@ export const useAnnotationCanvasState = (props) => {
129
140
  return map;
130
141
  }, [measurements]);
131
142
  const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
143
+ // Document-derived zoom floor (see computeZoomFloor). Behind a ref so the
144
+ // pan/zoom callbacks below stay referentially stable; the native Inner reads
145
+ // `zoomMin` from the returned state and mirrors it into a shared value for
146
+ // the pinch worklet.
147
+ const zoomMin = useMemo(() => computeZoomFloor(effectiveCanvas, width, height), [effectiveCanvas, width, height]);
148
+ const zoomLimitsRef = useRef({ min: zoomMin, max: DEFAULT_ZOOM_LIMITS.max });
149
+ zoomLimitsRef.current = { min: zoomMin, max: DEFAULT_ZOOM_LIMITS.max };
132
150
  // Frame the document the first time it carries a background image. The doc
133
151
  // hydrates asynchronously (it opens empty, then the persisted canvas — with
134
152
  // its background — arrives), and a high-res image at the 1:1 default viewport
@@ -141,8 +159,10 @@ export const useAnnotationCanvasState = (props) => {
141
159
  useEffect(() => {
142
160
  if (didInitialFitRef.current)
143
161
  return;
144
- if (backgroundLayersOf(canvas.viewport).length === 0)
162
+ if (backgroundLayersOf(canvas.viewport).length === 0 &&
163
+ !canvas.viewport.pdfDocument) {
145
164
  return;
165
+ }
146
166
  if (!(width > 0) || !(height > 0))
147
167
  return;
148
168
  didInitialFitRef.current = true;
@@ -290,7 +310,7 @@ export const useAnnotationCanvasState = (props) => {
290
310
  setViewport((v) => panBy(v, deltaScreen));
291
311
  },
292
312
  applyZoom(focalScreen, nextZoom) {
293
- setViewport((v) => zoomAt(v, focalScreen, nextZoom));
313
+ setViewport((v) => zoomAt(v, focalScreen, nextZoom, zoomLimitsRef.current));
294
314
  },
295
315
  }), [
296
316
  canvas,
@@ -396,7 +416,15 @@ export const useAnnotationCanvasState = (props) => {
396
416
  setViewport((v) => panBy(v, deltaScreen));
397
417
  }, []);
398
418
  const zoom = useCallback((focalScreen, nextZoom) => {
399
- setViewport((v) => zoomAt(v, focalScreen, nextZoom));
419
+ setViewport((v) => zoomAt(v, focalScreen, nextZoom, zoomLimitsRef.current));
420
+ }, []);
421
+ // `v.zoom * factor` inside the updater, so a burst of events that React
422
+ // batches into one commit composes instead of each recomputing the same
423
+ // target from the same stale base. See `zoomBy` on the API type.
424
+ const zoomBy = useCallback((focalScreen, factor) => {
425
+ if (!Number.isFinite(factor) || factor <= 0)
426
+ return;
427
+ setViewport((v) => zoomAt(v, focalScreen, v.zoom * factor));
400
428
  }, []);
401
429
  // Imperative API mirror — set via prop so it survives WithSkiaWeb's lazy
402
430
  // boundary on web.
@@ -862,6 +890,7 @@ export const useAnnotationCanvasState = (props) => {
862
890
  effectiveCanvas,
863
891
  worldTransform,
864
892
  viewport,
893
+ zoomMin,
865
894
  measurementsById,
866
895
  activeTool,
867
896
  toolState,
@@ -879,6 +908,7 @@ export const useAnnotationCanvasState = (props) => {
879
908
  dispatchLongPress,
880
909
  pan,
881
910
  zoom,
911
+ zoomBy,
882
912
  setViewport,
883
913
  };
884
914
  };
@@ -24,5 +24,13 @@ export declare const fitRectToScreen: (rect: {
24
24
  width: number;
25
25
  height: number;
26
26
  }, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
27
- export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number) => ViewportState;
27
+ export declare const DEFAULT_ZOOM_LIMITS: {
28
+ readonly min: 0.05;
29
+ readonly max: 50;
30
+ };
31
+ export interface ZoomLimits {
32
+ min: number;
33
+ max: number;
34
+ }
35
+ export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number, limits?: ZoomLimits) => ViewportState;
28
36
  export declare const panBy: (state: ViewportState, deltaScreen: Vec2) => ViewportState;
@@ -64,8 +64,13 @@ export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) =>
64
64
  };
65
65
  // Zoom toward a focal screen point so the world point under the cursor stays
66
66
  // fixed. Used for wheel-zoom on web and pinch on native.
67
- export const zoomAt = (state, focalScreen, nextZoom) => {
68
- const clampedZoom = Math.max(0.05, Math.min(50, nextZoom));
67
+ //
68
+ // `limits` defaults to the historical fixed bounds. A canvas whose document
69
+ // fits below the historical floor (a multi-page PDF filmstrip) passes its own
70
+ // floor — see computeZoomFloor in pdfDocument.ts.
71
+ export const DEFAULT_ZOOM_LIMITS = { min: 0.05, max: 50 };
72
+ export const zoomAt = (state, focalScreen, nextZoom, limits = DEFAULT_ZOOM_LIMITS) => {
73
+ const clampedZoom = Math.max(limits.min, Math.min(limits.max, nextZoom));
69
74
  const focalWorld = {
70
75
  x: focalScreen.x / state.zoom + state.pan.x,
71
76
  y: focalScreen.y / state.zoom + state.pan.y,
@@ -1,4 +1,4 @@
1
- import { type AnnotationBackgroundLayer, type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
1
+ import { type AnnotationBackgroundLayer, type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationPdfPageSize, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
2
2
  import type { AnnotationScope, ImageBlob } from '../AnnotationDataProvider.js';
3
3
  export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
4
4
  export interface UseAnnotationCanvasDocOptions {
@@ -30,6 +30,10 @@ export interface UseAnnotationCanvasDocResult {
30
30
  width: number;
31
31
  height: number;
32
32
  }) => Promise<string>;
33
+ addPdfDocument: (blob: ImageBlob, meta: {
34
+ pages: AnnotationPdfPageSize[];
35
+ fileName?: string;
36
+ }) => Promise<string>;
33
37
  updateBackgroundImage: (id: string, patch: Partial<Omit<AnnotationBackgroundLayer, 'id'>>) => void;
34
38
  removeBackgroundImage: (id: string) => Promise<void>;
35
39
  setBackgroundImage: (blob: ImageBlob, dims: {
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.js';
3
+ import { createPdfDocument, makePdfDocumentId, pdfDocumentExtent, } from '../../canvas/pdfDocument.js';
3
4
  import { backgroundLayersOf, placeNewBackgroundLayer, defaultBackgroundLayerLocked, } from '../../canvas/backgroundLayers.js';
4
5
  import { imageDocRect } from '../../canvas/viewport.js';
5
6
  import { FileUploadType } from '../../../types/firestore.js';
@@ -395,6 +396,30 @@ export const useAnnotationCanvasDoc = (options) => {
395
396
  });
396
397
  return layerId;
397
398
  }, [ensureFileId, uploadImage, onCommit, fallbackViewport]);
399
+ const addPdfDocument = useCallback(async (blob, meta) => {
400
+ if (meta.pages.length === 0) {
401
+ throw new Error('Cannot attach a PDF with no pages');
402
+ }
403
+ const fileId = await ensureFileId();
404
+ const id = makePdfDocumentId();
405
+ const ref = await uploadImage(fileId, `backgrounds/${id}`, blob);
406
+ const document = createPdfDocument({
407
+ id,
408
+ storagePath: ref.storagePath,
409
+ pages: meta.pages,
410
+ fileName: meta.fileName,
411
+ });
412
+ // One patch, two ops: the extent must land WITH the document so no
413
+ // render sees pages laid out past the doc rect (or a doc rect with no
414
+ // pages behind it). Undo reverts both together.
415
+ onCommit({
416
+ ops: [
417
+ { op: 'setPdfDocument', document },
418
+ { op: 'setViewport', patch: pdfDocumentExtent(document) },
419
+ ],
420
+ });
421
+ return id;
422
+ }, [ensureFileId, uploadImage, onCommit]);
398
423
  const updateBackgroundImage = useCallback((id, patch) => {
399
424
  onCommit({ ops: [{ op: 'updateBackgroundImage', id, patch }] });
400
425
  }, [onCommit]);
@@ -521,6 +546,7 @@ export const useAnnotationCanvasDoc = (options) => {
521
546
  refreshThumbnail,
522
547
  ensureFileId,
523
548
  addBackgroundImage,
549
+ addPdfDocument,
524
550
  updateBackgroundImage,
525
551
  removeBackgroundImage,
526
552
  setBackgroundImage,
package/dist/exports.d.ts CHANGED
@@ -31,9 +31,11 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
31
31
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
32
32
  export type { FileRef, MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
33
33
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
34
+ export type { CanvasBackgroundSlotArgs, RenderCanvasBackground, } from './annotation/canvas/backgroundSlot.js';
34
35
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
35
36
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
36
- export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
37
+ export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, type ViewportApi, type ViewportState, type ZoomLimits, } from './annotation/canvas/viewport.js';
38
+ export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, type PdfDocRect, } from './annotation/canvas/pdfDocument.js';
37
39
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
38
40
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
39
41
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
package/dist/exports.js CHANGED
@@ -38,7 +38,8 @@ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
38
38
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
39
39
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
40
40
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
41
- export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
41
+ export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './annotation/canvas/viewport.js';
42
+ export { DEFAULT_PDF_PAGE_GAP, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM, makePdfDocumentId, createPdfDocument, pdfPageDocRects, pdfDocumentExtent, pdfFirstPageDocRect, computeZoomFloor, } from './annotation/canvas/pdfDocument.js';
42
43
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
43
44
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
44
45
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
@@ -145,12 +145,27 @@ export interface AnnotationBackgroundLayer {
145
145
  transform: BackgroundLayerTransform;
146
146
  locked?: boolean;
147
147
  }
148
+ export interface AnnotationPdfPageSize {
149
+ width: number;
150
+ height: number;
151
+ }
152
+ export type AnnotationPdfLayout = 'continuousVertical';
153
+ export interface AnnotationPdfDocument {
154
+ id: AnnotationElementId;
155
+ storagePath: string;
156
+ fileName?: string;
157
+ pageCount: number;
158
+ pages: AnnotationPdfPageSize[];
159
+ layout: AnnotationPdfLayout;
160
+ gap: number;
161
+ }
148
162
  export interface AnnotationViewport {
149
163
  width: number;
150
164
  height: number;
151
165
  backgroundImage?: AnnotationBackgroundImage;
152
166
  backgroundFit?: BackgroundFit;
153
167
  backgroundImages?: AnnotationBackgroundLayer[];
168
+ pdfDocument?: AnnotationPdfDocument;
154
169
  }
155
170
  export type TileScalePlatform = 'web' | 'mobile';
156
171
  export type TileScaleTarget = 'measurement' | 'header' | 'file';
@@ -225,6 +240,9 @@ export type AnnotationPatchOp = {
225
240
  } | {
226
241
  op: 'setViewport';
227
242
  patch: Partial<AnnotationViewport>;
243
+ } | {
244
+ op: 'setPdfDocument';
245
+ document: AnnotationPdfDocument | null;
228
246
  } | {
229
247
  op: 'setTileScaleFactor';
230
248
  value: number;