@reekon-tools/boldr-utils 1.6.28 → 1.6.30

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 (79) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +8 -3
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +21 -17
  3. package/dist/annotation/canvas/Tool.d.ts +4 -3
  4. package/dist/annotation/canvas/stampLayout.d.ts +2 -1
  5. package/dist/annotation/canvas/stampLayout.js +23 -11
  6. package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
  7. package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
  8. package/dist/annotation/canvas/tools/panTool.js +1 -1
  9. package/dist/annotation/canvas/tools/selectTool.d.ts +4 -1
  10. package/dist/annotation/canvas/tools/selectTool.js +26 -14
  11. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +5 -1
  12. package/dist/annotation/canvas/useAnnotationCanvasState.js +56 -2
  13. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +1 -0
  14. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +12 -4
  15. package/dist/exports.d.ts +2 -2
  16. package/dist/exports.js +2 -2
  17. package/dist/types/annotation.d.ts +3 -0
  18. package/dist/types/annotation.js +14 -4
  19. package/package.json +1 -1
  20. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  21. package/dist/canvas/AnnotationCanvas.js +0 -10
  22. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  23. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  24. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  25. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  26. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  27. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  28. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  29. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  30. package/dist/canvas/Tool.d.ts +0 -38
  31. package/dist/canvas/Tool.js +0 -1
  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
