@reekon-tools/boldr-utils 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +11 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -1
- package/dist/annotation/canvas/viewport.d.ts +10 -0
- package/dist/annotation/canvas/viewport.js +26 -0
- package/dist/annotation/data/hooks/usePdfCanvasDoc.js +22 -13
- 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/export/buildJobCsv.js +2 -3
- package/dist/exports.d.ts +2 -2
- package/dist/exports.js +2 -2
- package/dist/formulas/calculateFormula.d.ts +1 -1
- package/dist/formulas/calculateFormula.js +22 -24
- package/dist/formulas/evaluateFormula.d.ts +10 -0
- package/dist/formulas/evaluateFormula.js +17 -2
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +6 -2
- package/dist/types/firestore.d.ts +8 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/package.json +1 -1
|
@@ -23,6 +23,17 @@ export interface AnnotationCanvasHandle {
|
|
|
23
23
|
animated?: boolean;
|
|
24
24
|
durationMs?: number;
|
|
25
25
|
}): void;
|
|
26
|
+
fitDocRect(rect: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}, opts?: {
|
|
32
|
+
animated?: boolean;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
paddingPx?: number;
|
|
35
|
+
maxZoom?: number;
|
|
36
|
+
}): void;
|
|
26
37
|
getTileScaleFactor(): number;
|
|
27
38
|
getHeaderTileScaleFactor(): number;
|
|
28
39
|
getFileTileScaleFactor(): number;
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
|
|
3
3
|
import { resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, resolveTileScaleFactor, stampTileDims, STAMP_TILE_SIZE, tileDocSizeForFactor, tileScaleFromDocSize, } from './stampLayout.js';
|
|
4
4
|
import { SNAP_RADIUS_PX } from './snapGeometry.js';
|
|
5
|
-
import { createViewportApi, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
|
|
5
|
+
import { createViewportApi, fitDocRectViewport, fitRectToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, DEFAULT_ZOOM_LIMITS, } from './viewport.js';
|
|
6
6
|
import { computeZoomFloor, pdfFirstPageDocRect } from './pdfDocument.js';
|
|
7
7
|
import { backgroundLayerDocRect, backgroundLayersOf, backgroundLayersUnionRect, } from './backgroundLayers.js';
|
|
8
8
|
import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
|
|
@@ -555,6 +555,22 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
555
555
|
setViewport(target);
|
|
556
556
|
}
|
|
557
557
|
},
|
|
558
|
+
fitDocRect(rect, opts) {
|
|
559
|
+
if (!(width > 0) || !(height > 0))
|
|
560
|
+
return;
|
|
561
|
+
const limits = zoomLimitsRef.current;
|
|
562
|
+
const target = fitDocRectViewport(rect, width, height, {
|
|
563
|
+
paddingPx: opts?.paddingPx,
|
|
564
|
+
minZoom: limits.min,
|
|
565
|
+
maxZoom: Math.min(opts?.maxZoom ?? limits.max, limits.max),
|
|
566
|
+
});
|
|
567
|
+
if (opts?.animated && animateViewport) {
|
|
568
|
+
animateViewport(target, opts.durationMs ?? 450);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
setViewport(target);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
558
574
|
getTileScaleFactor() {
|
|
559
575
|
return ctxRef.current.tileScaleFactor;
|
|
560
576
|
},
|
|
@@ -24,6 +24,16 @@ export declare const fitRectToScreen: (rect: {
|
|
|
24
24
|
width: number;
|
|
25
25
|
height: number;
|
|
26
26
|
}, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
|
|
27
|
+
export declare const fitDocRectViewport: (rect: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}, screenWidth: number, screenHeight: number, limits?: {
|
|
33
|
+
paddingPx?: number;
|
|
34
|
+
minZoom?: number;
|
|
35
|
+
maxZoom?: number;
|
|
36
|
+
}) => ViewportState;
|
|
27
37
|
export declare const DEFAULT_ZOOM_LIMITS: {
|
|
28
38
|
readonly min: 0.05;
|
|
29
39
|
readonly max: 50;
|
|
@@ -62,6 +62,32 @@ export const fitRectToScreen = (rect, screenWidth, screenHeight, padding = 0) =>
|
|
|
62
62
|
pan: { x: rect.x - offsetX / zoom, y: rect.y - offsetY / zoom },
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
|
+
// Frame a DOC-space rect (a text-search hit, a tile) centred in the screen:
|
|
66
|
+
// zoom so the rect plus `paddingPx` of screen padding fits, clamped to
|
|
67
|
+
// [minZoom, maxZoom] — so a two-word hit does not zoom to 50× and a whole-page
|
|
68
|
+
// rect cannot go below the document's floor — then centre the rect at that
|
|
69
|
+
// zoom (screen = (world − pan) · zoom, so pan = centre − half-screen / zoom).
|
|
70
|
+
export const fitDocRectViewport = (rect, screenWidth, screenHeight, limits = {}) => {
|
|
71
|
+
const safe = {
|
|
72
|
+
x: rect.x,
|
|
73
|
+
y: rect.y,
|
|
74
|
+
width: Math.max(1, rect.width),
|
|
75
|
+
height: Math.max(1, rect.height),
|
|
76
|
+
};
|
|
77
|
+
const fit = fitRectToScreen(safe, screenWidth, screenHeight, limits.paddingPx ?? 0);
|
|
78
|
+
const upper = limits.maxZoom ?? Number.POSITIVE_INFINITY;
|
|
79
|
+
const lower = limits.minZoom ?? 0;
|
|
80
|
+
const zoom = Math.max(lower, Math.min(fit.zoom, upper));
|
|
81
|
+
const cx = safe.x + safe.width / 2;
|
|
82
|
+
const cy = safe.y + safe.height / 2;
|
|
83
|
+
return {
|
|
84
|
+
zoom,
|
|
85
|
+
pan: {
|
|
86
|
+
x: cx - screenWidth / (2 * zoom),
|
|
87
|
+
y: cy - screenHeight / (2 * zoom),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
65
91
|
// Zoom toward a focal screen point so the world point under the cursor stays
|
|
66
92
|
// fixed. Used for wheel-zoom on web and pinch on native.
|
|
67
93
|
//
|
|
@@ -23,14 +23,17 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
23
23
|
const provider = useAnnotationData();
|
|
24
24
|
const { data: file, loading: fileLoading, error: fileError, } = useAnnotationDoc(scope, fileId);
|
|
25
25
|
const pdfCanvas = file?.fileData.pdfCanvas ?? null;
|
|
26
|
-
|
|
26
|
+
// The current page's doc, TAGGED with the page it answers for. On a page
|
|
27
|
+
// turn the previous page's doc is still in state until the new subscription
|
|
28
|
+
// reports, and hydrating the new page from it is exactly how annotations
|
|
29
|
+
// land on the wrong page. `state` null = the page has no doc yet.
|
|
30
|
+
const [pageSnapshot, setPageSnapshot] = useState(null);
|
|
27
31
|
const [pageLoading, setPageLoading] = useState(true);
|
|
28
32
|
const [pageError, setPageError] = useState(null);
|
|
29
33
|
const [working, setWorking] = useState(null);
|
|
30
34
|
const [saveStatus, setSaveStatus] = useState('idle');
|
|
31
35
|
const workingRef = useRef(null);
|
|
32
36
|
const pdfCanvasRef = useRef(null);
|
|
33
|
-
const pageDocRef = useRef(null);
|
|
34
37
|
const statusRef = useRef('idle');
|
|
35
38
|
const pageIndexRef = useRef(pageIndex);
|
|
36
39
|
const scopeRef = useRef(scope);
|
|
@@ -48,10 +51,9 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
48
51
|
const thumbnailPromiseRef = useRef(undefined);
|
|
49
52
|
// A page whose edits must be persisted although it is no longer the current
|
|
50
53
|
// page (the user turned the page with unsaved edits).
|
|
51
|
-
const outgoingRef = useRef(
|
|
54
|
+
const outgoingRef = useRef([]);
|
|
52
55
|
workingRef.current = working;
|
|
53
56
|
pdfCanvasRef.current = pdfCanvas;
|
|
54
|
-
pageDocRef.current = pageDoc;
|
|
55
57
|
statusRef.current = saveStatus;
|
|
56
58
|
scopeRef.current = scope;
|
|
57
59
|
fileIdRef.current = fileId;
|
|
@@ -65,14 +67,17 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
65
67
|
// Subscribe to the current page's doc.
|
|
66
68
|
useEffect(() => {
|
|
67
69
|
if (!scope || !fileId) {
|
|
68
|
-
|
|
70
|
+
setPageSnapshot(null);
|
|
69
71
|
setPageLoading(false);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
74
|
+
// Drop the previous page's snapshot synchronously so nothing can read it
|
|
75
|
+
// as this page's while the subscription is still connecting.
|
|
76
|
+
setPageSnapshot(null);
|
|
72
77
|
setPageLoading(true);
|
|
73
78
|
setPageError(null);
|
|
74
79
|
const unsubscribe = provider.subscribePage(scope, fileId, pageIndex, (doc) => {
|
|
75
|
-
|
|
80
|
+
setPageSnapshot({ pageIndex, state: doc });
|
|
76
81
|
setPageLoading(false);
|
|
77
82
|
}, (err) => {
|
|
78
83
|
setPageError(err);
|
|
@@ -154,10 +159,10 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
154
159
|
// One flush pass: an outgoing page first (if a page turn left one behind),
|
|
155
160
|
// then the current page when it is dirty.
|
|
156
161
|
const runFlush = useCallback(async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
await persist(
|
|
162
|
+
// Every page left behind with unsaved edits, in the order they were
|
|
163
|
+
// left: two quick turns must not lose the middle page.
|
|
164
|
+
while (outgoingRef.current.length > 0) {
|
|
165
|
+
await persist(outgoingRef.current.shift());
|
|
161
166
|
}
|
|
162
167
|
const canvas = workingRef.current;
|
|
163
168
|
const data = pdfCanvasRef.current;
|
|
@@ -200,11 +205,11 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
200
205
|
if (previous &&
|
|
201
206
|
data &&
|
|
202
207
|
(statusRef.current === 'dirty' || statusRef.current === 'error')) {
|
|
203
|
-
outgoingRef.current
|
|
208
|
+
outgoingRef.current.push({
|
|
204
209
|
pageIndex: pageIndexRef.current,
|
|
205
210
|
canvas: previous,
|
|
206
211
|
pdfCanvas: data,
|
|
207
|
-
};
|
|
212
|
+
});
|
|
208
213
|
void flush();
|
|
209
214
|
}
|
|
210
215
|
pageIndexRef.current = pageIndex;
|
|
@@ -225,6 +230,10 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
225
230
|
return;
|
|
226
231
|
if (pageIndex >= pdfCanvas.document.pages.length)
|
|
227
232
|
return;
|
|
233
|
+
// Only a snapshot for THIS page counts (see pageSnapshot).
|
|
234
|
+
if (!pageSnapshot || pageSnapshot.pageIndex !== pageIndex)
|
|
235
|
+
return;
|
|
236
|
+
const pageDoc = pageSnapshot.state;
|
|
228
237
|
if (workingRef.current === null) {
|
|
229
238
|
const next = pageStateToCanvasState(pdfCanvas, pageDoc, pageIndex);
|
|
230
239
|
workingRef.current = next;
|
|
@@ -241,7 +250,7 @@ export const usePdfCanvasDoc = (options) => {
|
|
|
241
250
|
workingRef.current = next;
|
|
242
251
|
setWorking(next);
|
|
243
252
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
244
|
-
}, [pdfCanvas,
|
|
253
|
+
}, [pdfCanvas, pageSnapshot, pageLoading, pageIndex]);
|
|
245
254
|
const onCommit = useCallback((patch) => {
|
|
246
255
|
editSeqRef.current += 1;
|
|
247
256
|
setWorking((prev) => {
|
|
@@ -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;
|