@reekon-tools/boldr-utils 1.6.18 → 1.6.19

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.
Files changed (82) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +7 -1
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +58 -5
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +5 -1
  4. package/dist/annotation/canvas/AnnotationCanvasSkia.js +53 -4
  5. package/dist/annotation/canvas/Tool.d.ts +5 -2
  6. package/dist/annotation/canvas/measurementGeometry.d.ts +1 -1
  7. package/dist/annotation/canvas/measurementGeometry.js +3 -2
  8. package/dist/annotation/canvas/stampLayout.d.ts +7 -1
  9. package/dist/annotation/canvas/stampLayout.js +66 -5
  10. package/dist/annotation/canvas/tools/measurementLineTool.d.ts +12 -0
  11. package/dist/annotation/canvas/tools/measurementLineTool.js +95 -0
  12. package/dist/annotation/canvas/tools/panTool.js +1 -1
  13. package/dist/annotation/canvas/tools/selectTool.js +75 -9
  14. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +1 -0
  15. package/dist/annotation/canvas/useAnnotationCanvasState.js +9 -0
  16. package/dist/exports.d.ts +1 -0
  17. package/dist/exports.js +1 -0
  18. package/dist/types/annotation.d.ts +4 -0
  19. package/dist/types/annotation.js +6 -0
  20. package/dist/types/firestore.d.ts +53 -0
  21. package/dist/types/firestore.js +49 -0
  22. package/package.json +1 -1
  23. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  24. package/dist/canvas/AnnotationCanvas.js +0 -10
  25. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  26. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  27. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  28. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  29. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  30. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  31. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  32. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  33. package/dist/canvas/Tool.d.ts +0 -38
  34. package/dist/canvas/Tool.js +0 -1
  35. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  36. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  37. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  38. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  39. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  40. package/dist/canvas/elements/ShapeElement.js +0 -62
  41. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  42. package/dist/canvas/elements/StrokeElement.js +0 -18
  43. package/dist/canvas/measurementPicker.d.ts +0 -10
  44. package/dist/canvas/measurementPicker.js +0 -1
  45. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  46. package/dist/canvas/measurementStampOverlay.js +0 -1
  47. package/dist/canvas/pointerAdapter.d.ts +0 -3
  48. package/dist/canvas/pointerAdapter.js +0 -19
  49. package/dist/canvas/stampLayout.d.ts +0 -5
  50. package/dist/canvas/stampLayout.js +0 -14
  51. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  52. package/dist/canvas/tools/measurementStampTool.js +0 -37
  53. package/dist/canvas/tools/panTool.d.ts +0 -5
  54. package/dist/canvas/tools/panTool.js +0 -25
  55. package/dist/canvas/tools/penTool.d.ts +0 -13
  56. package/dist/canvas/tools/penTool.js +0 -68
  57. package/dist/canvas/tools/selectTool.d.ts +0 -2
  58. package/dist/canvas/tools/selectTool.js +0 -182
  59. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  60. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  61. package/dist/canvas/viewport.d.ts +0 -16
  62. package/dist/canvas/viewport.js +0 -54
  63. package/dist/data/AnnotationDataContext.d.ts +0 -8
  64. package/dist/data/AnnotationDataContext.js +0 -11
  65. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  66. package/dist/data/AnnotationDataProvider.js +0 -4
  67. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  68. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  69. package/dist/data/canvasPersistence.d.ts +0 -3
  70. package/dist/data/canvasPersistence.js +0 -26
  71. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  72. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  73. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  74. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  75. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  76. package/dist/data/hooks/useAnnotationList.js +0 -26
  77. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  78. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  79. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  80. package/dist/hooks/useParseMeasurement.js +0 -14
  81. package/dist/utils/evaluateFormula.d.ts +0 -20
  82. package/dist/utils/evaluateFormula.js +0 -31
