@reekon-tools/boldr-utils 1.13.2 → 1.14.1

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 (83) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +2 -2
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +7 -7
  3. package/dist/annotation/canvas/Tool.d.ts +4 -3
  4. package/dist/annotation/canvas/measurementGeometry.d.ts +1 -1
  5. package/dist/annotation/canvas/measurementGeometry.js +5 -4
  6. package/dist/annotation/canvas/stampLayout.d.ts +2 -1
  7. package/dist/annotation/canvas/stampLayout.js +18 -2
  8. package/dist/annotation/canvas/tools/panTool.js +1 -1
  9. package/dist/annotation/canvas/tools/selectTool.js +14 -10
  10. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -0
  11. package/dist/annotation/canvas/useAnnotationCanvasState.js +24 -3
  12. package/dist/canvas/AnnotationCanvas.d.ts +11 -0
  13. package/dist/canvas/AnnotationCanvas.js +10 -0
  14. package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
  15. package/dist/canvas/AnnotationCanvas.native.js +6 -0
  16. package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
  17. package/dist/canvas/AnnotationCanvasInner.js +219 -0
  18. package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
  19. package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
  20. package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
  21. package/dist/canvas/AnnotationCanvasSkia.js +20 -0
  22. package/dist/canvas/Tool.d.ts +38 -0
  23. package/dist/canvas/Tool.js +1 -0
  24. package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
  25. package/dist/canvas/elements/BackgroundImageElement.js +37 -0
  26. package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
  27. package/dist/canvas/elements/MeasurementStampElement.js +30 -0
  28. package/dist/canvas/elements/ShapeElement.d.ts +7 -0
  29. package/dist/canvas/elements/ShapeElement.js +62 -0
  30. package/dist/canvas/elements/StrokeElement.d.ts +7 -0
  31. package/dist/canvas/elements/StrokeElement.js +18 -0
  32. package/dist/canvas/measurementPicker.d.ts +10 -0
  33. package/dist/canvas/measurementPicker.js +1 -0
  34. package/dist/canvas/measurementStampOverlay.d.ts +11 -0
  35. package/dist/canvas/measurementStampOverlay.js +1 -0
  36. package/dist/canvas/pointerAdapter.d.ts +3 -0
  37. package/dist/canvas/pointerAdapter.js +19 -0
  38. package/dist/canvas/stampLayout.d.ts +5 -0
  39. package/dist/canvas/stampLayout.js +14 -0
  40. package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
  41. package/dist/canvas/tools/measurementStampTool.js +37 -0
  42. package/dist/canvas/tools/panTool.d.ts +5 -0
  43. package/dist/canvas/tools/panTool.js +25 -0
  44. package/dist/canvas/tools/penTool.d.ts +13 -0
  45. package/dist/canvas/tools/penTool.js +68 -0
  46. package/dist/canvas/tools/selectTool.d.ts +2 -0
  47. package/dist/canvas/tools/selectTool.js +182 -0
  48. package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
  49. package/dist/canvas/useAnnotationCanvasState.js +210 -0
  50. package/dist/canvas/viewport.d.ts +16 -0
  51. package/dist/canvas/viewport.js +54 -0
  52. package/dist/data/AnnotationDataContext.d.ts +8 -0
  53. package/dist/data/AnnotationDataContext.js +11 -0
  54. package/dist/data/AnnotationDataProvider.d.ts +65 -0
  55. package/dist/data/AnnotationDataProvider.js +4 -0
  56. package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
  57. package/dist/data/InMemoryAnnotationProvider.js +197 -0
  58. package/dist/data/canvasPersistence.d.ts +3 -0
  59. package/dist/data/canvasPersistence.js +26 -0
  60. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
  61. package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
  62. package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
  63. package/dist/data/hooks/useAnnotationDoc.js +33 -0
  64. package/dist/data/hooks/useAnnotationList.d.ts +7 -0
  65. package/dist/data/hooks/useAnnotationList.js +26 -0
  66. package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
  67. package/dist/data/hooks/useAnnotationMutations.js +11 -0
  68. package/dist/exports.d.ts +1 -1
  69. package/dist/exports.js +1 -1
  70. package/dist/hooks/useParseMeasurement.d.ts +4 -0
  71. package/dist/hooks/useParseMeasurement.js +14 -0
  72. package/dist/types/annotation.d.ts +4 -0
  73. package/dist/types/annotation.js +25 -8
  74. package/dist/types/firestore.d.ts +1 -0
  75. package/dist/utils/evaluateFormula.d.ts +20 -0
  76. package/dist/utils/evaluateFormula.js +31 -0
  77. package/package.json +1 -1
  78. package/dist/theme/index.d.ts +0 -3
  79. package/dist/theme/index.js +0 -3
  80. package/dist/theme/preset.d.ts +0 -120
  81. package/dist/theme/preset.js +0 -25
  82. package/dist/theme/semantic.d.ts +0 -136
  83. package/dist/theme/semantic.js +0 -116
