@reekon-tools/boldr-utils 1.24.0 → 1.26.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 (78) hide show
  1. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
  2. package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -1
  3. package/dist/annotation/canvas/viewport.d.ts +10 -0
  4. package/dist/annotation/canvas/viewport.js +26 -0
  5. package/dist/annotation/data/hooks/usePdfCanvasDoc.js +22 -13
  6. package/dist/canvas/AnnotationCanvas.d.ts +11 -0
  7. package/dist/canvas/AnnotationCanvas.js +10 -0
  8. package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
  9. package/dist/canvas/AnnotationCanvas.native.js +6 -0
  10. package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
  11. package/dist/canvas/AnnotationCanvasInner.js +219 -0
  12. package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
  13. package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
  14. package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
  15. package/dist/canvas/AnnotationCanvasSkia.js +20 -0
  16. package/dist/canvas/Tool.d.ts +38 -0
  17. package/dist/canvas/Tool.js +1 -0
  18. package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
  19. package/dist/canvas/elements/BackgroundImageElement.js +37 -0
  20. package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
  21. package/dist/canvas/elements/MeasurementStampElement.js +30 -0
  22. package/dist/canvas/elements/ShapeElement.d.ts +7 -0
  23. package/dist/canvas/elements/ShapeElement.js +62 -0
  24. package/dist/canvas/elements/StrokeElement.d.ts +7 -0
  25. package/dist/canvas/elements/StrokeElement.js +18 -0
  26. package/dist/canvas/measurementPicker.d.ts +10 -0
  27. package/dist/canvas/measurementPicker.js +1 -0
  28. package/dist/canvas/measurementStampOverlay.d.ts +11 -0
  29. package/dist/canvas/measurementStampOverlay.js +1 -0
  30. package/dist/canvas/pointerAdapter.d.ts +3 -0
  31. package/dist/canvas/pointerAdapter.js +19 -0
  32. package/dist/canvas/stampLayout.d.ts +5 -0
  33. package/dist/canvas/stampLayout.js +14 -0
  34. package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
  35. package/dist/canvas/tools/measurementStampTool.js +37 -0
  36. package/dist/canvas/tools/panTool.d.ts +5 -0
  37. package/dist/canvas/tools/panTool.js +25 -0
  38. package/dist/canvas/tools/penTool.d.ts +13 -0
  39. package/dist/canvas/tools/penTool.js +68 -0
  40. package/dist/canvas/tools/selectTool.d.ts +2 -0
  41. package/dist/canvas/tools/selectTool.js +182 -0
  42. package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
  43. package/dist/canvas/useAnnotationCanvasState.js +210 -0
  44. package/dist/canvas/viewport.d.ts +16 -0
  45. package/dist/canvas/viewport.js +54 -0
  46. package/dist/data/AnnotationDataContext.d.ts +8 -0
  47. package/dist/data/AnnotationDataContext.js +11 -0
  48. package/dist/data/AnnotationDataProvider.d.ts +65 -0
  49. package/dist/data/AnnotationDataProvider.js +4 -0
  50. package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
  51. package/dist/data/InMemoryAnnotationProvider.js +197 -0
  52. package/dist/data/canvasPersistence.d.ts +3 -0
  53. package/dist/data/canvasPersistence.js +26 -0
  54. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
  55. package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
  56. package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
  57. package/dist/data/hooks/useAnnotationDoc.js +33 -0
  58. package/dist/data/hooks/useAnnotationList.d.ts +7 -0
  59. package/dist/data/hooks/useAnnotationList.js +26 -0
  60. package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
  61. package/dist/data/hooks/useAnnotationMutations.js +11 -0
  62. package/dist/export/buildJobCsv.js +2 -3
  63. package/dist/exports.d.ts +2 -2
  64. package/dist/exports.js +2 -2
  65. package/dist/formulas/calculateFormula.d.ts +1 -1
  66. package/dist/formulas/calculateFormula.js +22 -24
  67. package/dist/formulas/evaluateFormula.d.ts +10 -0
  68. package/dist/formulas/evaluateFormula.js +17 -2
  69. package/dist/hooks/useParseMeasurement.d.ts +4 -0
  70. package/dist/hooks/useParseMeasurement.js +14 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.js +1 -1
  73. package/dist/server.d.ts +2 -2
  74. package/dist/server.js +6 -2
  75. package/dist/types/firestore.d.ts +8 -0
  76. package/dist/utils/evaluateFormula.d.ts +20 -0
  77. package/dist/utils/evaluateFormula.js +31 -0
  78. package/package.json +1 -1
