@reekon-tools/boldr-utils 1.15.4 → 1.16.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 (82) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +134 -11
  2. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +4 -1
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.js +11 -6
  4. package/dist/annotation/canvas/backgroundLayers.d.ts +1 -0
  5. package/dist/annotation/canvas/backgroundLayers.js +8 -0
  6. package/dist/annotation/canvas/elements/PlaneElement.d.ts +3 -0
  7. package/dist/annotation/canvas/elements/PlaneElement.js +5 -5
  8. package/dist/annotation/canvas/measurementGeometry.js +3 -0
  9. package/dist/annotation/canvas/planeGeometry.js +21 -10
  10. package/dist/annotation/canvas/tools/selectTool.js +14 -3
  11. package/dist/calculator/index.d.ts +1 -0
  12. package/dist/calculator/index.js +1 -0
  13. package/dist/calculator/preview.d.ts +22 -0
  14. package/dist/calculator/preview.js +43 -0
  15. package/dist/calculator/schema.d.ts +14 -0
  16. package/dist/calculator/schema.js +2 -0
  17. package/dist/calculator/validate.d.ts +1 -0
  18. package/dist/calculator/validate.js +17 -0
  19. package/dist/exports.d.ts +1 -1
  20. package/dist/exports.js +1 -1
  21. package/dist/types/firestore.d.ts +2 -0
  22. package/package.json +1 -1
  23. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  24. package/dist/canvas/AnnotationCanvas.js +0 -10
  25. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  26. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  27. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  28. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  29. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  30. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  31. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  32. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  33. package/dist/canvas/Tool.d.ts +0 -38
  34. package/dist/canvas/Tool.js +0 -1
  35. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  36. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  37. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  38. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  39. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  40. package/dist/canvas/elements/ShapeElement.js +0 -62
  41. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  42. package/dist/canvas/elements/StrokeElement.js +0 -18
  43. package/dist/canvas/measurementPicker.d.ts +0 -10
  44. package/dist/canvas/measurementPicker.js +0 -1
  45. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  46. package/dist/canvas/measurementStampOverlay.js +0 -1
  47. package/dist/canvas/pointerAdapter.d.ts +0 -3
  48. package/dist/canvas/pointerAdapter.js +0 -19
  49. package/dist/canvas/stampLayout.d.ts +0 -5
  50. package/dist/canvas/stampLayout.js +0 -14
  51. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  52. package/dist/canvas/tools/measurementStampTool.js +0 -37
  53. package/dist/canvas/tools/panTool.d.ts +0 -5
  54. package/dist/canvas/tools/panTool.js +0 -25
  55. package/dist/canvas/tools/penTool.d.ts +0 -13
  56. package/dist/canvas/tools/penTool.js +0 -68
  57. package/dist/canvas/tools/selectTool.d.ts +0 -2
  58. package/dist/canvas/tools/selectTool.js +0 -182
  59. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  60. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  61. package/dist/canvas/viewport.d.ts +0 -16
  62. package/dist/canvas/viewport.js +0 -54
  63. package/dist/data/AnnotationDataContext.d.ts +0 -8
  64. package/dist/data/AnnotationDataContext.js +0 -11
  65. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  66. package/dist/data/AnnotationDataProvider.js +0 -4
  67. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  68. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  69. package/dist/data/canvasPersistence.d.ts +0 -3
  70. package/dist/data/canvasPersistence.js +0 -26
  71. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  72. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  73. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  74. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  75. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  76. package/dist/data/hooks/useAnnotationList.js +0 -26
  77. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  78. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  79. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  80. package/dist/hooks/useParseMeasurement.js +0 -14
  81. package/dist/utils/evaluateFormula.d.ts +0 -20
  82. package/dist/utils/evaluateFormula.js +0 -31
@@ -174,6 +174,7 @@ export declare const calculatorDefinitionSchema: z.ZodObject<{
174
174
  variableUnits: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
175
175
  resultUnit: z.ZodOptional<z.ZodString>;
176
176
  }, z.core.$strip>>;
177
+ previewFieldId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
177
178
  diagramFileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
179
  isPublic: z.ZodBoolean;
179
180
  sharedUserIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -127,6 +127,9 @@ export const calculatorDefinitionSchema = z.object({
127
127
  category: z.string().nullish(),
128
128
  fields: z.array(calculatorFieldSchema),
129
129
  equations: z.array(calculatorEquationSchema),
130
+ // Listed so zod's unknown-key stripping doesn't silently drop it (see the
131
+ // `category` note above). Referential checks live in the invariant pass.
132
+ previewFieldId: z.string().nullish(),
130
133
  diagramFileId: z.string().nullable().optional(),
131
134
  isPublic: z.boolean(),
132
135
  sharedUserIds: z.array(z.string()).optional(),
@@ -229,6 +232,20 @@ export const validateCalculatorDefinition = (input) => {
229
232
  }
230
233
  fieldIds.add(field.id);
231
234
  });
235
+ // --- Thumbnail headline field -------------------------------------------
236
+ // Warnings, not errors: a dangling or mis-typed previewFieldId must not brick
237
+ // a published calculator (or every instance that froze it) — the runtime
238
+ // helper (previewFieldFor) simply shows no headline for it.
239
+ if (typeof def.previewFieldId === 'string') {
240
+ const previewField = def.fields.find((f) => f.id === def.previewFieldId);
241
+ if (!previewField) {
242
+ push('preview-field-missing', 'previewFieldId', `Thumbnail result field "${def.previewFieldId}" does not exist`, 'warning');
243
+ }
244
+ else if (previewField.role !== 'output' ||
245
+ !isEquationTargetKind(previewField.kind)) {
246
+ push('preview-field-not-numeric-output', 'previewFieldId', `Thumbnail result field "${previewField.name}" must be a numeric output`, 'warning');
247
+ }
248
+ }
232
249
  const equationIds = new Set();
