@reekon-tools/boldr-utils 1.6.11 → 1.6.13

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 (89) hide show
  1. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.d.ts +5 -3
  2. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.js +36 -17
  3. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.native.d.ts +5 -3
  4. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +810 -0
  5. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +61 -0
  6. package/dist/annotation/canvas/AnnotationCanvasSkia.js +158 -0
  7. package/dist/annotation/canvas/Tool.d.ts +77 -0
  8. package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.d.ts +2 -2
  9. package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.js +17 -7
  10. package/dist/annotation/canvas/elements/ShapeElement.d.ts +7 -0
  11. package/dist/{canvas → annotation/canvas}/elements/ShapeElement.js +33 -5
  12. package/dist/annotation/canvas/elements/StrokeElement.d.ts +7 -0
  13. package/dist/annotation/canvas/elements/StrokeElement.js +45 -0
  14. package/dist/annotation/canvas/measurementGeometry.d.ts +43 -0
  15. package/dist/annotation/canvas/measurementGeometry.js +111 -0
  16. package/dist/{canvas → annotation/canvas}/measurementPicker.d.ts +1 -1
  17. package/dist/{canvas → annotation/canvas}/measurementStampOverlay.d.ts +2 -2
  18. package/dist/annotation/canvas/stampLayout.d.ts +1 -0
  19. package/dist/annotation/canvas/stampLayout.js +11 -0
  20. package/dist/annotation/canvas/strokeGeometry.d.ts +5 -0
  21. package/dist/annotation/canvas/strokeGeometry.js +41 -0
  22. package/dist/annotation/canvas/textGeometry.d.ts +24 -0
  23. package/dist/annotation/canvas/textGeometry.js +110 -0
  24. package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.d.ts +1 -1
  25. package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.js +1 -1
  26. package/dist/{canvas → annotation/canvas}/tools/panTool.js +3 -0
  27. package/dist/{canvas → annotation/canvas}/tools/penTool.d.ts +3 -1
  28. package/dist/{canvas → annotation/canvas}/tools/penTool.js +34 -5
  29. package/dist/annotation/canvas/tools/selectTool.js +446 -0
  30. package/dist/annotation/canvas/tools/textTool.d.ts +12 -0
  31. package/dist/annotation/canvas/tools/textTool.js +78 -0
  32. package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.d.ts +11 -3
  33. package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.js +142 -2
  34. package/dist/{canvas → annotation/canvas}/viewport.d.ts +1 -1
  35. package/dist/{data → annotation/data}/AnnotationDataProvider.d.ts +1 -1
  36. package/dist/{data → annotation/data}/InMemoryAnnotationProvider.d.ts +1 -1
  37. package/dist/{data → annotation/data}/InMemoryAnnotationProvider.js +1 -1
  38. package/dist/{data → annotation/data}/canvasPersistence.d.ts +1 -1
  39. package/dist/{data → annotation/data}/canvasPersistence.js +1 -1
  40. package/dist/annotation/data/coalescedRunner.d.ts +1 -0
  41. package/dist/annotation/data/coalescedRunner.js +48 -0
  42. package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.d.ts +1 -1
  43. package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.js +37 -16
  44. package/dist/exports.d.ts +23 -19
  45. package/dist/exports.js +18 -14
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.js +2 -2
  48. package/dist/index.native.d.ts +1 -1
  49. package/dist/index.native.js +1 -1
  50. package/dist/types/annotation.d.ts +22 -3
  51. package/dist/types/firestore.d.ts +0 -1
  52. package/dist/{hooks → utils}/useParseMeasurement.js +1 -1
  53. package/package.json +1 -1
  54. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  55. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  56. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  57. package/dist/canvas/Tool.d.ts +0 -38
  58. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  59. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  60. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  61. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  62. package/dist/canvas/elements/StrokeElement.js +0 -18
  63. package/dist/canvas/stampLayout.d.ts +0 -5
  64. package/dist/canvas/stampLayout.js +0 -14
  65. package/dist/canvas/tools/selectTool.js +0 -182
  66. package/dist/utils/evaluateFormula.d.ts +0 -20
  67. package/dist/utils/evaluateFormula.js +0 -31
  68. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.d.ts +0 -0
  69. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.js +0 -0
  70. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.d.ts +0 -0
  71. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.js +0 -0
  72. /package/dist/{canvas → annotation/canvas}/Tool.js +0 -0
  73. /package/dist/{canvas → annotation/canvas}/measurementPicker.js +0 -0
  74. /package/dist/{canvas → annotation/canvas}/measurementStampOverlay.js +0 -0
  75. /package/dist/{canvas → annotation/canvas}/pointerAdapter.d.ts +0 -0
  76. /package/dist/{canvas → annotation/canvas}/pointerAdapter.js +0 -0
  77. /package/dist/{canvas → annotation/canvas}/tools/panTool.d.ts +0 -0
  78. /package/dist/{canvas → annotation/canvas}/tools/selectTool.d.ts +0 -0
  79. /package/dist/{canvas → annotation/canvas}/viewport.js +0 -0
  80. /package/dist/{data → annotation/data}/AnnotationDataContext.d.ts +0 -0
  81. /package/dist/{data → annotation/data}/AnnotationDataContext.js +0 -0
  82. /package/dist/{data → annotation/data}/AnnotationDataProvider.js +0 -0
  83. /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.d.ts +0 -0
  84. /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.js +0 -0
  85. /package/dist/{data → annotation/data}/hooks/useAnnotationList.d.ts +0 -0
  86. /package/dist/{data → annotation/data}/hooks/useAnnotationList.js +0 -0
  87. /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.d.ts +0 -0
  88. /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.js +0 -0
  89. /package/dist/{hooks → utils}/useParseMeasurement.d.ts +0 -0