@@ -23,6 +23,17 @@ export interface AnnotationCanvasHandle {
23
23
  animated?: boolean;
24
24
  durationMs?: number;
25
25
  }): void;
26
+ fitDocRect(rect: {
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }, opts?: {
32
+ animated?: boolean;
33
+ durationMs?: number;
34
+ paddingPx?: number;
35
+ maxZoom?: number;
36
+ }): void;
26
37
  getTileScaleFactor(): number;
27
38
  getHeaderTileScaleFactor(): number;
28
39
  getFileTileScaleFactor(): number;
@@ -2,7 +2,7 @@ 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
4
  import { SNAP_RADIUS_PX } from './snapGeometry.js';
5
- import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
5
+ import { createViewportApi, fitDocRectViewport, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
6
6
  import { computeZoomFloor, pdfFirstPageDocRect } from './pdfDocument.js';
7
7
  import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
8
8
  import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
@@ -555,6 +555,22 @@ export const useAnnotationCanvasState = (props) => {
555
555
  setViewport(target);
556
556
  }
557
557
  },
558
+ fitDocRect(rect, opts) {
559
+ if (!(width > 0) || !(height > 0))
560
+ return;
561
+ const limits = zoomLimitsRef.current;
562
+ const target = fitDocRectViewport(rect, width, height, {
563
+ paddingPx: opts?.paddingPx,
564
+ minZoom: limits.min,
565
+ maxZoom: Math.min(opts?.maxZoom ?? limits.max, limits.max),
566
+ });
567
+ if (opts?.animated && animateViewport) {
568
+ animateViewport(target, opts.durationMs ?? 450);
569
+ }
570
+ else {
571
+ setViewport(target);
572
+ }
573
+ },
558
574
  getTileScaleFactor() {
559
575
  return ctxRef.current.tileScaleFactor;
560
576
  },
@@ -24,6 +24,16 @@ 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 fitDocRectViewport: (rect: {
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ }, screenWidth: number, screenHeight: number, limits?: {
33
+ paddingPx?: number;
34
+ minZoom?: number;
35
+ maxZoom?: number;
36
+ }) => ViewportState;
27
37
  export declare const DEFAULT_ZOOM_LIMITS: {
28
38
  readonly min: 0.05;
29
39
  readonly max: 50;
@@ -62,6 +62,32 @@ export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) =>
62
62
  pan: { x: rect.x - offsetX / zoom, y: rect.y - offsetY / zoom },
63
63
  };
64
64
  };
65
+ // Frame a DOC-space rect (a text-search hit, a tile) centred in the screen:
66
+ // zoom so the rect plus `paddingPx` of screen padding fits, clamped to
67
+ // [minZoom, maxZoom] — so a two-word hit does not zoom to 50× and a whole-page
68
+ // rect cannot go below the document's floor — then centre the rect at that
69
+ // zoom (screen = (world − pan) · zoom, so pan = centre − half-screen / zoom).
70
+ export const fitDocRectViewport = (rect, screenWidth, screenHeight, limits = {}) => {
71
+ const safe = {
72
+ x: rect.x,
73
+ y: rect.y,
74
+ width: Math.max(1, rect.width),
75
+ height: Math.max(1, rect.height),
76
+ };
77
+ const fit = fitRectToScreen(safe, screenWidth, screenHeight, limits.paddingPx ?? 0);
78
+ const upper = limits.maxZoom ?? Number.POSITIVE_INFINITY;
79
+ const lower = limits.minZoom ?? 0;
80
+ const zoom = Math.max(lower, Math.min(fit.zoom, upper));
81
+ const cx = safe.x + safe.width / 2;
82
+ const cy = safe.y + safe.height / 2;
83
+ return {
84
+ zoom,
85
+ pan: {
86
+ x: cx - screenWidth / (2 * zoom),
87
+ y: cy - screenHeight / (2 * zoom),
88
+ },
89
+ };
90
+ };
65
91
  // Zoom toward a focal screen point so the world point under the cursor stays
