@reekon-tools/boldr-utils 1.6.35 → 1.6.36
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/measurementLineTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +1 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +24 -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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PlacedMeasurementRef } from '../../../types/annotation.js';
|
|
2
|
+
import type { Tool } from '../Tool.js';
|
|
3
|
+
export interface MeasurementLineToolOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
minDragPx?: number;
|
|
7
|
+
autoSwitchToSelect?: boolean;
|
|
8
|
+
selectToolId?: string;
|
|
9
|
+
onAutoSwitch?: (toToolId: string) => void;
|
|
10
|
+
onPlaced?: (measurement: PlacedMeasurementRef) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const createMeasurementLineTool: (options?: MeasurementLineToolOptions) => Tool;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
|
|
2
|
+
import { DEFAULT_LINE_POS, recomputeAnchor } from '../measurementGeometry.js';
|
|
3
|
+
let counter = 0;
|
|
4
|
+
const makeId = () => `annotation-${Date.now().toString(36)}-${(counter++).toString(36)}`;
|
|
5
|
+
const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
|
|
6
|
+
// Build the blank line-measurement annotation for a drag from `a` to `b`: a
|
|
7
|
+
// 2-point line with a value tile magnetized to its center (linePos 0.5). The
|
|
8
|
+
// payload mirrors useAnnotationCanvasState's placeAnnotationAtCenter so a drawn
|
|
9
|
+
// line and a (legacy) center-placed one are byte-identical once committed.
|
|
10
|
+
const buildLineMeasurement = (opts) => {
|
|
11
|
+
const line = { a: opts.a, b: opts.b };
|
|
12
|
+
return {
|
|
13
|
+
id: opts.id,
|
|
14
|
+
layerId: opts.layerId,
|
|
15
|
+
placement: 'line',
|
|
16
|
+
line,
|
|
17
|
+
linePos: DEFAULT_LINE_POS,
|
|
18
|
+
// Center of the line; recomputeAnchor keeps this in sync on edits.
|
|
19
|
+
anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, {
|
|
20
|
+
x: (opts.a.x + opts.b.x) / 2,
|
|
21
|
+
y: (opts.a.y + opts.b.y) / 2,
|
|
22
|
+
}),
|
|
23
|
+
showLabel: true,
|
|
24
|
+
showValue: true,
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
// Drag-to-draw measurement-line tool. Press-drag rubber-bands a measurement
|
|
29
|
+
// annotation (a line with a blank value tile at its center); release commits
|
|
30
|
+
// it. Mirrors createShapeTool's interaction so "input lines" are placed the
|
|
31
|
+
// same way as shapes — draw to add, not tap-the-icon-to-add — replacing the
|
|
32
|
+
// old center-place affordance. The annotation stays blank (no measurement
|
|
33
|
+
// associated) until the user fills it via the tile / picker. Skia-free, like
|
|
34
|
+
// every tool factory; it renders its live preview through ctx.preview, so it
|
|
35
|
+
// works on web and (via the generic tool-pan dispatch) on native.
|
|
36
|
+
export const createMeasurementLineTool = (options = {}) => {
|
|
37
|
+
const minDragPx = options.minDragPx ?? 4;
|
|
38
|
+
const autoSwitchToSelect = options.autoSwitchToSelect ?? true;
|
|
39
|
+
const selectToolId = options.selectToolId ?? 'select';
|
|
40
|
+
return {
|
|
41
|
+
id: options.id ?? 'measure-line',
|
|
42
|
+
label: options.label ?? 'Measurement line',
|
|
43
|
+
cursor: 'crosshair',
|
|
44
|
+
onPointerDown(event) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'measurement-line-drawing',
|
|
47
|
+
id: makeId(),
|
|
48
|
+
startWorld: event.world,
|
|
49
|
+
startScreen: event.screen,
|
|
50
|
+
moved: false,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
onPointerMove(event, ctx, state) {
|
|
54
|
+
const s = state;
|
|
55
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
56
|
+
return s;
|
|
57
|
+
const measurement = buildLineMeasurement({
|
|
58
|
+
id: s.id,
|
|
59
|
+
layerId: firstLayerId(ctx.document),
|
|
60
|
+
a: s.startWorld,
|
|
61
|
+
b: event.world,
|
|
62
|
+
});
|
|
63
|
+
ctx.preview({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
64
|
+
return { ...s, moved: true };
|
|
65
|
+
},
|
|
66
|
+
onPointerUp(event, ctx, state) {
|
|
67
|
+
const s = state;
|
|
68
|
+
if (s?.kind !== 'measurement-line-drawing')
|
|
69
|
+
return;
|
|
70
|
+
const dx = event.screen.x - s.startScreen.x;
|
|
71
|
+
const dy = event.screen.y - s.startScreen.y;
|
|
72
|
+
if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx) {
|
|
73
|
+
// Accidental tap — discard the rubber-band, commit nothing.
|
|
74
|
+
ctx.preview({ ops: [] });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const measurement = buildLineMeasurement({
|
|
78
|
+
id: s.id,
|
|
79
|
+
layerId: firstLayerId(ctx.document),
|
|
80
|
+
a: s.startWorld,
|
|
81
|
+
b: event.world,
|
|
82
|
+
});
|
|
83
|
+
ctx.commit({ ops: [{ op: 'addMeasurement', measurement }] });
|
|
84
|
+
// Leave it selected (and hand back to select) so the blank line can be
|
|
85
|
+
// filled / moved straight away — the same flow as the text tool.
|
|
86
|
+
ctx.setSelection({ ids: [measurement.id] });
|
|
87
|
+
options.onPlaced?.(measurement);
|
|
88
|
+
if (autoSwitchToSelect)
|
|
89
|
+
options.onAutoSwitch?.(selectToolId);
|
|
90
|
+
},
|
|
91
|
+
onCancel(_state, ctx) {
|
|
92
|
+
ctx.preview({ ops: [] });
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
@@ -12,6 +12,7 @@ export interface AnnotationCanvasHandle {
|
|
|
12
12
|
resetView(): void;
|
|
13
13
|
getTileScaleFactor(): number;
|
|
14
14
|
getTileDocSize(factor: number): number;
|
|
15
|
+
ensureMeasurementVisible(id: AnnotationElementId): void;
|
|
15
16
|
placeMeasurementAtCenter(ref: MeasurementRef): void;
|
|
16
17
|
placeAnnotationAtCenter(opts?: {
|
|
17
18
|
defaultLengthDoc?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
|
-
import { resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
3
|
+
import { resolveTileScaleFactor, stampTileSize, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
4
4
|
import { createViewportApi, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
|
|
5
5
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
6
6
|
// The viewport that frames the document's content on screen. When a background
|
|
@@ -301,6 +301,29 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
301
301
|
getTileDocSize(factor) {
|
|
302
302
|
return tileDocSizeForFactor(factor, ctxRef.current.viewport.state.zoom);
|
|
303
303
|
},
|
|
304
|
+
ensureMeasurementVisible(id) {
|
|
305
|
+
const c = ctxRef.current;
|
|
306
|
+
const m = c.document.placedMeasurements.find((x) => x.id === id);
|
|
307
|
+
if (!m)
|
|
308
|
+
return;
|
|
309
|
+
// The tile is a screen-constant square centered on its anchor, so its
|
|
310
|
+
// half-edge is screen px regardless of zoom (stampTileSize).
|
|
311
|
+
const half = stampTileSize(m, c.tileScaleFactor, c.tileViewportScale) / 2;
|
|
312
|
+
const s = c.viewport.worldToScreen(m.anchor);
|
|
313
|
+
if (s.x - half >= 0 &&
|
|
314
|
+
s.x + half <= width &&
|
|
315
|
+
s.y - half >= 0 &&
|
|
316
|
+
s.y + half <= height) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
setViewport((v) => ({
|
|
320
|
+
zoom: v.zoom,
|
|
321
|
+
pan: {
|
|
322
|
+
x: m.anchor.x - width / (2 * v.zoom),
|
|
323
|
+
y: m.anchor.y - height / (2 * v.zoom),
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
},
|
|
304
327
|
placeMeasurementAtCenter(ref) {
|
|
305
328
|
const c = ctxRef.current;
|
|
306
329
|
const anchor = c.viewport.screenToWorld({
|
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
|
-
};
|