@@ -1,11 +1,12 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../types/annotation.js';
2
+ import { applyPatch, invertPatch, DEFAULT_LAYER_ID, } from '../../types/annotation.js';
3
3
  import { createViewportApi, panBy, zoomAt, DEFAULT_VIEWPORT, } from './viewport.js';
4
+ import { recomputeAnchor, rectCenter, DEFAULT_LINE_POS, } from './measurementGeometry.js';
4
5
  // Platform-agnostic state machine for the annotation canvas. Web and native
5
6
  // inners share this hook; each wraps it with platform-specific event
6
7
  // capture and JSX (div + DOM events vs. GestureDetector + RN Views).
7
8
  export const useAnnotationCanvasState = (props) => {
8
- const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, width, height, initialViewport, imperativeRef, } = props;
9
+ const { canvas, onCommit, tools, activeToolId, selection, onSelectionChange, measurements, pickMeasurement, requestTextInput, width, height, initialViewport, imperativeRef, } = props;
9
10
  const [viewport, setViewport] = useState(initialViewport ?? DEFAULT_VIEWPORT);
10
11
  const [toolState, setToolState] = useState(undefined);
11
12
  const [previewPatch, setPreviewPatch] = useState(null);
@@ -40,6 +41,9 @@ export const useAnnotationCanvasState = (props) => {
40
41
  requestPickMeasurement() {
41
42
  return pickMeasurement ? pickMeasurement() : Promise.resolve(null);
42
43
  },
44
+ requestTextInput(options) {
45
+ return requestTextInput ? requestTextInput(options) : Promise.resolve(null);
46
+ },
43
47
  applyPan(deltaScreen) {
44
48
  setViewport((v) => panBy(v, deltaScreen));
45
49
  },
@@ -53,6 +57,7 @@ export const useAnnotationCanvasState = (props) => {
53
57
  onCommit,
54
58
  onSelectionChange,
55
59
  pickMeasurement,
60
+ requestTextInput,
56
61
  ]);
57
62
  // Live ctx for imperative handle methods (which are created in an effect and
58
63
  // would otherwise capture a stale ctx/viewport).
@@ -164,6 +169,140 @@ export const useAnnotationCanvasState = (props) => {
164
169
  // responsible for switching to its move/select tool).
165
170
  c.setSelection({ ids: [placed.id] });
166
171
  },
