@reekon-tools/boldr-utils 1.6.11 → 1.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.d.ts +2 -2
  2. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.js +19 -14
  3. package/dist/{canvas → annotation/canvas}/AnnotationCanvasInner.native.d.ts +2 -2
  4. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +668 -0
  5. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +46 -0
  6. package/dist/annotation/canvas/AnnotationCanvasSkia.js +129 -0
  7. package/dist/{canvas → annotation/canvas}/Tool.d.ts +23 -1
  8. package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.d.ts +2 -2
  9. package/dist/{canvas → annotation/canvas}/elements/BackgroundImageElement.js +13 -6
  10. package/dist/annotation/canvas/elements/ShapeElement.d.ts +7 -0
  11. package/dist/{canvas → annotation/canvas}/elements/ShapeElement.js +5 -3
  12. package/dist/annotation/canvas/elements/StrokeElement.d.ts +7 -0
  13. package/dist/annotation/canvas/elements/StrokeElement.js +40 -0
  14. package/dist/annotation/canvas/measurementGeometry.d.ts +23 -0
  15. package/dist/annotation/canvas/measurementGeometry.js +74 -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 +4 -0
  21. package/dist/annotation/canvas/strokeGeometry.js +33 -0
  22. package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.d.ts +1 -1
  23. package/dist/{canvas → annotation/canvas}/tools/measurementStampTool.js +1 -1
  24. package/dist/{canvas → annotation/canvas}/tools/panTool.js +3 -0
  25. package/dist/{canvas → annotation/canvas}/tools/penTool.d.ts +2 -1
  26. package/dist/{canvas → annotation/canvas}/tools/penTool.js +32 -5
  27. package/dist/annotation/canvas/tools/selectTool.js +310 -0
  28. package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.d.ts +9 -2
  29. package/dist/{canvas → annotation/canvas}/useAnnotationCanvasState.js +115 -1
  30. package/dist/{canvas → annotation/canvas}/viewport.d.ts +1 -1
  31. package/dist/{data → annotation/data}/AnnotationDataProvider.d.ts +1 -1
  32. package/dist/{data → annotation/data}/InMemoryAnnotationProvider.d.ts +1 -1
  33. package/dist/{data → annotation/data}/InMemoryAnnotationProvider.js +1 -1
  34. package/dist/{data → annotation/data}/canvasPersistence.d.ts +1 -1
  35. package/dist/{data → annotation/data}/canvasPersistence.js +1 -1
  36. package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.d.ts +1 -1
  37. package/dist/{data → annotation/data}/hooks/useAnnotationCanvasDoc.js +2 -2
  38. package/dist/exports.d.ts +21 -19
  39. package/dist/exports.js +16 -14
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +2 -2
  42. package/dist/index.native.d.ts +1 -1
  43. package/dist/index.native.js +1 -1
  44. package/dist/types/annotation.d.ts +15 -3
  45. package/dist/types/firestore.d.ts +0 -1
  46. package/dist/{hooks → utils}/useParseMeasurement.js +1 -1
  47. package/package.json +1 -1
  48. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  49. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  50. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  51. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  52. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  53. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  54. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  55. package/dist/canvas/elements/StrokeElement.js +0 -18
  56. package/dist/canvas/stampLayout.d.ts +0 -5
  57. package/dist/canvas/stampLayout.js +0 -14
  58. package/dist/canvas/tools/selectTool.js +0 -182
  59. package/dist/utils/evaluateFormula.d.ts +0 -20
  60. package/dist/utils/evaluateFormula.js +0 -31
  61. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.d.ts +0 -0
  62. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.js +0 -0
  63. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.d.ts +0 -0
  64. /package/dist/{canvas → annotation/canvas}/AnnotationCanvas.native.js +0 -0
  65. /package/dist/{canvas → annotation/canvas}/Tool.js +0 -0
  66. /package/dist/{canvas → annotation/canvas}/measurementPicker.js +0 -0
  67. /package/dist/{canvas → annotation/canvas}/measurementStampOverlay.js +0 -0
  68. /package/dist/{canvas → annotation/canvas}/pointerAdapter.d.ts +0 -0
  69. /package/dist/{canvas → annotation/canvas}/pointerAdapter.js +0 -0
  70. /package/dist/{canvas → annotation/canvas}/tools/panTool.d.ts +0 -0
  71. /package/dist/{canvas → annotation/canvas}/tools/selectTool.d.ts +0 -0
  72. /package/dist/{canvas → annotation/canvas}/viewport.js +0 -0
  73. /package/dist/{data → annotation/data}/AnnotationDataContext.d.ts +0 -0
  74. /package/dist/{data → annotation/data}/AnnotationDataContext.js +0 -0
  75. /package/dist/{data → annotation/data}/AnnotationDataProvider.js +0 -0
  76. /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.d.ts +0 -0
  77. /package/dist/{data → annotation/data}/hooks/useAnnotationDoc.js +0 -0
  78. /package/dist/{data → annotation/data}/hooks/useAnnotationList.d.ts +0 -0
  79. /package/dist/{data → annotation/data}/hooks/useAnnotationList.js +0 -0
  80. /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.d.ts +0 -0
  81. /package/dist/{data → annotation/data}/hooks/useAnnotationMutations.js +0 -0
  82. /package/dist/{hooks → utils}/useParseMeasurement.d.ts +0 -0
