@reekon-tools/boldr-utils 1.8.4 → 1.8.5

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 (71) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +41 -10
  2. package/dist/annotation/canvas/measurementGeometry.js +8 -8
  3. package/dist/annotation/canvas/stampLayout.js +59 -54
  4. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -1
  5. package/dist/annotation/canvas/useAnnotationCanvasState.js +88 -38
  6. package/dist/calculator/evaluate.js +38 -4
  7. package/dist/calculator/schema.d.ts +12 -0
  8. package/dist/calculator/schema.js +11 -5
  9. package/dist/calculator/validate.d.ts +2 -0
  10. package/dist/calculator/validate.js +43 -1
  11. package/package.json +1 -1
  12. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  13. package/dist/canvas/AnnotationCanvas.js +0 -10
  14. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  15. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  16. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  17. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  18. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  19. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  20. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  21. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  22. package/dist/canvas/Tool.d.ts +0 -38
  23. package/dist/canvas/Tool.js +0 -1
  24. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  25. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  26. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  27. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  28. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  29. package/dist/canvas/elements/ShapeElement.js +0 -62
  30. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  31. package/dist/canvas/elements/StrokeElement.js +0 -18
  32. package/dist/canvas/measurementPicker.d.ts +0 -10
  33. package/dist/canvas/measurementPicker.js +0 -1
  34. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  35. package/dist/canvas/measurementStampOverlay.js +0 -1
  36. package/dist/canvas/pointerAdapter.d.ts +0 -3
  37. package/dist/canvas/pointerAdapter.js +0 -19
  38. package/dist/canvas/stampLayout.d.ts +0 -5
  39. package/dist/canvas/stampLayout.js +0 -14
  40. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  41. package/dist/canvas/tools/measurementStampTool.js +0 -37
  42. package/dist/canvas/tools/panTool.d.ts +0 -5
  43. package/dist/canvas/tools/panTool.js +0 -25
  44. package/dist/canvas/tools/penTool.d.ts +0 -13
  45. package/dist/canvas/tools/penTool.js +0 -68
  46. package/dist/canvas/tools/selectTool.d.ts +0 -2
  47. package/dist/canvas/tools/selectTool.js +0 -182
  48. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  49. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  50. package/dist/canvas/viewport.d.ts +0 -16
  51. package/dist/canvas/viewport.js +0 -54
  52. package/dist/data/AnnotationDataContext.d.ts +0 -8
  53. package/dist/data/AnnotationDataContext.js +0 -11
  54. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  55. package/dist/data/AnnotationDataProvider.js +0 -4
  56. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  57. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  58. package/dist/data/canvasPersistence.d.ts +0 -3
  59. package/dist/data/canvasPersistence.js +0 -26
  60. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  61. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  62. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  63. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  64. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  65. package/dist/data/hooks/useAnnotationList.js +0 -26
  66. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  67. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  68. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  69. package/dist/hooks/useParseMeasurement.js +0 -14
  70. package/dist/utils/evaluateFormula.d.ts +0 -20
  71. package/dist/utils/evaluateFormula.js +0 -31
@@ -13,11 +13,17 @@ import { unitsForDimension } from './units.js';
13
13
  // brackets) and every runtime field value is canonical — µm for length, µm²
14
14
  // area, µm³ volume, degrees for angle, raw for dimensionless (see units.ts).
15
15
  //
16
- // EXPRESSION SEMANTICS: equations are math.js expressions evaluated over a
17
- // canonical scope. Numeric literals inside an expression are therefore
18
- // canonical too (a bare `2` multiplying a length is a plain scalar; a bare
19
- // additive length constant would be micrometers). Authors should express
20
- // length constants as input fields with a defaultValue instead of literals.
16
+ // EXPRESSION SEMANTICS: equations are math.js expressions. By default they
17
+ // evaluate over a canonical scope, which makes numeric literals canonical too
18
+ // (a bare `2` multiplying a length is a plain scalar; a bare additive length
19
+ // constant would be micrometers). Per-equation `variableUnits` / `resultUnit`
20
+ // instead evaluate in author-chosen display units: each mapped variable is
21
+ // converted OUT of canonical into its unit before entering the scope, and the
22
+ // result is converted back FROM `resultUnit` into canonical. This is what
23
+ // makes literals meaningful ("roof area in ft² / 100 = roofing squares") —
24
+ // without it, a number-kind output consuming a measurement variable silently
25
+ // receives µm-scale values. The authoring UI always sets units; the bare
26
+ // canonical form survives for pre-existing definitions.
21
27
  // ---------------------------------------------------------------------------