233
250
  def.equations.forEach((eq, i) => {
234
251
  if (equationIds.has(eq.id)) {
package/dist/exports.d.ts CHANGED
@@ -32,7 +32,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
32
32
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
33
33
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
34
34
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
35
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
35
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
36
36
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
37
37
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
38
38
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
package/dist/exports.js CHANGED
@@ -37,7 +37,7 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
37
37
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
38
38
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
39
39
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
40
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
40
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
41
41
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
42
42
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
43
43
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
@@ -67,6 +67,7 @@ export interface Project extends FirestoreDoc, Timestamps {
67
67
  state: string;
68
68
  collectionId: string | null;
69
69
  index: number | null;
70
+ bgImgUpdatedAt?: number;
70
71
  }
71
72
  export declare enum TemplateType {
72
73
  Layout = "layout",
@@ -228,6 +229,7 @@ export interface Job extends FirestoreDoc, Timestamps {
228
229
  description?: string;
229
230
  starred?: boolean;
230
231
  lastModified: Date;
232
+ bgImgUpdatedAt?: number;
231
233
  }
232
234
  export interface Section extends Timestamps {
233
235
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.15.4",
3
+ "version": "1.16.0",
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;
@@ -1,20 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Canvas, Group } from '@shopify/react-native-skia';
3
- import { Units, } from '../types/firestore.js';
4
- import { BackgroundImageElement } from './elements/BackgroundImageElement.js';
5
- import { MeasurementStampElement } from './elements/MeasurementStampElement.js';
6
- import { ShapeElement } from './elements/ShapeElement.js';
7
- import { StrokeElement } from './elements/StrokeElement.js';
8
- // Platform-agnostic Skia subtree shared by web and native Inners.
9
- //
10
- // Call this as a FUNCTION (`AnnotationCanvasSkia({ ... })`), not as a JSX
11
- // component (`<AnnotationCanvasSkia ... />`). Used as a component, it adds
12
- // a React component boundary between the parent and Skia's `<Canvas>` that
13
- // breaks Skia's reconciler when the page first calls `useFont` — the JS
14
- // thread hangs in `MakeFreeTypeFaceFromData`. Symptoms only appeared on
15
- // Vite's dev server with a symlinked boldr-utils + React Refresh, but
16
- // since the function-call pattern works identically on native we use it
17
- // in both Inners for consistency. Don't add hooks here; this is a plain
18
- // JSX-returning helper, not a component.
19
- export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, measurementsById, fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, valueFont, labelFont, hideMeasurementStamps, penDrawingStroke, customPreview, }) => (_jsx(Canvas, { style: { width, height }, children: _jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.backgroundImage && (_jsx(BackgroundImageElement, { image: effectiveCanvas.viewport.backgroundImage, docWidth: effectiveCanvas.viewport.width, docHeight: effectiveCanvas.viewport.height, fit: effectiveCanvas.viewport.backgroundFit ?? 'contain', resolveUrl: resolveImageUrl })), effectiveCanvas.strokes.map((stroke) => (_jsx(StrokeElement, { stroke: stroke }, stroke.id))), effectiveCanvas.shapes.map((shape) => (_jsx(ShapeElement, { shape: shape, font: valueFont }, shape.id))), !hideMeasurementStamps &&
20
- effectiveCanvas.placedMeasurements.map((placed) => (_jsx(MeasurementStampElement, { placed: placed, measurement: measurementsById.get(placed.measurementId) ?? null, fallbackUnit: fallbackUnit ?? Units.Millimeters, fractionalTolerance: fractionalTolerance, decimalTolerance: decimalTolerance, valueFont: valueFont, labelFont: labelFont }, placed.id))), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), customPreview] }) }));
@@ -1,38 +0,0 @@
1
- import type { ReactNode, ComponentType } from 'react';
2
- import type { AnnotationCanvasState, AnnotationDocumentPatch, AnnotationElement, Selection, Vec2 } from '../types/annotation.js';
3
- import type { MeasurementRef } from './measurementPicker.js';
4
- import type { ViewportApi } from './viewport.js';
5
- export interface CanvasPointerEvent {
6
- pointerId: number;
7
- world: Vec2;
8
- screen: Vec2;
9
- pressure?: number;
10
- shiftKey?: boolean;
11
- altKey?: boolean;
12
- metaKey?: boolean;
13
- ctrlKey?: boolean;
14
- }
15
- export interface ToolContext {
16
- document: AnnotationCanvasState;
17
- selection: Selection | null;
18
- viewport: ViewportApi;
19
- preview(patch: AnnotationDocumentPatch): void;
20
- commit(patch: AnnotationDocumentPatch): void;
21
- setSelection(selection: Selection | null): void;
22
- requestPickMeasurement(): Promise<MeasurementRef | null>;
23
- applyPan(deltaScreen: Vec2): void;
24
- applyZoom(focalScreen: Vec2, nextZoom: number): void;
25
- }
26
- export type ToolState = unknown;
27
- export interface Tool {
28
- id: string;
29
- label: string;
30
- icon?: ComponentType;
31
- cursor?: string;
32
- onPointerDown?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
33
- onPointerMove?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
34
- onPointerUp?(event: CanvasPointerEvent, ctx: ToolContext, state: ToolState): ToolState | void;
35
- onCancel?(state: ToolState, ctx: ToolContext): void;
36
- renderPreview?(state: ToolState, ctx: ToolContext): ReactNode;
37
- hitTest?(element: AnnotationElement, worldPoint: Vec2): boolean;
38
- }