@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.
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +41 -10
- package/dist/annotation/canvas/measurementGeometry.js +8 -8
- package/dist/annotation/canvas/stampLayout.js +59 -54
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -1
- package/dist/annotation/canvas/useAnnotationCanvasState.js +88 -38
- package/dist/calculator/evaluate.js +38 -4
- package/dist/calculator/schema.d.ts +12 -0
- package/dist/calculator/schema.js +11 -5
- package/dist/calculator/validate.d.ts +2 -0
- package/dist/calculator/validate.js +43 -1
- package/package.json +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +0 -11
- package/dist/canvas/AnnotationCanvas.js +0 -10
- package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
- package/dist/canvas/AnnotationCanvas.native.js +0 -6
- package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
- package/dist/canvas/AnnotationCanvasInner.js +0 -219
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
- package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
- package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
- package/dist/canvas/AnnotationCanvasSkia.js +0 -20
- package/dist/canvas/Tool.d.ts +0 -38
- package/dist/canvas/Tool.js +0 -1
- package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
- package/dist/canvas/elements/BackgroundImageElement.js +0 -37
- package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
- package/dist/canvas/elements/MeasurementStampElement.js +0 -30
- package/dist/canvas/elements/ShapeElement.d.ts +0 -7
- package/dist/canvas/elements/ShapeElement.js +0 -62
- package/dist/canvas/elements/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/measurementPicker.d.ts +0 -10
- package/dist/canvas/measurementPicker.js +0 -1
- package/dist/canvas/measurementStampOverlay.d.ts +0 -11
- package/dist/canvas/measurementStampOverlay.js +0 -1
- package/dist/canvas/pointerAdapter.d.ts +0 -3
- package/dist/canvas/pointerAdapter.js +0 -19
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
- package/dist/canvas/tools/measurementStampTool.js +0 -37
- package/dist/canvas/tools/panTool.d.ts +0 -5
- package/dist/canvas/tools/panTool.js +0 -25
- package/dist/canvas/tools/penTool.d.ts +0 -13
- package/dist/canvas/tools/penTool.js +0 -68
- package/dist/canvas/tools/selectTool.d.ts +0 -2
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
- package/dist/canvas/useAnnotationCanvasState.js +0 -210
- package/dist/canvas/viewport.d.ts +0 -16
- package/dist/canvas/viewport.js +0 -54
- package/dist/data/AnnotationDataContext.d.ts +0 -8
- package/dist/data/AnnotationDataContext.js +0 -11
- package/dist/data/AnnotationDataProvider.d.ts +0 -65
- package/dist/data/AnnotationDataProvider.js +0 -4
- package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
- package/dist/data/InMemoryAnnotationProvider.js +0 -197
- package/dist/data/canvasPersistence.d.ts +0 -3
- package/dist/data/canvasPersistence.js +0 -26
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
- package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
- package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
- package/dist/data/hooks/useAnnotationDoc.js +0 -33
- package/dist/data/hooks/useAnnotationList.d.ts +0 -7
- package/dist/data/hooks/useAnnotationList.js +0 -26
- package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
- package/dist/data/hooks/useAnnotationMutations.js +0 -11
- package/dist/hooks/useParseMeasurement.d.ts +0 -4
- package/dist/hooks/useParseMeasurement.js +0 -14
- package/dist/utils/evaluateFormula.d.ts +0 -20
- 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
|
|
17
|
-
// canonical scope
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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,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] }) }));
|