@@ -1,210 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../types/annotation.js';
3
- import { createViewportApi, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
4
- // Platform-agnostic state machine for the annotation canvas. Web and native
5
- // inners share this hook; each wraps it with platform-specific event
6
- // capture and JSX (div + DOM events vs. GestureDetector + RN Views).
7
- export const useAnnotationCanvasState = (props) => {
8
- const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, width, height, initialViewport, imperativeRef, } = props;
9
- const [viewport, setViewport] = useState(initialViewport ?? DEFAULT_VIEWPORT);
10
- const [toolState, setToolState] = useState(undefined);
11
- const [previewPatch, setPreviewPatch] = useState(null);
12
- const undoStackRef = useRef([]);
13
- const redoStackRef = useRef([]);
14
- const activePointerIdRef = useRef(null);
15
- const activeTool = useMemo(() => tools.find((t) => t.id === activeToolId) ?? null, [tools, activeToolId]);
16
- const effectiveCanvas = useMemo(() => (previewPatch ? applyPatch(canvas, previewPatch) : canvas), [canvas, previewPatch]);
17
- const measurementsById = useMemo(() => {
18
- const map = new Map();
19
- measurements?.forEach((m) => map.set(m.id, m));
20
- return map;
21
- }, [measurements]);
22
- const viewportApi = useMemo(() => createViewportApi(viewport), [viewport]);
23
- const ctx = useMemo(() => ({
24
- document: canvas,
25
- selection,
26
- viewport: viewportApi,
27
- preview(patch) {
28
- setPreviewPatch(patch);
29
- },
30
- commit(patch) {
31
- const inverse = invertPatch(canvas, patch);
32
- undoStackRef.current.push({ forward: patch, inverse });
33
- redoStackRef.current = [];
34
- setPreviewPatch(null);
35
- onCommit(patch);
36
- },
37
- setSelection(s) {
38
- onSelectionChange(s);
39
- },
40
- requestPickMeasurement() {
41
- return pickMeasurement ? pickMeasurement() : Promise.resolve(null);
42
- },
43
- applyPan(deltaScreen) {
44
- setViewport((v) => panBy(v, deltaScreen));
45
- },
46
- applyZoom(focalScreen, nextZoom) {
47
- setViewport((v) => zoomAt(v, focalScreen, nextZoom));
48
- },
49
- }), [
50
- canvas,
51
- selection,
52
- viewportApi,
53
- onCommit,
54
- onSelectionChange,
55
- pickMeasurement,
56
- ]);
57
- // Live ctx for imperative handle methods (which are created in an effect and
58
- // would otherwise capture a stale ctx/viewport).
59
- const ctxRef = useRef(ctx);
60
- ctxRef.current = ctx;
61
- const dispatchPointerDown = useCallback((event) => {
62
- if (!activeTool)
63
- return;
64
- activePointerIdRef.current = event.pointerId;
65
- const next = activeTool.onPointerDown?.(event, ctx, toolState);
66
- if (next !== undefined)
67
- setToolState(next);
68
- }, [activeTool, ctx, toolState]);
69
- const dispatchPointerMove = useCallback((event) => {
70
- if (!activeTool)
71
- return;
72
- if (activePointerIdRef.current !== null &&
73
- event.pointerId !== activePointerIdRef.current) {
74
- return;
75
- }
76
- const next = activeTool.onPointerMove?.(event, ctx, toolState);
77
- if (next !== undefined)
78
- setToolState(next);
79
- }, [activeTool, ctx, toolState]);
80
- const dispatchPointerUp = useCallback((event) => {
81
- if (!activeTool)
82
- return;
83
- if (activePointerIdRef.current !== null &&
84
- event.pointerId !== activePointerIdRef.current) {
85
- return;
86
- }
87
- activeTool.onPointerUp?.(event, ctx, toolState);
88
- activePointerIdRef.current = null;
89
- setToolState(undefined);
90
- }, [activeTool, ctx, toolState]);
91
- const dispatchPointerCancel = useCallback(() => {
92
- if (activeTool)
93
- activeTool.onCancel?.(toolState, ctx);
94
- activePointerIdRef.current = null;
95
- setToolState(undefined);
96
- setPreviewPatch(null);
97
- }, [activeTool, ctx, toolState]);
98
- const pan = useCallback((deltaScreen) => {
99
- setViewport((v) => panBy(v, deltaScreen));
100
- }, []);
101
- const zoom = useCallback((focalScreen, nextZoom) => {
102
- setViewport((v) => zoomAt(v, focalScreen, nextZoom));
103
- }, []);
104
- // Imperative API mirror — set via prop so it survives WithSkiaWeb's lazy
105
- // boundary on web.
106
- useEffect(() => {
107
- if (!imperativeRef)
108
- return;
109
- imperativeRef.current = {
110
- undo() {
111
- const entry = undoStackRef.current.pop();
112
- if (!entry)
113
- return;
114
- redoStackRef.current.push(entry);
115
- onCommit(entry.inverse);
116
- },
117
- redo() {
118
- const entry = redoStackRef.current.pop();
119
- if (!entry)
120
- return;
121
- undoStackRef.current.push(entry);
122
- onCommit(entry.forward);
123
- },
124
- canUndo() {
125
- return undoStackRef.current.length > 0;
126
- },
127
- canRedo() {
128
- return redoStackRef.current.length > 0;
129
- },
130
- zoomToFit() {
131
- setViewport(() => {
132
- const docW = canvas.viewport.width;
133
- const docH = canvas.viewport.height;
134
- const z = Math.min(width / docW, height / docH);
135
- const offsetX = (width - docW * z) / 2;
136
- const offsetY = (height - docH * z) / 2;
137
- return { zoom: z, pan: { x: -offsetX / z, y: -offsetY / z } };
138
- });
139
- },
140
- resetView() {
141
- setViewport(DEFAULT_VIEWPORT);
142
- },
143
- placeMeasurementAtCenter(ref) {
144
- const c = ctxRef.current;
145
- const anchor = c.viewport.screenToWorld({
146
- x: width / 2,
147
- y: height / 2,
148
- });
149
- const placed = {
150
- id: `measurement-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
151
- layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
152
- measurementPath: ref.measurementPath,
153
- measurementId: ref.measurementId,
154
- groupId: ref.groupId,
155
- anchor,
156
- labelOverride: ref.label,
157
- unitOverride: ref.unit,
158
- showLabel: true,
159
- showValue: true,
160
- createdAt: Date.now(),
161
- };
162
- c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
163
- // Select it so the consumer can immediately move it (the consumer is
164
- // responsible for switching to its move/select tool).
165
- c.setSelection({ ids: [placed.id] });
166
- },
167
- };
168
- return () => {
169
- if (imperativeRef)
170
- imperativeRef.current = null;
171
- };
172
- }, [
173
- imperativeRef,
174
- onCommit,
175
- canvas.viewport.width,
176
- canvas.viewport.height,
177
- width,
178
- height,
179
- ]);
180
- const worldTransform = useMemo(() => [
181
- { scale: viewport.zoom },
182
- { translateX: -viewport.pan.x },
183
- { translateY: -viewport.pan.y },
184
- ], [viewport]);
185
- // Detect the built-in pen tool's in-flight state shape so the canvas can
186
- // render the preview (the tool factory is Skia-free and can't render).
187
- const penDrawingStroke = toolState &&
188
- typeof toolState === 'object' &&
189
- 'kind' in toolState &&
190
- toolState.kind === 'pen-drawing'
191
- ? toolState.stroke
192
- : null;
193
- return {
194
- effectiveCanvas,
195
- worldTransform,
196
- viewport,
197
- measurementsById,
198
- activeTool,
199
- toolState,
200
- ctx,
201
- penDrawingStroke,
202
- customPreviewState: toolState,
203
- dispatchPointerDown,
204
- dispatchPointerMove,
205
- dispatchPointerUp,
206
- dispatchPointerCancel,
207
- pan,
208
- zoom,
209
- };
210
- };
@@ -1,16 +0,0 @@
1
- import type { Vec2 } from '../types/annotation.js';
2
- export interface ViewportState {
3
- zoom: number;
4
- pan: Vec2;
5
- }
6
- export interface ViewportApi {
7
- state: ViewportState;
8
- screenToWorld(screen: Vec2): Vec2;
9
- worldToScreen(world: Vec2): Vec2;
10
- worldDistanceToScreen(distance: number): number;
11
- }
12
- export declare const createViewportApi: (state: ViewportState) => ViewportApi;
13
- export declare const DEFAULT_VIEWPORT: ViewportState;
14
- export declare const fitToScreen: (docWidth: number, docHeight: number, screenWidth: number, screenHeight: number, padding?: number) => ViewportState;
15
- export declare const zoomAt: (state: ViewportState, focalScreen: Vec2, nextZoom: number) => ViewportState;
16
- export declare const panBy: (state: ViewportState, deltaScreen: Vec2) => ViewportState;
@@ -1,54 +0,0 @@
1
- export const createViewportApi = (state) => ({
2
- state,
3
- screenToWorld: ({ x, y }) => ({
4
- x: x / state.zoom + state.pan.x,
5
- y: y / state.zoom + state.pan.y,
6
- }),
7
- worldToScreen: ({ x, y }) => ({
8
- x: (x - state.pan.x) * state.zoom,
9
- y: (y - state.pan.y) * state.zoom,
10
- }),
11
- worldDistanceToScreen: (d) => d * state.zoom,
12
- });
13
- export const DEFAULT_VIEWPORT = {
14
- zoom: 1,
15
- pan: { x: 0, y: 0 },
16
- };
17
- // Returns a viewport that fits `docWidth x docHeight` into `screenWidth x
18
- // screenHeight` with optional padding (screen-space pixels).
19
- export const fitToScreen = (docWidth, docHeight, screenWidth, screenHeight, padding = 16) => {
20
- const availableW = Math.max(1, screenWidth - padding * 2);
21
- const availableH = Math.max(1, screenHeight - padding * 2);
22
- const zoom = Math.min(availableW / docWidth, availableH / docHeight);
23
- const renderedW = docWidth * zoom;
24
- const renderedH = docHeight * zoom;
25
- const offsetX = (screenWidth - renderedW) / 2;
26
- const offsetY = (screenHeight - renderedH) / 2;
27
- return {
28
- zoom,
29
- pan: { x: -offsetX / zoom, y: -offsetY / zoom },
30
- };
31
- };
32
- // Zoom toward a focal screen point so the world point under the cursor stays
33
- // fixed. Used for wheel-zoom on web and pinch on native.
34
- export const zoomAt = (state, focalScreen, nextZoom) => {
35
- const clampedZoom = Math.max(0.05, Math.min(50, nextZoom));
36
- const focalWorld = {
37
- x: focalScreen.x / state.zoom + state.pan.x,
38
- y: focalScreen.y / state.zoom + state.pan.y,
39
- };
40
- return {
41
- zoom: clampedZoom,
42
- pan: {
43
- x: focalWorld.x - focalScreen.x / clampedZoom,
44
- y: focalWorld.y - focalScreen.y / clampedZoom,
45
- },
46
- };
47
- };
48
- export const panBy = (state, deltaScreen) => ({
49
- zoom: state.zoom,
50
- pan: {
51
- x: state.pan.x - deltaScreen.x / state.zoom,
52
- y: state.pan.y - deltaScreen.y / state.zoom,
53
- },
54
- });
@@ -1,8 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { AnnotationDataProvider } from './AnnotationDataProvider.js';
3
- export interface AnnotationDataProviderProps {
4
- value: AnnotationDataProvider;
5
- children: ReactNode;
6
- }
7
- export declare const AnnotationDataProviderContext: ({ value, children, }: AnnotationDataProviderProps) => import("react/jsx-runtime").JSX.Element;
8
- export declare const useAnnotationData: () => AnnotationDataProvider;
@@ -1,11 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from 'react';
3
- const AnnotationDataContext = createContext(null);
4
- export const AnnotationDataProviderContext = ({ value, children, }) => (_jsx(AnnotationDataContext.Provider, { value: value, children: children }));
5
- export const useAnnotationData = () => {
6
- const provider = useContext(AnnotationDataContext);
7
- if (!provider) {
8
- throw new Error('useAnnotationData must be used inside <AnnotationDataProviderContext>');
9
- }
10
- return provider;
11
- };
@@ -1,65 +0,0 @@
1
- import type { FileUpload, FileUploadType, Measurement } from '../types/firestore.js';
2
- export interface JobScope {
3
- orgId: string;
4
- projectId: string;
5
- jobId: string;
6
- }
7
- export interface JobGroupScope extends JobScope {
8
- groupId: string;
9
- }
10
- export type Unsubscribe = () => void;
11
- export type FieldOp = {
12
- kind: 'serverTimestamp';
13
- } | {
14
- kind: 'arrayUnion';
15
- values: unknown[];
16
- } | {
17
- kind: 'arrayRemove';
18
- values: unknown[];
19
- } | {
20
- kind: 'increment';
21
- by: number;
22
- } | {
23
- kind: 'delete';
24
- };
25
- export declare const isFieldOp: (v: unknown) => v is FieldOp;
26
- export type Patch<T> = {
27
- [K in keyof T]?: T[K] | FieldOp;
28
- };
29
- export interface ImageBlob {
30
- data: Blob | ArrayBuffer | string;
31
- contentType: string;
32
- filename?: string;
33
- }
34
- export interface UploadedImageRef {
35
- storagePath: string;
36
- downloadUrl: string;
37
- width?: number;
38
- height?: number;
39
- bytes?: number;
40
- }
41
- export type AnnotationFile = Extract<FileUpload, {
42
- type: FileUploadType.Canvas;
43
- }>;
44
- export interface AnnotationFileSummary {
45
- id: string;
46
- name: string;
47
- thumbnailUrl: string | null;
48
- fileType: 'sketch' | 'document';
49
- isLabel?: boolean;
50
- createdAt?: Date;
51
- createdBy?: AnnotationFile['createdBy'];
52
- }
53
- export interface AnnotationDataProvider {
54
- create(scope: JobGroupScope, seed: Partial<AnnotationFile>): Promise<string>;
55
- get(scope: JobGroupScope, fileId: string): Promise<AnnotationFile | null>;
56
- update(scope: JobGroupScope, fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
57
- delete(scope: JobGroupScope, fileId: string): Promise<void>;
58
- subscribe(scope: JobGroupScope, fileId: string, onNext: (doc: AnnotationFile | null) => void, onError?: (err: Error) => void): Unsubscribe;
59
- list(scope: JobGroupScope, onNext: (files: AnnotationFileSummary[]) => void, onError?: (err: Error) => void): Unsubscribe;
60
- subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
61
- subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void, onError?: (err: Error) => void): Unsubscribe;
62
- uploadImage(scope: JobGroupScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
63
- getImageUrl(scope: JobGroupScope, fileId: string, storagePath: string): Promise<string>;
64
- deleteImage(scope: JobGroupScope, fileId: string, storagePath: string): Promise<void>;
65
- }
@@ -1,4 +0,0 @@
1
- export const isFieldOp = (v) => !!v &&
2
- typeof v === 'object' &&
3
- 'kind' in v &&
4
- typeof v.kind === 'string';
@@ -1,30 +0,0 @@
1
- import { type Measurement } from '../types/firestore.js';
2
- import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef } from './AnnotationDataProvider.js';
3
- export declare class InMemoryAnnotationProvider implements AnnotationDataProvider {
4
- private docs;
5
- private measurements;
6
- private images;
7
- private docListeners;
8
- private listListeners;
9
- private groupMeasurementListeners;
10
- private jobMeasurementListeners;
11
- private nextId;
12
- setMeasurements(scope: JobGroupScope, measurements: Measurement[]): void;
13
- create(scope: JobGroupScope, seed: Partial<AnnotationFile>): Promise<string>;
14
- get(scope: JobGroupScope, fileId: string): Promise<AnnotationFile | null>;
15
- update(scope: JobGroupScope, fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
16
- delete(scope: JobGroupScope, fileId: string): Promise<void>;
17
- subscribe(scope: JobGroupScope, fileId: string, onNext: (doc: AnnotationFile | null) => void): Unsubscribe;
18
- list(scope: JobGroupScope, onNext: (files: AnnotationFileSummary[]) => void): Unsubscribe;
19
- subscribeGroupMeasurements(scope: JobGroupScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
20
- subscribeJobMeasurements(scope: JobScope, onNext: (measurements: Measurement[]) => void): Unsubscribe;
21
- uploadImage(scope: JobGroupScope, fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
22
- getImageUrl(_scope: JobGroupScope, fileId: string, storagePath: string): Promise<string>;
23
- deleteImage(_scope: JobGroupScope, fileId: string, storagePath: string): Promise<void>;
24
- private getBucket;
25
- private notifyDoc;
26
- private notifyList;
27
- private notifyGroupMeasurements;
28
- private notifyJobMeasurements;
29
- private collectJobMeasurements;
30
- }
@@ -1,197 +0,0 @@
1
- import { FileUploadType } from '../types/firestore.js';
2
- import { isFieldOp, } from './AnnotationDataProvider.js';
3
- const scopeKey = (s) => `${s.orgId}/${s.projectId}/${s.jobId}/${s.groupId}`;
4
- const jobKey = (s) => `${s.orgId}/${s.projectId}/${s.jobId}`;
5
- const summarize = (file) => ({
6
- id: file.id,
7
- name: file.name,
8
- thumbnailUrl: file.thumbnailUrl,
9
- fileType: file.fileData.fileType,
10
- isLabel: file.fileData.isLabel,
11
- createdAt: file.createdAt,
12
- createdBy: file.createdBy,
13
- });
14
- const applyFieldOps = (patch) => {
15
- const out = {};
16
- for (const [key, value] of Object.entries(patch)) {
17
- if (isFieldOp(value)) {
18
- out[key] = resolveFieldOp(value);
19
- }
20
- else {
21
- out[key] = value;
22
- }
23
- }
24
- return out;
25
- };
26
- const resolveFieldOp = (op) => {
27
- switch (op.kind) {
28
- case 'serverTimestamp':
29
- return new Date();
30
- case 'arrayUnion':
31
- return op.values;
32
- case 'arrayRemove':
33
- return [];
34
- case 'increment':
35
- return op.by;
36
- case 'delete':
37
- return undefined;
38
- }
39
- };
40
- // Simple test/dev provider. Stores documents and image blobs in memory and
41
- // fans out subscription notifications synchronously. Not designed for
42
- // performance — designed for predictable behavior in tests and Storybook.
43
- export class InMemoryAnnotationProvider {
44
- constructor() {
45
- this.docs = new Map();
46
- this.measurements = new Map();
47
- this.images = new Map();
48
- this.docListeners = new Map();
49
- this.listListeners = new Map();
50
- this.groupMeasurementListeners = new Map();
51
- this.jobMeasurementListeners = new Map();
52
- this.nextId = 1;
53
- }
54
- // Seed helpers for tests
55
- setMeasurements(scope, measurements) {
56
- this.measurements.set(scopeKey(scope), measurements);
57
- this.notifyGroupMeasurements(scope);
58
- this.notifyJobMeasurements({
59
- orgId: scope.orgId,
60
- projectId: scope.projectId,
61
- jobId: scope.jobId,
62
- });
63
- }
64
- async create(scope, seed) {
65
- const id = seed.id ?? `mem-${this.nextId++}`;
66
- const file = {
67
- id,
68
- name: seed.name ?? 'Untitled annotation',
69
- thumbnailUrl: seed.thumbnailUrl ?? null,
70
- createdAt: seed.createdAt ?? new Date(),
71
- createdBy: seed.createdBy,
72
- type: FileUploadType.Canvas,
73
- fileData: seed.fileData ?? { fileType: 'sketch' },
74
- };
75
- this.getBucket(scope).set(id, file);
76
- this.notifyDoc(scope, id);
77
- this.notifyList(scope);
78
- return id;
79
- }
80
- async get(scope, fileId) {
81
- return this.getBucket(scope).get(fileId) ?? null;
82
- }
83
- async update(scope, fileId, patch) {
84
- const bucket = this.getBucket(scope);
85
- const prev = bucket.get(fileId);
86
- if (!prev)
87
- throw new Error(`Annotation file ${fileId} not found`);
88
- const resolved = applyFieldOps(patch);
89
- const next = { ...prev, ...resolved };
90
- bucket.set(fileId, next);
91
- this.notifyDoc(scope, fileId);
92
- this.notifyList(scope);
93
- }
94
- async delete(scope, fileId) {
95
- this.getBucket(scope).delete(fileId);
96
- this.notifyDoc(scope, fileId);
97
- this.notifyList(scope);
98
- }
99
- subscribe(scope, fileId, onNext) {
100
- const key = `${scopeKey(scope)}/${fileId}`;
101
- const set = this.docListeners.get(key) ?? new Set();
102
- set.add(onNext);
103
- this.docListeners.set(key, set);
104
- queueMicrotask(() => onNext(this.getBucket(scope).get(fileId) ?? null));
105
- return () => {
106
- set.delete(onNext);
107
- };
108
- }
109
- list(scope, onNext) {
110
- const key = scopeKey(scope);
111
- const set = this.listListeners.get(key) ?? new Set();
112
- set.add(onNext);
113
- this.listListeners.set(key, set);
114
- queueMicrotask(() => onNext(Array.from(this.getBucket(scope).values()).map(summarize)));
115
- return () => {
116
- set.delete(onNext);
117
- };
118
- }
119
- subscribeGroupMeasurements(scope, onNext) {
120
- const key = scopeKey(scope);
121
- const set = this.groupMeasurementListeners.get(key) ?? new Set();
122
- set.add(onNext);
123
- this.groupMeasurementListeners.set(key, set);
124
- queueMicrotask(() => onNext(this.measurements.get(key) ?? []));
125
- return () => {
126
- set.delete(onNext);
127
- };
128
- }
129
- subscribeJobMeasurements(scope, onNext) {
130
- const key = jobKey(scope);
131
- const set = this.jobMeasurementListeners.get(key) ?? new Set();
132
- set.add(onNext);
133
- this.jobMeasurementListeners.set(key, set);
134
- queueMicrotask(() => onNext(this.collectJobMeasurements(scope)));
135
- return () => {
136
- set.delete(onNext);
137
- };
138
- }
139
- async uploadImage(scope, fileId, role, blob) {
140
- const storagePath = `${scopeKey(scope)}/${fileId}/${role}`;
141
- const ref = {
142
- storagePath,
143
- downloadUrl: `mem://${storagePath}`,
144
- };
145
- const bucket = this.images.get(fileId) ?? new Map();
146
- bucket.set(storagePath, { blob, ref });
147
- this.images.set(fileId, bucket);
148
- return ref;
149
- }
150
- async getImageUrl(_scope, fileId, storagePath) {
151
- const ref = this.images.get(fileId)?.get(storagePath);
152
- if (!ref)
153
- throw new Error(`Image ${storagePath} not found`);
154
- return ref.ref.downloadUrl;
155
- }
156
- async deleteImage(_scope, fileId, storagePath) {
157
- this.images.get(fileId)?.delete(storagePath);
158
- }
159
- getBucket(scope) {
160
- const key = scopeKey(scope);
161
- let bucket = this.docs.get(key);
162
- if (!bucket) {
163
- bucket = new Map();
164
- this.docs.set(key, bucket);
165
- }
166
- return bucket;
167
- }
168
- notifyDoc(scope, fileId) {
169
- const key = `${scopeKey(scope)}/${fileId}`;
170
- const doc = this.getBucket(scope).get(fileId) ?? null;
171
- this.docListeners.get(key)?.forEach((cb) => cb(doc));
172
- }
173
- notifyList(scope) {
174
- const key = scopeKey(scope);
175
- const files = Array.from(this.getBucket(scope).values()).map(summarize);
176
- this.listListeners.get(key)?.forEach((cb) => cb(files));
177
- }
178
- notifyGroupMeasurements(scope) {
179
- const key = scopeKey(scope);
180
- const ms = this.measurements.get(key) ?? [];
181
- this.groupMeasurementListeners.get(key)?.forEach((cb) => cb(ms));
182
- }
183
- notifyJobMeasurements(scope) {
184
- const key = jobKey(scope);
185
- const ms = this.collectJobMeasurements(scope);
186
- this.jobMeasurementListeners.get(key)?.forEach((cb) => cb(ms));
187
- }
188
- collectJobMeasurements(scope) {
189
- const prefix = `${jobKey(scope)}/`;
190
- const out = [];
191
- for (const [key, ms] of this.measurements) {
192
- if (key.startsWith(prefix))
193
- out.push(...ms);
194
- }
195
- return out;
196
- }
197
- }
@@ -1,3 +0,0 @@
1
- import { type AnnotationCanvasState, type AnnotationViewport } from '../types/annotation.js';
2
- import type { AnnotationFile } from './AnnotationDataProvider.js';
3
- export declare const hydrateCanvasState: (file: AnnotationFile | null, fallbackViewport?: Partial<AnnotationViewport>) => AnnotationCanvasState;
@@ -1,26 +0,0 @@
1
- import { createEmptyCanvasState, } from '../types/annotation.js';
2
- // Bring a persisted canvas forward to the current schema. Keyed on
3
- // `schemaVersion` so each client migrates identically and the provider can
4
- // stay a dumb transport. Identity for v1 today; add a `case 1: return
5
- // upgradeV1toV2(raw)` arm when the schema bumps.
6
- const migrateCanvasState = (raw) => {
7
- switch (raw.schemaVersion) {
8
- case 1:
9
- return raw;
10
- default:
11
- // Forward-compat: a doc written by a newer client. Trust it as-is
12
- // rather than risk a lossy downgrade-write.
13
- return raw;
14
- }
15
- };
16
- // Turn a loaded annotation file into a working canvas state ready to feed the
17
- // canvas. Missing/legacy docs (no `fileData.canvas`) start from an empty
18
- // canvas sized by `fallbackViewport`. This is the single entry point for going
19
- // from `useAnnotationDoc` data to an `AnnotationCanvasState` — consumers should
20
- // not call `createEmptyCanvasState` directly.
21
- export const hydrateCanvasState = (file, fallbackViewport) => {
22
- const persisted = file?.fileData.canvas;
23
- if (!persisted)
24
- return createEmptyCanvasState(fallbackViewport);
25
- return migrateCanvasState(persisted);
26
- };
@@ -1,33 +0,0 @@
1
- import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../types/annotation.js';
2
- import type { ImageBlob, JobGroupScope } from '../AnnotationDataProvider.js';
3
- export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
4
- export interface UseAnnotationCanvasDocOptions {
5
- scope: JobGroupScope | null;
6
- fileId: string | null;
7
- fallbackViewport?: Partial<AnnotationViewport>;
8
- debounceMs?: number;
9
- createSeed?: {
10
- name?: string;
11
- fileType?: 'sketch' | 'document';
12
- isLabel?: boolean;
13
- };
14
- onFileCreated?: (fileId: string) => void;
15
- onSaveError?: (error: unknown) => void;
16
- captureThumbnail?: () => Promise<ImageBlob | null>;
17
- debugLogging?: boolean;
18
- }
19
- export interface UseAnnotationCanvasDocResult {
20
- canvas: AnnotationCanvasState;
21
- onCommit: (patch: AnnotationDocumentPatch) => void;
22
- loading: boolean;
23
- error: Error | null;
24
- saveStatus: SaveStatus;
25
- save: () => Promise<void>;
26
- ensureFileId: () => Promise<string>;
27
- setBackgroundImage: (blob: ImageBlob, dims: {
28
- width: number;
29
- height: number;
30
- }, fit?: BackgroundFit) => Promise<void>;
31
- clearBackgroundImage: () => Promise<void>;
32
- }
33
- export declare const useAnnotationCanvasDoc: (options: UseAnnotationCanvasDocOptions) => UseAnnotationCanvasDocResult;