@reekon-tools/boldr-utils 1.6.30 → 1.6.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.js +21 -2
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +4 -3
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +3 -3
- package/dist/annotation/canvas/elements/BackgroundImageElement.js +67 -12
- 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/annotation/data/AnnotationDataProvider.d.ts +20 -9
- package/dist/annotation/data/AnnotationDataProvider.js +8 -0
- package/dist/annotation/data/InMemoryAnnotationProvider.d.ts +10 -10
- package/dist/annotation/data/InMemoryAnnotationProvider.js +30 -4
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.d.ts +2 -2
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +10 -3
- package/dist/annotation/data/hooks/useAnnotationDoc.d.ts +2 -2
- package/dist/annotation/data/hooks/useAnnotationDoc.js +3 -8
- package/dist/annotation/data/hooks/useAnnotationList.d.ts +2 -2
- package/dist/annotation/data/hooks/useAnnotationList.js +3 -1
- package/dist/annotation/data/hooks/useAnnotationMutations.d.ts +3 -3
- package/dist/annotation/data/hooks/useAnnotationMutations.js +9 -5
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/types/annotation.d.ts +1 -0
- package/dist/types/firestore.d.ts +6 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
37
37
|
style?: CSSProperties;
|
|
38
38
|
magnifierTopOffset?: number;
|
|
39
39
|
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
40
|
+
snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
|
|
40
41
|
}
|
|
41
42
|
export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
|
|
42
43
|
export interface GestureConfig {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Skia, useFont, useTypeface } from '@shopify/react-native-skia';
|
|
2
|
+
import { Skia, useCanvasRef, useFont, useTypeface, } from '@shopify/react-native-skia';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, } from 'react';
|
|
4
4
|
import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
|
|
5
5
|
import { TEXT_FONT_FAMILY } from './elements/ShapeElement.js';
|
|
@@ -21,7 +21,25 @@ const LONG_PRESS_MS = 500;
|
|
|
21
21
|
const LONG_PRESS_SLOP_PX = 10;
|
|
22
22
|
const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
|
|
23
23
|
export const AnnotationCanvasInner = (props) => {
|
|
24
|
-
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, } = props;
|
|
24
|
+
const { resolveImageUrl, stampFontSource, stampValueFontSize = 14, gestures, width, height, style, activeToolId, tools, snapshotRef, } = props;
|
|
25
|
+
// Snapshot plumbing for thumbnail capture: hold the <Canvas> ref and publish
|
|
26
|
+
// a capture fn on the consumer's snapshotRef. encodeToBytes() yields PNG.
|
|
27
|
+
const canvasRef = useCanvasRef();
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!snapshotRef)
|
|
30
|
+
return;
|
|
31
|
+
snapshotRef.current = () => {
|
|
32
|
+
const image = canvasRef.current?.makeImageSnapshot();
|
|
33
|
+
if (!image)
|
|
34
|
+
return null;
|
|
35
|
+
const bytes = image.encodeToBytes();
|
|
36
|
+
image.dispose?.();
|
|
37
|
+
return bytes ?? null;
|
|
38
|
+
};
|
|
39
|
+
return () => {
|
|
40
|
+
snapshotRef.current = null;
|
|
41
|
+
};
|
|
42
|
+
}, [snapshotRef, canvasRef]);
|
|
25
43
|
const wheelMode = gestures?.wheel ?? 'auto';
|
|
26
44
|
const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
|
|
27
45
|
const allowSpacePan = panTriggers.includes('space');
|
|
@@ -276,6 +294,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
276
294
|
effectiveCanvas: state.effectiveCanvas,
|
|
277
295
|
worldTransform: state.worldTransform,
|
|
278
296
|
resolveImageUrl,
|
|
297
|
+
canvasRef,
|
|
279
298
|
valueFont,
|
|
280
299
|
textFontMgr,
|
|
281
300
|
penDrawingStroke: state.penDrawingStroke,
|
|
@@ -38,5 +38,6 @@ export interface AnnotationCanvasInnerProps {
|
|
|
38
38
|
style?: ViewStyle;
|
|
39
39
|
magnifierTopOffset?: number;
|
|
40
40
|
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
41
|
+
snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
|
|
41
42
|
}
|
|
42
43
|
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type SkFont, type SkImage, type SkPath, type SkTypefaceFontProvider, type Transforms3d } from '@shopify/react-native-skia';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import { type CanvasRef, type SkFont, type SkImage, type SkPath, type SkTypefaceFontProvider, type Transforms3d } from '@shopify/react-native-skia';
|
|
2
|
+
import type { ReactNode, RefObject } from 'react';
|
|
3
3
|
import type { AnnotationCanvasState, AnnotationStroke, StrokeCap } from '../../types/annotation.js';
|
|
4
4
|
type AnimatedPoint = {
|
|
5
5
|
x: number;
|
|
@@ -88,6 +88,7 @@ export interface AnnotationCanvasSkiaProps {
|
|
|
88
88
|
};
|
|
89
89
|
customPreview?: ReactNode;
|
|
90
90
|
wrapContent?: (content: ReactNode) => ReactNode;
|
|
91
|
+
canvasRef?: RefObject<CanvasRef | null>;
|
|
91
92
|
}
|
|
92
|
-
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, backgroundSkImage, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
93
|
+
export declare const AnnotationCanvasSkia: ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, backgroundSkImage, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, canvasRef, }: AnnotationCanvasSkiaProps) => import("react/jsx-runtime").JSX.Element;
|
|
93
94
|
export {};
|
|
@@ -15,7 +15,7 @@ import { StrokeElement } from './elements/StrokeElement.js';
|
|
|
15
15
|
// against the document; the rectangle border stays thinner (it outlines a
|
|
16
16
|
// region rather than pointing at one).
|
|
17
17
|
const MEASUREMENT_LINE_COLOR = '#0066FF';
|
|
18
|
-
const MEASUREMENT_LINE_WIDTH =
|
|
18
|
+
const MEASUREMENT_LINE_WIDTH = 16;
|
|
19
19
|
const MEASUREMENT_RECT_WIDTH = 2;
|
|
20
20
|
// Endpoint handle color — constant selection chrome, independent of the (now
|
|
21
21
|
// editable) line color so the handles stay legible on any line color.
|
|
@@ -78,7 +78,7 @@ const SelectionBox = ({ bounds, isDragging, transform, }) => (_jsx(DraggableElem
|
|
|
78
78
|
// since the function-call pattern works identically on native we use it
|
|
79
79
|
// in both Inners for consistency. Don't add hooks here; this is a plain
|
|
80
80
|
// JSX-returning helper, not a component.
|
|
81
|
-
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, backgroundSkImage, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, }) => {
|
|
81
|
+
export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTransform, resolveImageUrl, backgroundSkImage, valueFont, textFontMgr, penDrawingStroke, livePreview, shapePreview, draggingId, dragTransform, resizingId, resizeTransform, selectedId, endpointDragId, shapeEndpointDragId, liveLineP1, liveLineP2, rectDragId, liveRect, shapeResizeId, shapeResizePath, shapeResizeBox, handleRadius, handleRingWidth, customPreview, wrapContent, canvasRef, }) => {
|
|
82
82
|
const world = (_jsxs(Group, { transform: worldTransform, children: [effectiveCanvas.viewport.backgroundImage && (_jsx(BackgroundImageElement, { image: effectiveCanvas.viewport.backgroundImage, docWidth: effectiveCanvas.viewport.width, docHeight: effectiveCanvas.viewport.height, fit: effectiveCanvas.viewport.backgroundFit ?? 'contain', resolveUrl: resolveImageUrl, skImage: backgroundSkImage })), effectiveCanvas.strokes.map((stroke) => (_jsx(DraggableElement, { isDragging: stroke.id === draggingId, transform: dragTransform, children: _jsx(StrokeElement, { stroke: stroke }) }, stroke.id))), effectiveCanvas.shapes.map((shape) => {
|
|
83
83
|
// Line/arrow shapes support endpoint editing. When selected they show
|
|
84
84
|
// grab handles at both ends; during an endpoint drag the line renders
|
|
@@ -220,5 +220,5 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
|
|
|
220
220
|
}
|
|
221
221
|
return null;
|
|
222
222
|
})(), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), shapePreview && (_jsxs(_Fragment, { children: [_jsx(Path, { path: shapePreview.path, color: shapePreview.color, style: "stroke", strokeWidth: shapePreview.width, strokeCap: toSkiaStrokeCap(shapePreview.cap), strokeJoin: "round", children: shapePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(shapePreview.width) })) }), _jsx(Path, { path: shapePreview.headPath, color: shapePreview.color, style: "fill" })] })), livePreview?.handoffPaths?.map((p, i) => (_jsx(Path, { path: p, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) }, i))), livePreview && (_jsx(Path, { path: livePreview.path, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) })), customPreview] }));
|
|
223
|
-
return (_jsx(Canvas, { style: { width, height }, children: wrapContent ? wrapContent(world) : world }));
|
|
223
|
+
return (_jsx(Canvas, { ref: canvasRef, style: { width, height }, children: wrapContent ? wrapContent(world) : world }));
|
|
224
224
|
};
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Image, useImage } from '@shopify/react-native-skia';
|
|
2
|
+
import { Group, Image, ImageSVG, Skia, useImage, } from '@shopify/react-native-skia';
|
|
3
3
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { imageDocRect } from '../viewport.js';
|
|
5
|
-
// Resolve a background image's (possibly expired) storage URL
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// magnifier overlay) — otherwise each canvas mounts its own loader and flashes
|
|
9
|
-
// a blank while it re-resolves + re-decodes. Returns null until loaded, or when
|
|
10
|
-
// `image` is null/undefined. Hooks run unconditionally regardless of `image`.
|
|
11
|
-
export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
5
|
+
// Resolve a background image's (possibly expired) storage URL. Shared by the
|
|
6
|
+
// raster and SVG loaders below.
|
|
7
|
+
const useBackgroundUrl = (image, resolveUrl) => {
|
|
12
8
|
const [url, setUrl] = useState(image?.downloadUrl ?? '');
|
|
13
9
|
// Hold resolveUrl in a ref so re-resolution is keyed only on the image
|
|
14
10
|
// identity, not the function's. Consumers commonly pass an inline closure
|
|
@@ -33,17 +29,76 @@ export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
|
33
29
|
return () => {
|
|
34
30
|
cancelled = true;
|
|
35
31
|
};
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
33
|
}, [image?.downloadUrl, image?.storagePath]);
|
|
34
|
+
return url;
|
|
35
|
+
};
|
|
36
|
+
// Null-safe SVG loader. NOT RN Skia's useSVG: that hook throws synchronously
|
|
37
|
+
// on a null/undefined source (despite its DataSourceParam type), so it can't
|
|
38
|
+
// be called unconditionally from a component that usually renders raster
|
|
39
|
+
// backgrounds. An empty url resolves to null (nothing to draw yet).
|
|
40
|
+
const useBackgroundSvg = (url) => {
|
|
41
|
+
const [svg, setSvg] = useState(null);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!url) {
|
|
44
|
+
setSvg(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url);
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
const next = Skia.SVG.MakeFromString(text);
|
|
53
|
+
if (!cancelled)
|
|
54
|
+
setSvg(next);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.warn('[BackgroundImageElement] failed to load SVG background', e);
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
63
|
+
}, [url]);
|
|
64
|
+
return svg;
|
|
65
|
+
};
|
|
66
|
+
// Resolve a background image's URL and decode it to an SkImage. Exported so a
|
|
67
|
+
// consumer can hoist the decode ABOVE the canvas and share one SkImage across
|
|
68
|
+
// multiple canvases (e.g. the main canvas + a magnifier overlay) — otherwise
|
|
69
|
+
// each canvas mounts its own loader and flashes a blank while it re-resolves +
|
|
70
|
+
// re-decodes. Returns null until loaded, or when `image` is null/undefined.
|
|
71
|
+
// Hooks run unconditionally regardless of `image`. Raster only — SVG
|
|
72
|
+
// backgrounds self-load in the element below (parsing an SVG is cheap).
|
|
73
|
+
export const useBackgroundSkImage = (image, resolveUrl) => {
|
|
74
|
+
const url = useBackgroundUrl(image, resolveUrl);
|
|
37
75
|
return useImage(url || null);
|
|
38
76
|
};
|
|
39
77
|
export const BackgroundImageElement = memo(({ image, docWidth, docHeight, fit = 'contain', resolveUrl, skImage: skImageProp, }) => {
|
|
40
|
-
|
|
41
|
-
//
|
|
78
|
+
const isSvg = image.format === 'svg';
|
|
79
|
+
// Self-load only when no pre-decoded image is provided; the hooks still run
|
|
80
|
+
// unconditionally (with a null source, so they no-op) to satisfy hook rules.
|
|
42
81
|
const provided = skImageProp !== undefined;
|
|
43
|
-
const loaded = useBackgroundSkImage(provided ? null : image, resolveUrl);
|
|
82
|
+
const loaded = useBackgroundSkImage(provided || isSvg ? null : image, resolveUrl);
|
|
83
|
+
const svgUrl = useBackgroundUrl(isSvg ? image : null, resolveUrl);
|
|
84
|
+
const svg = useBackgroundSvg(isSvg ? svgUrl : '');
|
|
44
85
|
const skImage = provided ? skImageProp : loaded;
|
|
86
|
+
const dims = imageDocRect(image.widthPx, image.heightPx, docWidth, docHeight, fit);
|
|
87
|
+
if (isSvg) {
|
|
88
|
+
if (!svg)
|
|
89
|
+
return null;
|
|
90
|
+
// Draw the SVG at its stamped intrinsic size inside a scale transform
|
|
91
|
+
// (rather than relying on ImageSVG's own fitting, which only applies to
|
|
92
|
+
// sources with a viewBox). The vector source re-rasterizes per frame, so
|
|
93
|
+
// it stays crisp at any canvas zoom.
|
|
94
|
+
return (_jsx(Group, { transform: [
|
|
95
|
+
{ translateX: dims.x },
|
|
96
|
+
{ translateY: dims.y },
|
|
97
|
+
{ scaleX: dims.width / image.widthPx },
|
|
98
|
+
{ scaleY: dims.height / image.heightPx },
|
|
99
|
+
], children: _jsx(ImageSVG, { svg: svg, x: 0, y: 0, width: image.widthPx, height: image.heightPx }) }));
|
|
100
|
+
}
|
|
45
101
|
if (!skImage)
|
|
46
102
|
return null;
|
|
47
|
-
const dims = imageDocRect(image.widthPx, image.heightPx, docWidth, docHeight, fit);
|
|
48
103
|
return (_jsx(Image, { image: skImage, x: dims.x, y: dims.y, width: dims.width, height: dims.height, fit: "fill" }));
|
|
49
104
|
});
|
|
@@ -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;
|
|
@@ -7,6 +7,14 @@ export interface JobScope {
|
|
|
7
7
|
export interface JobGroupScope extends JobScope {
|
|
8
8
|
groupId: string;
|
|
9
9
|
}
|
|
10
|
+
export interface TemplateScope {
|
|
11
|
+
kind: 'template';
|
|
12
|
+
orgId?: string;
|
|
13
|
+
templateId: string;
|
|
14
|
+
}
|
|
15
|
+
export type AnnotationScope = JobGroupScope | TemplateScope;
|
|
16
|
+
export declare const isTemplateScope: (s: AnnotationScope) => s is TemplateScope;
|
|
17
|
+
export declare const annotationScopeKey: (s: AnnotationScope | null) => string;
|
|
10
18
|
export type Unsubscribe = () => void;
|
|
11
19
|
export type FieldOp = {
|
|
12
20
|
kind: 'serverTimestamp';
|
|
@@ -26,6 +34,9 @@ export declare const isFieldOp: (v: unknown) => v is FieldOp;
|
|
|
26
34
|
export type Patch<T> = {
|
|
27
35
|
[K in keyof T]?: T[K] | FieldOp;
|
|
28
36
|
};
|
|
37
|
+
export type AnnotationFilePatch = Patch<AnnotationFile> & {
|
|
38
|
+
[dotPath: `fileData.${string}`]: unknown;
|
|
39
|
+
};
|
|
29
40
|
export interface ImageBlob {
|
|
30
41
|
data: Blob | ArrayBuffer | string;
|
|
31
42
|
contentType: string;
|
|
@@ -51,15 +62,15 @@ export interface AnnotationFileSummary {
|
|
|
51
62
|
createdBy?: AnnotationFile['createdBy'];
|
|
52
63
|
}
|
|
53
64
|
export interface AnnotationDataProvider {
|
|
54
|
-
create(scope:
|
|
55
|
-
get(scope:
|
|
56
|
-
update(scope:
|
|
57
|
-
delete(scope:
|
|
58
|
-
subscribe(scope:
|
|
59
|
-
list(scope:
|
|
65
|
+
create(scope: AnnotationScope, seed: Partial<AnnotationFile>): Promise<string>;
|
|
66
|
+
get(scope: AnnotationScope, fileId: string): Promise<AnnotationFile | null>;
|
|
67
|
+
update(scope: AnnotationScope, fileId: string, patch: AnnotationFilePatch): Promise<void>;
|
|
68
|
+
delete(scope: AnnotationScope, fileId: string): Promise<void>;
|
|
69
|
+
subscribe(scope: AnnotationScope, fileId: string, onNext: (doc: AnnotationFile | null) => void, onError?: (err: Error) => void): Unsubscribe;
|
|
70
|
+
list(scope: AnnotationScope, onNext: (files: AnnotationFileSummary[]) => void, onError?: (err: Error) => void): Unsubscribe;
|
|
60
71
|
subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
|
|
61
72
|
subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
|
|
62
|
-
uploadImage(scope:
|
|
63
|
-
getImageUrl(scope:
|
|
64
|
-
deleteImage(scope:
|
|
73
|
+
uploadImage(scope: AnnotationScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
|
|
74
|
+
getImageUrl(scope: AnnotationScope, fileId: string, storagePath: string): Promise<string>;
|
|
75
|
+
deleteImage(scope: AnnotationScope, fileId: string, storagePath: string): Promise<void>;
|
|
65
76
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export const isTemplateScope = (s) => 'kind' in s && s.kind === 'template';
|
|
2
|
+
// Stable string identity for a scope, for React hook dep arrays — scope
|
|
3
|
+
// objects are typically rebuilt every render.
|
|
4
|
+
export const annotationScopeKey = (s) => s == null
|
|
5
|
+
? ''
|
|
6
|
+
: isTemplateScope(s)
|
|
7
|
+
? `template/${s.orgId ?? ''}/${s.templateId}`
|
|
8
|
+
: `job/${s.orgId}/${s.projectId}/${s.jobId}/${s.groupId}`;
|
|
1
9
|
export const isFieldOp = (v) => !!v &&
|
|
2
10
|
typeof v === 'object' &&
|
|
3
11
|
'kind' in v &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Measurement } from '../../types/firestore.js';
|
|
2
|
-
import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type
|
|
2
|
+
import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFilePatch, type AnnotationFileSummary, type AnnotationScope, type ImageBlob, type JobGroupScope, type JobScope, type Unsubscribe, type UploadedImageRef } from './AnnotationDataProvider.js';
|
|
3
3
|
export declare class InMemoryAnnotationProvider implements AnnotationDataProvider {
|
|
4
4
|
private docs;
|
|
5
5
|
private measurements;
|
|
@@ -10,17 +10,17 @@ export declare class InMemoryAnnotationProvider implements AnnotationDataProvide
|
|
|
10
10
|
private jobMeasurementListeners;
|
|
11
11
|
private nextId;
|
|
12
12
|
setMeasurements(scope: JobGroupScope, measurements: Measurement[]): void;
|
|
13
|
-
create(scope:
|
|
14
|
-
get(scope:
|
|
15
|
-
update(scope:
|
|
16
|
-
delete(scope:
|
|
17
|
-
subscribe(scope:
|
|
18
|
-
list(scope:
|
|
13
|
+
create(scope: AnnotationScope, seed: Partial<AnnotationFile>): Promise<string>;
|
|
14
|
+
get(scope: AnnotationScope, fileId: string): Promise<AnnotationFile | null>;
|
|
15
|
+
update(scope: AnnotationScope, fileId: string, patch: AnnotationFilePatch): Promise<void>;
|
|
16
|
+
delete(scope: AnnotationScope, fileId: string): Promise<void>;
|
|
17
|
+
subscribe(scope: AnnotationScope, fileId: string, onNext: (doc: AnnotationFile | null) => void): Unsubscribe;
|
|
18
|
+
list(scope: AnnotationScope, onNext: (files: AnnotationFileSummary[]) => void): Unsubscribe;
|
|
19
19
|
subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
|
|
20
20
|
subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
|
|
21
|
-
uploadImage(scope:
|
|
22
|
-
getImageUrl(_scope:
|
|
23
|
-
deleteImage(_scope:
|
|
21
|
+
uploadImage(scope: AnnotationScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
|
|
22
|
+
getImageUrl(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<string>;
|
|
23
|
+
deleteImage(_scope: AnnotationScope, fileId: string, storagePath: string): Promise<void>;
|
|
24
24
|
private getBucket;
|
|
25
25
|
private notifyDoc;
|
|
26
26
|
private notifyList;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { FileUploadType } from '../../types/firestore.js';
|
|
2
|
-
import { isFieldOp, } from './AnnotationDataProvider.js';
|
|
3
|
-
const scopeKey =
|
|
4
|
-
|
|
2
|
+
import { annotationScopeKey, isFieldOp, } from './AnnotationDataProvider.js';
|
|
3
|
+
const scopeKey = annotationScopeKey;
|
|
4
|
+
// Prefix of annotationScopeKey for job scopes, so collectJobMeasurements can
|
|
5
|
+
// match group keys by startsWith.
|
|
6
|
+
const jobKey = (s) => `job/${s.orgId}/${s.projectId}/${s.jobId}`;
|
|
5
7
|
const summarize = (file) => ({
|
|
6
8
|
id: file.id,
|
|
7
9
|
name: file.name,
|
|
@@ -37,6 +39,30 @@ const resolveFieldOp = (op) => {
|
|
|
37
39
|
return undefined;
|
|
38
40
|
}
|
|
39
41
|
};
|
|
42
|
+
// Firestore-style dotted field paths: 'fileData.canvas' merges into the
|
|
43
|
+
// nested map (creating intermediate maps as needed) instead of replacing the
|
|
44
|
+
// whole top-level field — mirrors what the real SDKs do with dotted keys.
|
|
45
|
+
const applyDotPaths = (base, resolved) => {
|
|
46
|
+
const next = { ...base };
|
|
47
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
48
|
+
if (!key.includes('.')) {
|
|
49
|
+
next[key] = value;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const parts = key.split('.');
|
|
53
|
+
let target = next;
|
|
54
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
55
|
+
const existing = target[parts[i]];
|
|
56
|
+
target[parts[i]] =
|
|
57
|
+
existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
58
|
+
? { ...existing }
|
|
59
|
+
: {};
|
|
60
|
+
target = target[parts[i]];
|
|
61
|
+
}
|
|
62
|
+
target[parts[parts.length - 1]] = value;
|
|
63
|
+
}
|
|
64
|
+
return next;
|
|
65
|
+
};
|
|
40
66
|
// Simple test/dev provider. Stores documents and image blobs in memory and
|
|
41
67
|
// fans out subscription notifications synchronously. Not designed for
|
|
42
68
|
// performance — designed for predictable behavior in tests and Storybook.
|
|
@@ -86,7 +112,7 @@ export class InMemoryAnnotationProvider {
|
|
|
86
112
|
if (!prev)
|
|
87
113
|
throw new Error(`Annotation file ${fileId} not found`);
|
|
88
114
|
const resolved = applyFieldOps(patch);
|
|
89
|
-
const next =
|
|
115
|
+
const next = applyDotPaths(prev, resolved);
|
|
90
116
|
bucket.set(fileId, next);
|
|
91
117
|
this.notifyDoc(scope, fileId);
|
|
92
118
|
this.notifyList(scope);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnnotationScope, ImageBlob } from '../AnnotationDataProvider.js';
|
|
3
3
|
export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
|
|
4
4
|
export interface UseAnnotationCanvasDocOptions {
|
|
5
|
-
scope:
|
|
5
|
+
scope: AnnotationScope | null;
|
|
6
6
|
fileId: string | null;
|
|
7
7
|
fallbackViewport?: Partial<AnnotationViewport>;
|
|
8
8
|
debounceMs?: number;
|
|
@@ -223,10 +223,17 @@ export const useAnnotationCanvasDoc = (options) => {
|
|
|
223
223
|
}
|
|
224
224
|
else {
|
|
225
225
|
const doc = dataRef.current;
|
|
226
|
+
const fileType = doc?.fileData.fileType ?? createSeedRef.current?.fileType ?? 'sketch';
|
|
227
|
+
const isLabel = doc?.fileData.isLabel ?? createSeedRef.current?.isLabel;
|
|
228
|
+
// Dotted field paths so the write MERGES into fileData instead of
|
|
229
|
+
// replacing the map — the doc may carry sibling fileData keys the
|
|
230
|
+
// canvas doesn't own (a calculator file's columns/tableConfig/
|
|
231
|
+
// isCompleted, …) which a whole-map write would silently delete.
|
|
226
232
|
await updateRef.current(id, {
|
|
227
|
-
fileData:
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
'fileData.fileType': fileType,
|
|
234
|
+
...(isLabel !== undefined ? { 'fileData.isLabel': isLabel } : {}),
|
|
235
|
+
'fileData.canvas': canvasPayload,
|
|
236
|
+
'fileData.canvasRev': canvasRev,
|
|
230
237
|
});
|
|
231
238
|
if (debug) {
|
|
232
239
|
console.log('[useAnnotationCanvasDoc] updated file', id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnnotationFile, type AnnotationScope } from '../AnnotationDataProvider.js';
|
|
2
2
|
export interface UseAnnotationDocResult {
|
|
3
3
|
data: AnnotationFile | null;
|
|
4
4
|
loading: boolean;
|
|
5
5
|
error: Error | null;
|
|
6
6
|
}
|
|
7
|
-
export declare const useAnnotationDoc: (scope:
|
|
7
|
+
export declare const useAnnotationDoc: (scope: AnnotationScope | null, fileId: string | null) => UseAnnotationDocResult;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useAnnotationData } from '../AnnotationDataContext.js';
|
|
3
|
+
import { annotationScopeKey, } from '../AnnotationDataProvider.js';
|
|
3
4
|
export const useAnnotationDoc = (scope, fileId) => {
|
|
4
5
|
const provider = useAnnotationData();
|
|
5
6
|
const [data, setData] = useState(null);
|
|
@@ -21,13 +22,7 @@ export const useAnnotationDoc = (scope, fileId) => {
|
|
|
21
22
|
setLoading(false);
|
|
22
23
|
});
|
|
23
24
|
return unsubscribe;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
scope?.orgId,
|
|
27
|
-
scope?.projectId,
|
|
28
|
-
scope?.jobId,
|
|
29
|
-
scope?.groupId,
|
|
30
|
-
fileId,
|
|
31
|
-
]);
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
+
}, [provider, annotationScopeKey(scope), fileId]);
|
|
32
27
|
return { data, loading, error };
|
|
33
28
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnnotationFileSummary, type AnnotationScope } from '../AnnotationDataProvider.js';
|
|
2
2
|
export interface UseAnnotationListResult {
|
|
3
3
|
files: AnnotationFileSummary[];
|
|
4
4
|
loading: boolean;
|
|
5
5
|
error: Error | null;
|
|
6
6
|
}
|
|
7
|
-
export declare const useAnnotationList: (scope:
|
|
7
|
+
export declare const useAnnotationList: (scope: AnnotationScope | null) => UseAnnotationListResult;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useAnnotationData } from '../AnnotationDataContext.js';
|
|
3
|
+
import { annotationScopeKey, } from '../AnnotationDataProvider.js';
|
|
3
4
|
export const useAnnotationList = (scope) => {
|
|
4
5
|
const provider = useAnnotationData();
|
|
5
6
|
const [files, setFiles] = useState([]);
|
|
@@ -21,6 +22,7 @@ export const useAnnotationList = (scope) => {
|
|
|
21
22
|
setLoading(false);
|
|
22
23
|
});
|
|
23
24
|
return unsubscribe;
|
|
24
|
-
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
+
}, [provider, annotationScopeKey(scope)]);
|
|
25
27
|
return { files, loading, error };
|
|
26
28
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnnotationFile, type AnnotationFilePatch, type AnnotationScope, type ImageBlob, type UploadedImageRef } from '../AnnotationDataProvider.js';
|
|
2
2
|
export interface AnnotationMutations {
|
|
3
3
|
create(seed: Partial<AnnotationFile>): Promise<string>;
|
|
4
|
-
update(fileId: string, patch:
|
|
4
|
+
update(fileId: string, patch: AnnotationFilePatch): Promise<void>;
|
|
5
5
|
remove(fileId: string): Promise<void>;
|
|
6
6
|
uploadImage(fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
|
|
7
7
|
deleteImage(fileId: string, storagePath: string): Promise<void>;
|
|
8
8
|
}
|
|
9
|
-
export declare const useAnnotationMutations: (scope:
|
|
9
|
+
export declare const useAnnotationMutations: (scope: AnnotationScope) => AnnotationMutations;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import { useAnnotationData } from '../AnnotationDataContext.js';
|
|
3
|
+
import { annotationScopeKey, } from '../AnnotationDataProvider.js';
|
|
3
4
|
export const useAnnotationMutations = (scope) => {
|
|
4
5
|
const provider = useAnnotationData();
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
6
|
+
const scopeKey = annotationScopeKey(scope);
|
|
7
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
8
|
+
const create = useCallback((seed) => provider.create(scope, seed), [provider, scopeKey]);
|
|
9
|
+
const update = useCallback((fileId, patch) => provider.update(scope, fileId, patch), [provider, scopeKey]);
|
|
10
|
+
const remove = useCallback((fileId) => provider.delete(scope, fileId), [provider, scopeKey]);
|
|
11
|
+
const uploadImage = useCallback((fileId, role, blob) => provider.uploadImage(scope, fileId, role, blob), [provider, scopeKey]);
|
|
12
|
+
const deleteImage = useCallback((fileId, storagePath) => provider.deleteImage(scope, fileId, storagePath), [provider, scopeKey]);
|
|
13
|
+
/* eslint-enable react-hooks/exhaustive-deps */
|
|
10
14
|
return useMemo(() => ({ create, update, remove, uploadImage, deleteImage }), [create, update, remove, uploadImage, deleteImage]);
|
|
11
15
|
};
|
package/dist/exports.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from './types/annotation.js';
|
|
|
9
9
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
10
10
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
11
11
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
12
|
-
export { isFieldOp, type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type FieldOp, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef, } from './annotation/data/AnnotationDataProvider.js';
|
|
12
|
+
export { annotationScopeKey, isFieldOp, isTemplateScope, type AnnotationDataProvider, type AnnotationFile, type AnnotationFilePatch, type AnnotationFileSummary, type AnnotationScope, type FieldOp, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type TemplateScope, type Unsubscribe, type UploadedImageRef, } from './annotation/data/AnnotationDataProvider.js';
|
|
13
13
|
export { AnnotationDataProviderContext, useAnnotationData, type AnnotationDataProviderProps, } from './annotation/data/AnnotationDataContext.js';
|
|
14
14
|
export { useAnnotationDoc, type UseAnnotationDocResult, } from './annotation/data/hooks/useAnnotationDoc.js';
|
|
15
15
|
export { useAnnotationList, type UseAnnotationListResult, } from './annotation/data/hooks/useAnnotationList.js';
|
package/dist/exports.js
CHANGED
|
@@ -14,7 +14,7 @@ export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, gen
|
|
|
14
14
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
15
15
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
16
16
|
// Annotation data layer (SDK-neutral; apps provide their own provider).
|
|
17
|
-
export { isFieldOp, } from './annotation/data/AnnotationDataProvider.js';
|
|
17
|
+
export { annotationScopeKey, isFieldOp, isTemplateScope, } from './annotation/data/AnnotationDataProvider.js';
|
|
18
18
|
export { AnnotationDataProviderContext, useAnnotationData, } from './annotation/data/AnnotationDataContext.js';
|
|
19
19
|
export { useAnnotationDoc, } from './annotation/data/hooks/useAnnotationDoc.js';
|
|
20
20
|
export { useAnnotationList, } from './annotation/data/hooks/useAnnotationList.js';
|
|
@@ -126,6 +126,11 @@ export interface CalculatorFileData {
|
|
|
126
126
|
diagramFileId: string | null;
|
|
127
127
|
columns: Record<string, any>;
|
|
128
128
|
isCompleted: boolean;
|
|
129
|
+
canvas?: AnnotationCanvasState;
|
|
130
|
+
canvasRev?: {
|
|
131
|
+
clientId: string;
|
|
132
|
+
seq: number;
|
|
133
|
+
};
|
|
129
134
|
}
|
|
130
135
|
export interface LayoutGroupFileData {
|
|
131
136
|
layoutCount?: number;
|
|
@@ -184,7 +189,7 @@ export type FileUpload = FileUploadBase & ({
|
|
|
184
189
|
fileData?: undefined;
|
|
185
190
|
} | {
|
|
186
191
|
type: FileUploadType.Template;
|
|
187
|
-
fileData?:
|
|
192
|
+
fileData?: AnnotationFileData;
|
|
188
193
|
} | {
|
|
189
194
|
type: FileUploadType.Label;
|
|
190
195
|
fileData: Label;
|