@@ -0,0 +1,314 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { applyPatch, createEmptyCanvasState, } from '../../types/annotation.js';
3
+ import { FileUploadType } from '../../types/firestore.js';
4
+ import { useAnnotationDoc } from './useAnnotationDoc.js';
5
+ import { useAnnotationMutations } from './useAnnotationMutations.js';
6
+ import { hydrateCanvasState } from '../canvasPersistence.js';
7
+ // Stable placeholder so useAnnotationMutations (which requires a non-null
8
+ // scope) can be called unconditionally. Never used to write — flushes are
9
+ // guarded on a real scope.
10
+ const EMPTY_SCOPE = {
11
+ orgId: '',
12
+ projectId: '',
13
+ jobId: '',
14
+ groupId: '',
15
+ };
16
+ // Build the persisted fileData, omitting `isLabel` when undefined so the write
17
+ // contains no undefined values (Firestore rejects them on RN).
18
+ const buildFileData = (fileType, isLabel, canvas) => ({
19
+ fileType,
20
+ ...(isLabel !== undefined ? { isLabel } : {}),
21
+ canvas,
22
+ });
23
+ // Orchestrates load + auto-save for the annotation canvas. Hydrates the working
24
+ // state from the persisted doc, applies commits optimistically, and persists
25
+ // (debounced) through the data provider — creating the file on first save when
26
+ // no `fileId` was supplied. Composes the existing low-level hooks so it stays
27
+ // decoupled from any specific Firebase SDK.
28
+ export const useAnnotationCanvasDoc = (options) => {
29
+ const { scope, fileId, fallbackViewport, debounceMs = 800, createSeed, onFileCreated, onSaveError, captureThumbnail, debugLogging = false, } = options;
30
+ const { data, loading, error } = useAnnotationDoc(scope, fileId);
31
+ const { create, update, uploadImage, deleteImage } = useAnnotationMutations(scope ?? EMPTY_SCOPE);
32
+ const [working, setWorking] = useState(null);
33
+ const [saveStatus, setSaveStatus] = useState('idle');
34
+ // Refs let the debounced/unmount flush read the latest values without
35
+ // re-creating timers on every keystroke.
36
+ const workingRef = useRef(null);
37
+ const dataRef = useRef(data);
38
+ const statusRef = useRef('idle');
39
+ const timerRef = useRef(null);
40
+ const createRef = useRef(create);
41
+ const updateRef = useRef(update);
42
+ const fileIdRef = useRef(fileId);
43
+ // Id of a file this hook created on first save (when opened with no fileId).
44
+ // Used for subsequent updates and to recognize the caller echoing it back
45
+ // into `fileId`.
46
+ const createdIdRef = useRef(null);
47
+ const createSeedRef = useRef(createSeed);
48
+ const onFileCreatedRef = useRef(onFileCreated);
49
+ const onSaveErrorRef = useRef(onSaveError);
50
+ const captureThumbnailRef = useRef(captureThumbnail);
51
+ const uploadImageRef = useRef(uploadImage);
52
+ // Guards against overlapping thumbnail captures (each save fires one).
53
+ const thumbnailSavingRef = useRef(false);
54
+ const debugRef = useRef(debugLogging);
55
+ // JSON of the canvas we last wrote, to recognize (and ignore) the snapshot
56
+ // echo of our own write when reconciling incoming remote changes.
57
+ const lastSavedJsonRef = useRef(null);
58
+ // Guards against two creates racing if flushes overlap.
59
+ const creatingRef = useRef(false);
60
+ workingRef.current = working;
61
+ dataRef.current = data;
62
+ statusRef.current = saveStatus;
63
+ createRef.current = create;
64
+ updateRef.current = update;
65
+ createSeedRef.current = createSeed;
66
+ onFileCreatedRef.current = onFileCreated;
67
+ onSaveErrorRef.current = onSaveError;
68
+ captureThumbnailRef.current = captureThumbnail;
69
+ uploadImageRef.current = uploadImage;
70
+ debugRef.current = debugLogging;
71
+ const setStatus = useCallback((next) => {
72
+ statusRef.current = next;
73
+ setSaveStatus(next);
74
+ }, []);
75
+ // Fire-and-forget thumbnail capture + upload, run after each successful save.
76
+ // Stable identity (reads refs) so it doesn't churn `flush`'s deps. Skips when
77
+ // no capturer is supplied or a capture is already in flight.
78
+ const saveThumbnail = useCallback(async (fileId) => {
79
+ const capture = captureThumbnailRef.current;
80
+ if (!capture || thumbnailSavingRef.current)
81
+ return;
82
+ thumbnailSavingRef.current = true;
83
+ try {
84
+ const blob = await capture();
85
+ if (blob)
86
+ await uploadImageRef.current(fileId, 'thumbnail', blob);
87
+ }
88
+ catch (e) {
89
+ console.warn('[useAnnotationCanvasDoc] thumbnail save failed', e);
90
+ }
91
+ finally {
92
+ thumbnailSavingRef.current = false;
93
+ }
94
+ }, []);
95
+ // Reset working state when the target document changes so the next snapshot
96
+ // hydrates the new file rather than leaking the previous one. Skip the reset
97
+ // when the new fileId is one we just created (the caller echoing it back) —
98
+ // our working state is already correct and must not be wiped.
99
+ useEffect(() => {
100
+ if (fileIdRef.current === fileId)
101
+ return;
102
+ fileIdRef.current = fileId;
103
+ if (fileId !== null && fileId === createdIdRef.current)
104
+ return;
105
+ if (timerRef.current) {
106
+ clearTimeout(timerRef.current);
107
+ timerRef.current = null;
108
+ }
109
+ createdIdRef.current = null;
110
+ lastSavedJsonRef.current = null;
111
+ setWorking(null);
112
+ setStatus('idle');
113
+ }, [fileId, setStatus]);
114
+ // Hydrate / reconcile from the persisted doc.
115
+ useEffect(() => {
116
+ // First load — always hydrate.
117
+ if (workingRef.current === null) {
118
+ setWorking(hydrateCanvasState(data, fallbackViewport));
119
+ lastSavedJsonRef.current = data?.fileData.canvas
120
+ ? JSON.stringify(data.fileData.canvas)
121
+ : null;
122
+ return;
123
+ }
124
+ // A write of ours is queued or in flight — ignore the snapshot; it is
125
+ // either the echo of our write or about to be superseded by it.
126
+ if (statusRef.current === 'saving' || timerRef.current !== null)
127
+ return;
128
+ // Clean locally: accept a genuine remote change, but ignore the echo of
129
+ // our own last write (same content).
130
+ const incoming = data?.fileData.canvas;
131
+ if (!incoming)
132
+ return;
133
+ const incomingJson = JSON.stringify(incoming);
134
+ if (incomingJson === lastSavedJsonRef.current)
135
+ return;
136
+ lastSavedJsonRef.current = incomingJson;
137
+ setWorking(hydrateCanvasState(data, fallbackViewport));
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ }, [data]);
140
+ const flush = useCallback(async () => {
141
+ if (timerRef.current) {
142
+ clearTimeout(timerRef.current);
143
+ timerRef.current = null;
144
+ }
145
+ const canvas = workingRef.current;
146
+ // Nothing to persist, or no real target scope yet.
147
+ if (!canvas || !scope)
148
+ return;
149
+ if (creatingRef.current)
150
+ return;
151
+ const json = JSON.stringify(canvas);
152
+ const id = fileIdRef.current ?? createdIdRef.current;
153
+ const mode = id ? 'update' : 'create';
154
+ const debug = debugRef.current;
155
+ if (debug) {
156
+ console.log('[useAnnotationCanvasDoc] save attempt', {
157
+ mode,
158
+ fileId: id,
159
+ scope,
160
+ bytes: json.length,
161
+ strokes: canvas.strokes.length,
162
+ shapes: canvas.shapes.length,
163
+ measurements: canvas.placedMeasurements.length,
164
+ });
165
+ }
166
+ // Round-trip through JSON to drop `undefined` keys (e.g. an empty canvas's
167
+ // viewport.backgroundImage/backgroundFit). Firestore — RN in particular —
168
+ // rejects undefined field values unless ignoreUndefinedProperties is set.
169
+ const canvasPayload = JSON.parse(json);
170
+ setStatus('saving');
171
+ try {
172
+ if (!id) {
173
+ // First save with no file — create the doc seeded with the canvas.
174
+ creatingRef.current = true;
175
+ const seed = createSeedRef.current;
176
+ const newId = await createRef.current({
177
+ type: FileUploadType.Canvas,
178
+ ...(seed?.name !== undefined ? { name: seed.name } : {}),
179
+ fileData: buildFileData(seed?.fileType ?? 'sketch', seed?.isLabel, canvasPayload),
180
+ });
181
+ creatingRef.current = false;
182
+ createdIdRef.current = newId;
183
+ if (debug) {
184
+ console.log('[useAnnotationCanvasDoc] created file', newId);
185
+ }
186
+ onFileCreatedRef.current?.(newId);
187
+ }
188
+ else {
189
+ const doc = dataRef.current;
190
+ await updateRef.current(id, {
191
+ fileData: buildFileData(doc?.fileData.fileType ??
192
+ createSeedRef.current?.fileType ??
193
+ 'sketch', doc?.fileData.isLabel ?? createSeedRef.current?.isLabel, canvasPayload),
194
+ });
195
+ if (debug) {
196
+ console.log('[useAnnotationCanvasDoc] updated file', id);
197
+ }
198
+ }
199
+ lastSavedJsonRef.current = json;
200
+ // Refresh the file's thumbnail to match what was just saved (fire and
201
+ // forget — never blocks or fails the save).
202
+ const savedId = fileIdRef.current ?? createdIdRef.current;
203
+ if (savedId)
204
+ void saveThumbnail(savedId);
205
+ // If new edits landed mid-flight, stay dirty and let the next debounce
206
+ // (or unmount) flush them.
207
+ const latest = workingRef.current;
208
+ if (latest && JSON.stringify(latest) !== json) {
209
+ setStatus('dirty');
210
+ }
211
+ else {
212
+ setStatus('saved');
213
+ }
214
+ }
215
+ catch (e) {
216
+ creatingRef.current = false;
217
+ // Always log save failures with context — these are otherwise invisible
218
+ // (the canvas keeps working from local state).
219
+ console.error(`[useAnnotationCanvasDoc] ${mode} failed`, { fileId: id, scope, bytes: json.length }, e);
220
+ onSaveErrorRef.current?.(e);
221
+ setStatus('error');
222
+ }
223
+ }, [scope, setStatus, saveThumbnail]);
224
+ const onCommit = useCallback((patch) => {
225
+ setWorking((prev) => (prev ? applyPatch(prev, patch) : prev));
226
+ setStatus('dirty');
227
+ if (timerRef.current)
228
+ clearTimeout(timerRef.current);
229
+ timerRef.current = setTimeout(() => {
230
+ timerRef.current = null;
231
+ void flush();
232
+ }, debounceMs);
233
+ }, [debounceMs, flush, setStatus]);
234
+ const ensureFileId = useCallback(async () => {
235
+ const existing = fileIdRef.current ?? createdIdRef.current;
236
+ if (existing)
237
+ return existing;
238
+ // No file yet — seed an empty canvas if nothing has hydrated, then flush
239
+ // so the existing create branch mints the doc.
240
+ if (!workingRef.current) {
241
+ const seeded = createEmptyCanvasState(fallbackViewport);
242
+ workingRef.current = seeded;
243
+ setWorking(seeded);
244
+ }
245
+ await flush();
246
+ const id = fileIdRef.current ?? createdIdRef.current;
247
+ if (!id) {
248
+ throw new Error('Unable to create annotation file before background upload');
249
+ }
250
+ return id;
251
+ }, [flush, fallbackViewport]);
252
+ const setBackgroundImage = useCallback(async (blob, dims, fit = 'contain') => {
253
+ const id = await ensureFileId();
254
+ const ref = await uploadImage(id, 'background', blob);
255
+ onCommit({
256
+ ops: [
257
+ {
258
+ op: 'setViewport',
259
+ patch: {
260
+ backgroundImage: {
261
+ storagePath: ref.storagePath,
262
+ downloadUrl: ref.downloadUrl,
263
+ widthPx: dims.width,
264
+ heightPx: dims.height,
265
+ },
266
+ backgroundFit: fit,
267
+ },
268
+ },
269
+ ],
270
+ });
271
+ }, [ensureFileId, uploadImage, onCommit]);
272
+ const clearBackgroundImage = useCallback(async () => {
273
+ const bg = workingRef.current?.viewport.backgroundImage;
274
+ const id = fileIdRef.current ?? createdIdRef.current;
275
+ if (bg && id) {
276
+ try {
277
+ await deleteImage(id, bg.storagePath);
278
+ }
279
+ catch (e) {
280
+ // Non-fatal: still clear the viewport reference even if the storage
281
+ // object is already gone.
282
+ console.warn('[useAnnotationCanvasDoc] failed to delete background image', e);
283
+ }
284
+ }
285
+ onCommit({
286
+ ops: [
287
+ {
288
+ op: 'setViewport',
289
+ patch: { backgroundImage: undefined, backgroundFit: undefined },
290
+ },
291
+ ],
292
+ });
293
+ }, [deleteImage, onCommit]);
294
+ // Flush any pending edits on unmount.
295
+ useEffect(() => () => {
296
+ if (timerRef.current) {
297
+ clearTimeout(timerRef.current);
298
+ timerRef.current = null;
299
+ void flush();
300
+ }
301
+ }, [flush]);
302
+ const canvas = useMemo(() => working ?? createEmptyCanvasState(fallbackViewport), [working, fallbackViewport]);
303
+ return {
304
+ canvas,
305
+ onCommit,
306
+ loading,
307
+ error,
308
+ saveStatus,
309
+ save: flush,
310
+ ensureFileId,
311
+ setBackgroundImage,
312
+ clearBackgroundImage,
313
+ };
314
+ };
@@ -0,0 +1,7 @@
1
+ import type { AnnotationFile, JobGroupScope } from '../AnnotationDataProvider.js';
2
+ export interface UseAnnotationDocResult {
3
+ data: AnnotationFile | null;
4
+ loading: boolean;
5
+ error: Error | null;
6
+ }
7
+ export declare const useAnnotationDoc: (scope: JobGroupScope | null, fileId: string | null) => UseAnnotationDocResult;
@@ -0,0 +1,33 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useAnnotationData } from '../AnnotationDataContext.js';
3
+ export const useAnnotationDoc = (scope, fileId) => {
4
+ const provider = useAnnotationData();
5
+ const [data, setData] = useState(null);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState(null);
8
+ useEffect(() => {
9
+ if (!scope || !fileId) {
10
+ setData(null);
11
+ setLoading(false);
12
+ return;
13
+ }
14
+ setLoading(true);
15
+ setError(null);
16
+ const unsubscribe = provider.subscribe(scope, fileId, (doc) => {
17
+ setData(doc);
18
+ setLoading(false);
19
+ }, (err) => {
20
+ setError(err);
21
+ setLoading(false);
22
+ });
23
+ return unsubscribe;
24
+ }, [
25
+ provider,
26
+ scope?.orgId,
27
+ scope?.projectId,
28
+ scope?.jobId,
29
+ scope?.groupId,
30
+ fileId,
31
+ ]);
32
+ return { data, loading, error };
33
+ };
@@ -0,0 +1,7 @@
1
+ import type { AnnotationFileSummary, JobGroupScope } from '../AnnotationDataProvider.js';
2
+ export interface UseAnnotationListResult {
3
+ files: AnnotationFileSummary[];
4
+ loading: boolean;
5
+ error: Error | null;
6
+ }
7
+ export declare const useAnnotationList: (scope: JobGroupScope | null) => UseAnnotationListResult;
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useAnnotationData } from '../AnnotationDataContext.js';
3
+ export const useAnnotationList = (scope) => {
4
+ const provider = useAnnotationData();
5
+ const [files, setFiles] = useState([]);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState(null);
8
+ useEffect(() => {
9
+ if (!scope) {
10
+ setFiles([]);
11
+ setLoading(false);
12
+ return;
13
+ }
14
+ setLoading(true);
15
+ setError(null);
16
+ const unsubscribe = provider.list(scope, (next) => {
17
+ setFiles(next);
18
+ setLoading(false);
19
+ }, (err) => {
20
+ setError(err);
21
+ setLoading(false);
22
+ });
23
+ return unsubscribe;
24
+ }, [provider, scope?.orgId, scope?.projectId, scope?.jobId, scope?.groupId]);
25
+ return { files, loading, error };
26
+ };
@@ -0,0 +1,9 @@
1
+ import type { AnnotationFile, ImageBlob, JobGroupScope, Patch, UploadedImageRef } from '../AnnotationDataProvider.js';
2
+ export interface AnnotationMutations {
3
+ create(seed: Partial<AnnotationFile>): Promise<string>;
4
+ update(fileId: string, patch: Patch<AnnotationFile>): Promise<void>;
5
+ remove(fileId: string): Promise<void>;
6
+ uploadImage(fileId: string, role: 'background' | 'thumbnail', blob: ImageBlob): Promise<UploadedImageRef>;
7
+ deleteImage(fileId: string, storagePath: string): Promise<void>;
8
+ }
9
+ export declare const useAnnotationMutations: (scope: JobGroupScope) => AnnotationMutations;
@@ -0,0 +1,11 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useAnnotationData } from '../AnnotationDataContext.js';
3
+ export const useAnnotationMutations = (scope) => {
4
+ const provider = useAnnotationData();
5
+ const create = useCallback((seed) => provider.create(scope, seed), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
6
+ const update = useCallback((fileId, patch) => provider.update(scope, fileId, patch), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
7
+ const remove = useCallback((fileId) => provider.delete(scope, fileId), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
8
+ const uploadImage = useCallback((fileId, role, blob) => provider.uploadImage(scope, fileId, role, blob), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
9
+ const deleteImage = useCallback((fileId, storagePath) => provider.deleteImage(scope, fileId, storagePath), [provider, scope.orgId, scope.projectId, scope.jobId, scope.groupId]);
10
+ return useMemo(() => ({ create, update, remove, uploadImage, deleteImage }), [create, update, remove, uploadImage, deleteImage]);
11
+ };
package/dist/exports.d.ts CHANGED
@@ -29,7 +29,7 @@ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './a
29
29
  export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
30
30
  export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
31
31
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
32
- export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
32
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
33
33
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
34
34
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
35
35
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
package/dist/exports.js CHANGED
@@ -34,7 +34,7 @@ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMu
34
34
  export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
35
35
  export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
36
36
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
37
- export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
37
+ export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
38
38
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
39
39
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
40
40
  export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
@@ -0,0 +1,4 @@
1
+ export declare const useParseMeasurement: () => {
2
+ parseMeasurementInput: (input: string, defaultUnit?: string) => number | null;
3
+ error: string | null;
4
+ };
@@ -0,0 +1,14 @@
1
+ import { useState } from 'react';
2
+ import { parseMeasurement } from '../utils/parseMeasurement.js';
3
+ export const useParseMeasurement = () => {
4
+ const [error, setError] = useState(null);
5
+ const parseMeasurementInput = (input, defaultUnit = 'mm') => {
6
+ setError(null);
7
+ const result = parseMeasurement(input, defaultUnit);
8
+ if (result === null) {
9
+ setError('Invalid measurement. Please provide a valid number and unit.');
10
+ }
11
+ return result;
12
+ };
13
+ return { parseMeasurementInput, error };
14
+ };
@@ -120,6 +120,7 @@ export interface AnnotationViewport {
120
120
  backgroundImages?: AnnotationBackgroundLayer[];
121
121
  }
122
122
  export type TileScalePlatform = 'web' | 'mobile';
123
+ export type TileScaleTarget = 'measurement' | 'header';
123
124
  export interface AnnotationCanvasState {
124
125
  schemaVersion: 1;
125
126
  layers: AnnotationLayer[];
@@ -129,6 +130,8 @@ export interface AnnotationCanvasState {
129
130
  placedMeasurements: PlacedMeasurementRef[];
130
131
  tileScaleFactor?: number;
131
132
  tileScaleFactorMobile?: number;
133
+ headerTileScaleFactor?: number;
134
+ headerTileScaleFactorMobile?: number;
132
135
  tileDocSize?: number;
133
136
  externalPayloadPath?: string;
134
137
  }
@@ -190,6 +193,7 @@ export type AnnotationPatchOp = {
190
193
  op: 'setTileScaleFactor';
191
194
  value: number;
192
195
  platform?: TileScalePlatform;
196
+ target?: TileScaleTarget;
193
197
  } | {
194
198
  op: 'setTileDocSize';
195
199
  value: number;
@@ -120,6 +120,11 @@ const applyOp = (state, op) => {
120
120
  case 'setViewport':
121
121
  return { ...state, viewport: { ...state.viewport, ...op.patch } };
122
122
  case 'setTileScaleFactor':
123
+ if (op.target === 'header') {
124
+ return op.platform === 'mobile'
125
+ ? { ...state, headerTileScaleFactorMobile: op.value }
126
+ : { ...state, headerTileScaleFactor: op.value };
127
+ }
123
128
  return op.platform === 'mobile'
124
129
  ? { ...state, tileScaleFactorMobile: op.value }
125
130
  : { ...state, tileScaleFactor: op.value };
@@ -223,18 +228,30 @@ const invertOp = (before, op) => {
223
228
  }
224
229
  return { op: 'setViewport', patch: inversePatch };
225
230
  }
226
- case 'setTileScaleFactor':
227
- // Restore the prior EFFECTIVE factor for the same platform knob. Mobile
228
- // falls back to the shared/web value when it has no own value yet (the
229
- // pre-split doc case — mirrors resolveTileScaleFactor); absent → an
230
- // explicit 1 (visually identical), keeping the `value: number` contract.
231
+ case 'setTileScaleFactor': {
232
+ // Restore the prior EFFECTIVE factor for the same platform (and tile
233
+ // kind) knob. Mobile falls back to the shared/web value when it has no
234
+ // own value yet (the pre-split doc case — mirrors
235
+ // resolveTileScaleFactor), and the header knob further falls back to
236
+ // the measurement knob (mirrors resolveHeaderTileScaleFactor); absent →
237
+ // an explicit 1 (visually identical), keeping the `value: number`
238
+ // contract.
239
+ const measurementPrior = op.platform === 'mobile'
240
+ ? (before.tileScaleFactorMobile ?? before.tileScaleFactor)
241
+ : before.tileScaleFactor;
242
+ const prior = op.target === 'header'
243
+ ? ((op.platform === 'mobile'
244
+ ? (before.headerTileScaleFactorMobile ??
245
+ before.headerTileScaleFactor)
246
+ : before.headerTileScaleFactor) ?? measurementPrior)
247
+ : measurementPrior;
231
248
  return {
232
249
  op: 'setTileScaleFactor',
233
- value: (op.platform === 'mobile'
234
- ? (before.tileScaleFactorMobile ?? before.tileScaleFactor)
235
- : before.tileScaleFactor) ?? 1,
250
+ value: prior ?? 1,
236
251
  ...(op.platform && { platform: op.platform }),
252
+ ...(op.target && { target: op.target }),
237
253
  };
254
+ }
238
255
  case 'setTileDocSize':
239
256
  // Restore the prior calibration when there was one. A doc that never
240
257
  // had a calibration can't be reverted to "absent" through this op
@@ -94,6 +94,7 @@ export interface Template extends Timestamps {
94
94
  parentFolderId?: string | null;
95
95
  formulas?: Formula[];
96
96
  sharedOrgIds?: string[];
97
+ starred?: boolean;
97
98
  }
98
99
  export interface SelectedTemplate extends FirestoreDoc {
99
100
  count: number;
@@ -0,0 +1,20 @@
1
+ type MeasurementMap = Map<string, {
2
+ id: string;
3
+ value: number;
4
+ }[]>;
5
+ interface FormulaEvaluationOptions {
6
+ expression: string;
7
+ mappings: Record<string, string>;
8
+ group: {
9
+ id: string;
10
+ columns: Map<string, any>;
11
+ };
12
+ columns: {
13
+ id: string;
14
+ type: string;
15
+ }[];
16
+ measurementMap: MeasurementMap;
17
+ defaultUnit?: string;
18
+ }
19
+ export declare function evaluateFormula({ expression, mappings, group, columns, measurementMap, defaultUnit, }: FormulaEvaluationOptions): number | string;
20
+ export {};
@@ -0,0 +1,31 @@
1
+ import { create, all } from 'mathjs';
2
+ const math = create(all);
3
+ export function evaluateFormula({ expression, mappings, group, columns, measurementMap, defaultUnit = 'um', }) {
4
+ const scope = {};
5
+ for (const [variable, columnId] of Object.entries(mappings)) {
6
+ const rawValue = group.columns.get(columnId);
7
+ const columnDef = columns.find((c) => c.id === columnId);
8
+ if (columnDef?.type === 'Measurement') {
9
+ const groupMeasurements = measurementMap.get(group.id) || [];
10
+ const measurement = groupMeasurements.find((m) => m.id === rawValue);
11
+ if (measurement?.value !== undefined) {
12
+ scope[variable] = math.unit(measurement.value, defaultUnit);
13
+ continue;
14
+ }
15
+ }
16
+ const parsedValue = typeof rawValue === 'number' ? rawValue : parseFloat(rawValue || '');
17
+ scope[variable] = isNaN(parsedValue) ? 0 : parsedValue;
18
+ }
19
+ try {
20
+ const compiled = math.compile(expression);
21
+ const result = compiled.evaluate(scope);
22
+ if (math.typeOf(result) === 'Unit') {
23
+ return result.toNumber(defaultUnit);
24
+ }
25
+ return result;
26
+ }
27
+ catch (err) {
28
+ console.error(`Formula evaluation failed:`, err);
29
+ return 'Error';
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.13.2",
3
+ "version": "1.14.1",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,3 +0,0 @@
1
- export * from './colors.js';
2
- export * from './semantic.js';
3
- export * from './preset.js';
@@ -1,3 +0,0 @@
1
- export * from './colors.js';
2
- export * from './semantic.js';
3
- export * from './preset.js';