@reekon-tools/boldr-utils 1.6.28 → 1.6.29

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 (69) hide show
  1. package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
  2. package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
  3. package/dist/annotation/canvas/tools/selectTool.d.ts +4 -1
  4. package/dist/annotation/canvas/tools/selectTool.js +13 -1
  5. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +1 -0
  6. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +12 -4
  7. package/dist/exports.d.ts +1 -1
  8. package/dist/exports.js +1 -1
  9. package/package.json +1 -1
  10. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  11. package/dist/canvas/AnnotationCanvas.js +0 -10
  12. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  13. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  14. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  15. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  16. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  17. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  18. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  19. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  20. package/dist/canvas/Tool.d.ts +0 -38
  21. package/dist/canvas/Tool.js +0 -1
  22. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  23. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  24. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  25. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  26. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  27. package/dist/canvas/elements/ShapeElement.js +0 -62
  28. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  29. package/dist/canvas/elements/StrokeElement.js +0 -18
  30. package/dist/canvas/measurementPicker.d.ts +0 -10
  31. package/dist/canvas/measurementPicker.js +0 -1
  32. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  33. package/dist/canvas/measurementStampOverlay.js +0 -1
  34. package/dist/canvas/pointerAdapter.d.ts +0 -3
  35. package/dist/canvas/pointerAdapter.js +0 -19
  36. package/dist/canvas/stampLayout.d.ts +0 -5
  37. package/dist/canvas/stampLayout.js +0 -14
  38. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  39. package/dist/canvas/tools/measurementStampTool.js +0 -37
  40. package/dist/canvas/tools/panTool.d.ts +0 -5
  41. package/dist/canvas/tools/panTool.js +0 -25
  42. package/dist/canvas/tools/penTool.d.ts +0 -13
  43. package/dist/canvas/tools/penTool.js +0 -68
  44. package/dist/canvas/tools/selectTool.d.ts +0 -2
  45. package/dist/canvas/tools/selectTool.js +0 -182
  46. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  47. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  48. package/dist/canvas/viewport.d.ts +0 -16
  49. package/dist/canvas/viewport.js +0 -54
  50. package/dist/data/AnnotationDataContext.d.ts +0 -8
  51. package/dist/data/AnnotationDataContext.js +0 -11
  52. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  53. package/dist/data/AnnotationDataProvider.js +0 -4
  54. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  55. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  56. package/dist/data/canvasPersistence.d.ts +0 -3
  57. package/dist/data/canvasPersistence.js +0 -26
  58. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  59. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  60. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  61. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  62. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  63. package/dist/data/hooks/useAnnotationList.js +0 -26
  64. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  65. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  66. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  67. package/dist/hooks/useParseMeasurement.js +0 -14
  68. package/dist/utils/evaluateFormula.d.ts +0 -20
  69. package/dist/utils/evaluateFormula.js +0 -31
