@reekon-tools/boldr-utils 1.6.12 → 1.6.14

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 (41) hide show
  1. package/dist/annotation/canvas/AnnotationCanvas.native.d.ts +2 -2
  2. package/dist/annotation/canvas/AnnotationCanvasInner.d.ts +5 -2
  3. package/dist/annotation/canvas/AnnotationCanvasInner.js +58 -6
  4. package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +5 -2
  5. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +514 -59
  6. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +31 -1
  7. package/dist/annotation/canvas/AnnotationCanvasSkia.js +38 -9
  8. package/dist/annotation/canvas/Tool.d.ts +27 -0
  9. package/dist/annotation/canvas/elements/BackgroundImageElement.js +4 -1
  10. package/dist/annotation/canvas/elements/ShapeElement.js +68 -9
  11. package/dist/annotation/canvas/elements/StrokeElement.js +8 -3
  12. package/dist/annotation/canvas/measurementGeometry.d.ts +21 -0
  13. package/dist/annotation/canvas/measurementGeometry.js +98 -3
  14. package/dist/annotation/canvas/shapeGeometry.d.ts +5 -0
  15. package/dist/annotation/canvas/shapeGeometry.js +116 -0
  16. package/dist/annotation/canvas/strokeGeometry.d.ts +1 -0
  17. package/dist/annotation/canvas/strokeGeometry.js +8 -0
  18. package/dist/annotation/canvas/textGeometry.d.ts +24 -0
  19. package/dist/annotation/canvas/textGeometry.js +110 -0
  20. package/dist/annotation/canvas/tools/panTool.d.ts +1 -0
  21. package/dist/annotation/canvas/tools/panTool.js +38 -5
  22. package/dist/annotation/canvas/tools/penTool.d.ts +1 -0
  23. package/dist/annotation/canvas/tools/penTool.js +8 -2
  24. package/dist/annotation/canvas/tools/polygonTool.d.ts +11 -0
  25. package/dist/annotation/canvas/tools/polygonTool.js +162 -0
  26. package/dist/annotation/canvas/tools/selectTool.js +148 -51
  27. package/dist/annotation/canvas/tools/shapeTool.d.ts +25 -0
  28. package/dist/annotation/canvas/tools/shapeTool.js +111 -0
  29. package/dist/annotation/canvas/tools/textTool.d.ts +12 -0
  30. package/dist/annotation/canvas/tools/textTool.js +78 -0
  31. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +2 -1
  32. package/dist/annotation/canvas/useAnnotationCanvasState.js +56 -6
  33. package/dist/annotation/data/coalescedRunner.d.ts +1 -0
  34. package/dist/annotation/data/coalescedRunner.js +48 -0
  35. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +118 -38
  36. package/dist/exports.d.ts +9 -4
  37. package/dist/exports.js +8 -3
  38. package/dist/formulas/calculateFormula.js +1 -3
  39. package/dist/types/annotation.d.ts +9 -0
  40. package/dist/types/firestore.d.ts +4 -0
  41. package/package.json +1 -1
@@ -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
+ };
@@ -3,6 +3,7 @@ import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.j
3
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
@@ -15,11 +16,16 @@ const EMPTY_SCOPE = {
15
16
  };
16
17
  // Build the persisted fileData, omitting `isLabel` when undefined so the write
17
18
  // contains no undefined values (Firestore rejects them on RN).