@@ -0,0 +1,310 @@
1
+ import { STAMP_TILE_SIZE } from '../stampLayout.js';
2
+ import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, } from '../measurementGeometry.js';
3
+ const HIT_PADDING = 6;
4
+ // Hit-test in doc-space. Crude but fast — good enough for v1; tools can
5
+ // override via `hitTest` for more precision later.
6
+ const hitStroke = (stroke, p) => {
7
+ const r = stroke.width / 2 + HIT_PADDING;
8
+ const r2 = r * r;
9
+ for (let i = 0; i < stroke.points.length - 2; i += 2) {
10
+ if (segmentDistanceSq(p.x, p.y, stroke.points[i], stroke.points[i + 1], stroke.points[i + 2], stroke.points[i + 3]) <= r2) {
11
+ return true;
12
+ }
13
+ }
14
+ return false;
15
+ };
16
+ // Screen-space grab tolerance (px) for a measurement-annotation line, converted
17
+ // to doc space via zoom (the line itself is a thin world-space stroke).
18
+ const LINE_GRAB_PX = 12;
19
+ // Screen-px radius of the center snap detent when sliding a tile along its line.
20
+ // Converted to t-space per line via (SNAP_PX / zoom) / lineLength. The native
21
+ // slide worklet inlines the same value — keep them in sync.
22
+ const SLIDE_SNAP_PX = 12;
23
+ // Screen-px grab radius for a line-annotation endpoint handle (converted to doc
24
+ // space via zoom). A bit larger than the drawn handle so it's easy to grab.
25
+ const HANDLE_GRAB_PX = 22;
26
+ const hitMeasurement = (m, p, zoom = 1) => {
27
+ // The stamp renders as a constant *screen*-size square centered on the
28
+ // anchor, so its doc-space footprint shrinks as you zoom in. Convert the
29
+ // screen-space half-extent (+ padding) back to doc space via the zoom so
30
+ // the hit box always matches what's drawn.
31
+ const scale = m.scale ?? 1;
32
+ const half = ((STAMP_TILE_SIZE * scale) / 2 + HIT_PADDING) / zoom;
33
+ const dx = Math.abs(p.x - m.anchor.x);
34
+ const dy = Math.abs(p.y - m.anchor.y);
35
+ if (dx <= half && dy <= half)
36
+ return true;
37
+ // Measurement annotation: also grab anywhere along the line body.
38
+ if (m.line) {
39
+ const r = LINE_GRAB_PX / zoom;
40
+ if (segmentDistanceSq(p.x, p.y, m.line.a.x, m.line.a.y, m.line.b.x, m.line.b.y) <=
41
+ r * r) {
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ };
47
+ const segmentDistanceSq = (px, py, ax, ay, bx, by) => {
48
+ const abx = bx - ax;
49
+ const aby = by - ay;
50
+ const lenSq = abx * abx + aby * aby;
51
+ let t = lenSq === 0 ? 0 : ((px - ax) * abx + (py - ay) * aby) / lenSq;
52
+ t = Math.max(0, Math.min(1, t));
53
+ const cx = ax + t * abx;
54
+ const cy = ay + t * aby;
55
+ const dx = px - cx;
56
+ const dy = py - cy;
57
+ return dx * dx + dy * dy;
58
+ };
59
+ const findHit = (doc, world, zoom) => {
60
+ // Hit-test in z-order (top first): measurements > shapes > strokes.
61
+ for (let i = doc.placedMeasurements.length - 1; i >= 0; i--) {
62
+ const m = doc.placedMeasurements[i];
63
+ if (hitMeasurement(m, world, zoom))
64
+ return { id: m.id, kind: 'measurement' };
65
+ }
66
+ for (let i = doc.shapes.length - 1; i >= 0; i--) {
67
+ // Default shape hit test: bounding box of the shape's points + padding.
68
+ const s = doc.shapes[i];
69
+ const pts = s.geometry.points;
70
+ if (pts.length === 0)
71
+ continue;
72
+ let minX = pts[0].x;
73
+ let maxX = pts[0].x;
74
+ let minY = pts[0].y;
75
+ let maxY = pts[0].y;
76
+ for (let j = 1; j < pts.length; j++) {
77
+ const p = pts[j];
78
+ if (p.x < minX)
79
+ minX = p.x;
80
+ if (p.x > maxX)
81
+ maxX = p.x;
82
+ if (p.y < minY)
83
+ minY = p.y;
84
+ if (p.y > maxY)
85
+ maxY = p.y;
86
+ }
87
+ if (world.x >= minX - HIT_PADDING &&
88
+ world.x <= maxX + HIT_PADDING &&
89
+ world.y >= minY - HIT_PADDING &&
90
+ world.y <= maxY + HIT_PADDING) {
91
+ return { id: s.id, kind: 'shape' };
92
+ }
93
+ }
94
+ for (let i = doc.strokes.length - 1; i >= 0; i--) {
95
+ const s = doc.strokes[i];
96
+ if (hitStroke(s, world))
97
+ return { id: s.id, kind: 'stroke' };
98
+ }
99
+ return null;
100
+ };
101
+ const translatePatch = (elementKind, id, doc, delta) => {
102
+ if (elementKind === 'measurement') {
103
+ const m = doc.placedMeasurements.find((x) => x.id === id);
104
+ if (!m)
105
+ return null;
106
+ // Group move: translate the whole annotation uniformly. `anchor` stays
107
+ // consistent with the line (lerp is affine, so translating both endpoints
108
+ // translates the anchor by the same delta). Only emit keys that exist so
109
+ // the patch never carries `undefined` (Firestore rejects undefined values).
110
+ const tx = (p) => ({ x: p.x + delta.x, y: p.y + delta.y });
111
+ const patch = { anchor: tx(m.anchor) };
112
+ if (m.line)
113
+ patch.line = { a: tx(m.line.a), b: tx(m.line.b) };
114
+ if (m.leader)
115
+ patch.leader = { from: tx(m.leader.from), to: tx(m.leader.to) };
116
+ return { op: 'updateMeasurement', id, patch };
117
+ }
118
+ if (elementKind === 'shape') {
119
+ const s = doc.shapes.find((x) => x.id === id);
120
+ if (!s)
121
+ return null;
122
+ return {
123
+ op: 'updateShape',
124
+ id,
125
+ patch: {
126
+ geometry: {
127
+ ...s.geometry,
128
+ points: s.geometry.points.map((p) => ({
129
+ x: p.x + delta.x,
130
+ y: p.y + delta.y,
131
+ })),
132
+ },
133
+ },
134
+ };
135
+ }
136
+ const stroke = doc.strokes.find((x) => x.id === id);
137
+ if (!stroke)
138
+ return null;
139
+ const points = stroke.points.slice();
140
+ for (let i = 0; i < points.length; i += 2) {
141
+ points[i] = points[i] + delta.x;
142
+ points[i + 1] = points[i + 1] + delta.y;
143
+ }
144
+ return { op: 'updateStroke', id, patch: { points } };
145
+ };
146
+ // --- Measurement-annotation grab logic (shared by the native UI-thread drag
147
+ // via DragSelectionConfig AND the web pointer handlers — one source of truth) ---
148
+ // Grabbing the tile of a line annotation slides it along the line; everything
149
+ // else (bare stamps, grabs on the line body) is a group move.
150
+ const classifyGrab = (doc, id, world, zoom) => {
151
+ const m = doc.placedMeasurements.find((x) => x.id === id);
152
+ if (!m)
153
+ return 'move';
154
+ const scale = m.scale ?? 1;
155
+ const half = ((STAMP_TILE_SIZE * scale) / 2 + HIT_PADDING) / zoom;
156
+ const onTile = Math.abs(world.x - m.anchor.x) <= half &&
157
+ Math.abs(world.y - m.anchor.y) <= half;
158
+ return onTile && placementOf(m) === 'line' && m.line ? 'slide' : 'move';
159
+ };
160
+ // Slide the tile along its line by a world-space drag delta (relative to
161
+ // grab-start), clamped to [0,1] with a center snap, and rewrite anchor. The
162
+ // native slide worklet in AnnotationCanvasInner.native.tsx mirrors this math.
163
+ const slidePatch = (doc, id, delta, zoom) => {
164
+ const m = doc.placedMeasurements.find((x) => x.id === id);
165
+ if (!m || !m.line || placementOf(m) !== 'line')
166
+ return null;
167
+ const abx = m.line.b.x - m.line.a.x;
168
+ const aby = m.line.b.y - m.line.a.y;
169
+ const lenSq = abx * abx + aby * aby;
170
+ if (lenSq === 0)
171
+ return null;
172
+ let t = linePosOf(m) + (delta.x * abx + delta.y * aby) / lenSq;
173
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
174
+ const snapT = SLIDE_SNAP_PX / zoom / Math.sqrt(lenSq);
175
+ t = snapLinePos(t, snapT);
176
+ const anchor = lerp(m.line.a, m.line.b, t);
177
+ return { ops: [{ op: 'updateMeasurement', id, patch: { linePos: t, anchor } }] };
178
+ };
179
+ // Which endpoint handle of a (selected) line annotation is under `world`.
180
+ // Prefers the nearer endpoint when both are within range.
181
+ const findHandleHit = (doc, id, world, zoom) => {
182
+ const m = doc.placedMeasurements.find((x) => x.id === id);
183
+ if (!m || !m.line || placementOf(m) !== 'line')
184
+ return null;
185
+ const r2 = (HANDLE_GRAB_PX / zoom) ** 2;
186
+ const da = (world.x - m.line.a.x) ** 2 + (world.y - m.line.a.y) ** 2;
187
+ const db = (world.x - m.line.b.x) ** 2 + (world.y - m.line.b.y) ** 2;
188
+ if (da <= r2 && da <= db)
189
+ return 'a';
190
+ if (db <= r2)
191
+ return 'b';
192
+ return null;
193
+ };
194
+ // Move one endpoint by a world delta (resize/rotate); keep the tile at its
195
+ // linePos by rewriting anchor.
196
+ const endpointPatch = (doc, id, handle, delta) => {
197
+ const m = doc.placedMeasurements.find((x) => x.id === id);
198
+ if (!m || !m.line || placementOf(m) !== 'line')
199
+ return null;
200
+ const a = handle === 'a'
201
+ ? { x: m.line.a.x + delta.x, y: m.line.a.y + delta.y }
202
+ : m.line.a;
203
+ const b = handle === 'b'
204
+ ? { x: m.line.b.x + delta.x, y: m.line.b.y + delta.y }
205
+ : m.line.b;
206
+ const line = { a, b };
207
+ const anchor = recomputeAnchor(line, 'line', linePosOf(m), m.anchor);
208
+ return { ops: [{ op: 'updateMeasurement', id, patch: { line, anchor } }] };
209
+ };
210
+ // Patch for the current drag mode (web pointer path), from a world-space delta.
211
+ const dragPatch = (s, doc, delta, zoom) => {
212
+ if (s.mode === 'endpoint' && s.handle) {
213
+ return endpointPatch(doc, s.id, s.handle, delta);
214
+ }
215
+ if (s.mode === 'slide')
216
+ return slidePatch(doc, s.id, delta, zoom);
217
+ const op = translatePatch(s.elementKind, s.id, doc, delta);
218
+ return op ? { ops: [op] } : null;
219
+ };
220
+ export const createSelectTool = () => ({
221
+ id: 'select',
222
+ label: 'Select',
223
+ cursor: 'default',
224
+ // Native drags the hit element on the UI thread (see DragSelectionConfig),
225
+ // reusing the same hit-test and translate logic the pointer handlers below
226
+ // use for web — one source of truth.
227
+ dragSelection: {
228
+ hitTest: (doc, world, zoom) => findHit(doc, world, zoom),
229
+ buildTranslatePatch: (doc, id, kind, delta) => {
230
+ const op = translatePatch(kind, id, doc, delta);
231
+ return op ? { ops: [op] } : null;
232
+ },
233
+ classifyMeasurementGrab: classifyGrab,
234
+ buildSlidePatch: slidePatch,
235
+ hitTestHandle: findHandleHit,
236
+ buildEndpointPatch: endpointPatch,
237
+ },
238
+ // Web pointer path. Mirrors the native UI-thread drag using the same shared
239
+ // helpers: an endpoint handle on the selected annotation resizes the line;
240
+ // grabbing a line-annotation tile slides it; everything else group-moves.
241
+ onPointerDown(event, ctx) {
242
+ const { world } = event;
243
+ const zoom = ctx.viewport.state.zoom;
244
+ // Endpoint handles show only on the selected annotation — check first.
245
+ const selId = ctx.selection?.ids[0];
246
+ if (selId) {
247
+ const handle = findHandleHit(ctx.document, selId, world, zoom);
248
+ if (handle) {
249
+ ctx.setSelection({ ids: [selId] });
250
+ return {
251
+ kind: 'dragging',
252
+ id: selId,
253
+ elementKind: 'measurement',
254
+ mode: 'endpoint',
255
+ handle,
256
+ start: world,
257
+ delta: { x: 0, y: 0 },
258
+ };
259
+ }
260
+ }
261
+ const hit = findHit(ctx.document, world, zoom);
262
+ if (!hit) {
263
+ ctx.setSelection(null);
264
+ return { kind: 'idle' };
265
+ }
266
+ ctx.setSelection({ ids: [hit.id] });
267
+ const mode = hit.kind === 'measurement' &&
268
+ classifyGrab(ctx.document, hit.id, world, zoom) === 'slide'
269
+ ? 'slide'
270
+ : 'move';
271
+ return {
272
+ kind: 'dragging',
273
+ id: hit.id,
274
+ elementKind: hit.kind,
275
+ mode,
276
+ start: world,
277
+ delta: { x: 0, y: 0 },
278
+ };
279
+ },
280
+ onPointerMove(event, ctx, state) {
281
+ const s = state;
282
+ if (s?.kind !== 'dragging')
283
+ return s;
284
+ const delta = { x: event.world.x - s.start.x, y: event.world.y - s.start.y };
285
+ const patch = dragPatch(s, ctx.document, delta, ctx.viewport.state.zoom);
286
+ if (patch)
287
+ ctx.preview(patch);
288
+ return { ...s, delta };
289
+ },
290
+ onPointerUp(_event, ctx, state) {
291
+ const s = state;
292
+ if (s?.kind !== 'dragging')
293
+ return;
294
+ if (s.delta.x === 0 && s.delta.y === 0)
295
+ return;
296
+ const patch = dragPatch(s, ctx.document, s.delta, ctx.viewport.state.zoom);
297
+ if (patch)
298
+ ctx.commit(patch);
299
+ },
300
+ onCancel(_state, ctx) {
301
+ ctx.preview({ ops: [] });
302
+ },
303
+ hitTest(element, p) {
304
+ if (element.kind === 'measurement')
305
+ return hitMeasurement(element, p);
306
+ if (element.kind === 'stroke')
307
+ return hitStroke(element, p);
308
+ return false;
309
+ },
310
+ });
@@ -1,5 +1,5 @@
1
- import type { Measurement } from '../types/firestore.js';
2
- import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationStroke, type Selection, type Vec2 } from '../types/annotation.js';
1
+ import type { Measurement } from '../../types/firestore.js';
2
+ import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type Selection, type Vec2 } from '../../types/annotation.js';
3
3
  import type { MeasurementRef } from './measurementPicker.js';