@@ -0,0 +1,12 @@
1
+ import type { PlacedMeasurementRef } from '../../../types/annotation.js';
2
+ import type { Tool } from '../Tool.js';
3
+ export interface MeasurementLineToolOptions {
4
+ id?: string;
5
+ label?: string;
6
+ minDragPx?: number;
7
+ autoSwitchToSelect?: boolean;
8
+ selectToolId?: string;
9
+ onAutoSwitch?: (toToolId: string) => void;
10
+ onPlaced?: (measurement: PlacedMeasurementRef) => void;
11
+ }
12
+ export declare const createMeasurementLineTool: (options?: MeasurementLineToolOptions) => Tool;
@@ -0,0 +1,95 @@
1
+ import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
2
+ import { DEFAULT_LINE_POS, recomputeAnchor } from '../measurementGeometry.js';
3
+ let counter = 0;
4
+ const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
5
+ const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
6
+ // Build the blank line-measurement annotation for a drag from `a` to `b`: a
7
+ // 2-point line with a value tile magnetized to its center (linePos 0.5). The
8
+ // payload mirrors useAnnotationCanvasState's placeAnnotationAtCenter so a drawn
9
+ // line and a (legacy) center-placed one are byte-identical once committed.
10
+ const buildLineMeasurement = (opts) => {
11
+ const line = { a: opts.a, b: opts.b };
12
+ return {
13
+ id: opts.id,
14
+ layerId: opts.layerId,
15
+ placement: 'line',
16
+ line,
17
+ linePos: DEFAULT_LINE_POS,
18
+ // Center of the line; recomputeAnchor keeps this in sync on edits.
19
+ anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, {
20
+ x: (opts.a.x + opts.b.x) / 2,
21
+ y: (opts.a.y + opts.b.y) / 2,
22
+ }),
23
+ showLabel: true,
24
+ showValue: true,
25
+ createdAt: Date.now(),
26
+ };
27
+ };
28
+ // Drag-to-draw measurement-line tool. Press-drag rubber-bands a measurement
29
+ // annotation (a line with a blank value tile at its center); release commits
30
+ // it. Mirrors createShapeTool's interaction so "input lines" are placed the
31
+ // same way as shapes — draw to add, not tap-the-icon-to-add — replacing the
32
+ // old center-place affordance. The annotation stays blank (no measurement
33
+ // associated) until the user fills it via the tile / picker. Skia-free, like
34
+ // every tool factory; it renders its live preview through ctx.preview, so it
35
+ // works on web and (via the generic tool-pan dispatch) on native.
36
+ export const createMeasurementLineTool = (options = {}) => {
37
+ const minDragPx = options.minDragPx ?? 4;
38
+ const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
39
+ const selectToolId = options.selectToolId ?? 'select';
40
+ return {
41
+ id: options.id ?? 'measure-line',
42
+ label: options.label ?? 'Measurement line',
43
+ cursor: 'crosshair',
44
+ onPointerDown(event) {
45
+ return {
46
+ kind: 'measurement-line-drawing',
47
+ id: makeId(),
48
+ startWorld: event.world,
49
+ startScreen: event.screen,
50
+ moved: false,
51
+ };
52
+ },
53
+ onPointerMove(event, ctx, state) {
54
+ const s = state;
55
+ if (s?.kind !== 'measurement-line-drawing')
56
+ return s;
57
+ const measurement = buildLineMeasurement({
58
+ id: s.id,
59
+ layerId: firstLayerId(ctx.document),
60
+ a: s.startWorld,
61
+ b: event.world,
62
+ });
63
+ ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
64
+ return { ...s, moved: true };
65
+ },
66
+ onPointerUp(event, ctx, state) {
67
+ const s = state;
68
+ if (s?.kind !== 'measurement-line-drawing')
69
+ return;
70
+ const dx = event.screen.x - s.startScreen.x;
71
+ const dy = event.screen.y - s.startScreen.y;
72
+ if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
73
+ // Accidental tap — discard the rubber-band, commit nothing.
74
+ ctx.preview({ ops: [] });
75
+ return;
76
+ }
77
+ const measurement = buildLineMeasurement({
78
+ id: s.id,
79
+ layerId: firstLayerId(ctx.document),
80
+ a: s.startWorld,
81
+ b: event.world,
82
+ });
83
+ ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
84
+ // Leave it selected (and hand back to select) so the blank line can be
85
+ // filled / moved straight away — the same flow as the text tool.
86
+ ctx.setSelection({ ids: [measurement.id] });
87
+ options.onPlaced?.(measurement);
88
+ if (autoSwitchToSelect)
89
+ options.onAutoSwitch?.(selectToolId);
90
+ },
91
+ onCancel(_state, ctx) {
92
+ ctx.preview({ ops: [] });
93
+ },
94
+ };
95
+ };
@@ -1,2 +1,5 @@
1
1
  import type { Tool } from '../Tool.js';
2
- export declare const createSelectTool: () => Tool;
2
+ export interface SelectToolOptions {
3
+ panOnEmptyDrag?: boolean;
4
+ }
5
+ export declare const createSelectTool: (options?: SelectToolOptions) => Tool;
@@ -472,7 +472,7 @@ const dragPatch = (s, doc, delta, zoom) => {
472
472
  const op = translatePatch(s.elementKind, s.id, doc, delta);
473
473
  return op ? { ops: [op] } : null;
474
474
  };
475
- export const createSelectTool = () => ({
475
+ export const createSelectTool = (options = {}) => ({
476
476
  id: 'select',
477
477
  label: 'Select',
478
478
  cursor: 'default',
@@ -582,6 +582,11 @@ export const createSelectTool = () => ({
582
582
  const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale);
583
583
  if (!hit) {
584
584
  ctx.setSelection(null);
585
+ // Nothing under the pointer: with panOnEmptyDrag the gesture pans the
586
+ // viewport instead of dead-ending, so bare drags navigate the canvas.
587
+ if (options.panOnEmptyDrag) {
588
+ return { kind: 'panning', lastScreen: event.screen };
589
+ }
585
590
  return { kind: 'idle' };
586
591
  }
587
592
  // Re-tapping an already-selected text shape (without dragging) opens its
@@ -610,6 +615,13 @@ export const createSelectTool = () => ({
610
615
  },
611
616
  onPointerMove(event, ctx, state) {
612
617
  const s = state;
618
+ if (s?.kind === 'panning') {
619
+ ctx.applyPan({
620
+ x: event.screen.x - s.lastScreen.x,
621
+ y: event.screen.y - s.lastScreen.y,
622
+ });
623
+ return { ...s, lastScreen: event.screen };
624
+ }
613
625
  if (s?.kind !== 'dragging')
614
626
  return s;
615
627
  const delta = {
@@ -6,6 +6,7 @@ export interface UseAnnotationCanvasDocOptions {
6
6
  fileId: string | null;
7
7
  fallbackViewport?: Partial<AnnotationViewport>;
8
8
  debounceMs?: number;
9
+ autoSave?: boolean;
9
10
  createSeed?: {
10
11
  name?: string;
11
12
  fileType?: 'sketch' | 'document';
@@ -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
@@ -25,7 +25,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
25
25
  export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, 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
@@ -25,7 +25,7 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
25
25
  export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, 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';
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.29",
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;