@@ -32,7 +32,7 @@ const makeClientId = () => `${Date.now().toString(36)}-${Math.floor(Math.random(
32
32
  // no `fileId` was supplied. Composes the existing low-level hooks so it stays
33
33
  // decoupled from any specific Firebase SDK.
34
34
  export const useAnnotationCanvasDoc = (options) => {
35
- const { scope, fileId, fallbackViewport, debounceMs = 800, createSeed, onFileCreated, onSaveError, captureThumbnail, debugLogging = false, } = options;
35
+ const { scope, fileId, fallbackViewport, debounceMs = 800, autoSave = true, createSeed, onFileCreated, onSaveError, captureThumbnail, debugLogging = false, } = options;
36
36
  const { data, loading, error } = useAnnotationDoc(scope, fileId);
37
37
  const { create, update, uploadImage, deleteImage } = useAnnotationMutations(scope ?? EMPTY_SCOPE);
38
38
  const [working, setWorking] = useState(null);
@@ -142,8 +142,12 @@ export const useAnnotationCanvasDoc = (options) => {
142
142
  return;
143
143
  }
144
144
  // A write of ours is queued or in flight — ignore the snapshot; it is
145
- // either the echo of our write or about to be superseded by it.
146
- if (statusRef.current === 'saving' || timerRef.current !== null)
145
+ // either the echo of our write or about to be superseded by it. 'dirty'
146
+ // matters for the explicit-save mode (autoSave false), where local edits
147
+ // sit unsaved indefinitely and must not be clobbered by a remote write.
148
+ if (statusRef.current === 'saving' ||
149
+ statusRef.current === 'dirty' ||
150
+ timerRef.current !== null)
147
151
  return;
148
152
  const incoming = data?.fileData.canvas;
149
153
  if (!incoming)
@@ -292,13 +296,17 @@ export const useAnnotationCanvasDoc = (options) => {
292
296
  return next;
293
297
  });
294
298
  setStatus('dirty');
299
+ // Explicit-save mode: the edit stays local (status 'dirty') until the
300
+ // caller invokes save() — no debounce, no write.
301
+ if (!autoSave)
302
+ return;
295
303
  if (timerRef.current)
296
304
  clearTimeout(timerRef.current);
297
305
  timerRef.current = setTimeout(() => {
298
306
  timerRef.current = null;
299
307
  void flush();
300
308
  }, debounceMs);
301
- }, [debounceMs, flush, setStatus]);
309
+ }, [autoSave, debounceMs, flush, setStatus]);
302
310
  // Explicit save (Save button, "done" actions): also refreshes the file's
303
311
  // thumbnail, which autosaves deliberately skip. Resolves only after the
304
312
  // capture+upload too, so a caller may unmount the view right after.
package/dist/exports.d.ts CHANGED
@@ -22,10 +22,10 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
22
22
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
23
23
  export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
24
24
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
25
- export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
25
+ export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
26
26
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
27
27
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
28
- export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
28
+ export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
29
29
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
30
30
  export { createPanTool, type PanToolOptions, } from './annotation/canvas/tools/panTool.js';
31
31
  export { createTextTool, type TextToolOptions, } from './annotation/canvas/tools/textTool.js';
package/dist/exports.js CHANGED
@@ -22,10 +22,10 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
22
22
  export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
23
23
  export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
24
24
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
25
- export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
25
+ export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
26
26
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
27
27
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
28
- export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
28
+ export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
29
29
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
30
30
  export { createPanTool, } from './annotation/canvas/tools/panTool.js';
31
31
  export { createTextTool, } from './annotation/canvas/tools/textTool.js';
@@ -100,6 +100,7 @@ export interface AnnotationViewport {
100
100
  backgroundImage?: AnnotationBackgroundImage;
101
101
  backgroundFit?: BackgroundFit;
102
102
  }
103
+ export type TileScalePlatform = 'web' | 'mobile';
103
104
  export interface AnnotationCanvasState {
104
105
  schemaVersion: 1;
105
106
  layers: AnnotationLayer[];
@@ -108,6 +109,7 @@ export interface AnnotationCanvasState {
108
109
  shapes: AnnotationShape[];
109
110
  placedMeasurements: PlacedMeasurementRef[];
110
111
  tileScaleFactor?: number;
112
+ tileScaleFactorMobile?: number;
111
113
  externalPayloadPath?: string;
112
114
  }
113
115
  export type AnnotationElement = (AnnotationStroke & {
@@ -156,6 +158,7 @@ export type AnnotationPatchOp = {
156
158
  } | {
157
159
  op: 'setTileScaleFactor';
158
160
  value: number;
161
+ platform?: TileScalePlatform;
159
162
  } | {
160
163
  op: 'setLayers';
161
164
  layers: AnnotationLayer[];
@@ -74,7 +74,9 @@ const applyOp = (state, op) => {
74
74
  case 'setViewport':
75
75
  return { ...state, viewport: { ...state.viewport, ...op.patch } };
76
76
  case 'setTileScaleFactor':
77
- return { ...state, tileScaleFactor: op.value };
77
+ return op.platform === 'mobile'
78
+ ? { ...state, tileScaleFactorMobile: op.value }
79
+ : { ...state, tileScaleFactor: op.value };
78
80
  case 'setLayers':
79
81
  return { ...state, layers: op.layers };
80
82
  }
@@ -150,9 +152,17 @@ const invertOp = (before, op) => {
150
152
  return { op: 'setViewport', patch: inversePatch };
151
153
  }
152
154
  case 'setTileScaleFactor':
153
- // Restore the prior factor; absent an explicit 1 (visually identical to
154
- // absent), keeping the op's `value: number` contract.
155
- return { op: 'setTileScaleFactor', value: before.tileScaleFactor ?? 1 };
155
+ // Restore the prior EFFECTIVE factor for the same platform knob. Mobile
156
+ // falls back to the shared/web value when it has no own value yet (the
157
+ // pre-split doc case mirrors resolveTileScaleFactor); absent → an
158
+ // explicit 1 (visually identical), keeping the `value: number` contract.
159
+ return {
160
+ op: 'setTileScaleFactor',
161
+ value: (op.platform === 'mobile'
162
+ ? (before.tileScaleFactorMobile ?? before.tileScaleFactor)
163
+ : before.tileScaleFactor) ?? 1,
164
+ ...(op.platform && { platform: op.platform }),
165
+ };
156
166
  case 'setLayers':
157
167
  return { op: 'setLayers', layers: before.layers };
158
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.6.28",
3
+ "version": "1.6.30",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,11 +0,0 @@
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;
@@ -1,10 +0,0 @@
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 }));
@@ -1,8 +0,0 @@
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;
@@ -1,6 +0,0 @@
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
- };
@@ -1,39 +0,0 @@
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;
@@ -1,219 +0,0 @@
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
- };
@@ -1,35 +0,0 @@
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;
@@ -1,138 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useFont } from '@shopify/react-native-skia';
3
- import { useMemo, useRef } from 'react';
4
- import { StyleSheet, TouchableOpacity, View } from 'react-native';
5
- import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
6
- import { STAMP_TILE_SIZE } from './stampLayout.js';
7
- import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
8
- import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
9
- // Native fingerprint: one finger drives the active tool, two fingers
10
- // pan/zoom the viewport. Tap counts as a brief pointer down+up so tools
11
- // like measurement-stamp (which only listen to onPointerUp) work via tap.
12
- export const AnnotationCanvasInner = (props) => {
13
- const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, width, height, style, } = props;
14
- const valueFont = useFont(stampFontSource, stampValueFontSize);
15
- const labelFont = useFont(stampFontSource, stampLabelFontSize);
16
- const state = useAnnotationCanvasState(props);
17
- // Per-gesture refs so we always emit a matching down/move/up sequence.
18
- const pointerIdRef = useRef(1);
19
- const inFlightRef = useRef(null);
20
- const pinchStartZoomRef = useRef(1);
21
- const buildEvent = (pointerId, screen) => ({
22
- pointerId,
23
- screen,
24
- world: state.ctx.viewport.screenToWorld(screen),
25
- });
26
- const gesture = useMemo(() => {
27
- const toolPan = Gesture.Pan()
28
- .minPointers(1)
29
- .maxPointers(1)
30
- .runOnJS(true)
31
- .onBegin((e) => {
32
- const id = pointerIdRef.current++;
33
- const screen = { x: e.x, y: e.y };
34
- inFlightRef.current = { id, lastScreen: screen };
35
- state.dispatchPointerDown(buildEvent(id, screen));
36
- })
37
- .onUpdate((e) => {
38
- const f = inFlightRef.current;
39
- if (!f)
40
- return;
41
- const screen = { x: e.x, y: e.y };
42
- f.lastScreen = screen;
43
- state.dispatchPointerMove(buildEvent(f.id, screen));
44
- })
45
- .onEnd((e) => {
46
- const f = inFlightRef.current;
47
- if (!f)
48
- return;
49
- state.dispatchPointerUp(buildEvent(f.id, { x: e.x, y: e.y }));
50
- inFlightRef.current = null;
51
- })
52
- .onFinalize(() => {
53
- if (inFlightRef.current) {
54
- state.dispatchPointerCancel();
55
- inFlightRef.current = null;
56
- }
57
- });
58
- const tap = Gesture.Tap()
59
- .maxDuration(250)
60
- .runOnJS(true)
61
- .onEnd((e) => {
62
- const id = pointerIdRef.current++;
63
- const screen = { x: e.x, y: e.y };
64
- // Synthesize a down+up sequence so tools that only listen to
65
- // onPointerUp (e.g. measurement stamp) still fire.
66
- state.dispatchPointerDown(buildEvent(id, screen));
67
- state.dispatchPointerUp(buildEvent(id, screen));
68
- });
69
- const viewportPan = Gesture.Pan()
70
- .minPointers(2)
71
- .maxPointers(2)
72
- .runOnJS(true)
73
- .onChange((e) => {
74
- state.pan({ x: e.changeX, y: e.changeY });
75
- });
76
- const pinch = Gesture.Pinch()
77
- .runOnJS(true)
78
- .onBegin(() => {
79
- pinchStartZoomRef.current = state.ctx.viewport.state.zoom;
80
- })
81
- .onUpdate((e) => {
82
- state.zoom({ x: e.focalX, y: e.focalY }, pinchStartZoomRef.current * e.scale);
83
- });
84
- return Gesture.Race(tap, Gesture.Simultaneous(viewportPan, pinch), toolPan);
85
- }, [state]);
86
- const activeTool = props.tools.find((t) => t.id === props.activeToolId) ?? null;
87
- const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
88
- const { renderMeasurementStamp, selection } = props;
89
- return (_jsxs(GestureHandlerRootView, { style: [{ width, height }, style], children: [_jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: { width, height }, collapsable: false, children: AnnotationCanvasSkia({
90
- width,
91
- height,
92
- effectiveCanvas: state.effectiveCanvas,
93
- worldTransform: state.worldTransform,
94
- measurementsById: state.measurementsById,
95
- fallbackUnit,
96
- fractionalTolerance,
97
- decimalTolerance,
98
- resolveImageUrl,
99
- valueFont,
100
- labelFont,
101
- hideMeasurementStamps: !!renderMeasurementStamp,
102
- penDrawingStroke: state.penDrawingStroke,
103
- customPreview,
104
- }) }) }), renderMeasurementStamp && (_jsx(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
105
- const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
106
- const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
107
- const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
108
- const isSelected = selection?.ids.includes(placed.id) ?? false;
109
- return (_jsxs(View, { pointerEvents: "box-none", style: {
110
- position: 'absolute',
111
- left: 0,
112
- top: 0,
113
- width: size,
114
- height: size,
115
- transform: [
116
- { translateX: cx - size / 2 },
117
- { translateY: cy - size / 2 },
118
- ],
119
- }, children: [_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: renderMeasurementStamp({
120
- placed,
121
- measurement: state.measurementsById.get(placed.measurementId) ?? null,
122
- selected: isSelected,
123
- size,
124
- zoom: state.viewport.zoom,
125
- }) }), isSelected && (_jsx(TouchableOpacity, { accessibilityRole: "button", accessibilityLabel: "Remove measurement", hitSlop: 10, onPress: () => {
126
- state.ctx.commit({
127
- ops: [{ op: 'removeMeasurement', id: placed.id }],
128
- });
129
- state.ctx.setSelection(null);
130
- }, style: {
131
- position: 'absolute',
132
- top: -8,
133
- right: -8,
134
- width: 36,
135
- height: 36,
136
- } }))] }, placed.id));
137
- }) }))] }));
138
- };
@@ -1,27 +0,0 @@
1
- import { type SkFont } from '@shopify/react-native-skia';
2
- import type { ReactNode } from 'react';
3
- import type { AnnotationCanvasState, AnnotationStroke } from '../types/annotation.js';
4
- import { DecimalTolerance, FractionalTolerance, Units, type Measurement } from '../types/firestore.js';
5
- export interface AnnotationCanvasSkiaProps {
6
- width: number;
7
- height: number;
8
- effectiveCanvas: AnnotationCanvasState;
9
- worldTransform: Array<{
10
- scale: number;
11
- } | {
12
- translateX: number;
13
- } | {
14
- translateY: number;
15
- }>;
16
- measurementsById: Map<string, Measurement>;
17
- fallbackUnit?: Units;
18
- fractionalTolerance?: FractionalTolerance;
19
- decimalTolerance?: DecimalTolerance;
20
- resolveImageUrl?: (storagePath: string) => Promise<string>;
21
- valueFont: SkFont | null;
22
- labelFont: SkFont | null;
23
- hideMeasurementStamps?: boolean;
24
- penDrawingStroke: AnnotationStroke | null;
25
- customPreview?: ReactNode;
26
- }
27
- export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, customPreview, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;