4
4
  import type { CanvasPointerEvent, Tool, ToolContext, ToolState } from './Tool.js';
5
5
  import { type ViewportState } from './viewport.js';
@@ -11,6 +11,12 @@ export interface AnnotationCanvasHandle {
11
11
  zoomToFit(): void;
12
12
  resetView(): void;
13
13
  placeMeasurementAtCenter(ref: MeasurementRef): void;
14
+ placeAnnotationAtCenter(opts?: {
15
+ defaultLengthDoc?: number;
16
+ }): void;
17
+ setAnnotationType(id: AnnotationElementId, type: MeasurementPlacement): void;
18
+ associateMeasurement(id: AnnotationElementId, ref: MeasurementRef): void;
19
+ deleteSelected(): void;
14
20
  }
15
21
  export interface UseAnnotationCanvasStateProps {
16
22
  canvas: AnnotationCanvasState;
@@ -50,5 +56,6 @@ export interface AnnotationCanvasStateApi {
50
56
  dispatchPointerCancel(): void;
51
57
  pan(deltaScreen: Vec2): void;
52
58
  zoom(focalScreen: Vec2, nextZoom: number): void;
59
+ setViewport(next: ViewportState): void;
53
60
  }
54
61
  export declare const useAnnotationCanvasState: (props: UseAnnotationCanvasStateProps) => AnnotationCanvasStateApi;
@@ -1,6 +1,7 @@
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, 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).
@@ -164,6 +165,118 @@ export const useAnnotationCanvasState = (props) => {
164
165
  // responsible for switching to its move/select tool).
165
166
  c.setSelection({ ids: [placed.id] });
166
167
  },