66
92
  // fixed. Used for wheel-zoom on web and pinch on native.
67
93
  //
@@ -23,14 +23,17 @@ export const usePdfCanvasDoc = (options) => {
23
23
  const provider = useAnnotationData();
24
24
  const { data: file, loading: fileLoading, error: fileError, } = useAnnotationDoc(scope, fileId);
25
25
  const pdfCanvas = file?.fileData.pdfCanvas ?? null;
26
- const [pageDoc, setPageDoc] = useState(null);
26
+ // The current page's doc, TAGGED with the page it answers for. On a page
27
+ // turn the previous page's doc is still in state until the new subscription
28
+ // reports, and hydrating the new page from it is exactly how annotations
29
+ // land on the wrong page. `state` null = the page has no doc yet.
30
+ const [pageSnapshot, setPageSnapshot] = useState(null);
27
31
  const [pageLoading, setPageLoading] = useState(true);
28
32
  const [pageError, setPageError] = useState(null);
29
33
  const [working, setWorking] = useState(null);
30
34
  const [saveStatus, setSaveStatus] = useState('idle');
31
35
  const workingRef = useRef(null);
32
36
  const pdfCanvasRef = useRef(null);
33
- const pageDocRef = useRef(null);
34
37
  const statusRef = useRef('idle');
35
38
  const pageIndexRef = useRef(pageIndex);
36
39
  const scopeRef = useRef(scope);
@@ -48,10 +51,9 @@ export const usePdfCanvasDoc = (options) => {
48
51
  const thumbnailPromiseRef = useRef(undefined);
49
52
  // A page whose edits must be persisted although it is no longer the current
50
53
  // page (the user turned the page with unsaved edits).
51
- const outgoingRef = useRef(null);
54
+ const outgoingRef = useRef([]);
52
55
  workingRef.current = working;
53
56
  pdfCanvasRef.current = pdfCanvas;
54
- pageDocRef.current = pageDoc;
55
57
  statusRef.current = saveStatus;
56
58
  scopeRef.current = scope;
57
59
  fileIdRef.current = fileId;
@@ -65,14 +67,17 @@ export const usePdfCanvasDoc = (options) => {
65
67
  // Subscribe to the current page's doc.
66
68
  useEffect(() => {
67
69
  if (!scope || !fileId) {
68
- setPageDoc(null);
70
+ setPageSnapshot(null);
69
71
  setPageLoading(false);
70
72
  return;
71
73
  }
74
+ // Drop the previous page's snapshot synchronously so nothing can read it
75
+ // as this page's while the subscription is still connecting.
76
+ setPageSnapshot(null);
72
77
  setPageLoading(true);
73
78
  setPageError(null);
74
79
  const unsubscribe = provider.subscribePage(scope, fileId, pageIndex, (doc) => {
75
- setPageDoc(doc);
80
+ setPageSnapshot({ pageIndex, state: doc });
76
81
  setPageLoading(false);
77
82
  }, (err) => {
78
83
  setPageError(err);
@@ -154,10 +159,10 @@ export const usePdfCanvasDoc = (options) => {
154
159
  // One flush pass: an outgoing page first (if a page turn left one behind),
155
160
  // then the current page when it is dirty.
156
161
  const runFlush = useCallback(async () => {
157
- const outgoing = outgoingRef.current;
158
- if (outgoing) {
159
- outgoingRef.current = null;
160
- await persist(outgoing);
162
+ // Every page left behind with unsaved edits, in the order they were
163
+ // left: two quick turns must not lose the middle page.
164
+ while (outgoingRef.current.length > 0) {
165
+ await persist(outgoingRef.current.shift());
161
166
  }
162
167
  const canvas = workingRef.current;
163
168
  const data = pdfCanvasRef.current;
@@ -200,11 +205,11 @@ export const usePdfCanvasDoc = (options) => {
200
205
  if (previous &&
201
206
  data &&
202
207
  (statusRef.current === 'dirty' || statusRef.current === 'error')) {
203
- outgoingRef.current = {
208
+ outgoingRef.current.push({
204
209
  pageIndex: pageIndexRef.current,
205
210
  canvas: previous,
206
211
  pdfCanvas: data,
207
- };
212
+ });
208
213
  void flush();
209
214
  }
210
215
  pageIndexRef.current = pageIndex;
@@ -225,6 +230,10 @@ export const usePdfCanvasDoc = (options) => {
225
230
  return;
226
231
  if (pageIndex >= pdfCanvas.document.pages.length)
227
232
  return;
233
+ // Only a snapshot for THIS page counts (see pageSnapshot).
234
+ if (!pageSnapshot || pageSnapshot.pageIndex !== pageIndex)
235
+ return;
236
+ const pageDoc = pageSnapshot.state;
228
237
  if (workingRef.current === null) {
229
238
  const next = pageStateToCanvasState(pdfCanvas, pageDoc, pageIndex);
230
239
  workingRef.current = next;
@@ -241,7 +250,7 @@ export const usePdfCanvasDoc = (options) => {
241
250
  workingRef.current = next;
242
251
  setWorking(next);
243
252
  // eslint-disable-next-line react-hooks/exhaustive-deps
244
- }, [pdfCanvas, pageDoc, pageLoading, pageIndex]);
253
+ }, [pdfCanvas, pageSnapshot, pageLoading, pageIndex]);
245
254
  const onCommit = useCallback((patch) => {
246
255
  editSeqRef.current += 1;
247
256
  setWorking((prev) => {
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
+ export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
+ export interface CanvasKitOpts {
5
+ locateFile?: (file: string) => string;
6
+ }
7
+ export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
+ fallback?: ReactNode;
9
+ canvasKitOpts?: CanvasKitOpts;
10
+ };
11
+ export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
+ // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
+ // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
+ // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
+ // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
+ // and skips WithSkiaWeb entirely (no wasm to load).
8
+ export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
+ default: m.AnnotationCanvasInner,
10
+ })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -0,0 +1,8 @@
1
+ import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
+ export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
+ export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
+ export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
+ fallback?: unknown;
6
+ canvasKitOpts?: unknown;
7
+ };
8
+ export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
+ export const AnnotationCanvas = (props) => {
4
+ const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
+ return _jsx(AnnotationCanvasInner, { ...rest });
6
+ };
@@ -0,0 +1,39 @@
1
+ import { type CSSProperties, type MutableRefObject } from 'react';
2
+ import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
3
+ import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
4
+ import type { MeasurementRef } from './measurementPicker.js';
5
+ import type { Tool } from './Tool.js';
6
+ import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
7
+ import { type ViewportState } from './viewport.js';
8
+ import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
9
+ export type { AnnotationCanvasHandle };
10
+ export interface AnnotationCanvasInnerProps {
11
+ canvas: AnnotationCanvasState;
12
+ onCommit(patch: AnnotationDocumentPatch): void;
13
+ tools: Tool[];
14
+ activeToolId: string;
15
+ selection: Selection | null;
16
+ onSelectionChange(selection: Selection | null): void;
17
+ measurements?: Measurement[];
18
+ fallbackUnit?: Units;
19
+ fractionalTolerance?: FractionalTolerance;
20
+ decimalTolerance?: DecimalTolerance;
21
+ resolveImageUrl?: (storagePath: string) => Promise<string>;
22
+ pickMeasurement?: () => Promise<MeasurementRef | null>;
23
+ renderMeasurementStamp?: RenderMeasurementStamp;
24
+ stampFontSource?: unknown;
25
+ stampValueFontSize?: number;
26
+ stampLabelFontSize?: number;
27
+ gestures?: GestureConfig;
28
+ width: number;
29
+ height: number;
30
+ initialViewport?: ViewportState;
31
+ style?: CSSProperties;
32
+ imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
33
+ }
34
+ export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
35
+ export interface GestureConfig {
36
+ wheel?: 'zoom' | 'pan' | 'auto';
37
+ panTriggers?: PanTrigger[];
38
+ }
39
+ export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,219 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFont } from '@shopify/react-native-skia';
3
+ import { useCallback, useEffect, useRef, } from 'react';
4
+ import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
5
+ import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
6
+ import { STAMP_TILE_SIZE } from './stampLayout.js';
7
+ const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
8
+ export const AnnotationCanvasInner = (props) => {
9
+ const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, gestures, width, height, style, activeToolId, tools, } = props;
10
+ const wheelMode = gestures?.wheel ?? 'auto';
11
+ const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
12
+ const allowSpacePan = panTriggers.includes('space');
13
+ const allowMiddlePan = panTriggers.includes('middleMouse');
14
+ const allowRightPan = panTriggers.includes('rightMouse');
15
+ const valueFont = useFont(stampFontSource, stampValueFontSize);
16
+ const labelFont = useFont(stampFontSource, stampLabelFontSize);
17
+ const state = useAnnotationCanvasState(props);
18
+ const containerRef = useRef(null);
19
+ const panGestureRef = useRef(null);
20
+ const spaceDownRef = useRef(false);
21
+ const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
22
+ const toCanvasPointer = useCallback((event) => {
23
+ const rect = containerRef.current?.getBoundingClientRect();
24
+ const screen = {
25
+ x: event.clientX - (rect?.left ?? 0),
26
+ y: event.clientY - (rect?.top ?? 0),
27
+ };
28
+ return {
29
+ pointerId: event.pointerId,
30
+ screen,
31
+ world: state.ctx.viewport.screenToWorld(screen),
32
+ pressure: event.pressure || undefined,
33
+ shiftKey: event.shiftKey,
34
+ altKey: event.altKey,
35
+ metaKey: event.metaKey,
36
+ ctrlKey: event.ctrlKey,
37
+ };
38
+ }, [state.ctx.viewport]);
39
+ const isPanTriggerDown = useCallback((event) => {
40
+ if (allowMiddlePan && event.button === 1)
41
+ return true;
42
+ if (allowRightPan && event.button === 2)
43
+ return true;
44
+ if (allowSpacePan && event.button === 0 && spaceDownRef.current)
45
+ return true;
46
+ return false;
47
+ }, [allowMiddlePan, allowRightPan, allowSpacePan]);
48
+ const handlePointerDown = useCallback((event) => {
49
+ const rect = containerRef.current?.getBoundingClientRect();
50
+ const screen = {
51
+ x: event.clientX - (rect?.left ?? 0),
52
+ y: event.clientY - (rect?.top ?? 0),
53
+ };
54
+ if (isPanTriggerDown(event)) {
55
+ panGestureRef.current = { pointerId: event.pointerId, lastScreen: screen };
56
+ event.currentTarget.setPointerCapture(event.pointerId);
57
+ event.preventDefault();
58
+ return;
59
+ }
60
+ event.currentTarget.setPointerCapture(event.pointerId);
61
+ state.dispatchPointerDown(toCanvasPointer(event));
62
+ }, [state, toCanvasPointer, isPanTriggerDown]);
63
+ const handlePointerMove = useCallback((event) => {
64
+ const pan = panGestureRef.current;
65
+ if (pan && event.pointerId === pan.pointerId) {
66
+ const rect = containerRef.current?.getBoundingClientRect();
67
+ const screen = {
68
+ x: event.clientX - (rect?.left ?? 0),
69
+ y: event.clientY - (rect?.top ?? 0),
70
+ };
71
+ state.pan({
72
+ x: screen.x - pan.lastScreen.x,
73
+ y: screen.y - pan.lastScreen.y,
74
+ });
75
+ panGestureRef.current = { pointerId: pan.pointerId, lastScreen: screen };
76
+ return;
77
+ }
78
+ state.dispatchPointerMove(toCanvasPointer(event));
79
+ }, [state, toCanvasPointer]);
80
+ const handlePointerUp = useCallback((event) => {
81
+ const pan = panGestureRef.current;
82
+ if (pan && event.pointerId === pan.pointerId) {
83
+ panGestureRef.current = null;
84
+ return;
85
+ }
86
+ state.dispatchPointerUp(toCanvasPointer(event));
87
+ }, [state, toCanvasPointer]);
88
+ const handlePointerCancel = useCallback(() => {
89
+ panGestureRef.current = null;
90
+ state.dispatchPointerCancel();
91
+ }, [state]);
92
+ const handleWheel = useCallback((event) => {
93
+ const rect = containerRef.current?.getBoundingClientRect();
94
+ const focal = {
95
+ x: event.clientX - (rect?.left ?? 0),
96
+ y: event.clientY - (rect?.top ?? 0),
97
+ };
98
+ if (event.ctrlKey || event.metaKey) {
99
+ const factor = Math.exp(-event.deltaY / 200);
100
+ state.zoom(focal, state.ctx.viewport.state.zoom * factor);
101
+ return;
102
+ }
103
+ if (wheelMode === 'zoom') {
104
+ const factor = Math.exp(-event.deltaY / 300);
105
+ state.zoom(focal, state.ctx.viewport.state.zoom * factor);
106
+ return;
107
+ }
108
+ if (event.shiftKey) {
109
+ state.pan({ x: event.deltaY, y: event.deltaX });
110
+ return;
111
+ }
112
+ state.pan({ x: event.deltaX, y: event.deltaY });
113
+ }, [state, wheelMode]);
114
+ const handleContextMenu = useCallback((event) => {
115
+ if (allowRightPan)
116
+ event.preventDefault();
117
+ }, [allowRightPan]);
118
+ useEffect(() => {
119
+ const el = containerRef.current;
120
+ if (!el)
121
+ return;
122
+ const stop = (e) => {
123
+ if (e.target === el || el.contains(e.target)) {
124
+ e.preventDefault();
125
+ }
126
+ };
127
+ el.addEventListener('wheel', stop, { passive: false });
128
+ return () => el.removeEventListener('wheel', stop);
129
+ }, []);
130
+ useEffect(() => {
131
+ if (!allowSpacePan)
132
+ return;
133
+ const onKeyDown = (e) => {
134
+ if (e.code === 'Space' && !e.repeat) {
135
+ spaceDownRef.current = true;
136
+ if (containerRef.current)
137
+ containerRef.current.style.cursor = 'grab';
138
+ }
139
+ };
140
+ const onKeyUp = (e) => {
141
+ if (e.code === 'Space') {
142
+ spaceDownRef.current = false;
143
+ if (containerRef.current)
144
+ containerRef.current.style.cursor = '';
145
+ }
146
+ };
147
+ window.addEventListener('keydown', onKeyDown);
148
+ window.addEventListener('keyup', onKeyUp);
149
+ return () => {
150
+ window.removeEventListener('keydown', onKeyDown);
151
+ window.removeEventListener('keyup', onKeyUp);
152
+ };
153
+ }, [allowSpacePan]);
154
+ const containerStyle = {
155
+ position: 'relative',
156
+ width,
157
+ height,
158
+ overflow: 'hidden',
159
+ touchAction: 'none',
160
+ userSelect: 'none',
161
+ cursor: activeTool?.cursor ?? 'default',
162
+ ...style,
163
+ };
164
+ const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
165
+ const { renderMeasurementStamp, selection } = props;
166
+ return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
167
+ width,
168
+ height,
169
+ effectiveCanvas: state.effectiveCanvas,
170
+ worldTransform: state.worldTransform,
171
+ measurementsById: state.measurementsById,
172
+ fallbackUnit,
173
+ fractionalTolerance,
174
+ decimalTolerance,
175
+ resolveImageUrl,
176
+ valueFont,
177
+ labelFont,
178
+ hideMeasurementStamps: !!renderMeasurementStamp,
179
+ penDrawingStroke: state.penDrawingStroke,
180
+ customPreview,
181
+ }), renderMeasurementStamp && (_jsx("div", { style: {
182
+ position: 'absolute',
183
+ inset: 0,
184
+ pointerEvents: 'none',
185
+ }, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
186
+ const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
187
+ const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
188
+ const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
189
+ const isSelected = selection?.ids.includes(placed.id) ?? false;
190
+ return (_jsxs("div", { style: {
191
+ position: 'absolute',
192
+ left: 0,
193
+ top: 0,
194
+ width: size,
195
+ height: size,
196
+ transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
197
+ }, children: [renderMeasurementStamp({
198
+ placed,
199
+ measurement: state.measurementsById.get(placed.measurementId) ?? null,
200
+ selected: isSelected,
201
+ size,
202
+ zoom: state.viewport.zoom,
203
+ }), isSelected && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
204
+ e.stopPropagation();
205
+ state.ctx.commit({
206
+ ops: [{ op: 'removeMeasurement', id: placed.id }],
207
+ });
208
+ state.ctx.setSelection(null);
209
+ }, style: {
210
+ position: 'absolute',
211
+ top: -10,
212
+ right: -10,
213
+ width: 40,
214
+ height: 40,
215
+ cursor: 'pointer',
216
+ pointerEvents: 'auto',
217
+ } }))] }, placed.id));
218
+ }) }))] }));
219
+ };
@@ -0,0 +1,35 @@
1
+ import { type MutableRefObject } from 'react';
2
+ import { type ViewStyle } from 'react-native';
3
+ import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
4
+ import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
5
+ import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
6
+ import type { MeasurementRef } from './measurementPicker.js';
7
+ import type { Tool } from './Tool.js';
8
+ import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
9
+ import type { ViewportState } from './viewport.js';
10
+ export type { AnnotationCanvasHandle };
11
+ export interface AnnotationCanvasInnerProps {
12
+ canvas: AnnotationCanvasState;
13
+ onCommit(patch: AnnotationDocumentPatch): void;
14
+ tools: Tool[];
15
+ activeToolId: string;
16
+ selection: Selection | null;
17
+ onSelectionChange(selection: Selection | null): void;
18
+ measurements?: Measurement[];
19
+ fallbackUnit?: Units;
20
+ fractionalTolerance?: FractionalTolerance;
21
+ decimalTolerance?: DecimalTolerance;
22
+ resolveImageUrl?: (storagePath: string) => Promise<string>;
23
+ pickMeasurement?: () => Promise<MeasurementRef | null>;
24
+ renderMeasurementStamp?: RenderMeasurementStamp;
25
+ stampFontSource?: unknown;
26
+ stampValueFontSize?: number;
27
+ stampLabelFontSize?: number;
28
+ gestures?: unknown;
29
+ width: number;
30
+ height: number;
31
+ initialViewport?: ViewportState;
32
+ style?: ViewStyle;
33
+ imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
34
+ }
35
+ export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;