22
28
  /**
23
29
  * Version of the definition format itself. Bumped only on breaking schema
@@ -157,6 +157,8 @@ export declare const calculatorDefinitionSchema: z.ZodObject<{
157
157
  targetFieldId: z.ZodString;
158
158
  expression: z.ZodString;
159
159
  variableToFieldId: z.ZodRecord<z.ZodString, z.ZodString>;
160
+ variableUnits: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
161
+ resultUnit: z.ZodOptional<z.ZodString>;
160
162
  }, z.core.$strip>>;
161
163
  diagramFileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
162
164
  isPublic: z.ZodBoolean;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { create, all } from 'mathjs';
3
3
  import { ColumnType, DecimalTolerance, FractionalTolerance, } from '../types/firestore.js';
4
- import { CALCULATOR_SCHEMA_VERSION, equationForField, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
4
+ import { CALCULATOR_SCHEMA_VERSION, equationForField, fieldDimension, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
5
5
  import { CALCULATOR_UNIT_INFO, unitDimension } from './units.js';
6
6
  import { isCalculatorCategoryId } from './categories.js';
7
7
  const math = create(all);
@@ -94,6 +94,8 @@ const calculatorEquationSchema = z.object({
94
94
  targetFieldId: z.string().min(1),
95
95
  expression: z.string().min(1),
96
96
  variableToFieldId: z.record(z.string(), z.string()),
97
+ variableUnits: z.record(z.string(), calculatorUnitSchema).optional(),
98
+ resultUnit: calculatorUnitSchema.optional(),
97
99
  });
98
100
  export const calculatorDefinitionSchema = z.object({
99
101
  id: z.string().min(1),
@@ -303,6 +305,46 @@ export const validateCalculatorDefinition = (input) => {
303
305
  push('self-referencing-equation', `${path}.variableToFieldId.${variable}`, `Equation for "${eq.targetFieldId}" references its own target`);
304
306
  }
305
307
  }
308
+ // --- Unit annotations --------------------------------------------------
309
+ for (const [variable, unit] of Object.entries(eq.variableUnits ?? {})) {
310
+ const unitPath = `${path}.variableUnits.${variable}`;
311
+ const mappedId = eq.variableToFieldId[variable];
312
+ if (mappedId == null) {
313
+ push('unit-for-unmapped-variable', unitPath, `Variable ${variable} has a unit but no field mapping`, 'warning');
314
+ continue;
315
+ }
316
+ const mapped = fieldById.get(mappedId);
317
+ if (!mapped)
318
+ continue; // unknown-mapped-field already reported above
319
+ const dim = fieldDimension(mapped);
320
+ if (dim === 'none') {
321
+ push('variable-unit-on-dimensionless-field', unitPath, `Variable ${variable} maps to "${mapped.name}", which has no unit dimension`);
322
+ }
323
+ else if (unitDimension(unit) !== dim) {
324
+ push('variable-unit-dimension-mismatch', unitPath, `Unit "${unit}" is not a ${dim} unit (variable ${variable} maps to "${mapped.name}")`);
325
+ }
326
+ }
327
+ if (eq.resultUnit != null && target) {
328
+ const dim = fieldDimension(target);
329
+ if (dim === 'none') {
330
+ push('result-unit-on-number-target', `${path}.resultUnit`, `Equation target "${target.name}" is a number field and takes the result as-is; remove the result unit`);
331
+ }
332
+ else if (unitDimension(eq.resultUnit) !== dim) {
333
+ push('result-unit-dimension-mismatch', `${path}.resultUnit`, `Result unit "${eq.resultUnit}" is not a ${dim} unit (target "${target.name}")`);
334
+ }
335
+ }
336
+ // The canonical-leak trap: a number target consuming a unit-carrying
337
+ // field without a variable unit receives raw µm-scale values.
338
+ if (target?.kind === ColumnType.Number) {
339
+ for (const [variable, fieldId] of Object.entries(eq.variableToFieldId)) {
340
+ const mapped = fieldById.get(fieldId);
341
+ if (!mapped || fieldDimension(mapped) === 'none')
342
+ continue;
343
+ if (eq.variableUnits?.[variable] == null) {
344
+ push('canonical-value-into-number-target', `${path}.variableToFieldId.${variable}`, `"${target.name}" is a number field but variable ${variable} ("${mapped.name}") has no display unit — its canonical (µm-scale) value will be used. Pick a unit for ${variable}.`, 'warning');
345
+ }
346
+ }
347
+ }
306
348
  const symbols = expressionSymbols(eq.expression);
307
349
  if (!symbols.ok) {
308
350
  push('expression-invalid', `${path}.expression`, `Expression does not parse: ${symbols.error}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
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] }) }));