18
- const buildFileData = (fileType, isLabel, canvas) => ({
19
+ const buildFileData = (fileType, isLabel, canvas, canvasRev) => ({
19
20
  fileType,
20
21
  ...(isLabel !== undefined ? { isLabel } : {}),
21
22
  canvas,
23
+ canvasRev,
22
24
  });
25
+ // Random id for this editing session, used to stamp writes (see canvasRev in
26
+ // AnnotationFileData). Uniqueness only needs to hold across the handful of
27
+ // clients that ever touch one annotation doc.
28
+ const makeClientId = () => `${Date.now().toString(36)}-${Math.floor(Math.random() * 0x100000000).toString(36)}`;
23
29
  // Orchestrates load + auto-save for the annotation canvas. Hydrates the working
24
30
  // state from the persisted doc, applies commits optimistically, and persists
25
31
  // (debounced) through the data provider — creating the file on first save when
@@ -51,12 +57,30 @@ export const useAnnotationCanvasDoc = (options) => {
51
57
  const uploadImageRef = useRef(uploadImage);
52
58
  // Guards against overlapping thumbnail captures (each save fires one).
53
59
  const thumbnailSavingRef = useRef(false);
60
+ // Set by the public `save()` so the next successful flush refreshes the
61
+ // thumbnail; debounced autosaves leave it false (capture is too costly to
62
+ // run mid-session — see captureThumbnail). Consumed (cleared) by each flush
63
+ // pass that attempts a write, so a failed explicit save drops the request
64
+ // instead of leaking the capture into some later autosave.
65
+ const thumbnailRequestedRef = useRef(false);
66
+ // In-flight capture+upload from the latest flush, so `save()` can await it —
67
+ // callers that navigate away right after saving must not unmount the view
68
+ // mid-capture.
69
+ const thumbnailPromiseRef = useRef(undefined);
54
70
  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);
71
+ // This session's identity + write counter, stamped into `fileData.canvasRev`
72
+ // on every flush. An incoming snapshot carrying our clientId is the echo of
73
+ // our own write — state we already hold — and must not be re-applied (doing
74
+ // so replaces every object identity in the working canvas, busting all
75
+ // memoization downstream).
76
+ const clientIdRef = useRef(null);
77
+ if (clientIdRef.current === null)
78
+ clientIdRef.current = makeClientId();
79
+ const writeSeqRef = useRef(0);
80
+ // Monotonic local-edit counter: bumped on every commit. A flush snapshots it
81
+ // before writing and compares after, so "did edits land mid-flight?" is an
82
+ // integer comparison instead of re-serializing the whole document.
83
+ const editSeqRef = useRef(0);
60
84
  workingRef.current = working;
61
85
  dataRef.current = data;
62
86
  statusRef.current = saveStatus;
@@ -107,7 +131,6 @@ export const useAnnotationCanvasDoc = (options) => {
107
131
  timerRef.current = null;
108
132
  }
109
133
  createdIdRef.current = null;
110
- lastSavedJsonRef.current = null;
111
134
  setWorking(null);
112
135
  setStatus('idle');
113
136
  }, [fileId, setStatus]);
@@ -116,39 +139,48 @@ export const useAnnotationCanvasDoc = (options) => {
116
139
  // First load — always hydrate.
117
140
  if (workingRef.current === null) {
118
141
  setWorking(hydrateCanvasState(data, fallbackViewport));
119
- lastSavedJsonRef.current = data?.fileData.canvas
120
- ? JSON.stringify(data.fileData.canvas)
121
- : null;
122
142
  return;
123
143
  }
124
144
  // A write of ours is queued or in flight — ignore the snapshot; it is
125
145
  // either the echo of our write or about to be superseded by it.
126
146
  if (statusRef.current === 'saving' || timerRef.current !== null)
127
147
  return;
128
- // Clean locally: accept a genuine remote change, but ignore the echo of
129
- // our own last write (same content).
130
148
  const incoming = data?.fileData.canvas;
131
149
  if (!incoming)
132
150
  return;
133
- const incomingJson = JSON.stringify(incoming);
134
- if (incomingJson === lastSavedJsonRef.current)
151
+ // Ignore the echo of this session's own writes: the doc carries the
152
+ // canvasRev we stamped, so its content is (at most as new as) what we
153
+ // already hold. Only a doc written by ANOTHER client is a genuine remote
154
+ // change worth re-hydrating — which replaces all element identities, so
155
+ // it must never happen on the routine save → echo round-trip.
156
+ if (data?.fileData.canvasRev?.clientId === clientIdRef.current)
135
157
  return;
136
- lastSavedJsonRef.current = incomingJson;
137
158
  setWorking(hydrateCanvasState(data, fallbackViewport));
138
159
  // eslint-disable-next-line react-hooks/exhaustive-deps
139
160
  }, [data]);
