@reekon-tools/boldr-utils 1.15.0 → 1.15.2

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 (88) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.js +5 -2
  2. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +10 -3
  3. package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -1
  4. package/dist/annotation/canvas/AnnotationCanvasSkia.js +17 -3
  5. package/dist/annotation/canvas/Tool.d.ts +1 -0
  6. package/dist/annotation/canvas/elements/PlaneElement.d.ts +18 -0
  7. package/dist/annotation/canvas/elements/PlaneElement.js +115 -0
  8. package/dist/annotation/canvas/planeGeometry.d.ts +48 -0
  9. package/dist/annotation/canvas/planeGeometry.js +507 -0
  10. package/dist/annotation/canvas/tools/measurementTool.d.ts +1 -0
  11. package/dist/annotation/canvas/tools/measurementTool.js +13 -0
  12. package/dist/annotation/canvas/tools/planeTool.d.ts +12 -0
  13. package/dist/annotation/canvas/tools/planeTool.js +357 -0
  14. package/dist/annotation/canvas/tools/selectTool.js +9 -0
  15. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +3 -1
  16. package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -0
  17. package/dist/canvas/AnnotationCanvas.d.ts +11 -0
  18. package/dist/canvas/AnnotationCanvas.js +10 -0
  19. package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
  20. package/dist/canvas/AnnotationCanvas.native.js +6 -0
  21. package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
  22. package/dist/canvas/AnnotationCanvasInner.js +219 -0
  23. package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
  24. package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
  25. package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
  26. package/dist/canvas/AnnotationCanvasSkia.js +20 -0
  27. package/dist/canvas/Tool.d.ts +38 -0
  28. package/dist/canvas/Tool.js +1 -0
  29. package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
  30. package/dist/canvas/elements/BackgroundImageElement.js +37 -0
  31. package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
  32. package/dist/canvas/elements/MeasurementStampElement.js +30 -0
  33. package/dist/canvas/elements/ShapeElement.d.ts +7 -0
  34. package/dist/canvas/elements/ShapeElement.js +62 -0
  35. package/dist/canvas/elements/StrokeElement.d.ts +7 -0
  36. package/dist/canvas/elements/StrokeElement.js +18 -0
  37. package/dist/canvas/measurementPicker.d.ts +10 -0
  38. package/dist/canvas/measurementPicker.js +1 -0
  39. package/dist/canvas/measurementStampOverlay.d.ts +11 -0
  40. package/dist/canvas/measurementStampOverlay.js +1 -0
  41. package/dist/canvas/pointerAdapter.d.ts +3 -0
  42. package/dist/canvas/pointerAdapter.js +19 -0
  43. package/dist/canvas/stampLayout.d.ts +5 -0
  44. package/dist/canvas/stampLayout.js +14 -0
  45. package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
  46. package/dist/canvas/tools/measurementStampTool.js +37 -0
  47. package/dist/canvas/tools/panTool.d.ts +5 -0
  48. package/dist/canvas/tools/panTool.js +25 -0
  49. package/dist/canvas/tools/penTool.d.ts +13 -0
  50. package/dist/canvas/tools/penTool.js +68 -0
  51. package/dist/canvas/tools/selectTool.d.ts +2 -0
  52. package/dist/canvas/tools/selectTool.js +182 -0
  53. package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
  54. package/dist/canvas/useAnnotationCanvasState.js +210 -0
  55. package/dist/canvas/viewport.d.ts +16 -0
  56. package/dist/canvas/viewport.js +54 -0
  57. package/dist/data/AnnotationDataContext.d.ts +8 -0
  58. package/dist/data/AnnotationDataContext.js +11 -0
  59. package/dist/data/AnnotationDataProvider.d.ts +65 -0
  60. package/dist/data/AnnotationDataProvider.js +4 -0
  61. package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
  62. package/dist/data/InMemoryAnnotationProvider.js +197 -0
  63. package/dist/data/canvasPersistence.d.ts +3 -0
  64. package/dist/data/canvasPersistence.js +26 -0
  65. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
  66. package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
  67. package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
  68. package/dist/data/hooks/useAnnotationDoc.js +33 -0
  69. package/dist/data/hooks/useAnnotationList.d.ts +7 -0
  70. package/dist/data/hooks/useAnnotationList.js +26 -0
  71. package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
  72. package/dist/data/hooks/useAnnotationMutations.js +11 -0
  73. package/dist/exports.d.ts +2 -0
  74. package/dist/exports.js +2 -0
  75. package/dist/hooks/useParseMeasurement.d.ts +4 -0
  76. package/dist/hooks/useParseMeasurement.js +14 -0
  77. package/dist/types/annotation.d.ts +37 -0
  78. package/dist/types/annotation.js +30 -0
  79. package/dist/types/firestore.d.ts +5 -0
  80. package/dist/utils/evaluateFormula.d.ts +20 -0
  81. package/dist/utils/evaluateFormula.js +31 -0
  82. package/package.json +1 -1
  83. package/dist/theme/index.d.ts +0 -3
  84. package/dist/theme/index.js +0 -3
  85. package/dist/theme/preset.d.ts +0 -120
  86. package/dist/theme/preset.js +0 -25
  87. package/dist/theme/semantic.d.ts +0 -136
  88. package/dist/theme/semantic.js +0 -116
