@reekon-tools/boldr-utils 1.6.31 → 1.6.33
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/tools/measurementTool.d.ts +6 -1
- package/dist/annotation/canvas/tools/measurementTool.js +34 -1
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +1 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +35 -28
- package/dist/canvas/AnnotationCanvas.d.ts +11 -0
- package/dist/canvas/AnnotationCanvas.js +10 -0
- package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
- package/dist/canvas/AnnotationCanvas.native.js +6 -0
- package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
- package/dist/canvas/AnnotationCanvasInner.js +219 -0
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
- package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
- package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
- package/dist/canvas/AnnotationCanvasSkia.js +20 -0
- package/dist/canvas/Tool.d.ts +38 -0
- package/dist/canvas/Tool.js +1 -0
- package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
- package/dist/canvas/elements/BackgroundImageElement.js +37 -0
- package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
- package/dist/canvas/elements/MeasurementStampElement.js +30 -0
- package/dist/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/canvas/elements/ShapeElement.js +62 -0
- package/dist/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/canvas/elements/StrokeElement.js +18 -0
- package/dist/canvas/measurementPicker.d.ts +10 -0
- package/dist/canvas/measurementPicker.js +1 -0
- package/dist/canvas/measurementStampOverlay.d.ts +11 -0
- package/dist/canvas/measurementStampOverlay.js +1 -0
- package/dist/canvas/pointerAdapter.d.ts +3 -0
- package/dist/canvas/pointerAdapter.js +19 -0
- package/dist/canvas/stampLayout.d.ts +5 -0
- package/dist/canvas/stampLayout.js +14 -0
- package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
- package/dist/canvas/tools/measurementStampTool.js +37 -0
- package/dist/canvas/tools/panTool.d.ts +5 -0
- package/dist/canvas/tools/panTool.js +25 -0
- package/dist/canvas/tools/penTool.d.ts +13 -0
- package/dist/canvas/tools/penTool.js +68 -0
- package/dist/canvas/tools/selectTool.d.ts +2 -0
- package/dist/canvas/tools/selectTool.js +182 -0
- package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
- package/dist/canvas/useAnnotationCanvasState.js +210 -0
- package/dist/canvas/viewport.d.ts +16 -0
- package/dist/canvas/viewport.js +54 -0
- package/dist/data/AnnotationDataContext.d.ts +8 -0
- package/dist/data/AnnotationDataContext.js +11 -0
- package/dist/data/AnnotationDataProvider.d.ts +65 -0
- package/dist/data/AnnotationDataProvider.js +4 -0
- package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
- package/dist/data/InMemoryAnnotationProvider.js +197 -0
- package/dist/data/canvasPersistence.d.ts +3 -0
- package/dist/data/canvasPersistence.js +26 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
- package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
- package/dist/data/hooks/useAnnotationDoc.js +33 -0
- package/dist/data/hooks/useAnnotationList.d.ts +7 -0
- package/dist/data/hooks/useAnnotationList.js +26 -0
- package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
- package/dist/data/hooks/useAnnotationMutations.js +11 -0
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +1 -0
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/dist/utils/selectValues.d.ts +3 -0
- package/dist/utils/selectValues.js +17 -0
- package/package.json +1 -1
- package/dist/annotation/canvas/tools/measurementLineTool.d.ts +0 -12
- package/dist/annotation/canvas/tools/measurementLineTool.js +0 -95
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlacedMeasurementRef } from '../../../types/annotation.js';
|
|
1
|
+
import type { PlacedMeasurementRef, StrokeCap } from '../../../types/annotation.js';
|
|
2
2
|
import type { Tool } from '../Tool.js';
|
|
3
3
|
export type MeasurementToolPlacement = 'line' | 'rectangle' | 'none';
|
|
4
4
|
export interface MeasurementToolOptions {
|
|
@@ -11,5 +11,10 @@ export interface MeasurementToolOptions {
|
|
|
11
11
|
selectToolId?: string;
|
|
12
12
|
onAutoSwitch?: (toToolId: string) => void;
|
|
13
13
|
onPlaced?: (measurement: PlacedMeasurementRef) => void;
|
|
14
|
+
color?: string;
|
|
15
|
+
width?: number;
|
|
16
|
+
cap?: StrokeCap;
|
|
17
|
+
startCap?: StrokeCap;
|
|
18
|
+
dash?: boolean;
|
|
14
19
|
}
|
|
15
20
|
export declare const createMeasurementTool: (options?: MeasurementToolOptions) => Tool;
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
2
|
import { DEFAULT_LINE_POS, recomputeAnchor, rectCenter, } from '../measurementGeometry.js';
|
|
3
|
+
// The `line*` style fields for a placed measurement, built from the tool's
|
|
4
|
+
// style options. Undefined fields are dropped (never emitted) so the render
|
|
5
|
+
// defaults apply and Firestore never sees an `undefined` value.
|
|
6
|
+
const lineStyleFields = (placement, style) => {
|
|
7
|
+
if (placement === 'none')
|
|
8
|
+
return {};
|
|
9
|
+
const out = {};
|
|
10
|
+
if (style.color !== undefined)
|
|
11
|
+
out.lineColor = style.color;
|
|
12
|
+
if (style.width !== undefined)
|
|
13
|
+
out.lineWidth = style.width;
|
|
14
|
+
if (style.dash !== undefined)
|
|
15
|
+
out.lineDash = style.dash;
|
|
16
|
+
// End/start caps are only meaningful on the open `line` path.
|
|
17
|
+
if (placement === 'line') {
|
|
18
|
+
if (style.cap !== undefined)
|
|
19
|
+
out.lineCap = style.cap;
|
|
20
|
+
if (style.startCap !== undefined)
|
|
21
|
+
out.lineStartCap = style.startCap;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
3
25
|
let counter = 0;
|
|
4
26
|
const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
5
27
|
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
@@ -8,13 +30,14 @@ const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
|
8
30
|
// placeAnnotationAtCenter so a drawn annotation is identical to a (legacy)
|
|
9
31
|
// center-placed one once committed.
|
|
10
32
|
const buildMeasurement = (opts) => {
|
|
11
|
-
const { id, layerId, placement, linePos, a, b } = opts;
|
|
33
|
+
const { id, layerId, placement, linePos, a, b, style } = opts;
|
|
12
34
|
const base = {
|
|
13
35
|
id,
|
|
14
36
|
layerId,
|
|
15
37
|
showLabel: true,
|
|
16
38
|
showValue: true,
|
|
17
39
|
createdAt: Date.now(),
|
|
40
|
+
...lineStyleFields(placement, style),
|
|
18
41
|
};
|
|
19
42
|
if (placement === 'rectangle') {
|
|
20
43
|
const rect = { a, b };
|
|
@@ -51,6 +74,13 @@ export const createMeasurementTool = (options = {}) => {
|
|
|
51
74
|
const minDragPx = options.minDragPx ?? 4;
|
|
52
75
|
const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
|
|
53
76
|
const selectToolId = options.selectToolId ?? 'select';
|
|
77
|
+
const style = {
|
|
78
|
+
color: options.color,
|
|
79
|
+
width: options.width,
|
|
80
|
+
cap: options.cap,
|
|
81
|
+
startCap: options.startCap,
|
|
82
|
+
dash: options.dash,
|
|
83
|
+
};
|
|
54
84
|
const place = (ctx, measurement) => {
|
|
55
85
|
ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
56
86
|
options.onPlaced?.(measurement);
|
|
@@ -78,6 +108,7 @@ export const createMeasurementTool = (options = {}) => {
|
|
|
78
108
|
linePos,
|
|
79
109
|
a: event.world,
|
|
80
110
|
b: event.world,
|
|
111
|
+
style,
|
|
81
112
|
}));
|
|
82
113
|
},
|
|
83
114
|
};
|
|
@@ -108,6 +139,7 @@ export const createMeasurementTool = (options = {}) => {
|
|
|
108
139
|
linePos,
|
|
109
140
|
a: s.startWorld,
|
|
110
141
|
b: event.world,
|
|
142
|
+
style,
|
|
111
143
|
});
|
|
112
144
|
ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
113
145
|
return { ...s, moved: true };
|
|
@@ -130,6 +162,7 @@ export const createMeasurementTool = (options = {}) => {
|
|
|
130
162
|
linePos,
|
|
131
163
|
a: s.startWorld,
|
|
132
164
|
b: event.world,
|
|
165
|
+
style,
|
|
133
166
|
}));
|
|
134
167
|
},
|
|
135
168
|
onCancel(_state, ctx) {
|
|
@@ -16,6 +16,7 @@ export interface AnnotationCanvasHandle {
|
|
|
16
16
|
}): void;
|
|
17
17
|
setAnnotationType(id: AnnotationElementId, type: MeasurementPlacement): void;
|
|
18
18
|
centerOnLine(id: AnnotationElementId): void;
|
|
19
|
+
setLinePos(id: AnnotationElementId, linePos: number): void;
|
|
19
20
|
removeMeasurementValue(id: AnnotationElementId): void;
|
|
20
21
|
associateMeasurement(id: AnnotationElementId, ref: MeasurementRef): void;
|
|
21
22
|
bindColumn(id: AnnotationElementId, binding: {
|
|
@@ -195,6 +195,37 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
195
195
|
useEffect(() => {
|
|
196
196
|
if (!imperativeRef)
|
|
197
197
|
return;
|
|
198
|
+
// Shared body for centerOnLine / setLinePos: ensure the annotation is a
|
|
199
|
+
// 'line' (synthesizing a default horizontal line around the anchor if it
|
|
200
|
+
// has none) and pin its tile at `linePos` along that line.
|
|
201
|
+
const applyLinePos = (id, linePos) => {
|
|
202
|
+
const c = ctxRef.current;
|
|
203
|
+
const m = c.document.placedMeasurements.find((x) => x.id === id);
|
|
204
|
+
if (!m)
|
|
205
|
+
return;
|
|
206
|
+
let line = m.line;
|
|
207
|
+
if (!line) {
|
|
208
|
+
const len = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
|
|
209
|
+
line = {
|
|
210
|
+
a: { x: m.anchor.x - len / 2, y: m.anchor.y },
|
|
211
|
+
b: { x: m.anchor.x + len / 2, y: m.anchor.y },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
c.commit({
|
|
215
|
+
ops: [
|
|
216
|
+
{
|
|
217
|
+
op: 'updateMeasurement',
|
|
218
|
+
id,
|
|
219
|
+
patch: {
|
|
220
|
+
placement: 'line',
|
|
221
|
+
line,
|
|
222
|
+
linePos,
|
|
223
|
+
anchor: recomputeAnchor(line, 'line', linePos, m.anchor),
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
};
|
|
198
229
|
imperativeRef.current = {
|
|
199
230
|
undo() {
|
|
200
231
|
const entry = undoStackRef.current.pop();
|
|
@@ -340,34 +371,10 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
340
371
|
});
|
|
341
372
|
},
|
|
342
373
|
centerOnLine(id) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Same default-line synthesis as setAnnotationType('line') so centering
|
|
348
|
-
// a floating tile also gives it a line to sit on.
|
|
349
|
-
let line = m.line;
|
|
350
|
-
if (!line) {
|
|
351
|
-
const len = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
|
|
352
|
-
line = {
|
|
353
|
-
a: { x: m.anchor.x - len / 2, y: m.anchor.y },
|
|
354
|
-
b: { x: m.anchor.x + len / 2, y: m.anchor.y },
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
c.commit({
|
|
358
|
-
ops: [
|
|
359
|
-
{
|
|
360
|
-
op: 'updateMeasurement',
|
|
361
|
-
id,
|
|
362
|
-
patch: {
|
|
363
|
-
placement: 'line',
|
|
364
|
-
line,
|
|
365
|
-
linePos: 0.5,
|
|
366
|
-
anchor: recomputeAnchor(line, 'line', 0.5, m.anchor),
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
});
|
|
374
|
+
applyLinePos(id, 0.5);
|
|
375
|
+
},
|
|
376
|
+
setLinePos(id, linePos) {
|
|
377
|
+
applyLinePos(id, linePos);
|
|
371
378
|
},
|
|
372
379
|
removeMeasurementValue(id) {
|
|
373
380
|
const c = ctxRef.current;
|
|
@@ -0,0 +1,11 @@
|
|
|
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;
|
|
@@ -0,0 +1,10 @@
|
|
|
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 }));
|
|
@@ -0,0 +1,8 @@
|
|
|
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;
|
|
@@ -0,0 +1,6 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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;
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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;
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
};
|