140
- const flush = useCallback(async () => {
141
- if (timerRef.current) {
142
- clearTimeout(timerRef.current);
143
- timerRef.current = null;
144
- }
161
+ // One pass of the persist logic: snapshots the latest working state and
162
+ // creates-or-updates the doc. NOT called directly — it runs through the
163
+ // single-flight `flush` below, which guarantees two passes never overlap (so
164
+ // a create-on-first-save can't race a concurrent flush) and re-invokes this
165
+ // to drain any work that arrived mid-pass. Because it re-reads `workingRef`
166
+ // each call, a trailing pass always persists the newest canvas.
167
+ const runFlush = useCallback(async () => {
145
168
  const canvas = workingRef.current;
146
169
  // Nothing to persist, or no real target scope yet.
147
170
  if (!canvas || !scope)
148
171
  return;
149
- if (creatingRef.current)
150
- return;
151
172
  const json = JSON.stringify(canvas);
173
+ // Edits up to this point are covered by this write; anything committed
174
+ // while the write is in flight bumps editSeqRef past this snapshot.
175
+ const editSeqAtFlush = editSeqRef.current;
176
+ // Claim the thumbnail request for THIS pass: honored on success, dropped
177
+ // on failure (the next explicit save re-requests it).
178
+ const wantThumbnail = thumbnailRequestedRef.current;
179
+ thumbnailRequestedRef.current = false;
180
+ const canvasRev = {
181
+ clientId: clientIdRef.current,
182
+ seq: ++writeSeqRef.current,
183
+ };
152
184
  const id = fileIdRef.current ?? createdIdRef.current;
153
185
  const mode = id ? 'update' : 'create';
154
186
  const debug = debugRef.current;
@@ -168,18 +200,18 @@ export const useAnnotationCanvasDoc = (options) => {
168
200
  // rejects undefined field values unless ignoreUndefinedProperties is set.
169
201
  const canvasPayload = JSON.parse(json);
170
202
  setStatus('saving');
203
+ let createdThisPass = false;
171
204
  try {
172
205
  if (!id) {
173
206
  // First save with no file — create the doc seeded with the canvas.
174
- creatingRef.current = true;
175
207
  const seed = createSeedRef.current;
176
208
  const newId = await createRef.current({
177
209
  type: FileUploadType.Canvas,
178
210
  ...(seed?.name !== undefined ? { name: seed.name } : {}),
179
- fileData: buildFileData(seed?.fileType ?? 'sketch', seed?.isLabel, canvasPayload),
211
+ fileData: buildFileData(seed?.fileType ?? 'sketch', seed?.isLabel, canvasPayload, canvasRev),
180
212
  });
181
- creatingRef.current = false;
182
213
  createdIdRef.current = newId;
214
+ createdThisPass = true;
183
215
  if (debug) {
184
216
  console.log('[useAnnotationCanvasDoc] created file', newId);
185
217
  }
@@ -190,22 +222,27 @@ export const useAnnotationCanvasDoc = (options) => {
190
222
  await updateRef.current(id, {
191
223
  fileData: buildFileData(doc?.fileData.fileType ??
192
224
  createSeedRef.current?.fileType ??
193
- 'sketch', doc?.fileData.isLabel ?? createSeedRef.current?.isLabel, canvasPayload),
225
+ 'sketch', doc?.fileData.isLabel ?? createSeedRef.current?.isLabel, canvasPayload, canvasRev),
194
226
  });
195
227
  if (debug) {
196
228
  console.log('[useAnnotationCanvasDoc] updated file', id);
197
229
  }
198
230
  }
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).
231
+ // Refresh the thumbnail when an explicit save() asked for it, or when
232
+ // this pass CREATED the file every file gets at least one thumbnail
233
+ // so the grid never shows a blank tile for a canvas the user drew and
234
+ // then backed out of without an explicit save. Routine debounced
235
+ // autosaves skip it: capture means a full view snapshot + encode on the
236
+ // JS thread, which would jank the very drawing session that triggered
237
+ // the save. Fire-and-forget for the flush itself; save() awaits the
238
+ // stashed promise so explicit savers can navigate safely after.
202
239
  const savedId = fileIdRef.current ?? createdIdRef.current;
203
- if (savedId)
204
- void saveThumbnail(savedId);
240
+ if (savedId && (wantThumbnail || createdThisPass)) {
241
+ thumbnailPromiseRef.current = saveThumbnail(savedId);
242
+ }
205
243
  // If new edits landed mid-flight, stay dirty and let the next debounce
206
244
  // (or unmount) flush them.
207
- const latest = workingRef.current;
208
- if (latest && JSON.stringify(latest) !== json) {
245
+ if (editSeqRef.current !== editSeqAtFlush) {
209
246
  setStatus('dirty');
210
247
  }
211
248
  else {
@@ -213,16 +250,47 @@ export const useAnnotationCanvasDoc = (options) => {
213
250
  }
214
251
  }
215
252
  catch (e) {
216
- creatingRef.current = false;
217
253
  // Always log save failures with context — these are otherwise invisible
218
254
  // (the canvas keeps working from local state).
219
255
  console.error(`[useAnnotationCanvasDoc] ${mode} failed`, { fileId: id, scope, bytes: json.length }, e);
220
256
  onSaveErrorRef.current?.(e);
221
257
  setStatus('error');
258
+ // Swallowed (not re-thrown): autosave/unmount call this fire-and-forget,
259
+ // and the canvas keeps working from local state. Callers that need to know
260
+ // a create succeeded (ensureFileId) detect it via the absence of an id
261
+ // after the flush drains, not via a rejection.
222
262
  }
223
263
  }, [scope, setStatus, saveThumbnail]);
264
+ // Stable indirection so the coalesced runner (created once) always calls the
265
+ // latest `runFlush` without being re-created when its deps change.
266
+ const runFlushRef = useRef(runFlush);
267
+ runFlushRef.current = runFlush;
268
+ // Single-flight runner: concurrent callers join the in-flight pass; work that
269
+ // arrives mid-pass triggers exactly one trailing pass. Created once.
270
+ const flushRunner = useMemo(() => createCoalescedRunner(() => runFlushRef.current()), []);
271
+ // Public flush: cancel any pending debounce, then run (or join) a persist
272
+ // pass. Resolves once the chain is fully drained, so `await flush()` is safe
273
+ // for create-before-upload and explicit Save.
274
+ const flush = useCallback(() => {
275
+ if (timerRef.current) {
276
+ clearTimeout(timerRef.current);
277
+ timerRef.current = null;
278
+ }
279
+ return flushRunner();
280
+ }, [flushRunner]);
224
281
  const onCommit = useCallback((patch) => {
225
- setWorking((prev) => (prev ? applyPatch(prev, patch) : prev));
282
+ editSeqRef.current += 1;
283
+ setWorking((prev) => {
284
+ if (!prev)
285
+ return prev;
286
+ const next = applyPatch(prev, patch);
287
+ // Keep the flush snapshot in lockstep with editSeqRef: a flush that
288
+ // starts before React re-renders must not pair this commit's seq bump
289
+ // with the pre-commit canvas (it would mark the edit 'saved' without
290
+ // ever writing it).
291
+ workingRef.current = next;
292
+ return next;
293
+ });
226
294
  setStatus('dirty');
227
295
  if (timerRef.current)
228
296
  clearTimeout(timerRef.current);
@@ -231,12 +299,24 @@ export const useAnnotationCanvasDoc = (options) => {
231
299
  void flush();
232
300
  }, debounceMs);
233
301
  }, [debounceMs, flush, setStatus]);
302
+ // Explicit save (Save button, "done" actions): also refreshes the file's
303
+ // thumbnail, which autosaves deliberately skip. Resolves only after the
304
+ // capture+upload too, so a caller may unmount the view right after.
305
+ const save = useCallback(async () => {
306
+ thumbnailRequestedRef.current = true;
307
+ await flush();
308
+ await thumbnailPromiseRef.current;
309
+ }, [flush]);
234
310
  const ensureFileId = useCallback(async () => {
235
311
  const existing = fileIdRef.current ?? createdIdRef.current;
236
312
  if (existing)
237
313
  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.
314
+ // No file yet — seed an empty canvas if nothing has hydrated, then flush so
315
+ // the create branch mints the doc. `flush` is single-flight: if the
316
+ // debounced autosave is already creating, this call JOINS that create and
317
+ // waits for it to drain (rather than racing it and dropping the upload, the
318
+ // old bug). After the drain, the id is present unless the create truly
319
+ // failed — so the throw below is now a genuine error, not a race.
240
320
  if (!workingRef.current) {
241
321
  const seeded = createEmptyCanvasState(fallbackViewport);
242
322
  workingRef.current = seeded;
@@ -306,7 +386,7 @@ export const useAnnotationCanvasDoc = (options) => {
306
386
  loading,
307
387
  error,
308
388
  saveStatus,
309
- save: flush,
389
+ save,
310
390
  ensureFileId,
311
391
  setBackgroundImage,
312
392
  clearBackgroundImage,
package/dist/exports.d.ts CHANGED
@@ -18,13 +18,18 @@ export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
18
18
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
19
19
  export type { AnnotationCanvasHandle } from './annotation/canvas/useAnnotationCanvasState.js';
20
20
  export type { GestureConfig, PanTrigger, AnnotationCanvasInnerProps, } from './annotation/canvas/AnnotationCanvasInner.js';
21
- export type { CanvasPointerEvent, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
21
+ export type { CanvasPointerEvent, RequestTextInput, ShapeDrawConfig, Tool, ToolContext, ToolState, } from './annotation/canvas/Tool.js';
22
22
  export type { MeasurementRef, PickMeasurement, } from './annotation/canvas/measurementPicker.js';
23
23
  export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './annotation/canvas/measurementStampOverlay.js';
24
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';
25
+ export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
26
26
  export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
27
27
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
28
- export { createPanTool, type PanToolOptions } from './annotation/canvas/tools/panTool.js';
29
- export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.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 { createShapeTool, buildShapeFromDrag, type ShapeToolOptions, } from './annotation/canvas/tools/shapeTool.js';
31
+ export { createPolygonTool, type PolygonToolOptions, } from './annotation/canvas/tools/polygonTool.js';
32
+ export { annotationKindFor, hitShapeOutline, shapePointsFromDrag, type ShapeToolKind, } from './annotation/canvas/shapeGeometry.js';
33
+ export { DEFAULT_TEXT_FONT_SIZE, MIN_TEXT_FONT_SIZE, MAX_TEXT_FONT_SIZE, textShapeBounds, textResizeGeometry, type ResizeGeometry, type TextBounds, } from './annotation/canvas/textGeometry.js';
34
+ export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, normalizeRect, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, DEFAULT_LINE_POS, type NormalizedRect, type RectCorner, } from './annotation/canvas/measurementGeometry.js';
30
35
  export { toSkiaStrokeCap, arrowheadTriangle, arrowheadLength, } from './annotation/canvas/strokeGeometry.js';
package/dist/exports.js CHANGED
@@ -22,9 +22,14 @@ export { useAnnotationCanvasDoc, } from './annotation/data/hooks/useAnnotationCa
22
22
  export { hydrateCanvasState } from './annotation/data/canvasPersistence.js';
23
23
  export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotationProvider.js';
24
24
  export { createViewportApi, fitToScreen, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
25
- export { createPenTool } from './annotation/canvas/tools/penTool.js';
25
+ export { createPenTool, } from './annotation/canvas/tools/penTool.js';
26
26
  export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
27
27
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
28
- export { createPanTool } from './annotation/canvas/tools/panTool.js';
29
- export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.js';
28
+ export { createPanTool, } from './annotation/canvas/tools/panTool.js';
29
+ export { createTextTool, } from './annotation/canvas/tools/textTool.js';
30
+ export { createShapeTool, buildShapeFromDrag, } from './annotation/canvas/tools/shapeTool.js';
31
+ export { createPolygonTool, } from './annotation/canvas/tools/polygonTool.js';
32
+ export { annotationKindFor, hitShapeOutline, shapePointsFromDrag, } from './annotation/canvas/shapeGeometry.js';
33
+ export { DEFAULT_TEXT_FONT_SIZE, MIN_TEXT_FONT_SIZE, MAX_TEXT_FONT_SIZE, textShapeBounds, textResizeGeometry, } from './annotation/canvas/textGeometry.js';
34
+ export { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, normalizeRect, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.js';
30
35
  export { toSkiaStrokeCap, arrowheadTriangle, arrowheadLength, } from './annotation/canvas/strokeGeometry.js';
@@ -105,9 +105,7 @@ export const calculateFormula = (formula, formulas, columns, tableConfig, measur
105
105
  // Validate that all required inputs are filled out
106
106
  const missingInputs = [];
107
107
  for (const [variable, mapping] of Object.entries(currentMappings)) {
108
- const mappingObj = typeof mapping === 'string'
109
- ? { id: mapping, type: 'column' }
110
- : mapping;
108
+ const mappingObj = typeof mapping === 'string' ? { id: mapping, type: 'column' } : mapping;
111
109
  // Only check column references (formula references are handled separately)
112
110
  if (mappingObj.type === 'column' || !mappingObj.type) {
113
111
  const columnId = mappingObj.id;
@@ -13,6 +13,7 @@ export interface AnnotationStroke {
13
13
  color: string;
14
14
  width: number;
15
15
  cap?: StrokeCap;
16
+ dash?: boolean;
16
17
  points: number[];
17
18
  pressure?: number[];
18
19
  createdAt: number;
@@ -24,6 +25,8 @@ export interface AnnotationShapeStyle {
24
25
  strokeWidth?: number;
25
26
  fontSize?: number;
26
27
  fontFamily?: string;
28
+ dash?: boolean;
29
+ cap?: StrokeCap;
27
30
  }
28
31
  export interface AnnotationShape {
29
32
  id: AnnotationElementId;
@@ -32,6 +35,7 @@ export interface AnnotationShape {
32
35
  geometry: {
33
36
  points: Vec2[];
34
37
  rotation?: number;
38
+ closed?: boolean;
35
39
  };
36
40
  style: AnnotationShapeStyle;
37
41
  text?: string;
@@ -51,9 +55,14 @@ export interface PlacedMeasurementRef {
51
55
  b: Vec2;
52
56
  };
53
57
  linePos?: number;
58
+ rect?: {
59
+ a: Vec2;
60
+ b: Vec2;
61
+ };
54
62
  lineColor?: string;
55
63
  lineWidth?: number;
56
64
  lineCap?: StrokeCap;
65
+ lineDash?: boolean;
57
66
  leader?: {
58
67
  from: Vec2;
59
68
  to: Vec2;
@@ -113,6 +113,10 @@ export interface AnnotationFileData {
113
113
  fileType: 'sketch' | 'document';
114
114
  isLabel?: boolean;
115
115
  canvas?: AnnotationCanvasState;
116
+ canvasRev?: {
117
+ clientId: string;
118
+ seq: number;
119
+ };
116
120
  }
117
121
  export interface CalculatorFileData {
118
122
  templateId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.6.12",
3
+ "version": "1.6.14",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",