@@ -0,0 +1,357 @@
1
+ import { DEFAULT_LAYER_ID } from '../../../types/annotation.js';
2
+ import { isQuadConvexNonDegenerate, normalizeQuadOrder, planeEdgeLine, } from '../planeGeometry.js';
3
+ import { linePosOf, recomputeAnchor, DEFAULT_LINE_POS, } from '../measurementGeometry.js';
4
+ import { FormulaColors } from '../../../theme/colors.js';
5
+ // Screen-px grab radius for an existing plane's corner/endpoint handles while
6
+ // the tool is active (matches the drawn handle affordance scale).
7
+ const HANDLE_GRAB_PX = 18;
8
+ // Screen-px radius of the placed-corner dots drawn while marking a quad.
9
+ const VERTEX_DOT_PX = 6;
10
+ // Preview chrome color — matches PlaneElement's calibration purple.
11
+ const PLANE_COLOR = FormulaColors.purple;
12
+ let counter = 0;
13
+ const makeId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(counter++).toString(36)}`;
14
+ const firstLayerId = (doc) => doc.layers[0]?.id ?? DEFAULT_LAYER_ID;
15
+ const distSq = (a, b) => {
16
+ const dx = a.x - b.x;
17
+ const dy = a.y - b.y;
18
+ return dx * dx + dy * dy;
19
+ };
20
+ // The document's (single, v1) committed calibration this tool can edit.
21
+ const committedPlane = (doc) => doc.planes?.[0] ?? null;
22
+ // The grabbable points of a committed plane: quad corners, or the reference
23
+ // line's endpoints.
24
+ const planeHandles = (plane) => plane.mode === 'perspective'
25
+ ? plane.corners
26
+ : [plane.refLine.a, plane.refLine.b];
27
+ // The committed plane with one handle moved to `p`.
28
+ const withHandleMoved = (plane, index, p) => {
29
+ if (plane.mode === 'perspective') {
30
+ const corners = plane.corners.map((c, i) => (i === index ? p : c));
31
+ return { ...plane, corners };
32
+ }
33
+ return {
34
+ ...plane,
35
+ refLine: index === 0 ? { ...plane.refLine, a: p } : { ...plane.refLine, b: p },
36
+ };
37
+ };
38
+ // The dimension slots a mode carries a tile for.
39
+ const edgesForMode = (mode) => mode === 'perspective' ? ['top', 'right', 'bottom', 'left'] : ['ref'];
40
+ // A fresh dimension tile magnetized to one plane edge (see the planeEdge doc
41
+ // on PlacedMeasurementRef).
42
+ const buildDimensionTile = (doc, plane, edge) => {
43
+ const line = planeEdgeLine(plane, edge);
44
+ if (!line)
45
+ return null;
46
+ return {
47
+ id: makeId('plane-dim'),
48
+ layerId: firstLayerId(doc),
49
+ placement: 'line',
50
+ line,
51
+ linePos: DEFAULT_LINE_POS,
52
+ anchor: recomputeAnchor(line, 'line', DEFAULT_LINE_POS, line.a),
53
+ planeId: plane.id,
54
+ planeEdge: edge,
55
+ // The tile's line IS the plane edge PlaneElement already draws — style it
56
+ // as the same cyan stroke so it disappears into the outline instead of
57
+ // painting the default measurement blue on top of it.
58
+ lineColor: PLANE_COLOR,
59
+ lineWidth: 4,
60
+ showLabel: true,
61
+ showValue: true,
62
+ createdAt: Date.now(),
63
+ };
64
+ };
65
+ // Ops that pin every dimension tile of `plane` back onto its (just-moved)
66
+ // edge, preserving each tile's slid position along it. Emitted alongside the
67
+ // setPlane in the same patch, so a corner drag is one undo step.
68
+ const syncDimensionTileOps = (doc, plane) => {
69
+ const ops = [];
70
+ for (const m of doc.placedMeasurements) {
71
+ if (m.planeId !== plane.id || !m.planeEdge)
72
+ continue;
73
+ const line = planeEdgeLine(plane, m.planeEdge);
74
+ if (!line)
75
+ continue;
76
+ ops.push({
77
+ op: 'updateMeasurement',
78
+ id: m.id,
79
+ patch: {
80
+ line,
81
+ anchor: recomputeAnchor(line, 'line', linePosOf(m), m.anchor),
82
+ },
83
+ });
84
+ }
85
+ return ops;
86
+ };
87
+ // Ops that remove a calibration AND its dimension tiles — the replace flow
88
+ // when new geometry is marked while a plane exists (v1: one per canvas).
89
+ const removePlaneOps = (doc, plane) => [
90
+ ...doc.placedMeasurements
91
+ .filter((m) => m.planeId === plane.id)
92
+ .map((m) => ({ op: 'removeMeasurement', id: m.id })),
93
+ { op: 'removePlane', id: plane.id },
94
+ ];
95
+ // Plane-calibration tool. Two jobs, both Skia-free and JS-dispatched on native
96
+ // (like polygonTool/measurementTool):
97
+ // - MARK new geometry: scale mode rubber-bands a reference line
98
+ // (measurementTool's drag pattern); perspective mode places 4 corners by
99
+ // tapping (polygonTool's pattern). Completion COMMITS the plane at once —
100
+ // geometry only, dimensions blank — together with a dimension tile per
101
+ // edge, replacing any existing plane (and its tiles) in the same undoable
102
+ // patch. The user then enters real-world sizes through the tiles.
103
+ // - EDIT the committed plane: while active, grabbing a corner/endpoint drags
104
+ // it with a live preview (the grid and dimension tiles follow through the
105
+ // preview patch) and commits one setPlane (+tile sync) per drag — a single
106
+ // undo step. A drag that would make the quad non-convex snaps back on
107
+ // release.
108
+ // In-progress marks render through preview patches (line/polygon draft shapes
109
+ // + vertex dots), so no platform-specific preview plumbing is needed.
110
+ export const createPlaneTool = (options) => {
111
+ const mode = options.mode;
112
+ const minDragPx = options.minDragPx ?? 4;
113
+ // In-progress quad corners (span gestures — the canvas clears ToolState per
114
+ // pointer-up, so they live in the factory closure like polygonTool's).
115
+ let vertices = [];
116
+ let draftId = null;
117
+ // Preview of the in-progress mark: the polyline/line so far plus a dot per
118
+ // placed vertex. Preview-only shapes riding the preview patch — never
119
+ // committed.
120
+ const previewOps = (doc, zoom, rubber) => {
121
+ const pts = rubber ? [...vertices, rubber] : [...vertices];
122
+ const ops = [];
123
+ if (pts.length >= 2) {
124
+ ops.push({
125
+ op: 'addShape',
126
+ shape: {
127
+ id: `${draftId}-draft`,
128
+ layerId: firstLayerId(doc),
129
+ kind: 'polygon',
130
+ geometry: { points: pts, closed: pts.length >= 4 },
131
+ style: { stroke: PLANE_COLOR, strokeWidth: 6 / zoom, dash: true },
132
+ createdAt: 0,
133
+ },
134
+ });
135
+ }
136
+ pts.forEach((v, i) => {
137
+ const r = VERTEX_DOT_PX / zoom;
138
+ ops.push({
139
+ op: 'addShape',
140
+ shape: {
141
+ id: `${draftId}-v${i}`,
142
+ layerId: firstLayerId(doc),
143
+ kind: 'ellipse',
144
+ geometry: {
145
+ points: [
146
+ { x: v.x - r, y: v.y - r },
147
+ { x: v.x + r, y: v.y + r },
148
+ ],
149
+ },
150
+ style: { fill: PLANE_COLOR, stroke: '#FFFFFF', strokeWidth: r / 3 },
151
+ createdAt: 0,
152
+ },
153
+ });
154
+ });
155
+ return ops;
156
+ };
157
+ const reset = () => {
158
+ vertices = [];
159
+ draftId = null;
160
+ };
161
+ // Commit freshly marked geometry: the plane (dimensions blank until the
162
+ // user enters them) plus its dimension tiles, replacing any existing plane
163
+ // — one undoable patch.
164
+ const commitPlane = (ctx, geometry) => {
165
+ const doc = ctx.document;
166
+ const plane = geometry.mode === 'scale'
167
+ ? {
168
+ id: makeId('plane'),
169
+ mode: 'scale',
170
+ refLine: geometry.refLine,
171
+ createdAt: Date.now(),
172
+ }
173
+ : {
174
+ id: makeId('plane'),
175
+ mode: 'perspective',
176
+ corners: geometry.corners,
177
+ createdAt: Date.now(),
178
+ };
179
+ const existing = committedPlane(doc);
180
+ const ops = [
181
+ ...(existing ? removePlaneOps(doc, existing) : []),
182
+ { op: 'setPlane', plane },
183
+ ...edgesForMode(plane.mode).flatMap((edge) => {
184
+ const tile = buildDimensionTile(doc, plane, edge);
185
+ return tile
186
+ ? [{ op: 'addMeasurement', measurement: tile }]
187
+ : [];
188
+ }),
189
+ ];
190
+ reset();
191
+ ctx.preview({ ops: [] });
192
+ ctx.commit({ ops });
193
+ options.onPlaced?.(plane);
194
+ };
195
+ // Which committed-plane handle a press landed on, if any.
196
+ const hitHandle = (ctx, world) => {
197
+ const plane = committedPlane(ctx.document);
198
+ if (!plane)
199
+ return null;
200
+ const tol = HANDLE_GRAB_PX / ctx.viewport.state.zoom;
201
+ const tolSq = tol * tol;
202
+ const handles = planeHandles(plane);
203
+ for (let i = 0; i < handles.length; i++) {
204
+ if (distSq(world, handles[i]) <= tolSq)
205
+ return i;
206
+ }
207
+ return null;
208
+ };
209
+ return {
210
+ id: options.id ?? `plane-${mode}`,
211
+ label: options.label ??
212
+ (mode === 'perspective' ? 'Perspective plane' : 'Scale reference'),
213
+ cursor: 'crosshair',
214
+ planeEditing: true,
215
+ onPointerDown(event, ctx) {
216
+ // Editing an existing plane wins over starting new geometry — but only
217
+ // when nothing is mid-mark (a quad in progress keeps collecting taps).
218
+ if (vertices.length === 0) {
219
+ const handleIndex = hitHandle(ctx, event.world);
220
+ if (handleIndex != null) {
221
+ return { kind: 'plane-handle-drag', handleIndex, moved: false };
222
+ }
223
+ }
224
+ if (mode === 'scale') {
225
+ return {
226
+ kind: 'plane-scale-drawing',
227
+ startWorld: event.world,
228
+ startScreen: event.screen,
229
+ moved: false,
230
+ };
231
+ }
232
+ return { kind: 'plane-quad-pointing' };
233
+ },
234
+ onPointerMove(event, ctx, state) {
235
+ const s = state;
236
+ if (!s)
237
+ return s;
238
+ if (s.kind === 'plane-handle-drag') {
239
+ const plane = committedPlane(ctx.document);
240
+ if (!plane)
241
+ return s;
242
+ const moved = withHandleMoved(plane, s.handleIndex, event.world);
243
+ // Dimension tiles ride along in the same preview so the whole
244
+ // calibration — outline, grid, tiles — tracks the finger.
245
+ ctx.preview({
246
+ ops: [
247
+ { op: 'setPlane', plane: moved },
248
+ ...syncDimensionTileOps(ctx.document, moved),
249
+ ],
250
+ });
251
+ return { ...s, moved: true };
252
+ }
253
+ // New geometry can't be marked while a calibration exists (delete it
254
+ // first) — suppress the rubber-band so nothing appears to happen and
255
+ // the pointer-up path explains why.
256
+ if (committedPlane(ctx.document))
257
+ return s;
258
+ if (s.kind === 'plane-scale-drawing') {
259
+ if (!draftId)
260
+ draftId = makeId('plane-draft');
261
+ vertices = [s.startWorld];
262
+ ctx.preview({
263
+ ops: previewOps(ctx.document, ctx.viewport.state.zoom, event.world),
264
+ });
265
+ vertices = [];
266
+ return { ...s, moved: true };
267
+ }
268
+ // Quad marking: rubber the next edge from the placed corners.
269
+ if (vertices.length > 0) {
270
+ ctx.preview({
271
+ ops: previewOps(ctx.document, ctx.viewport.state.zoom, event.world),
272
+ });
273
+ }
274
+ return s;
275
+ },
276
+ onPointerUp(event, ctx, state) {
277
+ const s = state;
278
+ if (!s)
279
+ return;
280
+ if (s.kind === 'plane-handle-drag') {
281
+ ctx.preview({ ops: [] });
282
+ if (!s.moved)
283
+ return;
284
+ const plane = committedPlane(ctx.document);
285
+ if (!plane)
286
+ return;
287
+ const next = withHandleMoved(plane, s.handleIndex, event.world);
288
+ // A corner drag that breaks the quad snaps back instead of committing
289
+ // a plane the homography can't invert.
290
+ if (next.mode === 'perspective' &&
291
+ !isQuadConvexNonDegenerate(next.corners)) {
292
+ options.onInvalidQuad?.();
293
+ return;
294
+ }
295
+ ctx.commit({
296
+ ops: [
297
+ { op: 'setPlane', plane: next },
298
+ ...syncDimensionTileOps(ctx.document, next),
299
+ ],
300
+ });
301
+ return;
302
+ }
303
+ // Creation is blocked while a calibration exists (only its handles are
304
+ // editable) — the user deletes it first to re-mark.
305
+ if (committedPlane(ctx.document)) {
306
+ options.onExistingPlane?.();
307
+ return;
308
+ }
309
+ if (s.kind === 'plane-scale-drawing') {
310
+ ctx.preview({ ops: [] });
311
+ const dx = event.screen.x - s.startScreen.x;
312
+ const dy = event.screen.y - s.startScreen.y;
313
+ if (!s.moved || dx * dx + dy * dy < minDragPx * minDragPx)
314
+ return;
315
+ commitPlane(ctx, {
316
+ mode: 'scale',
317
+ refLine: { a: s.startWorld, b: event.world },
318
+ });
319
+ return;
320
+ }
321
+ // Quad marking: each tap places a corner; the 4th completes.
322
+ if (!draftId)
323
+ draftId = makeId('plane-draft');
324
+ vertices = [...vertices, { x: event.world.x, y: event.world.y }];
325
+ if (vertices.length < 4) {
326
+ ctx.preview({
327
+ ops: previewOps(ctx.document, ctx.viewport.state.zoom),
328
+ });
329
+ return;
330
+ }
331
+ const corners = normalizeQuadOrder(vertices);
332
+ if (!corners) {
333
+ // Drop the offending 4th corner; the first three stay marked.
334
+ vertices = vertices.slice(0, 3);
335
+ ctx.preview({
336
+ ops: previewOps(ctx.document, ctx.viewport.state.zoom),
337
+ });
338
+ options.onInvalidQuad?.();
339
+ return;
340
+ }
341
+ commitPlane(ctx, { mode: 'perspective', corners });
342
+ },
343
+ // Gesture interrupted (a second finger landed and the viewport pan took
344
+ // over): keep the placed corners and re-establish their preview — the
345
+ // canvas clears the preview patch before calling this.
346
+ onCancel(_state, ctx) {
347
+ if (vertices.length === 0)
348
+ return;
349
+ ctx.preview({ ops: previewOps(ctx.document, ctx.viewport.state.zoom) });
350
+ },
351
+ // Switching tools mid-mark discards the unfinished geometry — unlike a
352
+ // polygon, a partial calibration has no committable meaning.
353
+ onDeactivate() {
354
+ reset();
355
+ },
356
+ };
357
+ };
@@ -209,6 +209,11 @@ const classifyGrab = (doc, id, world, zoom, viewportTileScale = 1, tileScaleFact
209
209
  const halfH = (dims.height / 2 + HIT_PADDING) / zoom;
210
210
  const onTile = Math.abs(world.x - m.anchor.x) <= halfW &&
211
211
  Math.abs(world.y - m.anchor.y) <= halfH;
212
+ // A plane-dimension tile is locked to its calibration edge: EVERY grab
213
+ // slides it along the edge line — a group move would pull it off the plane
214
+ // geometry it annotates (the plane tool owns moving the geometry itself).
215
+ if (m.planeEdge && placementOf(m) === 'line' && m.line)
216
+ return 'slide';
212
217
  return onTile && placementOf(m) === 'line' && m.line ? 'slide' : 'move';
213
218
  };
214
219
  // Slide the tile along its line by a world-space drag delta (relative to
@@ -238,6 +243,10 @@ const findHandleHit = (doc, id, world, zoom) => {
238
243
  const m = doc.placedMeasurements.find((x) => x.id === id);
239
244
  if (!m || !m.line || placementOf(m) !== 'line')
240
245
  return null;
246
+ // A plane-dimension tile's line IS the calibration edge — no endpoint
247
+ // editing here; the plane tool drags the plane's corners instead.
248
+ if (m.planeEdge)
249
+ return null;
241
250
  const r2 = (HANDLE_GRAB_PX / zoom) ** 2;
242
251
  const da = (world.x - m.line.a.x) ** 2 + (world.y - m.line.a.y) ** 2;
243
252
  const db = (world.x - m.line.b.x) ** 2 + (world.y - m.line.b.y) ** 2;
@@ -1,5 +1,5 @@
1
1
  import type { Measurement } from '../../types/firestore.js';
2
- import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type Selection, type TileScalePlatform, type Vec2 } from '../../types/annotation.js';
2
+ import { type AnnotationCanvasState, type AnnotationDocumentPatch, type AnnotationElementId, type AnnotationStroke, type MeasurementPlacement, type PlaneCalibration, type Selection, type TileScalePlatform, type Vec2 } from '../../types/annotation.js';
3
3
  import type { FileRef, MeasurementRef } from './measurementPicker.js';
4
4
  import type { CanvasPointerEvent, RequestTextInput, Tool, ToolContext, ToolState } from './Tool.js';
5
5
  import { type ViewportState } from './viewport.js';
@@ -51,6 +51,8 @@ export interface AnnotationCanvasHandle {
51
51
  placeFileAtCenter(ref: FileRef): AnnotationElementId;
52
52
  setGroupHeaderCollapsed(id: AnnotationElementId, collapsed: boolean): void;
53
53
  deleteSelected(): void;
54
+ setPlane(plane: PlaneCalibration): void;
55
+ removePlane(id: string): void;
54
56
  }
55
57
  export interface UseAnnotationCanvasStateProps {
56
58
  canvas: AnnotationCanvasState;
@@ -788,6 +788,23 @@ export const useAnnotationCanvasState = (props) => {
788
788
  c.commit({ ops });
789
789
  c.setSelection(null);
790
790
  },
791
+ setPlane(plane) {
792
+ ctxRef.current.commit({ ops: [{ op: 'setPlane', plane }] });
793
+ },
794
+ removePlane(id) {
795
+ // The calibration's dimension tiles (planeId) go with it — orphaned
796
+ // entry tiles would keep rendering with nothing to write to. One
797
+ // undoable patch.
798
+ const c = ctxRef.current;
799
+ c.commit({
800
+ ops: [
801
+ ...c.document.placedMeasurements
802
+ .filter((m) => m.planeId === id)
803
+ .map((m) => ({ op: 'removeMeasurement', id: m.id })),
804
+ { op: 'removePlane', id },
805
+ ],
806
+ });
807
+ },
791
808
  };
792
809
  return () => {
793
810
  if (imperativeRef)
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
+ export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
+ export interface CanvasKitOpts {
5
+ locateFile?: (file: string) => string;
6
+ }
7
+ export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
+ fallback?: ReactNode;
9
+ canvasKitOpts?: CanvasKitOpts;
10
+ };
11
+ export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
+ // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
+ // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
+ // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
+ // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
+ // and skips WithSkiaWeb entirely (no wasm to load).
8
+ export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
+ default: m.AnnotationCanvasInner,
10
+ })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -0,0 +1,8 @@
1
+ import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
+ export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
+ export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
+ export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
+ fallback?: unknown;
6
+ canvasKitOpts?: unknown;
7
+ };
8
+ export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
+ export const AnnotationCanvas = (props) => {
4
+ const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
+ return _jsx(AnnotationCanvasInner, { ...rest });
6
+ };
@@ -0,0 +1,39 @@
1
+ import { type CSSProperties, type MutableRefObject } from 'react';
2
+ import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
3
+ import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
4
+ import type { MeasurementRef } from './measurementPicker.js';
5
+ import type { Tool } from './Tool.js';
6
+ import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
7
+ import { type ViewportState } from './viewport.js';
8
+ import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
9
+ export type { AnnotationCanvasHandle };
10
+ export interface AnnotationCanvasInnerProps {
11
+ canvas: AnnotationCanvasState;
12
+ onCommit(patch: AnnotationDocumentPatch): void;
13
+ tools: Tool[];
14
+ activeToolId: string;
15
+ selection: Selection | null;
16
+ onSelectionChange(selection: Selection | null): void;
17
+ measurements?: Measurement[];
18
+ fallbackUnit?: Units;
19
+ fractionalTolerance?: FractionalTolerance;
20
+ decimalTolerance?: DecimalTolerance;
21
+ resolveImageUrl?: (storagePath: string) => Promise<string>;
22
+ pickMeasurement?: () => Promise<MeasurementRef | null>;
23
+ renderMeasurementStamp?: RenderMeasurementStamp;
24
+ stampFontSource?: unknown;
25
+ stampValueFontSize?: number;
26
+ stampLabelFontSize?: number;
27
+ gestures?: GestureConfig;
28
+ width: number;
29
+ height: number;
30
+ initialViewport?: ViewportState;
31
+ style?: CSSProperties;
32
+ imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
33
+ }
34
+ export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
35
+ export interface GestureConfig {
36
+ wheel?: 'zoom' | 'pan' | 'auto';
37
+ panTriggers?: PanTrigger[];
38
+ }
39
+ export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;