@reekon-tools/boldr-utils 1.15.1 → 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.
- package/dist/annotation/canvas/AnnotationCanvasInner.js +5 -2
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +10 -3
- package/dist/annotation/canvas/AnnotationCanvasSkia.d.ts +2 -1
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +17 -3
- package/dist/annotation/canvas/Tool.d.ts +1 -0
- package/dist/annotation/canvas/elements/PlaneElement.d.ts +18 -0
- package/dist/annotation/canvas/elements/PlaneElement.js +115 -0
- package/dist/annotation/canvas/planeGeometry.d.ts +48 -0
- package/dist/annotation/canvas/planeGeometry.js +507 -0
- package/dist/annotation/canvas/tools/measurementTool.d.ts +1 -0
- package/dist/annotation/canvas/tools/measurementTool.js +13 -0
- package/dist/annotation/canvas/tools/planeTool.d.ts +12 -0
- package/dist/annotation/canvas/tools/planeTool.js +357 -0
- package/dist/annotation/canvas/tools/selectTool.js +9 -0
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +3 -1
- package/dist/annotation/canvas/useAnnotationCanvasState.js +17 -0
- package/dist/canvas/AnnotationCanvas.d.ts +11 -0
- package/dist/canvas/AnnotationCanvas.js +10 -0
- package/dist/canvas/AnnotationCanvas.native.d.ts +8 -0
- package/dist/canvas/AnnotationCanvas.native.js +6 -0
- package/dist/canvas/AnnotationCanvasInner.d.ts +39 -0
- package/dist/canvas/AnnotationCanvasInner.js +219 -0
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +35 -0
- package/dist/canvas/AnnotationCanvasInner.native.js +138 -0
- package/dist/canvas/AnnotationCanvasSkia.d.ts +27 -0
- package/dist/canvas/AnnotationCanvasSkia.js +20 -0
- package/dist/canvas/Tool.d.ts +38 -0
- package/dist/canvas/Tool.js +1 -0
- package/dist/canvas/elements/BackgroundImageElement.d.ts +9 -0
- package/dist/canvas/elements/BackgroundImageElement.js +37 -0
- package/dist/canvas/elements/MeasurementStampElement.d.ts +13 -0
- package/dist/canvas/elements/MeasurementStampElement.js +30 -0
- package/dist/canvas/elements/ShapeElement.d.ts +7 -0
- package/dist/canvas/elements/ShapeElement.js +62 -0
- package/dist/canvas/elements/StrokeElement.d.ts +7 -0
- package/dist/canvas/elements/StrokeElement.js +18 -0
- package/dist/canvas/measurementPicker.d.ts +10 -0
- package/dist/canvas/measurementPicker.js +1 -0
- package/dist/canvas/measurementStampOverlay.d.ts +11 -0
- package/dist/canvas/measurementStampOverlay.js +1 -0
- package/dist/canvas/pointerAdapter.d.ts +3 -0
- package/dist/canvas/pointerAdapter.js +19 -0
- package/dist/canvas/stampLayout.d.ts +5 -0
- package/dist/canvas/stampLayout.js +14 -0
- package/dist/canvas/tools/measurementStampTool.d.ts +9 -0
- package/dist/canvas/tools/measurementStampTool.js +37 -0
- package/dist/canvas/tools/panTool.d.ts +5 -0
- package/dist/canvas/tools/panTool.js +25 -0
- package/dist/canvas/tools/penTool.d.ts +13 -0
- package/dist/canvas/tools/penTool.js +68 -0
- package/dist/canvas/tools/selectTool.d.ts +2 -0
- package/dist/canvas/tools/selectTool.js +182 -0
- package/dist/canvas/useAnnotationCanvasState.d.ts +54 -0
- package/dist/canvas/useAnnotationCanvasState.js +210 -0
- package/dist/canvas/viewport.d.ts +16 -0
- package/dist/canvas/viewport.js +54 -0
- package/dist/data/AnnotationDataContext.d.ts +8 -0
- package/dist/data/AnnotationDataContext.js +11 -0
- package/dist/data/AnnotationDataProvider.d.ts +65 -0
- package/dist/data/AnnotationDataProvider.js +4 -0
- package/dist/data/InMemoryAnnotationProvider.d.ts +30 -0
- package/dist/data/InMemoryAnnotationProvider.js +197 -0
- package/dist/data/canvasPersistence.d.ts +3 -0
- package/dist/data/canvasPersistence.js +26 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +33 -0
- package/dist/data/hooks/useAnnotationCanvasDoc.js +314 -0
- package/dist/data/hooks/useAnnotationDoc.d.ts +7 -0
- package/dist/data/hooks/useAnnotationDoc.js +33 -0
- package/dist/data/hooks/useAnnotationList.d.ts +7 -0
- package/dist/data/hooks/useAnnotationList.js +26 -0
- package/dist/data/hooks/useAnnotationMutations.d.ts +9 -0
- package/dist/data/hooks/useAnnotationMutations.js +11 -0
- package/dist/exports.d.ts +2 -0
- package/dist/exports.js +2 -0
- package/dist/hooks/useParseMeasurement.d.ts +4 -0
- package/dist/hooks/useParseMeasurement.js +14 -0
- package/dist/types/annotation.d.ts +37 -0
- package/dist/types/annotation.js +30 -0
- package/dist/utils/evaluateFormula.d.ts +20 -0
- package/dist/utils/evaluateFormula.js +31 -0
- package/package.json +1 -1
|
@@ -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;
|