172
+ placeAnnotationAtCenter(opts) {
173
+ const c = ctxRef.current;
174
+ const center = c.viewport.screenToWorld({
175
+ x: width / 2,
176
+ y: height / 2,
177
+ });
178
+ // Default line length: a quarter of the doc width, clamped to a sane
179
+ // range so it's a grabbable size at any canvas scale.
180
+ const len = opts?.defaultLengthDoc ??
181
+ Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
182
+ const line = {
183
+ a: { x: center.x - len / 2, y: center.y },
184
+ b: { x: center.x + len / 2, y: center.y },
185
+ };
186
+ const placed = {
187
+ id: `annotation-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
188
+ layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
189
+ placement: 'line',
190
+ line,
191
+ linePos: DEFAULT_LINE_POS,
192
+ // Center of the line; recomputeAnchor keeps this in sync on edits.
193
+ anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, center),
194
+ showLabel: true,
195
+ showValue: true,
196
+ createdAt: Date.now(),
197
+ };
198
+ c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
199
+ c.setSelection({ ids: [placed.id] });
200
+ },
201
+ setAnnotationType(id, type) {
202
+ const c = ctxRef.current;
203
+ const m = c.document.placedMeasurements.find((x) => x.id === id);
204
+ if (!m)
205
+ return;
206
+ if (type === 'rectangle') {
207
+ // Reuse the existing rect if the annotation had one; otherwise
208
+ // synthesize a square centered on the anchor (same size clamp as
209
+ // the default line) so the tile doesn't jump.
210
+ const side = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
211
+ const rect = m.rect ?? {
212
+ a: { x: m.anchor.x - side / 2, y: m.anchor.y - side / 2 },
213
+ b: { x: m.anchor.x + side / 2, y: m.anchor.y + side / 2 },
214
+ };
215
+ c.commit({
216
+ ops: [
217
+ {
218
+ op: 'updateMeasurement',
219
+ id,
220
+ patch: {
221
+ placement: 'rectangle',
222
+ rect,
223
+ anchor: rectCenter(rect),
224
+ },
225
+ },
226
+ ],
227
+ });
228
+ return;
229
+ }
230
+ if (type === 'line') {
231
+ let line = m.line;
232
+ if (!line) {
233
+ const len = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
234
+ line = {
235
+ a: { x: m.anchor.x - len / 2, y: m.anchor.y },
236
+ b: { x: m.anchor.x + len / 2, y: m.anchor.y },
237
+ };
238
+ }
239
+ const linePos = m.linePos ?? DEFAULT_LINE_POS;
240
+ c.commit({
241
+ ops: [
242
+ {
243
+ op: 'updateMeasurement',
244
+ id,
245
+ patch: {
246
+ placement: 'line',
247
+ line,
248
+ linePos,
249
+ anchor: recomputeAnchor(line, 'line', linePos, m.anchor),
250
+ },
251
+ },
252
+ ],
253
+ });
254
+ return;
255
+ }
256
+ // 'none' — bare stamp. Keep the anchor (and the line data, which simply
257
+ // stops rendering); switching back to 'line' restores it.
258
+ c.commit({
259
+ ops: [{ op: 'updateMeasurement', id, patch: { placement: 'none' } }],
260
+ });
261
+ },
262
+ associateMeasurement(id, ref) {
263
+ const c = ctxRef.current;
264
+ c.commit({
265
+ ops: [
266
+ {
267
+ op: 'updateMeasurement',
268
+ id,
269
+ patch: {
270
+ measurementId: ref.measurementId,
271
+ measurementPath: ref.measurementPath,
272
+ groupId: ref.groupId,
273
+ labelOverride: ref.label,
274
+ unitOverride: ref.unit,
275
+ },
276
+ },
277
+ ],
278
+ });
279
+ },
280
+ deleteSelected() {
281
+ const c = ctxRef.current;
282
+ const ids = c.selection?.ids;
283
+ if (!ids || ids.length === 0)
284
+ return;
285
+ const idSet = new Set(ids);
286
+ const doc = c.document;
287
+ // Build one remove op per selected element, dispatched by which
288
+ // collection owns the id. A single multi-op commit makes the whole
289
+ // deletion one undo step (inverse re-adds each element).
290
+ const ops = [
291
+ ...doc.strokes
292
+ .filter((s) => idSet.has(s.id))
293
+ .map((s) => ({ op: 'removeStroke', id: s.id })),
294
+ ...doc.shapes
295
+ .filter((s) => idSet.has(s.id))
296
+ .map((s) => ({ op: 'removeShape', id: s.id })),
297
+ ...doc.placedMeasurements
298
+ .filter((m) => idSet.has(m.id))
299
+ .map((m) => ({ op: 'removeMeasurement', id: m.id })),
300
+ ];
301
+ if (ops.length === 0)
302
+ return;
303
+ c.commit({ ops });
304
+ c.setSelection(null);
305
+ },
167
306
  };
168
307
  return () => {
169
308
  if (imperativeRef)
@@ -206,5 +345,6 @@ export const useAnnotationCanvasState = (props) => {
206
345
  dispatchPointerCancel,
207
346
  pan,
208
347
  zoom,
348
+ setViewport,
209
349
  };
210
350
  };
@@ -1,4 +1,4 @@
1
- import type { Vec2 } from '../types/annotation.js';
1
+ import type { Vec2 } from '../../types/annotation.js';
2
2
  export interface ViewportState {
3
3
  zoom: number;
4
4
  pan: Vec2;
@@ -1,4 +1,4 @@
1
- import type { FileUpload, FileUploadType, Measurement } from '../types/firestore.js';
1
+ import type { FileUpload, FileUploadType, Measurement } from '../../types/firestore.js';
2
2
  export interface JobScope {
3
3
  orgId: string;
4
4
  projectId: string;
@@ -1,4 +1,4 @@
1
- import { type Measurement } from '../types/firestore.js';
1
+ import { type Measurement } from '../../types/firestore.js';
2
2
  import { type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef } from './AnnotationDataProvider.js';
3
3
  export declare class InMemoryAnnotationProvider implements AnnotationDataProvider {
4
4
  private docs;
@@ -1,4 +1,4 @@
1
- import { FileUploadType } from '../types/firestore.js';
1
+ import { FileUploadType } from '../../types/firestore.js';
2
2
  import { isFieldOp, } from './AnnotationDataProvider.js';
3
3
  const scopeKey = (s) => `${s.orgId}/${s.projectId}/${s.jobId}/${s.groupId}`;
4
4
  const jobKey = (s) => `${s.orgId}/${s.projectId}/${s.jobId}`;
@@ -1,3 +1,3 @@
1
- import { type AnnotationCanvasState, type AnnotationViewport } from '../types/annotation.js';
1
+ import { type AnnotationCanvasState, type AnnotationViewport } from '../../types/annotation.js';
2
2
  import type { AnnotationFile } from './AnnotationDataProvider.js';
3
3
  export declare const hydrateCanvasState: (file: AnnotationFile | null, fallbackViewport?: Partial<AnnotationViewport>) => AnnotationCanvasState;
@@ -1,4 +1,4 @@
1
- import { createEmptyCanvasState, } from '../types/annotation.js';
1
+ import { createEmptyCanvasState, } from '../../types/annotation.js';
2
2
  // Bring a persisted canvas forward to the current schema. Keyed on
3
3
  // `schemaVersion` so each client migrates identically and the provider can
4
4
  // stay a dumb transport. Identity for v1 today; add a `case 1: return
@@ -0,0 +1 @@
1
+ export declare const createCoalescedRunner: (task: () => Promise<void>) => (() => Promise<void>);
@@ -0,0 +1,48 @@
1
+ // A single-flight async runner with trailing coalescing.
2
+ //
3
+ // Wraps an async `task` so that overlapping callers don't run it concurrently
4
+ // and don't drop work:
5
+ // • While a run is in flight, every additional call returns that same
6
+ // in-flight promise (single-flight) instead of starting a second run.
7
+ // • If any call arrives during a run, exactly ONE trailing run is scheduled
8
+ // after the current one finishes — so work that arrived mid-run is never
9
+ // lost. The trailing run re-invokes `task`, which is expected to re-read the
10
+ // latest state each time (it takes no arguments by design).
11
+ // • The returned promise resolves only once the chain is fully drained, so an
12
+ // `await`-ing caller can rely on all queued work having completed.
13
+ // • A task rejection is propagated to the awaiting callers of that run but
14
+ // does NOT wedge the runner: `inFlight` is always cleared, and a pending
15
+ // trailing run still fires, so the runner self-heals after a transient
16
+ // failure.
17
+ //
18
+ // This is the concurrency primitive behind the annotation autosave/flush: it
19
+ // guarantees a create-on-first-save can't race a concurrent flush (the old
20
+ // boolean-guard approach silently dropped the second writer).
21
+ export const createCoalescedRunner = (task) => {
22
+ let inFlight = null;
23
+ let pending = false;
24
+ const run = () => {
25
+ if (inFlight) {
26
+ // A run is already underway — remember that more work arrived and join
27
+ // the in-flight promise rather than starting a second concurrent run.
28
+ pending = true;
29
+ return inFlight;
30
+ }
31
+ inFlight = (async () => {
32
+ try {
33
+ await task();
34
+ }
35
+ finally {
36
+ inFlight = null;
37
+ if (pending) {
38
+ // Work arrived mid-run: drain it with a single trailing run, which
39
+ // re-reads the latest state. Recurses until quiescent.
40
+ pending = false;
41
+ await run();
42
+ }
43
+ }
44
+ })();
45
+ return inFlight;
46
+ };
47
+ return run;
48
+ };
@@ -1,4 +1,4 @@
1
- import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../types/annotation.js';
1
+ import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationViewport, type BackgroundFit } from '../../../types/annotation.js';
2
2
  import type { ImageBlob, JobGroupScope } from '../AnnotationDataProvider.js';
3
3
  export type SaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
4
4
  export interface UseAnnotationCanvasDocOptions {
@@ -1,8 +1,9 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { applyPatch, createEmptyCanvasState, } from '../../types/annotation.js';
3
- import { FileUploadType } from '../../types/firestore.js';
2
+ import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.js';
3
+ import { FileUploadType } from '../../../types/firestore.js';
4
4
  import { useAnnotationDoc } from './useAnnotationDoc.js';
5
5
  import { useAnnotationMutations } from './useAnnotationMutations.js';
6
+ import { createCoalescedRunner } from '../coalescedRunner.js';
6
7
  import { hydrateCanvasState } from '../canvasPersistence.js';
7
8
  // Stable placeholder so useAnnotationMutations (which requires a non-null
8
9
  // scope) can be called unconditionally. Never used to write — flushes are
@@ -55,8 +56,6 @@ export const useAnnotationCanvasDoc = (options) => {
55
56
  // JSON of the canvas we last wrote, to recognize (and ignore) the snapshot
56
57
  // echo of our own write when reconciling incoming remote changes.
57
58
  const lastSavedJsonRef = useRef(null);
58
- // Guards against two creates racing if flushes overlap.
59
- const creatingRef = useRef(false);
60
59
  workingRef.current = working;
61
60
  dataRef.current = data;
62
61
  statusRef.current = saveStatus;
@@ -137,17 +136,17 @@ export const useAnnotationCanvasDoc = (options) => {
137
136
  setWorking(hydrateCanvasState(data, fallbackViewport));
138
137
  // eslint-disable-next-line react-hooks/exhaustive-deps
139
138
  }, [data]);
140
- const flush = useCallback(async () => {
141
- if (timerRef.current) {
142
- clearTimeout(timerRef.current);
143
- timerRef.current = null;
144
- }
139
+ // One pass of the persist logic: snapshots the latest working state and
140
+ // creates-or-updates the doc. NOT called directly — it runs through the
141
+ // single-flight `flush` below, which guarantees two passes never overlap (so
142
+ // a create-on-first-save can't race a concurrent flush) and re-invokes this
143
+ // to drain any work that arrived mid-pass. Because it re-reads `workingRef`
144
+ // each call, a trailing pass always persists the newest canvas.
145
+ const runFlush = useCallback(async () => {
145
146
  const canvas = workingRef.current;
146
147
  // Nothing to persist, or no real target scope yet.
147
148
  if (!canvas || !scope)
148
149
  return;
149
- if (creatingRef.current)
150
- return;
151
150
  const json = JSON.stringify(canvas);
152
151
  const id = fileIdRef.current ?? createdIdRef.current;
153
152
  const mode = id ? 'update' : 'create';
@@ -171,14 +170,12 @@ export const useAnnotationCanvasDoc = (options) => {
171
170
  try {
172
171
  if (!id) {
173
172
  // First save with no file — create the doc seeded with the canvas.
174
- creatingRef.current = true;
175
173
  const seed = createSeedRef.current;
176
174
  const newId = await createRef.current({
177
175
  type: FileUploadType.Canvas,
178
176
  ...(seed?.name !== undefined ? { name: seed.name } : {}),
179
177
  fileData: buildFileData(seed?.fileType ?? 'sketch', seed?.isLabel, canvasPayload),
180
178
  });
181
- creatingRef.current = false;
182
179
  createdIdRef.current = newId;
183
180
  if (debug) {
184
181
  console.log('[useAnnotationCanvasDoc] created file', newId);
@@ -213,14 +210,34 @@ export const useAnnotationCanvasDoc = (options) => {
213
210
  }
214
211
  }
215
212
  catch (e) {
216
- creatingRef.current = false;
217
213
  // Always log save failures with context — these are otherwise invisible
218
214
  // (the canvas keeps working from local state).
219
215
  console.error(`[useAnnotationCanvasDoc] ${mode} failed`, { fileId: id, scope, bytes: json.length }, e);
220
216
  onSaveErrorRef.current?.(e);
221
217
  setStatus('error');
218
+ // Swallowed (not re-thrown): autosave/unmount call this fire-and-forget,
219
+ // and the canvas keeps working from local state. Callers that need to know
220
+ // a create succeeded (ensureFileId) detect it via the absence of an id
221
+ // after the flush drains, not via a rejection.
222
222
  }
223
223
  }, [scope, setStatus, saveThumbnail]);
224
+ // Stable indirection so the coalesced runner (created once) always calls the
225
+ // latest `runFlush` without being re-created when its deps change.
226
+ const runFlushRef = useRef(runFlush);
227
+ runFlushRef.current = runFlush;
228
+ // Single-flight runner: concurrent callers join the in-flight pass; work that
229
+ // arrives mid-pass triggers exactly one trailing pass. Created once.
230
+ const flushRunner = useMemo(() => createCoalescedRunner(() => runFlushRef.current()), []);
231
+ // Public flush: cancel any pending debounce, then run (or join) a persist
232
+ // pass. Resolves once the chain is fully drained, so `await flush()` is safe
233
+ // for create-before-upload and explicit Save.
234
+ const flush = useCallback(() => {
235
+ if (timerRef.current) {
236
+ clearTimeout(timerRef.current);
237
+ timerRef.current = null;
238
+ }
239
+ return flushRunner();
240
+ }, [flushRunner]);
224
241
  const onCommit = useCallback((patch) => {
225
242
  setWorking((prev) => (prev ? applyPatch(prev, patch) : prev));
226
243
  setStatus('dirty');
@@ -235,8 +252,12 @@ export const useAnnotationCanvasDoc = (options) => {
235
252
  const existing = fileIdRef.current ?? createdIdRef.current;
236
253
  if (existing)
237
254
  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.
255
+ // No file yet — seed an empty canvas if nothing has hydrated, then flush so
256
+ // the create branch mints the doc. `flush` is single-flight: if the
257
+ // debounced autosave is already creating, this call JOINS that create and
258
+ // waits for it to drain (rather than racing it and dropping the upload, the
259
+ // old bug). After the drain, the id is present unless the create truly
260
+ // failed — so the throw below is now a genuine error, not a race.
240
261
  if (!workingRef.current) {
241
262
  const seeded = createEmptyCanvasState(fallbackViewport);
242
263
  workingRef.current = seeded;
package/dist/exports.d.ts CHANGED
@@ -2,27 +2,31 @@ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearF
2
2
  export { calculateFormula } from './formulas/calculateFormula.js';
3
3
  export { convertMicrometers } from './utils/micrometersToUnit.js';
4
4
  export { parseMeasurement } from './utils/parseMeasurement.js';
5
- export { useParseMeasurement } from './hooks/useParseMeasurement.js';
5
+ export { useParseMeasurement } from './utils/useParseMeasurement.js';
6
6
  export * from './types/firestore.js';
7
7
  export * from './types/layout.js';
8
8
  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
- export { isFieldOp, type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type FieldOp, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef, } from './data/AnnotationDataProvider.js';
12
- export { AnnotationDataProviderContext, useAnnotationData, type AnnotationDataProviderProps, } from './data/AnnotationDataContext.js';
13
- export { useAnnotationDoc, type UseAnnotationDocResult, } from './data/hooks/useAnnotationDoc.js';
14
- export { useAnnotationList, type UseAnnotationListResult, } from './data/hooks/useAnnotationList.js';
15
- export { useAnnotationMutations, type AnnotationMutations, } from './data/hooks/useAnnotationMutations.js';
16
- export { useAnnotationCanvasDoc, type SaveStatus, type UseAnnotationCanvasDocOptions, type UseAnnotationCanvasDocResult, } from './data/hooks/useAnnotationCanvasDoc.js';
17
- export { hydrateCanvasState } from './data/canvasPersistence.js';
18
- export { InMemoryAnnotationProvider } from './data/InMemoryAnnotationProvider.js';
19
- export type { AnnotationCanvasHandle } from './canvas/useAnnotationCanvasState.js';
20
- export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './canvas/AnnotationCanvasInner.js';
21
- export type { CanvasPointerEvent, Tool, ToolContext, ToolState, } from './canvas/Tool.js';
22
- export type { MeasurementRef, PickMeasurement, } from './canvas/measurementPicker.js';
23
- export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './canvas/measurementStampOverlay.js';
24
- export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './canvas/viewport.js';
25
- export { createPenTool, type PenToolOptions } from './canvas/tools/penTool.js';
26
- export { createSelectTool } from './canvas/tools/selectTool.js';
27
- export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './canvas/tools/measurementStampTool.js';
28
- export { createPanTool, type PanToolOptions } from './canvas/tools/panTool.js';
11
+ 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 { AnnotationDataProviderContext, useAnnotationData, type AnnotationDataProviderProps, } from './annotation/data/AnnotationDataContext.js';
13
+ export { useAnnotationDoc, type UseAnnotationDocResult, } from './annotation/data/hooks/useAnnotationDoc.js';
14
+ export { useAnnotationList, type UseAnnotationListResult, } from './annotation/data/hooks/useAnnotationList.js';
15
+ export { useAnnotationMutations, type AnnotationMutations, } from './annotation/data/hooks/useAnnotationMutations.js';
16
+ export { useAnnotationCanvasDoc, type SaveStatus, type UseAnnotationCanvasDocOptions, type UseAnnotationCanvasDocResult, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
17
+ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
18
+ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
19
+ export type { AnnotationCanvasHandle } from './annotation/canvas/useAnnotationCanvasState.js';
20
+ export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './annotation/canvas/AnnotationCanvasInner.js';
21
+ export type { CanvasPointerEvent, RequestTextInput, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
22
+ export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
23
+ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
24
+ export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
25
+ export { createPenTool, type PenToolOptions } from './annotation/canvas/tools/penTool.js';
26
+ export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
27
+ export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
28
+ export { createPanTool, type PanToolOptions } from './annotation/canvas/tools/panTool.js';
29
+ export { createTextTool, type TextToolOptions, } from './annotation/canvas/tools/textTool.js';
30
+ export { DEFAULT_TEXT_FONT_SIZE, MIN_TEXT_FONT_SIZE, MAX_TEXT_FONT_SIZE, textShapeBounds, textResizeGeometry, type ResizeGeometry, type TextBounds, } from './annotation/canvas/textGeometry.js';
31
+ export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, normalizeRect, rectCenter, rectCornerPoint, oppositeRectCorner, DEFAULT_LINE_POS, type NormalizedRect, type RectCorner, } from './annotation/canvas/measurementGeometry.js';
32
+ export { toSkiaStrokeCap, arrowheadTriangle, arrowheadLength, } from './annotation/canvas/strokeGeometry.js';
package/dist/exports.js CHANGED
@@ -6,23 +6,27 @@ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearF
6
6
  export { calculateFormula } from './formulas/calculateFormula.js';
7
7
  export { convertMicrometers } from './utils/micrometersToUnit.js';
8
8
  export { parseMeasurement } from './utils/parseMeasurement.js';
9
- export { useParseMeasurement } from './hooks/useParseMeasurement.js';
9
+ export { useParseMeasurement } from './utils/useParseMeasurement.js';
10
10
  export * from './types/firestore.js';
11
11
  export * from './types/layout.js';
12
12
  export * from './types/annotation.js';
13
13
  export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, } from './utils/tolerance.js';
14
14
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
15
15
  // Annotation data layer (SDK-neutral; apps provide their own provider).
16
- export { isFieldOp, } from './data/AnnotationDataProvider.js';
17
- export { AnnotationDataProviderContext, useAnnotationData, } from './data/AnnotationDataContext.js';
18
- export { useAnnotationDoc, } from './data/hooks/useAnnotationDoc.js';
19
- export { useAnnotationList, } from './data/hooks/useAnnotationList.js';
20
- export { useAnnotationMutations, } from './data/hooks/useAnnotationMutations.js';
21
- export { useAnnotationCanvasDoc, } from './data/hooks/useAnnotationCanvasDoc.js';
22
- export { hydrateCanvasState } from './data/canvasPersistence.js';
23
- export { InMemoryAnnotationProvider } from './data/InMemoryAnnotationProvider.js';
24
- export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './canvas/viewport.js';
25
- export { createPenTool } from './canvas/tools/penTool.js';
26
- export { createSelectTool } from './canvas/tools/selectTool.js';
27
- export { createMeasurementStampTool, } from './canvas/tools/measurementStampTool.js';
28
- export { createPanTool } from './canvas/tools/panTool.js';
16
+ export { isFieldOp, } from './annotation/data/AnnotationDataProvider.js';
17
+ export { AnnotationDataProviderContext, useAnnotationData, } from './annotation/data/AnnotationDataContext.js';
18
+ export { useAnnotationDoc, } from './annotation/data/hooks/useAnnotationDoc.js';
19
+ export { useAnnotationList, } from './annotation/data/hooks/useAnnotationList.js';
20
+ export { useAnnotationMutations, } from './annotation/data/hooks/useAnnotationMutations.js';
21
+ export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCanvasDoc.js';
22
+ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
23
+ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
24
+ export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
25
+ export { createPenTool } from './annotation/canvas/tools/penTool.js';
26
+ export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
27
+ export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
28
+ export { createPanTool } from './annotation/canvas/tools/panTool.js';
29
+ export { createTextTool, } from './annotation/canvas/tools/textTool.js';
30
+ export { DEFAULT_TEXT_FONT_SIZE, MIN_TEXT_FONT_SIZE, MAX_TEXT_FONT_SIZE, textShapeBounds, textResizeGeometry, } from './annotation/canvas/textGeometry.js';
31
+ export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, normalizeRect, rectCenter, rectCornerPoint, oppositeRectCorner, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.js';
32
+ export { toSkiaStrokeCap, arrowheadTriangle, arrowheadLength, } from './annotation/canvas/strokeGeometry.js';
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearF
3
3
  export { calculateFormula } from './formulas/calculateFormula.js';
4
4
  export { convertMicrometers } from './utils/micrometersToUnit.js';
5
5
  export { parseMeasurement } from './utils/parseMeasurement.js';
6
- export { useParseMeasurement } from './hooks/useParseMeasurement.js';
6
+ export { useParseMeasurement } from './utils/useParseMeasurement.js';
7
7
  export * from './types/firestore.js';
8
8
  export * from './types/layout.js';
9
9
  export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
10
- export { AnnotationCanvas, type AnnotationCanvasProps, type CanvasKitOpts, } from './canvas/AnnotationCanvas.js';
10
+ export { AnnotationCanvas, type AnnotationCanvasProps, type CanvasKitOpts, } from './annotation/canvas/AnnotationCanvas.js';
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearF
7
7
  export { calculateFormula } from './formulas/calculateFormula.js';
8
8
  export { convertMicrometers } from './utils/micrometersToUnit.js';
9
9
  export { parseMeasurement } from './utils/parseMeasurement.js';
10
- export { useParseMeasurement } from './hooks/useParseMeasurement.js';
10
+ export { useParseMeasurement } from './utils/useParseMeasurement.js';
11
11
  export * from './types/firestore.js';
12
12
  export * from './types/layout.js';
13
13
  export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, } from './utils/tolerance.js';
14
- export { AnnotationCanvas, } from './canvas/AnnotationCanvas.js';
14
+ export { AnnotationCanvas, } from './annotation/canvas/AnnotationCanvas.js';
@@ -1,5 +1,5 @@
1
1
  export * from './exports.js';
2
- export { AnnotationCanvas, type AnnotationCanvasProps, } from './canvas/AnnotationCanvas.native.js';
2
+ export { AnnotationCanvas, type AnnotationCanvasProps, } from './annotation/canvas/AnnotationCanvas.native.js';
3
3
  export interface CanvasKitOpts {
4
4
  locateFile?: (file: string) => string;
5
5
  }
@@ -3,4 +3,4 @@
3
3
  // WithSkiaWeb / canvaskit-wasm), avoiding the `fs` import that Metro
4
4
  // chokes on.
5
5
  export * from './exports.js';
6
- export { AnnotationCanvas, } from './canvas/AnnotationCanvas.native.js';
6
+ export { AnnotationCanvas, } from './annotation/canvas/AnnotationCanvas.native.js';
@@ -5,12 +5,15 @@ export interface Vec2 {
5
5
  x: number;
6
6
  y: number;
7
7
  }
8
+ export type StrokeCap = 'butt' | 'round' | 'square' | 'arrow';
8
9
  export interface AnnotationStroke {
9
10
  id: AnnotationElementId;
10
11
  layerId: AnnotationLayerId;
11
12
  tool: 'pen' | 'marker' | 'highlighter';
12
13
  color: string;
13
14
  width: number;
15
+ cap?: StrokeCap;
16
+ dash?: boolean;
14
17
  points: number[];
15
18
  pressure?: number[];
16
19
  createdAt: number;
@@ -22,6 +25,7 @@ export interface AnnotationShapeStyle {
22
25
  strokeWidth?: number;
23
26
  fontSize?: number;
24
27
  fontFamily?: string;
28
+ dash?: boolean;
25
29
  }
26
30
  export interface AnnotationShape {
27
31
  id: AnnotationElementId;
@@ -35,13 +39,28 @@ export interface AnnotationShape {
35
39
  text?: string;
36
40
  createdAt: number;
37
41
  }
42
+ export type MeasurementPlacement = 'none' | 'line' | 'rectangle';
38
43
  export interface PlacedMeasurementRef {
39
44
  id: AnnotationElementId;
40
45
  layerId: AnnotationLayerId;
41
- measurementPath: string;
42
- measurementId: string;
43
- groupId: string;
46
+ measurementPath?: string;
47
+ measurementId?: string;
48
+ groupId?: string;
44
49
  anchor: Vec2;
50
+ placement?: MeasurementPlacement;
51
+ line?: {
52
+ a: Vec2;
53
+ b: Vec2;
54
+ };
55
+ linePos?: number;
56
+ rect?: {
57
+ a: Vec2;
58
+ b: Vec2;
59
+ };
60
+ lineColor?: string;
61
+ lineWidth?: number;
62
+ lineCap?: StrokeCap;
63
+ lineDash?: boolean;
45
64
  leader?: {
46
65
  from: Vec2;
47
66
  to: Vec2;
@@ -220,7 +220,6 @@ export interface Section extends Timestamps {
220
220
  tableConfig: ColumnConfig[];
221
221
  measurements: string[];
222
222
  isTemplate?: boolean;
223
- viewMode?: 'rows' | 'columns' | 'transposed';
224
223
  }
225
224
  export interface Group extends FirestoreDoc, Timestamps {
226
225
  sectionId: string;
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { parseMeasurement } from '../utils/parseMeasurement.js';
2
+ import { parseMeasurement } from './parseMeasurement.js';
3
3
  export const useParseMeasurement = () => {
4
4
  const [error, setError] = useState(null);
5
5
  const parseMeasurementInput = (input, defaultUnit = 'mm') => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.6.11",
3
+ "version": "1.6.13",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",