168
+ placeAnnotationAtCenter(opts) {
169
+ const c = ctxRef.current;
170
+ const center = c.viewport.screenToWorld({
171
+ x: width / 2,
172
+ y: height / 2,
173
+ });
174
+ // Default line length: a quarter of the doc width, clamped to a sane
175
+ // range so it's a grabbable size at any canvas scale.
176
+ const len = opts?.defaultLengthDoc ??
177
+ Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
178
+ const line = {
179
+ a: { x: center.x - len / 2, y: center.y },
180
+ b: { x: center.x + len / 2, y: center.y },
181
+ };
182
+ const placed = {
183
+ id: `annotation-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`,
184
+ layerId: c.document.layers[0]?.id ?? DEFAULT_LAYER_ID,
185
+ placement: 'line',
186
+ line,
187
+ linePos: DEFAULT_LINE_POS,
188
+ // Center of the line; recomputeAnchor keeps this in sync on edits.
189
+ anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, center),
190
+ showLabel: true,
191
+ showValue: true,
192
+ createdAt: Date.now(),
193
+ };
194
+ c.commit({ ops: [{ op: 'addMeasurement', measurement: placed }] });
195
+ c.setSelection({ ids: [placed.id] });
196
+ },
197
+ setAnnotationType(id, type) {
198
+ const c = ctxRef.current;
199
+ const m = c.document.placedMeasurements.find((x) => x.id === id);
200
+ if (!m)
201
+ return;
202
+ if (type === 'rectangle')
203
+ return; // not supported in v1
204
+ if (type === 'line') {
205
+ let line = m.line;
206
+ if (!line) {
207
+ const len = Math.min(400, Math.max(120, canvas.viewport.width * 0.25));
208
+ line = {
209
+ a: { x: m.anchor.x - len / 2, y: m.anchor.y },
210
+ b: { x: m.anchor.x + len / 2, y: m.anchor.y },
211
+ };
212
+ }
213
+ const linePos = m.linePos ?? DEFAULT_LINE_POS;
214
+ c.commit({
215
+ ops: [
216
+ {
217
+ op: 'updateMeasurement',
218
+ id,
219
+ patch: {
220
+ placement: 'line',
221
+ line,
222
+ linePos,
223
+ anchor: recomputeAnchor(line, 'line', linePos, m.anchor),
224
+ },
225
+ },
226
+ ],
227
+ });
228
+ return;
229
+ }
230
+ // 'none' — bare stamp. Keep the anchor (and the line data, which simply
231
+ // stops rendering); switching back to 'line' restores it.
232
+ c.commit({
233
+ ops: [{ op: 'updateMeasurement', id, patch: { placement: 'none' } }],
234
+ });
235
+ },
236
+ associateMeasurement(id, ref) {
237
+ const c = ctxRef.current;
238
+ c.commit({
239
+ ops: [
240
+ {
241
+ op: 'updateMeasurement',
242
+ id,
243
+ patch: {
244
+ measurementId: ref.measurementId,
245
+ measurementPath: ref.measurementPath,
246
+ groupId: ref.groupId,
247
+ labelOverride: ref.label,
248
+ unitOverride: ref.unit,
249
+ },
250
+ },
251
+ ],
252
+ });
253
+ },
254
+ deleteSelected() {
255
+ const c = ctxRef.current;
256
+ const ids = c.selection?.ids;
257
+ if (!ids || ids.length === 0)
258
+ return;
259
+ const idSet = new Set(ids);
260
+ const doc = c.document;
261
+ // Build one remove op per selected element, dispatched by which
262
+ // collection owns the id. A single multi-op commit makes the whole
263
+ // deletion one undo step (inverse re-adds each element).
264
+ const ops = [
265
+ ...doc.strokes
266
+ .filter((s) => idSet.has(s.id))
267
+ .map((s) => ({ op: 'removeStroke', id: s.id })),
268
+ ...doc.shapes
269
+ .filter((s) => idSet.has(s.id))
270
+ .map((s) => ({ op: 'removeShape', id: s.id })),
271
+ ...doc.placedMeasurements
272
+ .filter((m) => idSet.has(m.id))
273
+ .map((m) => ({ op: 'removeMeasurement', id: m.id })),
274
+ ];
275
+ if (ops.length === 0)
276
+ return;
277
+ c.commit({ ops });
278
+ c.setSelection(null);
279
+ },
167
280
  };
168
281
  return () => {
169
282
  if (imperativeRef)
@@ -206,5 +319,6 @@ export const useAnnotationCanvasState = (props) => {
206
319
  dispatchPointerCancel,
207
320
  pan,
208
321
  zoom,
322
+ setViewport,
209
323
  };
210
324
  };
@@ -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
@@ -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,6 +1,6 @@
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
6
  import { hydrateCanvasState } from '../canvasPersistence.js';
package/dist/exports.d.ts CHANGED
@@ -2,27 +2,29 @@ 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, 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 { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.js';
30
+ export { toSkiaStrokeCap, arrowheadTriangle, arrowheadLength, } from './annotation/canvas/strokeGeometry.js';
package/dist/exports.js CHANGED
@@ -6,23 +6,25 @@ 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 { recomputeAnchor, projectToLinePos, snapLinePos, lerp, lineLength, placementOf, linePosOf, DEFAULT_LINE_POS, } from './annotation/canvas/measurementGeometry.js';
30
+ 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';