@toolpath/viewer 1.4.0 → 2.0.0

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/index.js CHANGED
@@ -1,15 +1,20 @@
1
1
  import {
2
+ ANGLE_ARC_FRACTION,
3
+ ANGLE_ARC_SEGMENTS,
2
4
  AXES_PLANE_OFFSET,
3
5
  AXES_PLANE_SCALE,
6
+ AXIS_COLORS,
4
7
  CAD_CAMERA_UP,
5
8
  CANDIDATE_WEIGHT,
6
9
  CAP_HATCH,
7
10
  CHAMFER,
8
11
  CONE_AXIS,
12
+ CONTROL_SCHEME_OPTIONS,
9
13
  CadCameraControls,
10
14
  DEFAULT_FIT_MARGIN,
11
15
  DEFAULT_SECTION_NORMAL,
12
16
  DEFAULT_THEME,
17
+ DELTA_DASH_PIXELS,
13
18
  DIRECTION_COLORS,
14
19
  DISABLED_SECTION,
15
20
  DOUBLE_TAP_MS,
@@ -24,6 +29,9 @@ import {
24
29
  HIGHLIGHT_WEIGHT,
25
30
  HOVER_WEIGHT,
26
31
  MAX_FRAME_RATIO,
32
+ MEASURE_LINE_PIXELS,
33
+ MEASURE_MARKER_PIXELS,
34
+ MEASURE_RENDER_ORDER,
27
35
  MIN_FRAME_RATIO,
28
36
  NO_MODIFIERS,
29
37
  ORBIT_TARGET_COLOR,
@@ -37,12 +45,17 @@ import {
37
45
  OUTLINE_SCALE,
38
46
  PERSPECTIVE_FOV,
39
47
  PICKED_SURFACE_LABEL,
48
+ POINTS_PER_MEASUREMENT,
40
49
  PREVIEW_SCALE,
41
50
  PartMesh,
42
51
  PartReportFormatError,
43
52
  REGION_ATTRIBUTE,
53
+ SECTION_GIZMO_FRAME_SCALE,
44
54
  SECTION_RENDER_ORDER,
45
55
  SHAFT_RADIUS,
56
+ SNAP_MARKER_PIXELS,
57
+ SNAP_PIXELS,
58
+ STOCK_OBJECT,
46
59
  SectionView,
47
60
  TAP_SLOP,
48
61
  UnsupportedKernelVersionError,
@@ -50,6 +63,9 @@ import {
50
63
  VIEW_SIGNS,
51
64
  Viewer,
52
65
  adaptedUp,
66
+ angleArc,
67
+ angleArcRadius,
68
+ angleAt,
53
69
  applyCapTheme,
54
70
  applyHighlightLayers,
55
71
  applyProjection,
@@ -76,19 +92,30 @@ import {
76
92
  currentViewDirection,
77
93
  cycleOwner,
78
94
  defaultBounds,
95
+ deltaBetween,
96
+ deltaLegs,
79
97
  directionColor,
80
98
  directionIndexOf,
81
99
  directionLabel,
100
+ distanceBetween,
82
101
  dragPlane,
83
102
  excludedFromFrame,
84
103
  featureTypeRank,
85
104
  fitDistance,
86
105
  focusForPick,
106
+ formatDegrees,
107
+ formatMillimetres,
87
108
  groupByDirection,
88
109
  hitUnderRay,
89
110
  labelGeometry,
90
111
  labelTexture,
112
+ lockToAxis,
113
+ measurementFromPoints,
114
+ measurementLabel,
115
+ measurementLabelAnchor,
116
+ midpoint,
91
117
  movedFar,
118
+ nextMeasurementId,
92
119
  normalizePartReport,
93
120
  orbitTargetOpacity,
94
121
  orthographicHalfHeight,
@@ -103,6 +130,7 @@ import {
103
130
  retargetPose,
104
131
  sameDirection,
105
132
  screenLength,
133
+ screenPoint,
106
134
  sectionAnchor,
107
135
  sectionBounds,
108
136
  sectionConstant,
@@ -110,10 +138,12 @@ import {
110
138
  sectionDepthConstant,
111
139
  sectionDepthRange,
112
140
  sectionFromPick,
141
+ sectionMeasurement,
113
142
  sectionOffset,
114
143
  sectionOptionsFromState,
115
144
  sectionPlane,
116
145
  smoothRegionNormals,
146
+ snapAt,
117
147
  squaredUp,
118
148
  startPosition,
119
149
  surfaceUnderRay,
@@ -131,21 +161,760 @@ import {
131
161
  viewUp,
132
162
  viewVector,
133
163
  visualSurfaces
134
- } from "./chunk-HH3W5T42.js";
164
+ } from "./chunk-4TBLMR4Z.js";
165
+
166
+ // src/hover-card.tsx
167
+ import { useEffect, useState } from "react";
168
+ import { jsx } from "react/jsx-runtime";
169
+ var HoverCard = ({ pick, children, className: className2, offset = 16 }) => {
170
+ const [pointer, setPointer] = useState(pick?.pointer);
171
+ useEffect(() => {
172
+ setPointer(pick?.pointer);
173
+ if (pick === null) return;
174
+ const follow = (event) => setPointer({ clientX: event.clientX, clientY: event.clientY });
175
+ window.addEventListener("pointermove", follow);
176
+ return () => window.removeEventListener("pointermove", follow);
177
+ }, [pick]);
178
+ if (pick === null || pointer === void 0) return null;
179
+ const style = {
180
+ position: "fixed",
181
+ left: pointer.clientX + offset,
182
+ top: pointer.clientY + offset,
183
+ pointerEvents: "none",
184
+ zIndex: 4
185
+ };
186
+ return /* @__PURE__ */ jsx(
187
+ "div",
188
+ {
189
+ className: ["viewer-hover-card", className2].filter(Boolean).join(" "),
190
+ "data-viewer-hover-card": "true",
191
+ role: "tooltip",
192
+ style,
193
+ children: children(pick)
194
+ }
195
+ );
196
+ };
197
+
198
+ // src/viewer-toolbar.tsx
199
+ import { createContext, useContext } from "react";
200
+
201
+ // src/toolbar-icons.tsx
202
+ import { jsx as jsx2 } from "react/jsx-runtime";
203
+ var SvgIcon = ({ viewBox = "0 0 24 24", fill = "none", children }) => /* @__PURE__ */ jsx2(
204
+ "svg",
205
+ {
206
+ className: "viewer-toolbar-icon",
207
+ "data-viewer-toolbar-icon": "true",
208
+ viewBox,
209
+ fill,
210
+ stroke: fill === "none" ? "currentColor" : "none",
211
+ strokeWidth: "1.5",
212
+ strokeLinecap: "round",
213
+ strokeLinejoin: "round",
214
+ "aria-hidden": "true",
215
+ children
216
+ }
217
+ );
218
+ var FitIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5M8 8h8v8H8z" }) });
219
+ var ResetIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 10a9 9 0 1 1 2 8M3 4v6h6" }) });
220
+ var TopIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 7l8-4 8 4-8 4zM4 7v10l8 4 8-4V7M12 11v10" }) });
221
+ var StockIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 6l9-4 9 4v12l-9 4-9-4zM3 6l9 4 9-4M12 10v12M7 8v8l5 2 5-2V8" }) });
222
+ var AxesIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M5 19V3m0 16h16M5 19l10-10M2 6l3-3 3 3m10 10 3 3-3 3M11 9h4v4" }) });
223
+ var GridIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 8l9-5 9 5-9 5zM3 8v8l9 5 9-5V8M12 13v8M7.5 5.5v8M16.5 5.5v8" }) });
224
+ var BananaIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { viewBox: "0 0 576 512", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M284.2 245.6c12.99 6.929 25.35 15.14 36.08 25.8L334.5 285.6l65.75-23.47c14.75-5.265 30.18-7.849 45.81-8.389c1.154-10.73 1.764-21.38 1.764-31.87c0-118.5-81.33-221.9-119.7-221.9c-21.01 0-40.91 17.04-40.91 39.25c0 16.18 16.74 41.9 16.74 103C303.1 170.1 300.6 203.1 284.2 245.6zM575.1 389.6c0-3.687-.8637-7.429-2.687-10.93l-15.12-29.11c-21.05-40.53-63.08-64.51-106.9-64.51c-13.43 0-27.02 2.252-40.22 6.969l-84.84 30.27l-28.59-28.41C274.4 270.9 243.7 258.1 212.8 258.1c-23.72 0-47.57 6.97-68.29 21.2L106.3 306.4c-6.732 4.631-10.35 12.07-10.35 19.63c0 14.93 12.7 23.87 24.04 23.87c4.695 0 9.443-1.376 13.61-4.26l38.13-26.23c12.43-8.525 26.71-12.69 40.91-12.69c10.64 0 21.24 2.339 30.97 6.934c-50.62 62.23-128.3 99.85-211.4 99.85C14.42 413.5 0 427.8 0 445.5v31.38c0 17.68 14.66 32.02 32.46 32.02l28.98.0009c14.15 0 34.69 1.098 59.07 1.098c93.51 0 243.4-16.15 304.8-172.4c9.021-3.22 17.95-4.712 26.53-4.712c27.65 0 51.7 15.49 63.7 38.55l15.12 29.11c3.484 6.723 11.74 12.98 21.41 12.98C564.1 413.6 575.1 403.8 575.1 389.6z" }) });
225
+ var DirectionsIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M7 3v18m-4-4 4 4 4-4M17 21V3m-4 4 4-4 4 4" }) });
226
+ var HoverIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v12H9l-5 4zm4 5h8m-8 3h5" }) });
227
+ var FocusIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v16H4zM8 8h8v8H8zM2 12h4m12 0h4M12 2v4m0 12v4" }) });
228
+ var WireframeIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 7l8-4 8 4v10l-8 4-8-4zM4 7l8 4 8-4M12 11v10M4 17l8-4 8 4M12 3v10" }) });
229
+ var SectionIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 6h9v12H3zM12 3v18M16 6h2m3 3v2m0 3v2m-5 3h-2" }) });
230
+ var MeasureIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M3 16L16 3l5 5L8 21zM8 11l3 3m1-7 3 3m-11 5 3 3" }) });
231
+ var ICONS = {
232
+ fit: FitIcon,
233
+ reset: ResetIcon,
234
+ top: TopIcon,
235
+ stock: StockIcon,
236
+ axes: AxesIcon,
237
+ grid: GridIcon,
238
+ banana: BananaIcon,
239
+ directions: DirectionsIcon,
240
+ hover: HoverIcon,
241
+ focus: FocusIcon,
242
+ wireframe: WireframeIcon,
243
+ section: SectionIcon,
244
+ measure: MeasureIcon
245
+ };
246
+ var ToolbarIcon = ({ name }) => {
247
+ const Icon = ICONS[name];
248
+ return /* @__PURE__ */ jsx2(Icon, {});
249
+ };
250
+
251
+ // src/viewer-toolbar.tsx
252
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
253
+ var ViewerToolbarContext = createContext(null);
254
+ var ViewerToolbarProvider = ({
255
+ controls,
256
+ children
257
+ }) => /* @__PURE__ */ jsx3(ViewerToolbarContext.Provider, { value: controls, children });
258
+ var useViewerToolbar = () => {
259
+ const controls = useContext(ViewerToolbarContext);
260
+ if (!controls) throw new Error("useViewerToolbar must be used inside <ViewerToolbarProvider>");
261
+ return controls;
262
+ };
263
+ var buttonName = (action) => `${action.slice(0, 1).toUpperCase()}${action.slice(1)}Button`;
264
+ var ToolbarButton = ({ action, icon, label, toggle = true }) => {
265
+ const control = useViewerToolbar()[action];
266
+ if (!control) {
267
+ throw new Error(`<ViewerToolbar.${buttonName(action)}> requires a '${action}' toolbar control`);
268
+ }
269
+ const pressed = control.pressed ?? false;
270
+ return /* @__PURE__ */ jsxs(
271
+ "button",
272
+ {
273
+ className: "viewer-toolbar-button",
274
+ "data-viewer-toolbar-action": action,
275
+ type: "button",
276
+ "aria-label": label(pressed),
277
+ "aria-pressed": toggle ? pressed : void 0,
278
+ onClick: control.onClick,
279
+ children: [
280
+ /* @__PURE__ */ jsx3(ToolbarIcon, { name: icon }),
281
+ /* @__PURE__ */ jsx3("span", { className: "viewer-toolbar-tooltip", "data-viewer-toolbar-tooltip": "true", role: "tooltip", children: label(pressed) })
282
+ ]
283
+ }
284
+ );
285
+ };
286
+ var StockButton = () => /* @__PURE__ */ jsx3(
287
+ ToolbarButton,
288
+ {
289
+ action: "stock",
290
+ icon: "stock",
291
+ label: (pressed) => pressed ? "Hide stock" : "Show stock"
292
+ }
293
+ );
294
+ var AxesButton = () => /* @__PURE__ */ jsx3(
295
+ ToolbarButton,
296
+ {
297
+ action: "axes",
298
+ icon: "axes",
299
+ label: (pressed) => pressed ? "Hide axis" : "Show axis"
300
+ }
301
+ );
302
+ var GridButton = () => /* @__PURE__ */ jsx3(
303
+ ToolbarButton,
304
+ {
305
+ action: "grid",
306
+ icon: "grid",
307
+ label: (pressed) => pressed ? "Hide grid" : "Show grid"
308
+ }
309
+ );
310
+ var BananaButton = () => /* @__PURE__ */ jsx3(
311
+ ToolbarButton,
312
+ {
313
+ action: "banana",
314
+ icon: "banana",
315
+ label: (pressed) => pressed ? "Banana for scale (on)" : "Banana for scale"
316
+ }
317
+ );
318
+ var DirectionsButton = () => /* @__PURE__ */ jsx3(
319
+ ToolbarButton,
320
+ {
321
+ action: "directions",
322
+ icon: "directions",
323
+ label: () => "Highlight faces by direction"
324
+ }
325
+ );
326
+ var HoverButton = () => /* @__PURE__ */ jsx3(
327
+ ToolbarButton,
328
+ {
329
+ action: "hover",
330
+ icon: "hover",
331
+ label: (pressed) => pressed ? "Disable feature hover" : "Enable feature hover"
332
+ }
333
+ );
334
+ var FocusButton = () => /* @__PURE__ */ jsx3(
335
+ ToolbarButton,
336
+ {
337
+ action: "focus",
338
+ icon: "focus",
339
+ label: (pressed) => pressed ? "Show full part" : "Focus selection"
340
+ }
341
+ );
342
+ var WireframeButton = () => /* @__PURE__ */ jsx3(ToolbarButton, { action: "wireframe", icon: "wireframe", label: () => "Wireframe" });
343
+ var SectionButton = () => /* @__PURE__ */ jsx3(
344
+ ToolbarButton,
345
+ {
346
+ action: "section",
347
+ icon: "section",
348
+ label: (pressed) => pressed ? "Exit section" : "Section"
349
+ }
350
+ );
351
+ var MeasureButton = () => /* @__PURE__ */ jsx3(
352
+ ToolbarButton,
353
+ {
354
+ action: "measure",
355
+ icon: "measure",
356
+ label: (pressed) => pressed ? "Exit measure" : "Measure"
357
+ }
358
+ );
359
+ var FitButton = () => /* @__PURE__ */ jsx3(ToolbarButton, { action: "fit", icon: "fit", label: () => "Fit", toggle: false });
360
+ var ResetButton = () => /* @__PURE__ */ jsx3(ToolbarButton, { action: "reset", icon: "reset", label: () => "Reset", toggle: false });
361
+ var TopButton = () => /* @__PURE__ */ jsx3(ToolbarButton, { action: "top", icon: "top", label: () => "Top view", toggle: false });
362
+ var Divider = () => /* @__PURE__ */ jsx3("span", { className: "viewer-toolbar-divider", "data-viewer-toolbar-divider": "true" });
363
+ var Controls = ({ children, className: className2 }) => /* @__PURE__ */ jsx3(
364
+ "div",
365
+ {
366
+ className: ["viewer-toolbar", className2].filter(Boolean).join(" "),
367
+ "data-viewer-toolbar-controls": "true",
368
+ role: "group",
369
+ "aria-label": "Viewer controls",
370
+ children
371
+ }
372
+ );
373
+ var DefaultControls = () => {
374
+ const controls = useViewerToolbar();
375
+ const display = [
376
+ controls.stock ? /* @__PURE__ */ jsx3(StockButton, {}, "stock") : null,
377
+ controls.axes ? /* @__PURE__ */ jsx3(AxesButton, {}, "axes") : null,
378
+ controls.grid ? /* @__PURE__ */ jsx3(GridButton, {}, "grid") : null,
379
+ controls.banana ? /* @__PURE__ */ jsx3(BananaButton, {}, "banana") : null
380
+ ].filter(Boolean);
381
+ const analysis = [
382
+ controls.directions ? /* @__PURE__ */ jsx3(DirectionsButton, {}, "directions") : null,
383
+ controls.hover ? /* @__PURE__ */ jsx3(HoverButton, {}, "hover") : null,
384
+ controls.focus ? /* @__PURE__ */ jsx3(FocusButton, {}, "focus") : null,
385
+ controls.wireframe ? /* @__PURE__ */ jsx3(WireframeButton, {}, "wireframe") : null,
386
+ controls.section ? /* @__PURE__ */ jsx3(SectionButton, {}, "section") : null,
387
+ controls.measure ? /* @__PURE__ */ jsx3(MeasureButton, {}, "measure") : null
388
+ ].filter(Boolean);
389
+ const camera = [
390
+ controls.fit ? /* @__PURE__ */ jsx3(FitButton, {}, "fit") : null,
391
+ controls.reset ? /* @__PURE__ */ jsx3(ResetButton, {}, "reset") : null,
392
+ controls.top ? /* @__PURE__ */ jsx3(TopButton, {}, "top") : null
393
+ ].filter(Boolean);
394
+ return /* @__PURE__ */ jsxs(Controls, { children: [
395
+ display,
396
+ display.length && analysis.length ? /* @__PURE__ */ jsx3(Divider, {}) : null,
397
+ analysis,
398
+ (display.length || analysis.length) && camera.length ? /* @__PURE__ */ jsx3(Divider, {}) : null,
399
+ camera
400
+ ] });
401
+ };
402
+ var ViewerToolbarRoot = ({ className: className2, children }) => {
403
+ useViewerToolbar();
404
+ return /* @__PURE__ */ jsx3(
405
+ "div",
406
+ {
407
+ className: ["viewer-toolbar-stack", className2].filter(Boolean).join(" "),
408
+ "data-viewer-toolbar": "true",
409
+ children: children ?? /* @__PURE__ */ jsx3(DefaultControls, {})
410
+ }
411
+ );
412
+ };
413
+ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
414
+ Controls,
415
+ Divider,
416
+ StockButton,
417
+ AxesButton,
418
+ GridButton,
419
+ BananaButton,
420
+ DirectionsButton,
421
+ HoverButton,
422
+ FocusButton,
423
+ WireframeButton,
424
+ SectionButton,
425
+ MeasureButton,
426
+ FitButton,
427
+ ResetButton,
428
+ TopButton
429
+ });
430
+
431
+ // src/stock.tsx
432
+ import { useThree } from "@react-three/fiber";
433
+ import { useEffect as useEffect2, useLayoutEffect, useMemo, useState as useState2 } from "react";
434
+
435
+ // src/render/stock-mesh.ts
436
+ import { Mesh } from "three";
437
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
438
+ async function parseStockGlb(data) {
439
+ const { scene } = await new GLTFLoader().parseAsync(data, "");
440
+ try {
441
+ return stockMeshGeometries(scene);
442
+ } finally {
443
+ scene.traverse((object) => {
444
+ if (!(object instanceof Mesh)) return;
445
+ object.geometry.dispose();
446
+ for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
447
+ material.dispose();
448
+ }
449
+ });
450
+ }
451
+ }
452
+ function stockMeshGeometries(scene) {
453
+ const geometries = [];
454
+ scene.updateWorldMatrix(true, true);
455
+ scene.traverse((object) => {
456
+ if (!(object instanceof Mesh)) return;
457
+ const geometry = object.geometry.clone().applyMatrix4(object.matrixWorld);
458
+ if (!geometry.hasAttribute("normal")) geometry.computeVertexNormals();
459
+ geometries.push(geometry);
460
+ });
461
+ if (geometries.length === 0) throw new Error("The stock GLB contains no mesh.");
462
+ return geometries;
463
+ }
464
+
465
+ // src/render/stock-mesh-buffer.ts
466
+ function createStockMeshBuffer() {
467
+ const owned = /* @__PURE__ */ new Set();
468
+ let disposed = false;
469
+ return {
470
+ request(decode, ready, failed) {
471
+ disposed = false;
472
+ let active = true;
473
+ void decode().then(
474
+ (geometries) => {
475
+ if (!active || disposed) {
476
+ geometries.forEach((geometry) => geometry.dispose());
477
+ return;
478
+ }
479
+ geometries.forEach((geometry) => owned.add(geometry));
480
+ ready(geometries);
481
+ },
482
+ (cause) => {
483
+ if (active && !disposed) {
484
+ failed(cause);
485
+ }
486
+ }
487
+ );
488
+ return () => {
489
+ active = false;
490
+ };
491
+ },
492
+ commit(geometries) {
493
+ const visible = new Set(geometries);
494
+ for (const geometry of owned) {
495
+ if (!visible.has(geometry)) {
496
+ owned.delete(geometry);
497
+ geometry.dispose();
498
+ }
499
+ }
500
+ },
501
+ dispose() {
502
+ disposed = true;
503
+ owned.forEach((geometry) => geometry.dispose());
504
+ owned.clear();
505
+ }
506
+ };
507
+ }
508
+
509
+ // src/render/stock.ts
510
+ import {
511
+ Box3,
512
+ BoxGeometry,
513
+ CylinderGeometry,
514
+ EdgesGeometry,
515
+ Group,
516
+ LineBasicMaterial,
517
+ LineSegments,
518
+ Matrix4,
519
+ Mesh as Mesh2,
520
+ MeshLambertMaterial,
521
+ Quaternion,
522
+ Vector3
523
+ } from "three";
524
+ function boxFigure(bounds) {
525
+ return {
526
+ lower: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
527
+ upper: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }
528
+ };
529
+ }
530
+ var CYLINDER_SEGMENTS = 64;
531
+ var CYLINDER_AXIS = new Vector3(0, 1, 0);
532
+ function fixedBoxStockBounds(geometry, dimensions, position = "model_centered", positionOffset = 0, offset = { x: 0, y: 0, z: 0 }) {
533
+ if (![dimensions.x, dimensions.y, dimensions.z].every(Number.isFinite) || [dimensions.x, dimensions.y, dimensions.z].some((value) => value <= 0)) {
534
+ throw new RangeError("Fixed stock dimensions must be finite and positive.");
535
+ }
536
+ if (!Number.isFinite(positionOffset)) {
537
+ throw new RangeError("Fixed stock position offset must be finite.");
538
+ }
539
+ if (![offset.x, offset.y, offset.z].every(Number.isFinite)) {
540
+ throw new RangeError("Fixed stock offset must be finite.");
541
+ }
542
+ const part = partBounds(geometry);
543
+ const center = part.getCenter(new Vector3());
544
+ const z = fixedStockLowerZ(part, dimensions.z, position, positionOffset);
545
+ return new Box3(
546
+ new Vector3(center.x - dimensions.x / 2, center.y - dimensions.y / 2, z),
547
+ new Vector3(center.x + dimensions.x / 2, center.y + dimensions.y / 2, z + dimensions.z)
548
+ ).translate(new Vector3(offset.x, offset.y, offset.z));
549
+ }
550
+ function fixedCylinderStock(geometry, size, position = "model_centered", positionOffset = 0) {
551
+ if (![size.diameter, size.length].every((value) => Number.isFinite(value) && value > 0)) {
552
+ throw new RangeError("Fixed stock diameter and length must be finite and positive.");
553
+ }
554
+ if (!Number.isFinite(positionOffset)) {
555
+ throw new RangeError("Fixed stock position offset must be finite.");
556
+ }
557
+ const part = partBounds(geometry);
558
+ const center = part.getCenter(new Vector3());
559
+ return {
560
+ origin: {
561
+ x: center.x,
562
+ y: center.y,
563
+ z: fixedStockLowerZ(part, size.length, position, positionOffset)
564
+ },
565
+ axis: { x: 0, y: 0, z: 1 },
566
+ diameter: size.diameter,
567
+ length: size.length
568
+ };
569
+ }
570
+ function fixedStockLowerZ(part, length, position, positionOffset) {
571
+ if (position === "offset_from_top") return part.max.z + positionOffset - length;
572
+ if (position === "offset_from_bottom") return part.min.z - positionOffset;
573
+ return (part.min.z + part.max.z - length) / 2;
574
+ }
575
+ function cylinderStockGeometry(input) {
576
+ const figure = input.partGeometry ? fixedCylinderStock(input.partGeometry, input, input.position, input.positionOffset) : input;
577
+ const { origin, diameter, length } = figure;
578
+ if (![origin.x, origin.y, origin.z].every(Number.isFinite)) {
579
+ throw new RangeError("Cylinder stock origin must be finite.");
580
+ }
581
+ if (![diameter, length].every((value) => Number.isFinite(value) && value > 0)) {
582
+ throw new RangeError("Cylinder stock diameter and length must be finite and positive.");
583
+ }
584
+ const axis = new Vector3(figure.axis.x, figure.axis.y, figure.axis.z);
585
+ const norm = axis.length();
586
+ if (!Number.isFinite(norm) || norm === 0) {
587
+ throw new RangeError("Cylinder stock axis must be finite and non-zero.");
588
+ }
589
+ axis.divideScalar(norm);
590
+ const center = axis.clone().multiplyScalar(length / 2).add(new Vector3(origin.x, origin.y, origin.z));
591
+ return new CylinderGeometry(diameter / 2, diameter / 2, length, CYLINDER_SEGMENTS).applyQuaternion(new Quaternion().setFromUnitVectors(CYLINDER_AXIS, axis)).translate(center.x, center.y, center.z);
592
+ }
593
+ function boxStockGeometry(input) {
594
+ const figure = input.partGeometry ? boxFigure(
595
+ input.dimensions ? fixedBoxStockBounds(
596
+ input.partGeometry,
597
+ input.dimensions,
598
+ input.position,
599
+ input.positionOffset,
600
+ input.offset
601
+ ) : boxStockBounds(input.partGeometry, input.allowance, input.offset)
602
+ ) : input;
603
+ return orientedBoxStockGeometry(figure);
604
+ }
605
+ function orientedBoxStockGeometry(figure) {
606
+ const { lower, upper } = figure;
607
+ const frame = figure.frame ?? {
608
+ location: { x: 0, y: 0, z: 0 },
609
+ axis: { x: 0, y: 0, z: 1 },
610
+ refDirection: { x: 1, y: 0, z: 0 }
611
+ };
612
+ const { location, axis, refDirection } = frame;
613
+ if (![location, axis, refDirection, lower, upper].every(
614
+ (vector) => [vector.x, vector.y, vector.z].every(Number.isFinite)
615
+ )) {
616
+ throw new RangeError("Box stock frame and corners must be finite.");
617
+ }
618
+ const size = new Vector3(upper.x - lower.x, upper.y - lower.y, upper.z - lower.z);
619
+ if (size.toArray().some((value) => !(value > 0))) {
620
+ throw new RangeError("Box stock upper corner must be above its lower corner on every axis.");
621
+ }
622
+ const z = new Vector3(axis.x, axis.y, axis.z).normalize();
623
+ const x = new Vector3(refDirection.x, refDirection.y, refDirection.z);
624
+ const reference = x.length();
625
+ x.addScaledVector(z, -x.dot(z));
626
+ if (!(z.lengthSq() > 0 && x.length() > 1e-9 * reference)) {
627
+ throw new RangeError(
628
+ "Box stock frame axis and reference direction must be non-zero and not parallel."
629
+ );
630
+ }
631
+ x.normalize();
632
+ const y = new Vector3().crossVectors(z, x);
633
+ return new BoxGeometry(size.x, size.y, size.z).translate((lower.x + upper.x) / 2, (lower.y + upper.y) / 2, (lower.z + upper.z) / 2).applyMatrix4(new Matrix4().makeBasis(x, y, z).setPosition(location.x, location.y, location.z));
634
+ }
635
+ function boxStockBounds(geometry, allowance = 0, offset = { x: 0, y: 0, z: 0 }) {
636
+ const padding = typeof allowance === "number" ? new Vector3(allowance, allowance, allowance) : "wall" in allowance ? new Vector3(allowance.wall, allowance.wall, allowance.floor) : new Vector3(allowance.x, allowance.y, allowance.z);
637
+ if (padding.toArray().some((value) => !Number.isFinite(value) || value < 0)) {
638
+ throw new RangeError("Stock allowance must be finite and non-negative.");
639
+ }
640
+ if (![offset.x, offset.y, offset.z].every(Number.isFinite)) {
641
+ throw new RangeError("Stock offset must be finite.");
642
+ }
643
+ const box = partBounds(geometry);
644
+ box.expandByVector(padding).translate(new Vector3(offset.x, offset.y, offset.z));
645
+ const size = box.getSize(new Vector3());
646
+ if (size.toArray().some((value) => !Number.isFinite(value) || value <= 0)) {
647
+ throw new RangeError("Stock dimensions must be finite and positive.");
648
+ }
649
+ return box;
650
+ }
651
+ function partBounds(geometry) {
652
+ const position = geometry.getAttribute("position");
653
+ if (!position || position.count === 0)
654
+ throw new RangeError("Stock needs non-empty part geometry.");
655
+ const box = new Box3();
656
+ const point = new Vector3();
657
+ for (let index = 0; index < position.count; index += 1) {
658
+ point.fromBufferAttribute(position, index);
659
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y) || !Number.isFinite(point.z)) {
660
+ throw new RangeError("Stock needs finite part coordinates.");
661
+ }
662
+ box.expandByPoint(point);
663
+ }
664
+ return box;
665
+ }
666
+ function createStock(geometry, legacyWorkpiece = false) {
667
+ const material = new MeshLambertMaterial({ transparent: true, depthWrite: false });
668
+ const edgeMaterial = new LineBasicMaterial({ transparent: true, depthWrite: false });
669
+ const mesh = new Mesh2(geometry, material);
670
+ const object = new Group();
671
+ object.userData[STOCK_OBJECT] = true;
672
+ mesh.renderOrder = legacyWorkpiece ? 2 : 5;
673
+ mesh.raycast = () => {
674
+ };
675
+ object.add(mesh);
676
+ if (legacyWorkpiece) {
677
+ const shadow = mesh.clone();
678
+ shadow.raycast = () => {
679
+ };
680
+ object.add(shadow);
681
+ edgeMaterial.depthWrite = true;
682
+ }
683
+ let edges = null;
684
+ return {
685
+ object,
686
+ material,
687
+ edgeMaterial,
688
+ /**
689
+ * Shows or hides the outline, building it the first time it is shown: the
690
+ * edge pass reads every triangle, and stock cut by a toolpath has a great
691
+ * many of them.
692
+ */
693
+ showEdges(show) {
694
+ if (show && !edges) {
695
+ edges = new LineSegments(new EdgesGeometry(geometry, 15), edgeMaterial);
696
+ edges.renderOrder = legacyWorkpiece ? 1 : 6;
697
+ edges.raycast = () => {
698
+ };
699
+ object.add(edges);
700
+ }
701
+ if (edges) edges.visible = show;
702
+ },
703
+ dispose() {
704
+ material.dispose();
705
+ edgeMaterial.dispose();
706
+ edges?.geometry.dispose();
707
+ }
708
+ };
709
+ }
710
+
711
+ // src/stock.tsx
712
+ import { jsx as jsx4 } from "react/jsx-runtime";
713
+ var Stock = ({ geometry, glb, box, cylinder, ...appearance }) => geometry ? /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance }) : glb ? /* @__PURE__ */ jsx4(GlbStock, { glb, ...appearance }) : /* @__PURE__ */ jsx4(ShapeStock, { box, cylinder, ...appearance });
714
+ var GeometryStock = ({
715
+ geometry,
716
+ renderStyle = "overlay",
717
+ color = renderStyle === "legacy-workpiece" ? 16777215 : 12176354,
718
+ emissive = renderStyle === "legacy-workpiece" ? 3948625 : 0,
719
+ flatShading = renderStyle === "legacy-workpiece",
720
+ opacity = 0.2,
721
+ edgeColor = renderStyle === "legacy-workpiece" ? 0 : 11058648,
722
+ edgeOpacity = renderStyle === "legacy-workpiece" ? 0.5 : 0.75,
723
+ showEdges = true
724
+ }) => {
725
+ const invalidate = useThree((state) => state.invalidate);
726
+ const stock = useMemo(
727
+ () => createStock(geometry, renderStyle === "legacy-workpiece"),
728
+ [geometry, renderStyle]
729
+ );
730
+ useEffect2(() => () => stock.dispose(), [stock]);
731
+ useLayoutEffect(() => {
732
+ stock.material.color.setHex(color);
733
+ stock.material.emissive.setHex(emissive);
734
+ if (stock.material.flatShading !== flatShading) {
735
+ stock.material.flatShading = flatShading;
736
+ stock.material.needsUpdate = true;
737
+ }
738
+ stock.material.opacity = opacity;
739
+ stock.material.depthWrite = renderStyle === "legacy-workpiece" && opacity === 1;
740
+ stock.material.polygonOffset = renderStyle === "legacy-workpiece" && opacity === 1;
741
+ stock.material.polygonOffsetFactor = 1;
742
+ stock.material.polygonOffsetUnits = 1;
743
+ stock.edgeMaterial.color.setHex(edgeColor);
744
+ stock.edgeMaterial.opacity = edgeOpacity;
745
+ stock.showEdges(showEdges);
746
+ invalidate();
747
+ }, [
748
+ color,
749
+ emissive,
750
+ flatShading,
751
+ edgeColor,
752
+ edgeOpacity,
753
+ invalidate,
754
+ opacity,
755
+ renderStyle,
756
+ showEdges,
757
+ stock
758
+ ]);
759
+ return /* @__PURE__ */ jsx4("primitive", { object: stock.object, dispose: null });
760
+ };
761
+ var GlbStock = ({
762
+ glb,
763
+ retainPrevious = false,
764
+ ...appearance
765
+ }) => {
766
+ const buffer = useMemo(() => createStockMeshBuffer(), []);
767
+ const [decoded, setDecoded] = useState2(null);
768
+ useEffect2(() => () => buffer.dispose(), [buffer]);
769
+ useLayoutEffect(() => {
770
+ buffer.commit(decoded?.geometries ?? []);
771
+ }, [buffer, decoded]);
772
+ useEffect2(
773
+ () => buffer.request(
774
+ () => parseStockGlb(glb),
775
+ (geometries) => {
776
+ setDecoded({ source: glb, geometries });
777
+ },
778
+ (cause) => {
779
+ setDecoded({
780
+ source: glb,
781
+ error: cause instanceof Error ? cause : new Error(String(cause))
782
+ });
783
+ }
784
+ ),
785
+ [buffer, glb]
786
+ );
787
+ if (!decoded || !retainPrevious && decoded.source !== glb) return null;
788
+ if (decoded.error) throw decoded.error;
789
+ return decoded.geometries?.map((geometry) => /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance }, geometry.uuid));
790
+ };
791
+ var keepNonFinite = (_key, value) => typeof value === "number" && !Number.isFinite(value) ? String(value) : value;
792
+ var restoreNonFinite = (_key, value) => value === "NaN" || value === "Infinity" || value === "-Infinity" ? Number(value) : value;
793
+ var ShapeStock = ({
794
+ box,
795
+ cylinder,
796
+ ...appearance
797
+ }) => {
798
+ const isBox = box !== void 0;
799
+ const partGeometry = box?.partGeometry ?? cylinder?.partGeometry;
800
+ const figure = JSON.stringify(
801
+ box ? box.partGeometry ? {
802
+ dimensions: box.dimensions,
803
+ allowance: box.allowance,
804
+ offset: box.offset,
805
+ position: box.position,
806
+ positionOffset: box.positionOffset
807
+ } : { frame: box.frame, lower: box.lower, upper: box.upper } : cylinder.partGeometry ? {
808
+ diameter: cylinder.diameter,
809
+ length: cylinder.length,
810
+ position: cylinder.position,
811
+ positionOffset: cylinder.positionOffset
812
+ } : {
813
+ origin: cylinder.origin,
814
+ axis: cylinder.axis,
815
+ diameter: cylinder.diameter,
816
+ length: cylinder.length
817
+ },
818
+ keepNonFinite
819
+ );
820
+ const geometry = useMemo(() => {
821
+ const options = JSON.parse(figure, restoreNonFinite);
822
+ const input = partGeometry ? { ...options, partGeometry } : options;
823
+ return isBox ? boxStockGeometry(input) : cylinderStockGeometry(input);
824
+ }, [figure, isBox, partGeometry]);
825
+ useEffect2(() => () => geometry.dispose(), [geometry]);
826
+ return /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance });
827
+ };
828
+
829
+ // src/banana.tsx
830
+ import { useGLTF } from "@react-three/drei";
831
+ import { useThree as useThree2 } from "@react-three/fiber";
832
+ import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef } from "react";
833
+
834
+ // src/render/banana.ts
835
+ import { Vector3 as Vector32 } from "three";
836
+ var BANANA_GAP = 0.1;
837
+ function bananaPosition(part, banana) {
838
+ const size = part.getSize(new Vector32());
839
+ const reach = Math.max(size.x, size.y, size.z);
840
+ const centre = part.getCenter(new Vector32());
841
+ return new Vector32(
842
+ part.max.x + reach * BANANA_GAP - banana.min.x,
843
+ centre.y - (banana.min.y + banana.max.y) / 2,
844
+ part.min.z - banana.min.z
845
+ );
846
+ }
847
+ function bananaFrameBounds(part, banana, position) {
848
+ return part.clone().union(banana.clone().translate(position));
849
+ }
850
+
851
+ // src/banana.tsx
852
+ import { jsx as jsx5 } from "react/jsx-runtime";
853
+ var BANANA_MODEL_URL = new URL("./assets/banana.glb", import.meta.url).href;
854
+ var SKIN = "#e8b530";
855
+ var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
856
+ var Banana = ({ onPlaced }) => {
857
+ const part = useContentBox();
858
+ const invalidate = useThree2((state) => state.invalidate);
859
+ const gltf = useGLTF(BANANA_MODEL_URL);
860
+ const group = useRef(null);
861
+ const geometry = useMemo2(() => {
862
+ let found = null;
863
+ gltf.scene.traverse((object) => {
864
+ const mesh = object;
865
+ if (found === null && mesh.isMesh) found = mesh.geometry;
866
+ });
867
+ const source = found;
868
+ if (!source) return null;
869
+ const own = source.clone();
870
+ own.computeVertexNormals();
871
+ own.rotateY(Math.PI / 2);
872
+ own.rotateX(Math.PI / 2);
873
+ own.computeBoundingBox();
874
+ return own;
875
+ }, [gltf]);
876
+ useLayoutEffect2(() => {
877
+ const node = group.current;
878
+ const own = geometry?.boundingBox;
879
+ if (!node || !own || part.isEmpty()) return;
880
+ const position = bananaPosition(part, own);
881
+ node.position.copy(position);
882
+ node.visible = true;
883
+ node.updateWorldMatrix(true, true);
884
+ onPlaced?.(bananaFrameBounds(part, own, position));
885
+ invalidate();
886
+ }, [geometry, invalidate, onPlaced, part]);
887
+ if (!geometry) return null;
888
+ return /* @__PURE__ */ jsx5("group", { ref: group, userData: FURNITURE, visible: false, children: /* @__PURE__ */ jsx5("mesh", { geometry, children: /* @__PURE__ */ jsx5("meshStandardMaterial", { color: SKIN, roughness: 0.85, metalness: 0 }) }) });
889
+ };
890
+
891
+ // src/render/direction-highlights.ts
892
+ function directionHighlights(model, activeDirection = null) {
893
+ const features = model.features.map((feature) => ({ feature, index: directionIndexOf(model, feature.machiningDirection) })).filter(({ index }) => index >= 0 && (activeDirection === null || index === activeDirection)).sort(
894
+ (a, b) => featureTypeRank(a.feature.featureType) - featureTypeRank(b.feature.featureType) || a.index - b.index || (a.feature.tag < b.feature.tag ? -1 : a.feature.tag > b.feature.tag ? 1 : 0)
895
+ );
896
+ const colors = /* @__PURE__ */ new Map();
897
+ for (const { feature, index } of features) {
898
+ for (const region of model.regionIndex.regionsForFeature(feature.tag)) {
899
+ if (!colors.has(region)) colors.set(region, { region, color: directionColor(index) });
900
+ }
901
+ }
902
+ return [...colors.values()].sort((a, b) => a.region - b.region);
903
+ }
135
904
 
136
905
  // src/primitives.tsx
137
906
  import { GizmoHelper } from "@react-three/drei";
138
- import { useThree } from "@react-three/fiber";
139
- import { useEffect, useMemo, useRef, useState } from "react";
907
+ import { useThree as useThree3 } from "@react-three/fiber";
908
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
140
909
 
141
910
  // src/render/grid.ts
142
- import { BufferGeometry, Float32BufferAttribute, Vector3 } from "three";
911
+ import { BufferGeometry, Float32BufferAttribute, Vector3 as Vector33 } from "three";
143
912
  var STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1e3];
144
913
  var TARGET_CELLS = 10;
145
914
  var OVERHANG = 1.6;
146
915
  function gridSpec(box) {
147
- const size = box.getSize(new Vector3());
148
- const center = box.getCenter(new Vector3());
916
+ const size = box.getSize(new Vector33());
917
+ const center = box.getCenter(new Vector33());
149
918
  const largest = Math.max(size.x, size.y, 1e-6);
150
919
  const ideal = largest / TARGET_CELLS;
151
920
  const step = [...STEPS].reverse().find((candidate) => candidate <= ideal) ?? STEPS[0];
@@ -189,26 +958,26 @@ function gridFor(box, options = {}) {
189
958
  }
190
959
 
191
960
  // src/primitives.tsx
192
- import { jsx, jsxs } from "react/jsx-runtime";
193
- var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
961
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
962
+ var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
194
963
  var Grid = ({ step, extent, color, opacity = 0.35 }) => {
195
964
  const theme = resolveTheme();
196
965
  const box = useContentBox();
197
- const geometry = useMemo(() => gridFor(box, { step, extent }), [box, extent, step]);
198
- useEffect(() => () => geometry?.dispose(), [geometry]);
966
+ const geometry = useMemo3(() => gridFor(box, { step, extent }), [box, extent, step]);
967
+ useEffect3(() => () => geometry?.dispose(), [geometry]);
199
968
  if (!geometry) return null;
200
- return /* @__PURE__ */ jsx("lineSegments", { geometry, renderOrder: -1, raycast: () => null, userData: FURNITURE, children: /* @__PURE__ */ jsx("lineBasicMaterial", { color: color ?? theme.cubeEdge, transparent: true, opacity }) });
969
+ return /* @__PURE__ */ jsx6("lineSegments", { geometry, renderOrder: -1, raycast: () => null, userData: FURNITURE2, children: /* @__PURE__ */ jsx6("lineBasicMaterial", { color: color ?? theme.cubeEdge, transparent: true, opacity }) });
201
970
  };
202
- var Axes = ({ size = 25 }) => /* @__PURE__ */ jsx("axesHelper", { args: [size], userData: FURNITURE, raycast: () => null });
971
+ var Axes = ({ size = 25 }) => /* @__PURE__ */ jsx6("axesHelper", { args: [size], userData: FURNITURE2, raycast: () => null });
203
972
  var ViewCube = ({
204
973
  alignment = "top-right",
205
974
  margin = [80, 80],
206
975
  theme,
207
976
  onViewChange
208
977
  }) => {
209
- const resolved = useMemo(() => resolveTheme(theme), [theme]);
210
- const zones = useMemo(() => cubeZones(), []);
211
- return /* @__PURE__ */ jsx(GizmoHelper, { alignment, margin, children: /* @__PURE__ */ jsx(CubePanels, { zones, theme: resolved, onViewChange }) });
978
+ const resolved = useMemo3(() => resolveTheme(theme), [theme]);
979
+ const zones = useMemo3(() => cubeZones(), []);
980
+ return /* @__PURE__ */ jsx6(GizmoHelper, { alignment, margin, children: /* @__PURE__ */ jsx6(CubePanels, { zones, theme: resolved, onViewChange }) });
212
981
  };
213
982
  var CubePanels = ({
214
983
  zones,
@@ -216,15 +985,15 @@ var CubePanels = ({
216
985
  onViewChange
217
986
  }) => {
218
987
  const controls = useViewerControls();
219
- const invalidate = useThree((state) => state.invalidate);
220
- const groupRef = useRef(null);
221
- const [hovered, setHovered] = useState(null);
222
- const panels = useMemo(
988
+ const invalidate = useThree3((state) => state.invalidate);
989
+ const groupRef = useRef2(null);
990
+ const [hovered, setHovered] = useState3(null);
991
+ const panels = useMemo3(
223
992
  () => zones.map((zone) => ({ zone, geometry: panelGeometry(zone) })),
224
993
  [zones]
225
994
  );
226
- const outline = useMemo(() => cubeOutlineGeometry(zones), [zones]);
227
- const labels = useMemo(
995
+ const outline = useMemo3(() => cubeOutlineGeometry(zones), [zones]);
996
+ const labels = useMemo3(
228
997
  () => zones.filter((zone) => zone.kind === "face").map((zone) => ({
229
998
  zone,
230
999
  geometry: labelGeometry(zone),
@@ -232,7 +1001,7 @@ var CubePanels = ({
232
1001
  })),
233
1002
  [theme.cubeLabel, zones]
234
1003
  );
235
- useEffect(
1004
+ useEffect3(
236
1005
  () => () => {
237
1006
  for (const panel of panels) panel.geometry.dispose();
238
1007
  for (const label of labels) {
@@ -251,8 +1020,8 @@ var CubePanels = ({
251
1020
  setHovered(name);
252
1021
  invalidate();
253
1022
  };
254
- return /* @__PURE__ */ jsxs("group", { ref: groupRef, scale: 38, children: [
255
- panels.map(({ zone, geometry }) => /* @__PURE__ */ jsx(
1023
+ return /* @__PURE__ */ jsxs2("group", { ref: groupRef, scale: 38, children: [
1024
+ panels.map(({ zone, geometry }) => /* @__PURE__ */ jsx6(
256
1025
  "mesh",
257
1026
  {
258
1027
  geometry,
@@ -265,7 +1034,7 @@ var CubePanels = ({
265
1034
  event.stopPropagation();
266
1035
  choose(zone);
267
1036
  },
268
- children: /* @__PURE__ */ jsx(
1037
+ children: /* @__PURE__ */ jsx6(
269
1038
  "meshLambertMaterial",
270
1039
  {
271
1040
  color: hovered === zone.name ? theme.hover : theme.cube,
@@ -277,19 +1046,19 @@ var CubePanels = ({
277
1046
  },
278
1047
  zone.name
279
1048
  )),
280
- labels.map(({ zone, geometry, texture }) => /* @__PURE__ */ jsx("mesh", { geometry, raycast: () => null, children: /* @__PURE__ */ jsx("meshBasicMaterial", { map: texture, transparent: true }) }, `${zone.name}-label`)),
281
- /* @__PURE__ */ jsx("lineSegments", { geometry: outline, raycast: () => null, children: /* @__PURE__ */ jsx("lineBasicMaterial", { color: theme.cubeEdge }) }),
282
- /* @__PURE__ */ jsx("ambientLight", { intensity: 1.4 }),
283
- /* @__PURE__ */ jsx("directionalLight", { position: [viewVector("top-front-right").x, 1, 2], intensity: 1.1 })
1049
+ labels.map(({ zone, geometry, texture }) => /* @__PURE__ */ jsx6("mesh", { geometry, raycast: () => null, children: /* @__PURE__ */ jsx6("meshBasicMaterial", { map: texture, transparent: true }) }, `${zone.name}-label`)),
1050
+ /* @__PURE__ */ jsx6("lineSegments", { geometry: outline, raycast: () => null, children: /* @__PURE__ */ jsx6("lineBasicMaterial", { color: theme.cubeEdge }) }),
1051
+ /* @__PURE__ */ jsx6("ambientLight", { intensity: 1.4 }),
1052
+ /* @__PURE__ */ jsx6("directionalLight", { position: [viewVector("top-front-right").x, 1, 2], intensity: 1.1 })
284
1053
  ] });
285
1054
  };
286
1055
 
287
1056
  // src/direction-arrows.tsx
288
- import { useThree as useThree2 } from "@react-three/fiber";
289
- import { useMemo as useMemo2 } from "react";
290
- import { Quaternion, Vector3 as Vector32 } from "three";
291
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
292
- var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
1057
+ import { useThree as useThree4 } from "@react-three/fiber";
1058
+ import { useMemo as useMemo4 } from "react";
1059
+ import { Quaternion as Quaternion2, Vector3 as Vector34 } from "three";
1060
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
1061
+ var FURNITURE3 = { [EXCLUDE_FROM_FRAME]: true };
293
1062
  var DirectionArrows = ({
294
1063
  directions,
295
1064
  activeDirection = null,
@@ -301,7 +1070,7 @@ var DirectionArrows = ({
301
1070
  visible = true
302
1071
  }) => {
303
1072
  const box = useContentBox();
304
- const resolved = useMemo2(() => resolveTheme(theme), [theme]);
1073
+ const resolved = useMemo4(() => resolveTheme(theme), [theme]);
305
1074
  const shown = shownDirection === void 0 ? activeDirection : shownDirection;
306
1075
  const drawn = (index) => {
307
1076
  if (shown === null) return true;
@@ -313,10 +1082,10 @@ var DirectionArrows = ({
313
1082
  // Excluded from framing: the arrows are deliberately placed outside the
314
1083
  // part, so a Fit that measured them would frame the arrows and leave the
315
1084
  // part small in the middle of them.
316
- /* @__PURE__ */ jsxs2("group", { userData: FURNITURE2, children: [
1085
+ /* @__PURE__ */ jsxs3("group", { userData: FURNITURE3, children: [
317
1086
  directions.map((direction, index) => {
318
1087
  if (!drawn(index)) return null;
319
- return /* @__PURE__ */ jsx2(
1088
+ return /* @__PURE__ */ jsx7(
320
1089
  Arrow,
321
1090
  {
322
1091
  direction,
@@ -328,7 +1097,7 @@ var DirectionArrows = ({
328
1097
  `candidate-${index}`
329
1098
  );
330
1099
  }),
331
- namedDirections.map((named) => /* @__PURE__ */ jsx2(
1100
+ namedDirections.map((named) => /* @__PURE__ */ jsx7(
332
1101
  Arrow,
333
1102
  {
334
1103
  direction: named.direction,
@@ -339,17 +1108,17 @@ var DirectionArrows = ({
339
1108
  },
340
1109
  `named-${named.index}`
341
1110
  )),
342
- previewDirection ? /* @__PURE__ */ jsx2(Arrow, { direction: previewDirection, box, color: resolved.hover, opacity: 0.95, onTop: true }) : null
1111
+ previewDirection ? /* @__PURE__ */ jsx7(Arrow, { direction: previewDirection, box, color: resolved.hover, opacity: 0.95, onTop: true }) : null
343
1112
  ] })
344
1113
  );
345
1114
  };
346
1115
  var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
347
- const invalidate = useThree2((state) => state.invalidate);
348
- const { tip, length, quaternion } = useMemo2(() => {
1116
+ const invalidate = useThree4((state) => state.invalidate);
1117
+ const { tip, length, quaternion } = useMemo4(() => {
349
1118
  const placement = arrowPlacement(direction, box);
350
- const aim = new Quaternion().setFromUnitVectors(
1119
+ const aim = new Quaternion2().setFromUnitVectors(
351
1120
  CONE_AXIS,
352
- new Vector32(direction.x, direction.y, direction.z).normalize()
1121
+ new Vector34(direction.x, direction.y, direction.z).normalize()
353
1122
  );
354
1123
  return { tip: placement.tip, length: placement.length, quaternion: aim };
355
1124
  }, [box, direction]);
@@ -362,234 +1131,32 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
362
1131
  invalidate();
363
1132
  }
364
1133
  } : {};
365
- return /* @__PURE__ */ jsxs2("group", { position: tip, quaternion, renderOrder: onTop ? 10 : 3, children: [
366
- /* @__PURE__ */ jsxs2("mesh", { position: [0, head * 0.5, 0], rotation: [Math.PI, 0, 0], ...press, children: [
367
- /* @__PURE__ */ jsx2("coneGeometry", { args: [length * HEAD_RADIUS, head, 20] }),
368
- /* @__PURE__ */ jsx2("meshBasicMaterial", { color, transparent: true, opacity, depthTest: !onTop })
1134
+ return /* @__PURE__ */ jsxs3("group", { position: tip, quaternion, renderOrder: onTop ? 10 : 3, children: [
1135
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, head * 0.5, 0], rotation: [Math.PI, 0, 0], ...press, children: [
1136
+ /* @__PURE__ */ jsx7("coneGeometry", { args: [length * HEAD_RADIUS, head, 20] }),
1137
+ /* @__PURE__ */ jsx7("meshBasicMaterial", { color, transparent: true, opacity, depthTest: !onTop })
369
1138
  ] }),
370
- /* @__PURE__ */ jsxs2("mesh", { position: [0, head + shaft * 0.5, 0], ...press, children: [
371
- /* @__PURE__ */ jsx2("cylinderGeometry", { args: [length * SHAFT_RADIUS, length * SHAFT_RADIUS, shaft, 12] }),
372
- /* @__PURE__ */ jsx2("meshBasicMaterial", { color, transparent: true, opacity, depthTest: !onTop })
1139
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, head + shaft * 0.5, 0], ...press, children: [
1140
+ /* @__PURE__ */ jsx7("cylinderGeometry", { args: [length * SHAFT_RADIUS, length * SHAFT_RADIUS, shaft, 12] }),
1141
+ /* @__PURE__ */ jsx7("meshBasicMaterial", { color, transparent: true, opacity, depthTest: !onTop })
373
1142
  ] })
374
1143
  ] });
375
1144
  };
376
1145
 
377
1146
  // src/section-tool.tsx
378
- import { useFrame, useThree as useThree3 } from "@react-three/fiber";
379
- import { useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
1147
+ import { useFrame, useThree as useThree5 } from "@react-three/fiber";
1148
+ import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState4, useSyncExternalStore } from "react";
380
1149
  import {
381
1150
  BufferGeometry as BufferGeometry2,
382
1151
  DoubleSide,
383
- Quaternion as Quaternion2,
1152
+ Quaternion as Quaternion3,
384
1153
  Raycaster,
385
- Vector2 as Vector22,
386
- Vector3 as Vector34
1154
+ Vector2,
1155
+ Vector3 as Vector35
387
1156
  } from "three";
388
-
389
- // src/render/measure.ts
390
- import { Vector2, Vector3 as Vector33 } from "three";
391
- var MEASURE_RENDER_ORDER = 7;
392
- var AXIS_COLORS = { x: 16739179, y: 7331978, z: 7314431 };
393
- var SNAP_PIXELS = {
394
- vertex: 14,
395
- midpoint: 12,
396
- edge: 9
397
- };
398
- var SNAP_MARKER_PIXELS = {
399
- vertex: 6,
400
- midpoint: 5,
401
- edge: 4,
402
- face: 3.5
403
- };
404
- var MEASURE_MARKER_PIXELS = 3.5;
405
- var MEASURE_LINE_PIXELS = {
406
- line: 2,
407
- leg: 1.5,
408
- edge: 3
409
- };
410
- var DELTA_DASH_PIXELS = 6;
411
- var ANGLE_ARC_FRACTION = 0.3;
412
- var ANGLE_ARC_SEGMENTS = 24;
413
- function snapAt(hit, edges, camera, viewport) {
414
- const face = {
415
- kind: "face",
416
- point: hit.point.clone(),
417
- normal: hit.normal.clone(),
418
- edge: null
419
- };
420
- if (!edges) return face;
421
- const positions = edges.positions;
422
- const segments = Math.floor(positions.length / 6);
423
- if (segments === 0) return face;
424
- const inverse = edges.matrixWorld.clone().invert();
425
- const p = hit.point.clone().applyMatrix4(inverse);
426
- const n = hit.normal.clone().transformDirection(inverse);
427
- const reach = screenLength(camera, hit.point, viewport, SNAP_PIXELS.vertex) * 1.5;
428
- const reachSq = reach * reach;
429
- const pointer = screenPoint(camera, hit.point, viewport);
430
- const best = {
431
- vertex: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 },
432
- midpoint: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 },
433
- edge: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 }
434
- };
435
- const a = new Vector33();
436
- const b = new Vector33();
437
- const ab = new Vector33();
438
- const ap = new Vector33();
439
- const q = new Vector33();
440
- const pixel = new Vector2();
441
- const consider = (kind, point, index, t) => {
442
- const distance = screenPoint(
443
- camera,
444
- q.copy(point).applyMatrix4(edges.matrixWorld),
445
- viewport,
446
- pixel
447
- ).distanceTo(pointer);
448
- if (distance <= SNAP_PIXELS[kind] && distance < best[kind].distance) {
449
- best[kind] = { distance, index, t };
450
- }
451
- };
452
- for (let index = 0; index < segments; index += 1) {
453
- const offset = index * 6;
454
- a.set(positions[offset], positions[offset + 1], positions[offset + 2]);
455
- b.set(positions[offset + 3], positions[offset + 4], positions[offset + 5]);
456
- if (Math.abs(ap.subVectors(a, p).dot(n)) > reach) continue;
457
- if (Math.abs(ap.subVectors(b, p).dot(n)) > reach) continue;
458
- ab.subVectors(b, a);
459
- ap.subVectors(p, a);
460
- const lengthSq = ab.lengthSq();
461
- const t = lengthSq === 0 ? 0 : Math.min(1, Math.max(0, ap.dot(ab) / lengthSq));
462
- q.copy(a).addScaledVector(ab, t);
463
- if (q.distanceToSquared(p) > reachSq) continue;
464
- consider("vertex", a, index, 0);
465
- consider("vertex", b, index, 1);
466
- consider("midpoint", q.copy(a).addScaledVector(ab, 0.5), index, 0.5);
467
- consider("edge", q.copy(a).addScaledVector(ab, t), index, t);
468
- }
469
- for (const kind of ["vertex", "midpoint", "edge"]) {
470
- const found = best[kind];
471
- if (found.index < 0) continue;
472
- const offset = found.index * 6;
473
- const start = new Vector33(positions[offset], positions[offset + 1], positions[offset + 2]);
474
- const end = new Vector33(positions[offset + 3], positions[offset + 4], positions[offset + 5]);
475
- const point = start.clone().lerp(end, found.t);
476
- start.applyMatrix4(edges.matrixWorld);
477
- end.applyMatrix4(edges.matrixWorld);
478
- point.applyMatrix4(edges.matrixWorld);
479
- return {
480
- kind,
481
- point,
482
- normal: hit.normal.clone(),
483
- edge: kind === "vertex" ? null : [start, end]
484
- };
485
- }
486
- return face;
487
- }
488
- function screenPoint(camera, point, viewport, into = new Vector2()) {
489
- const ndc = point.clone().project(camera);
490
- return into.set((ndc.x + 1) / 2 * viewport.width, (1 - ndc.y) / 2 * viewport.height);
491
- }
492
- var POINTS_PER_MEASUREMENT = {
493
- distance: 2,
494
- angle: 3
495
- };
496
- function measurementFromPoints(kind, points, id) {
497
- if (points.length < POINTS_PER_MEASUREMENT[kind]) return null;
498
- const [a, b, c] = points;
499
- if (kind === "distance") return { id, kind, points: [copy(a), copy(b)] };
500
- return { id, kind, points: [copy(a), copy(b), copy(c)] };
501
- }
502
- function nextMeasurementId(held) {
503
- return held.reduce((top, entry) => Math.max(top, entry.id), 0) + 1;
504
- }
505
- function distanceBetween(a, b) {
506
- return Math.hypot(b.x - a.x, b.y - a.y, b.z - a.z);
507
- }
508
- function deltaBetween(a, b) {
509
- return { x: b.x - a.x, y: b.y - a.y, z: b.z - a.z };
510
- }
511
- function midpoint(a, b) {
512
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
513
- }
514
- function angleAt(vertex, a, b) {
515
- const u = new Vector33(a.x - vertex.x, a.y - vertex.y, a.z - vertex.z);
516
- const w = new Vector33(b.x - vertex.x, b.y - vertex.y, b.z - vertex.z);
517
- if (u.lengthSq() === 0 || w.lengthSq() === 0) return 0;
518
- return u.angleTo(w) * 180 / Math.PI;
519
- }
520
- function lockToAxis(from, to) {
521
- const delta = deltaBetween(from, to);
522
- let axis = "x";
523
- for (const candidate of ["y", "z"]) {
524
- if (Math.abs(delta[candidate]) > Math.abs(delta[axis])) axis = candidate;
525
- }
526
- return { axis, point: { ...from, [axis]: to[axis] } };
527
- }
528
- function deltaLegs(a, b) {
529
- const delta = deltaBetween(a, b);
530
- const threshold = Math.max(1e-9, distanceBetween(a, b) * 1e-6);
531
- const legs = [];
532
- let cursor = copy(a);
533
- for (const axis of ["x", "y", "z"]) {
534
- if (Math.abs(delta[axis]) <= threshold) continue;
535
- const to = { ...cursor, [axis]: b[axis] };
536
- legs.push({ axis, from: cursor, to, length: Math.abs(delta[axis]) });
537
- cursor = to;
538
- }
539
- return legs.length > 1 ? legs : [];
540
- }
541
- function angleArc(vertex, a, b, radius, segments = ANGLE_ARC_SEGMENTS) {
542
- const v = new Vector33(vertex.x, vertex.y, vertex.z);
543
- const u = new Vector33(a.x, a.y, a.z).sub(v);
544
- const w = new Vector33(b.x, b.y, b.z).sub(v);
545
- if (u.lengthSq() === 0 || w.lengthSq() === 0) return [];
546
- u.normalize();
547
- w.normalize();
548
- const theta = u.angleTo(w);
549
- const sine = Math.sin(theta);
550
- if (sine < 1e-6) {
551
- return [v.clone().addScaledVector(u, radius), v.clone().addScaledVector(w, radius)];
552
- }
553
- const points = [];
554
- for (let i = 0; i <= segments; i += 1) {
555
- const t = i / segments;
556
- const direction = u.clone().multiplyScalar(Math.sin((1 - t) * theta) / sine).addScaledVector(w, Math.sin(t * theta) / sine);
557
- points.push(v.clone().addScaledVector(direction, radius));
558
- }
559
- return points;
560
- }
561
- function angleArcRadius(vertex, a, b) {
562
- return Math.min(distanceBetween(vertex, a), distanceBetween(vertex, b)) * ANGLE_ARC_FRACTION;
563
- }
564
- function formatMillimetres(mm, digits = 2) {
565
- return `${mm.toFixed(digits)} mm`;
566
- }
567
- function formatDegrees(degrees, digits = 1) {
568
- return `${degrees.toFixed(digits)}\xB0`;
569
- }
570
- function measurementLabel(measurement, format = formatMillimetres) {
571
- if (measurement.kind === "distance") {
572
- const [a2, b2] = measurement.points;
573
- return format(distanceBetween(a2, b2));
574
- }
575
- const [a, vertex, b] = measurement.points;
576
- return formatDegrees(angleAt(vertex, a, b));
577
- }
578
- function measurementLabelAnchor(measurement) {
579
- if (measurement.kind === "distance") return midpoint(measurement.points[0], measurement.points[1]);
580
- const [a, vertex, b] = measurement.points;
581
- const arc = angleArc(vertex, a, b, angleArcRadius(vertex, a, b) * 1.6, 2);
582
- const middle = arc[1] ?? arc[0];
583
- return middle ? { x: middle.x, y: middle.y, z: middle.z } : copy(vertex);
584
- }
585
- function copy(v) {
586
- return { x: v.x, y: v.y, z: v.z };
587
- }
588
-
589
- // src/section-tool.tsx
590
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
591
- var FURNITURE3 = { [EXCLUDE_FROM_FRAME]: true };
592
- var Z = new Vector34(0, 0, 1);
1157
+ import { Fragment, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1158
+ var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
1159
+ var Z = new Vector35(0, 0, 1);
593
1160
  var AXIS_ROTATIONS = {
594
1161
  x: [0, Math.PI / 2, 0],
595
1162
  y: [Math.PI / 2, 0, 0],
@@ -601,12 +1168,12 @@ var SectionTool = ({ theme }) => {
601
1168
  const store = useSectionStore();
602
1169
  const section = useSyncExternalStore(store.subscribe, store.get, store.get);
603
1170
  const box = useContentBox();
604
- const resolved = useMemo3(() => resolveTheme(theme), [theme]);
605
- useEffect2(() => {
1171
+ const resolved = useMemo5(() => resolveTheme(theme), [theme]);
1172
+ useEffect4(() => {
606
1173
  store.setEngaged(true);
607
1174
  return () => store.setEngaged(false);
608
1175
  }, [store]);
609
- useEffect2(() => {
1176
+ useEffect4(() => {
610
1177
  if (!section) return void 0;
611
1178
  const clear = (event) => {
612
1179
  if (event.key === "Escape") store.set(null);
@@ -615,43 +1182,43 @@ var SectionTool = ({ theme }) => {
615
1182
  return () => window.removeEventListener("keydown", clear);
616
1183
  }, [section, store]);
617
1184
  if (box.isEmpty()) return null;
618
- return /* @__PURE__ */ jsx3("group", { userData: FURNITURE3, children: section ? /* @__PURE__ */ jsx3(CutSheet, { box, section, theme: resolved }) : /* @__PURE__ */ jsx3(Picker, { box, theme: resolved }) });
1185
+ return /* @__PURE__ */ jsx8("group", { userData: FURNITURE4, children: section ? /* @__PURE__ */ jsx8(CutSheet, { box, section, theme: resolved }) : /* @__PURE__ */ jsx8(Picker, { box, theme: resolved }) });
619
1186
  };
620
1187
  var useSquare = () => {
621
- const square = useMemo3(
1188
+ const square = useMemo5(
622
1189
  () => new BufferGeometry2().setFromPoints([
623
- new Vector34(-1, 1, 0),
624
- new Vector34(1, 1, 0),
625
- new Vector34(1, -1, 0),
626
- new Vector34(-1, -1, 0)
1190
+ new Vector35(-1, 1, 0),
1191
+ new Vector35(1, 1, 0),
1192
+ new Vector35(1, -1, 0),
1193
+ new Vector35(-1, -1, 0)
627
1194
  ]),
628
1195
  []
629
1196
  );
630
- useEffect2(() => () => square.dispose(), [square]);
1197
+ useEffect4(() => () => square.dispose(), [square]);
631
1198
  return square;
632
1199
  };
633
1200
  var Picker = ({ box, theme }) => {
634
1201
  const store = useSectionStore();
635
- const camera = useThree3((state) => state.camera);
636
- const scene = useThree3((state) => state.scene);
637
- const domElement = useThree3((state) => state.gl.domElement);
638
- const invalidate = useThree3((state) => state.invalidate);
1202
+ const camera = useThree5((state) => state.camera);
1203
+ const scene = useThree5((state) => state.scene);
1204
+ const domElement = useThree5((state) => state.gl.domElement);
1205
+ const invalidate = useThree5((state) => state.invalidate);
639
1206
  const isTap = useTapGuard();
640
1207
  const square = useSquare();
641
- const previewRef = useRef2(null);
642
- const hover = useRef2(null);
643
- const [hoveredPlane, setHoveredPlane] = useState2(null);
644
- const span = useMemo3(() => box.getSize(new Vector34()).length(), [box]);
645
- const centre = useMemo3(() => box.getCenter(new Vector34()), [box]);
1208
+ const previewRef = useRef3(null);
1209
+ const hover = useRef3(null);
1210
+ const [hoveredPlane, setHoveredPlane] = useState4(null);
1211
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1212
+ const centre = useMemo5(() => box.getCenter(new Vector35()), [box]);
646
1213
  const previewSize = span * PREVIEW_SCALE;
647
- useEffect2(() => {
1214
+ useEffect4(() => {
648
1215
  store.setPicking(true);
649
1216
  return () => store.setPicking(false);
650
1217
  }, [store]);
651
- useEffect2(() => {
1218
+ useEffect4(() => {
652
1219
  const raycaster = new Raycaster();
653
- const pointer = new Vector22();
654
- const aim = new Vector34();
1220
+ const pointer = new Vector2();
1221
+ const aim = new Vector35();
655
1222
  const hide = () => {
656
1223
  if (!hover.current) return;
657
1224
  hover.current = null;
@@ -678,6 +1245,7 @@ var Picker = ({ box, theme }) => {
678
1245
  invalidate();
679
1246
  };
680
1247
  const up = (event) => {
1248
+ move(event);
681
1249
  const hit = hover.current;
682
1250
  if (event.button !== 0 || !hit || !isTap(event)) return;
683
1251
  store.set({
@@ -695,7 +1263,7 @@ var Picker = ({ box, theme }) => {
695
1263
  domElement.removeEventListener("pointerup", up);
696
1264
  };
697
1265
  }, [box, camera, domElement, invalidate, isTap, scene, span, store]);
698
- const [planes, setPlanes] = useState2(() => axesPlanes(box, camera.position));
1266
+ const [planes, setPlanes] = useState4(() => axesPlanes(box, camera.position));
699
1267
  const key = planes.map((plane) => plane.sign).join();
700
1268
  useFrame(() => {
701
1269
  const next = [camera.position.x, camera.position.y, camera.position.z].map((eye, axis) => eye - [centre.x, centre.y, centre.z][axis] < 0 ? -1 : 1).join();
@@ -721,11 +1289,11 @@ var Picker = ({ box, theme }) => {
721
1289
  store.set(plane.options);
722
1290
  }
723
1291
  });
724
- return /* @__PURE__ */ jsxs3(Fragment, { children: [
725
- /* @__PURE__ */ jsxs3("group", { ref: previewRef, visible: false, children: [
726
- /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
727
- /* @__PURE__ */ jsx3("planeGeometry", { args: [previewSize, previewSize] }),
728
- /* @__PURE__ */ jsx3(
1292
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
1293
+ /* @__PURE__ */ jsxs4("group", { ref: previewRef, visible: false, children: [
1294
+ /* @__PURE__ */ jsxs4("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
1295
+ /* @__PURE__ */ jsx8("planeGeometry", { args: [previewSize, previewSize] }),
1296
+ /* @__PURE__ */ jsx8(
729
1297
  "meshBasicMaterial",
730
1298
  {
731
1299
  color: theme.sectionOutline,
@@ -736,14 +1304,14 @@ var Picker = ({ box, theme }) => {
736
1304
  }
737
1305
  )
738
1306
  ] }),
739
- /* @__PURE__ */ jsx3(
1307
+ /* @__PURE__ */ jsx8(
740
1308
  "lineLoop",
741
1309
  {
742
1310
  geometry: square,
743
1311
  scale: previewSize / 2,
744
1312
  renderOrder: SECTION_RENDER_ORDER.outline,
745
1313
  raycast: () => null,
746
- children: /* @__PURE__ */ jsx3(
1314
+ children: /* @__PURE__ */ jsx8(
747
1315
  "lineBasicMaterial",
748
1316
  {
749
1317
  color: theme.sectionOutline,
@@ -756,15 +1324,15 @@ var Picker = ({ box, theme }) => {
756
1324
  }
757
1325
  )
758
1326
  ] }),
759
- planes.map((plane) => /* @__PURE__ */ jsxs3(
1327
+ planes.map((plane) => /* @__PURE__ */ jsxs4(
760
1328
  "group",
761
1329
  {
762
1330
  position: [plane.position.x, plane.position.y, plane.position.z],
763
1331
  rotation: AXIS_ROTATIONS[plane.axis],
764
1332
  children: [
765
- /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, ...planeHandlers(plane), children: [
766
- /* @__PURE__ */ jsx3("planeGeometry", { args: [plane.size, plane.size] }),
767
- /* @__PURE__ */ jsx3(
1333
+ /* @__PURE__ */ jsxs4("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, ...planeHandlers(plane), children: [
1334
+ /* @__PURE__ */ jsx8("planeGeometry", { args: [plane.size, plane.size] }),
1335
+ /* @__PURE__ */ jsx8(
768
1336
  "meshBasicMaterial",
769
1337
  {
770
1338
  color: AXIS_COLORS[plane.axis],
@@ -775,14 +1343,14 @@ var Picker = ({ box, theme }) => {
775
1343
  }
776
1344
  )
777
1345
  ] }),
778
- /* @__PURE__ */ jsx3(
1346
+ /* @__PURE__ */ jsx8(
779
1347
  "lineLoop",
780
1348
  {
781
1349
  geometry: square,
782
1350
  scale: plane.size / 2,
783
1351
  renderOrder: SECTION_RENDER_ORDER.outline,
784
1352
  raycast: () => null,
785
- children: /* @__PURE__ */ jsx3("lineBasicMaterial", { color: AXIS_COLORS[plane.axis], transparent: true, opacity: 0.7 })
1353
+ children: /* @__PURE__ */ jsx8("lineBasicMaterial", { color: AXIS_COLORS[plane.axis], transparent: true, opacity: 0.7 })
786
1354
  }
787
1355
  )
788
1356
  ]
@@ -793,22 +1361,22 @@ var Picker = ({ box, theme }) => {
793
1361
  };
794
1362
  var CutSheet = ({ box, section, theme }) => {
795
1363
  const square = useSquare();
796
- const cut = useMemo3(() => resolveSectionPlane(section, box), [box, section]);
797
- const span = useMemo3(() => box.getSize(new Vector34()).length(), [box]);
798
- const placement = useMemo3(() => {
1364
+ const cut = useMemo5(() => resolveSectionPlane(section, box), [box, section]);
1365
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1366
+ const placement = useMemo5(() => {
799
1367
  if (!cut) return null;
800
1368
  const position = sectionAnchor(box, cut.plane).addScaledVector(
801
1369
  cut.plane.normal,
802
1370
  -span * SHEET_SETBACK
803
1371
  );
804
- return { position, quaternion: new Quaternion2().setFromUnitVectors(Z, cut.plane.normal) };
1372
+ return { position, quaternion: new Quaternion3().setFromUnitVectors(Z, cut.plane.normal) };
805
1373
  }, [box, cut, span]);
806
1374
  if (!placement) return null;
807
1375
  const size = span * OUTLINE_SCALE;
808
- return /* @__PURE__ */ jsxs3("group", { position: placement.position, quaternion: placement.quaternion, children: [
809
- /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
810
- /* @__PURE__ */ jsx3("planeGeometry", { args: [size, size] }),
811
- /* @__PURE__ */ jsx3(
1376
+ return /* @__PURE__ */ jsxs4("group", { position: placement.position, quaternion: placement.quaternion, children: [
1377
+ /* @__PURE__ */ jsxs4("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
1378
+ /* @__PURE__ */ jsx8("planeGeometry", { args: [size, size] }),
1379
+ /* @__PURE__ */ jsx8(
812
1380
  "meshBasicMaterial",
813
1381
  {
814
1382
  color: theme.sectionOutline,
@@ -819,14 +1387,14 @@ var CutSheet = ({ box, section, theme }) => {
819
1387
  }
820
1388
  )
821
1389
  ] }),
822
- /* @__PURE__ */ jsx3(
1390
+ /* @__PURE__ */ jsx8(
823
1391
  "lineLoop",
824
1392
  {
825
1393
  geometry: square,
826
1394
  scale: size / 2,
827
1395
  renderOrder: SECTION_RENDER_ORDER.outline,
828
1396
  raycast: () => null,
829
- children: /* @__PURE__ */ jsx3(
1397
+ children: /* @__PURE__ */ jsx8(
830
1398
  "lineBasicMaterial",
831
1399
  {
832
1400
  color: theme.sectionOutline,
@@ -843,32 +1411,32 @@ var CutSheet = ({ box, section, theme }) => {
843
1411
 
844
1412
  // src/measure-tool.tsx
845
1413
  import { Html, Line } from "@react-three/drei";
846
- import { useFrame as useFrame2, useThree as useThree4 } from "@react-three/fiber";
1414
+ import { useFrame as useFrame2, useThree as useThree6 } from "@react-three/fiber";
847
1415
  import {
848
- useEffect as useEffect3,
849
- useLayoutEffect,
850
- useMemo as useMemo4,
851
- useRef as useRef3,
852
- useState as useState3,
1416
+ useEffect as useEffect5,
1417
+ useLayoutEffect as useLayoutEffect3,
1418
+ useMemo as useMemo6,
1419
+ useRef as useRef4,
1420
+ useState as useState5,
853
1421
  useSyncExternalStore as useSyncExternalStore2
854
1422
  } from "react";
855
1423
  import {
856
- Matrix4,
1424
+ Matrix4 as Matrix42,
857
1425
  Raycaster as Raycaster2,
858
1426
  SphereGeometry,
859
- Vector2 as Vector23,
860
- Vector3 as Vector36
1427
+ Vector2 as Vector22,
1428
+ Vector3 as Vector37
861
1429
  } from "three";
862
1430
 
863
1431
  // src/render/resample.ts
864
- import { Vector3 as Vector35 } from "three";
1432
+ import { Vector3 as Vector36 } from "three";
865
1433
  var ON_PLANE = 1e-7;
866
1434
  var STITCH = 1e-5;
867
1435
  var COLLINEAR = 1e-6;
868
1436
  function clipSegments(positions, plane) {
869
1437
  const out = [];
870
- const a = new Vector35();
871
- const b = new Vector35();
1438
+ const a = new Vector36();
1439
+ const b = new Vector36();
872
1440
  const count = Math.floor(positions.length / 6);
873
1441
  for (let i = 0; i < count; i += 1) {
874
1442
  a.set(positions[i * 6], positions[i * 6 + 1], positions[i * 6 + 2]);
@@ -883,11 +1451,11 @@ function clipSegments(positions, plane) {
883
1451
  return Float32Array.from(out);
884
1452
  }
885
1453
  function sectionContour(positions, regionOf, plane) {
886
- const a = new Vector35();
887
- const b = new Vector35();
888
- const c = new Vector35();
889
- const p = new Vector35();
890
- const q = new Vector35();
1454
+ const a = new Vector36();
1455
+ const b = new Vector36();
1456
+ const c = new Vector36();
1457
+ const p = new Vector36();
1458
+ const q = new Vector36();
891
1459
  const byRegion = /* @__PURE__ */ new Map();
892
1460
  const triangleCount = Math.floor(positions.length / 9);
893
1461
  for (let t = 0; t < triangleCount; t += 1) {
@@ -945,17 +1513,17 @@ function stitch(segments, out) {
945
1513
  return into.set(segments[o], segments[o + 1], segments[o + 2]);
946
1514
  };
947
1515
  const direction = (i, into) => {
948
- const s = new Vector35();
949
- const e = new Vector35();
1516
+ const s = new Vector36();
1517
+ const e = new Vector36();
950
1518
  point(i, 0, s);
951
1519
  point(i, 1, e);
952
1520
  return into.subVectors(e, s).normalize();
953
1521
  };
954
1522
  const consumed = new Uint8Array(count);
955
- const d = new Vector35();
956
- const n = new Vector35();
1523
+ const d = new Vector36();
1524
+ const n = new Vector36();
957
1525
  const next = (i, end) => {
958
- const at = point(i, end, new Vector35());
1526
+ const at = point(i, end, new Vector36());
959
1527
  const list = ends.get(key(at.x, at.y, at.z)) ?? [];
960
1528
  if (list.length !== 2) return null;
961
1529
  const other = list.find((entry) => entry >> 1 !== i);
@@ -984,8 +1552,8 @@ function stitch(segments, out) {
984
1552
  };
985
1553
  const back = walk(0);
986
1554
  const forward = walk(1);
987
- const s = point(back.index, back.end, new Vector35());
988
- const e = point(forward.index, forward.end, new Vector35());
1555
+ const s = point(back.index, back.end, new Vector36());
1556
+ const e = point(forward.index, forward.end, new Vector36());
989
1557
  out.push(s.x, s.y, s.z, e.x, e.y, e.z);
990
1558
  }
991
1559
  }
@@ -993,8 +1561,8 @@ function pointInContour(point, plane, contour) {
993
1561
  const [u, v] = planeAxes(plane);
994
1562
  const px = point.dot(u);
995
1563
  const py = point.dot(v);
996
- const a = new Vector35();
997
- const b = new Vector35();
1564
+ const a = new Vector36();
1565
+ const b = new Vector36();
998
1566
  let inside = false;
999
1567
  const count = Math.floor(contour.length / 6);
1000
1568
  for (let i = 0; i < count; i += 1) {
@@ -1011,15 +1579,15 @@ function pointInContour(point, plane, contour) {
1011
1579
  return inside;
1012
1580
  }
1013
1581
  function capHit(ray, plane, contour) {
1014
- const point = ray.intersectPlane(plane, new Vector35());
1582
+ const point = ray.intersectPlane(plane, new Vector36());
1015
1583
  if (!point) return null;
1016
1584
  return pointInContour(point, plane, contour) ? point : null;
1017
1585
  }
1018
1586
  function planeAxes(plane) {
1019
1587
  const n = plane.normal;
1020
- const seed = Math.abs(n.x) < 0.9 ? new Vector35(1, 0, 0) : new Vector35(0, 1, 0);
1021
- const u = new Vector35().crossVectors(n, seed).normalize();
1022
- const v = new Vector35().crossVectors(n, u).normalize();
1588
+ const seed = Math.abs(n.x) < 0.9 ? new Vector36(1, 0, 0) : new Vector36(0, 1, 0);
1589
+ const u = new Vector36().crossVectors(n, seed).normalize();
1590
+ const v = new Vector36().crossVectors(n, u).normalize();
1023
1591
  return [u, v];
1024
1592
  }
1025
1593
  function sampleSection(meshPositions, regionOf, edgePositions, plane) {
@@ -1032,8 +1600,8 @@ function sampleSection(meshPositions, regionOf, edgePositions, plane) {
1032
1600
  }
1033
1601
 
1034
1602
  // src/measure-tool.tsx
1035
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1036
- var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
1603
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1604
+ var FURNITURE5 = { [EXCLUDE_FROM_FRAME]: true };
1037
1605
  var MEASURE_LABEL_CLASS = "toolpath-measure-label";
1038
1606
  var MeasureTool = ({
1039
1607
  mode = "distance",
@@ -1046,26 +1614,26 @@ var MeasureTool = ({
1046
1614
  }) => {
1047
1615
  const store = useSectionStore();
1048
1616
  const box = useContentBox();
1049
- const resolved = useMemo4(() => resolveTheme(theme), [theme]);
1617
+ const resolved = useMemo6(() => resolveTheme(theme), [theme]);
1050
1618
  const picking = useSyncExternalStore2(store.subscribe, store.isPicking, store.isPicking);
1051
- const [held, setHeld] = useState3([]);
1619
+ const [held, setHeld] = useState5([]);
1052
1620
  const controlled = measurements !== void 0;
1053
1621
  const list = controlled ? measurements : held;
1054
- const [draft, setDraft] = useState3([]);
1055
- useEffect3(() => {
1622
+ const [draft, setDraft] = useState5([]);
1623
+ useEffect5(() => {
1056
1624
  store.setEngaged(true);
1057
1625
  return () => store.setEngaged(false);
1058
1626
  }, [store]);
1059
- useEffect3(() => {
1627
+ useEffect5(() => {
1060
1628
  setDraft([]);
1061
1629
  }, [mode]);
1062
1630
  const commit = (next) => {
1063
1631
  if (!controlled) setHeld(next);
1064
1632
  onChange?.(next);
1065
1633
  };
1066
- const latest = useRef3({ list, commit });
1634
+ const latest = useRef4({ list, commit });
1067
1635
  latest.current = { list, commit };
1068
- useEffect3(() => {
1636
+ useEffect5(() => {
1069
1637
  const onKey = (event) => {
1070
1638
  if (event.key === "Escape") {
1071
1639
  setDraft([]);
@@ -1093,8 +1661,8 @@ var MeasureTool = ({
1093
1661
  };
1094
1662
  if (box.isEmpty()) return null;
1095
1663
  const label = className(labelClassName);
1096
- return /* @__PURE__ */ jsxs4("group", { userData: FURNITURE4, children: [
1097
- picking ? null : /* @__PURE__ */ jsx4(
1664
+ return /* @__PURE__ */ jsxs5("group", { userData: FURNITURE5, children: [
1665
+ picking ? null : /* @__PURE__ */ jsx9(
1098
1666
  Snapper,
1099
1667
  {
1100
1668
  mode,
@@ -1105,7 +1673,7 @@ var MeasureTool = ({
1105
1673
  label
1106
1674
  }
1107
1675
  ),
1108
- draft.map((point, index) => /* @__PURE__ */ jsx4(
1676
+ draft.map((point, index) => /* @__PURE__ */ jsx9(
1109
1677
  ScreenDot,
1110
1678
  {
1111
1679
  point,
@@ -1114,8 +1682,8 @@ var MeasureTool = ({
1114
1682
  },
1115
1683
  index
1116
1684
  )),
1117
- draft.length === 2 ? /* @__PURE__ */ jsx4(Segment, { points: [draft[0], draft[1]], color: resolved.measure }) : null,
1118
- list.map((measurement) => /* @__PURE__ */ jsx4(
1685
+ draft.length === 2 ? /* @__PURE__ */ jsx9(Segment, { points: [draft[0], draft[1]], color: resolved.measure }) : null,
1686
+ list.map((measurement) => /* @__PURE__ */ jsx9(
1119
1687
  MeasurementView,
1120
1688
  {
1121
1689
  measurement,
@@ -1129,30 +1697,30 @@ var MeasureTool = ({
1129
1697
  ] });
1130
1698
  };
1131
1699
  var NO_POINTER = { pointerEvents: "none" };
1132
- var NOWHERE = [new Vector36(), new Vector36()];
1700
+ var NOWHERE = [new Vector37(), new Vector37()];
1133
1701
  var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1134
- const camera = useThree4((state) => state.camera);
1135
- const scene = useThree4((state) => state.scene);
1136
- const size = useThree4((state) => state.size);
1137
- const domElement = useThree4((state) => state.gl.domElement);
1138
- const invalidate = useThree4((state) => state.invalidate);
1702
+ const camera = useThree6((state) => state.camera);
1703
+ const scene = useThree6((state) => state.scene);
1704
+ const size = useThree6((state) => state.size);
1705
+ const domElement = useThree6((state) => state.gl.domElement);
1706
+ const invalidate = useThree6((state) => state.invalidate);
1139
1707
  const isTap = useTapGuard();
1140
- const marker = useRef3(null);
1141
- const edgeLine = useRef3(null);
1142
- const band = useRef3(null);
1143
- const bandLabel = useRef3(null);
1144
- const bandText = useRef3(null);
1145
- const snap = useRef3(null);
1708
+ const marker = useRef4(null);
1709
+ const edgeLine = useRef4(null);
1710
+ const band = useRef4(null);
1711
+ const bandLabel = useRef4(null);
1712
+ const bandText = useRef4(null);
1713
+ const snap = useRef4(null);
1146
1714
  const dot = useDot();
1147
- useLayoutEffect(() => {
1715
+ useLayoutEffect3(() => {
1148
1716
  if (edgeLine.current) edgeLine.current.visible = false;
1149
1717
  if (band.current) band.current.visible = false;
1150
1718
  }, []);
1151
- const current = useRef3({ mode, draft, onPlace, format, size });
1719
+ const current = useRef4({ mode, draft, onPlace, format, size });
1152
1720
  current.current = { mode, draft, onPlace, format, size };
1153
- useEffect3(() => {
1721
+ useEffect5(() => {
1154
1722
  const raycaster = new Raycaster2();
1155
- const pointer = new Vector23();
1723
+ const pointer = new Vector22();
1156
1724
  const samples = new SampleCache();
1157
1725
  const hide = () => {
1158
1726
  if (!snap.current) return;
@@ -1182,7 +1750,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1182
1750
  const snapped = snapAt(target, target.edges, camera, viewport);
1183
1751
  const from = placed[placed.length - 1];
1184
1752
  const lock = shift && from ? lockToAxis(from, snapped.point) : null;
1185
- const found = lock ? { ...snapped, point: new Vector36(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1753
+ const found = lock ? { ...snapped, point: new Vector37(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1186
1754
  snap.current = found;
1187
1755
  const node = marker.current;
1188
1756
  if (node) {
@@ -1256,7 +1824,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1256
1824
  window.removeEventListener("keyup", key);
1257
1825
  };
1258
1826
  }, [camera, domElement, invalidate, isTap, scene, theme.measure]);
1259
- useEffect3(() => {
1827
+ useEffect5(() => {
1260
1828
  if (draft.length === 0 && band.current) {
1261
1829
  band.current.visible = false;
1262
1830
  if (bandLabel.current) bandLabel.current.visible = false;
@@ -1267,9 +1835,9 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1267
1835
  invalidate();
1268
1836
  }
1269
1837
  }, [draft, invalidate]);
1270
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1271
- /* @__PURE__ */ jsx4("group", { ref: marker, visible: false, children: /* @__PURE__ */ jsx4("mesh", { geometry: dot, renderOrder: MEASURE_RENDER_ORDER, raycast: () => null, children: /* @__PURE__ */ jsx4("meshBasicMaterial", { color: theme.measureSnap, depthTest: false, depthWrite: false }) }) }),
1272
- /* @__PURE__ */ jsx4(
1838
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1839
+ /* @__PURE__ */ jsx9("group", { ref: marker, visible: false, children: /* @__PURE__ */ jsx9("mesh", { geometry: dot, renderOrder: MEASURE_RENDER_ORDER, raycast: () => null, children: /* @__PURE__ */ jsx9("meshBasicMaterial", { color: theme.measureSnap, depthTest: false, depthWrite: false }) }) }),
1840
+ /* @__PURE__ */ jsx9(
1273
1841
  Line,
1274
1842
  {
1275
1843
  ref: edgeLine,
@@ -1283,7 +1851,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1283
1851
  raycast: () => null
1284
1852
  }
1285
1853
  ),
1286
- /* @__PURE__ */ jsx4(
1854
+ /* @__PURE__ */ jsx9(
1287
1855
  Line,
1288
1856
  {
1289
1857
  ref: band,
@@ -1297,7 +1865,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1297
1865
  raycast: () => null
1298
1866
  }
1299
1867
  ),
1300
- /* @__PURE__ */ jsx4("group", { ref: bandLabel, visible: false, children: /* @__PURE__ */ jsx4(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx4(
1868
+ /* @__PURE__ */ jsx9("group", { ref: bandLabel, visible: false, children: /* @__PURE__ */ jsx9(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx9(
1301
1869
  "div",
1302
1870
  {
1303
1871
  ref: bandText,
@@ -1344,7 +1912,7 @@ function targetUnderRay(raycaster, scene, samples) {
1344
1912
  edges: samples.edgesFor(hit.object)
1345
1913
  };
1346
1914
  }
1347
- const local = new Matrix4();
1915
+ const local = new Matrix42();
1348
1916
  for (const mesh of cutMeshes(scene)) {
1349
1917
  const planes = clippingPlanesOf(mesh);
1350
1918
  const sample = samples.for(mesh, planes);
@@ -1407,7 +1975,7 @@ var SampleCache = class {
1407
1975
  const key = planes.map((plane) => `${plane.normal.x},${plane.normal.y},${plane.normal.z},${plane.constant}`).join("|");
1408
1976
  const cached = this.held.get(mesh);
1409
1977
  if (cached && cached.key === key) return cached;
1410
- const local = new Matrix4().copy(mesh.matrixWorld).invert();
1978
+ const local = new Matrix42().copy(mesh.matrixWorld).invert();
1411
1979
  const localPlanes = planes.map((plane) => plane.clone().applyMatrix4(local));
1412
1980
  const positions = mesh.geometry.getAttribute("position")?.array ?? [];
1413
1981
  const regionOf = this.regionsOf(mesh.geometry);
@@ -1462,35 +2030,35 @@ var MeasurementView = ({
1462
2030
  const anchor = measurementLabelAnchor(measurement);
1463
2031
  if (measurement.kind === "distance") {
1464
2032
  const [a2, b2] = measurement.points;
1465
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1466
- /* @__PURE__ */ jsx4(Segment, { points: [a2, b2], color: theme.measure }),
1467
- /* @__PURE__ */ jsx4(ScreenDot, { point: a2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1468
- /* @__PURE__ */ jsx4(ScreenDot, { point: b2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1469
- showDeltas ? /* @__PURE__ */ jsx4(DeltaLegs, { a: a2, b: b2, id: measurement.id, format, label }) : null,
1470
- /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "distance", id: measurement.id, className: label })
2033
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2034
+ /* @__PURE__ */ jsx9(Segment, { points: [a2, b2], color: theme.measure }),
2035
+ /* @__PURE__ */ jsx9(ScreenDot, { point: a2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
2036
+ /* @__PURE__ */ jsx9(ScreenDot, { point: b2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
2037
+ showDeltas ? /* @__PURE__ */ jsx9(DeltaLegs, { a: a2, b: b2, id: measurement.id, format, label }) : null,
2038
+ /* @__PURE__ */ jsx9(Label, { at: anchor, text, kind: "distance", id: measurement.id, className: label })
1471
2039
  ] });
1472
2040
  }
1473
2041
  const [a, vertex, b] = measurement.points;
1474
2042
  const arc = angleArc(vertex, a, b, angleArcRadius(vertex, a, b));
1475
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1476
- /* @__PURE__ */ jsx4(Segment, { points: [vertex, a], color: theme.measure }),
1477
- /* @__PURE__ */ jsx4(Segment, { points: [vertex, b], color: theme.measure }),
1478
- /* @__PURE__ */ jsx4(Polyline, { points: arc, color: theme.measure }),
1479
- /* @__PURE__ */ jsx4(ScreenDot, { point: a, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1480
- /* @__PURE__ */ jsx4(ScreenDot, { point: vertex, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1481
- /* @__PURE__ */ jsx4(ScreenDot, { point: b, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1482
- /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "angle", id: measurement.id, className: label })
2043
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2044
+ /* @__PURE__ */ jsx9(Segment, { points: [vertex, a], color: theme.measure }),
2045
+ /* @__PURE__ */ jsx9(Segment, { points: [vertex, b], color: theme.measure }),
2046
+ /* @__PURE__ */ jsx9(Polyline, { points: arc, color: theme.measure }),
2047
+ /* @__PURE__ */ jsx9(ScreenDot, { point: a, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
2048
+ /* @__PURE__ */ jsx9(ScreenDot, { point: vertex, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
2049
+ /* @__PURE__ */ jsx9(ScreenDot, { point: b, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
2050
+ /* @__PURE__ */ jsx9(Label, { at: anchor, text, kind: "angle", id: measurement.id, className: label })
1483
2051
  ] });
1484
2052
  };
1485
2053
  var DeltaLegs = ({ a, b, id, format, label }) => {
1486
- const camera = useThree4((state) => state.camera);
1487
- const size = useThree4((state) => state.size);
1488
- const invalidate = useThree4((state) => state.invalidate);
1489
- const legs = useMemo4(() => deltaLegs(a, b), [a, b]);
1490
- const lines = useRef3([]);
1491
- const centre = useMemo4(() => {
2054
+ const camera = useThree6((state) => state.camera);
2055
+ const size = useThree6((state) => state.size);
2056
+ const invalidate = useThree6((state) => state.invalidate);
2057
+ const legs = useMemo6(() => deltaLegs(a, b), [a, b]);
2058
+ const lines = useRef4([]);
2059
+ const centre = useMemo6(() => {
1492
2060
  const at = midpoint(a, b);
1493
- return new Vector36(at.x, at.y, at.z);
2061
+ return new Vector37(at.x, at.y, at.z);
1494
2062
  }, [a, b]);
1495
2063
  useFrame2(() => {
1496
2064
  const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
@@ -1504,16 +2072,16 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1504
2072
  }
1505
2073
  if (changed) invalidate();
1506
2074
  });
1507
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1508
- legs.map((leg, index) => /* @__PURE__ */ jsx4(
2075
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2076
+ legs.map((leg, index) => /* @__PURE__ */ jsx9(
1509
2077
  Line,
1510
2078
  {
1511
2079
  ref: (line) => {
1512
2080
  lines.current[index] = line;
1513
2081
  },
1514
2082
  points: [
1515
- new Vector36(leg.from.x, leg.from.y, leg.from.z),
1516
- new Vector36(leg.to.x, leg.to.y, leg.to.z)
2083
+ new Vector37(leg.from.x, leg.from.y, leg.from.z),
2084
+ new Vector37(leg.to.x, leg.to.y, leg.to.z)
1517
2085
  ],
1518
2086
  color: AXIS_COLORS[leg.axis],
1519
2087
  lineWidth: MEASURE_LINE_PIXELS.leg,
@@ -1528,7 +2096,7 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1528
2096
  },
1529
2097
  leg.axis
1530
2098
  )),
1531
- legs.map((leg) => /* @__PURE__ */ jsx4(
2099
+ legs.map((leg) => /* @__PURE__ */ jsx9(
1532
2100
  Label,
1533
2101
  {
1534
2102
  at: midpoint(leg.from, leg.to),
@@ -1542,14 +2110,14 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1542
2110
  ))
1543
2111
  ] });
1544
2112
  };
1545
- var Label = ({ at, text, kind, id, axis, className: name }) => /* @__PURE__ */ jsx4("group", { position: [at.x, at.y, at.z], children: /* @__PURE__ */ jsx4(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx4("div", { className: name, "data-measure-label": kind, "data-measurement-id": id, "data-axis": axis, children: text }) }) });
2113
+ var Label = ({ at, text, kind, id, axis, className: name }) => /* @__PURE__ */ jsx9("group", { position: [at.x, at.y, at.z], children: /* @__PURE__ */ jsx9(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx9("div", { className: name, "data-measure-label": kind, "data-measurement-id": id, "data-axis": axis, children: text }) }) });
1546
2114
  var ScreenDot = ({ point, pixels, color }) => {
1547
- const camera = useThree4((state) => state.camera);
1548
- const size = useThree4((state) => state.size);
1549
- const invalidate = useThree4((state) => state.invalidate);
1550
- const node = useRef3(null);
2115
+ const camera = useThree6((state) => state.camera);
2116
+ const size = useThree6((state) => state.size);
2117
+ const invalidate = useThree6((state) => state.invalidate);
2118
+ const node = useRef4(null);
1551
2119
  const dot = useDot();
1552
- const position = useMemo4(() => new Vector36(point.x, point.y, point.z), [point]);
2120
+ const position = useMemo6(() => new Vector37(point.x, point.y, point.z), [point]);
1553
2121
  useFrame2(() => {
1554
2122
  const group = node.current;
1555
2123
  if (!group) return;
@@ -1559,21 +2127,21 @@ var ScreenDot = ({ point, pixels, color }) => {
1559
2127
  invalidate();
1560
2128
  }
1561
2129
  });
1562
- return /* @__PURE__ */ jsx4("group", { ref: node, position, children: /* @__PURE__ */ jsx4("mesh", { geometry: dot, renderOrder: MEASURE_RENDER_ORDER, raycast: () => null, children: /* @__PURE__ */ jsx4("meshBasicMaterial", { color, depthTest: false, depthWrite: false }) }) });
2130
+ return /* @__PURE__ */ jsx9("group", { ref: node, position, children: /* @__PURE__ */ jsx9("mesh", { geometry: dot, renderOrder: MEASURE_RENDER_ORDER, raycast: () => null, children: /* @__PURE__ */ jsx9("meshBasicMaterial", { color, depthTest: false, depthWrite: false }) }) });
1563
2131
  };
1564
2132
  var Segment = ({ points, color }) => {
1565
2133
  const [a, b] = points;
1566
- const ends = useMemo4(
1567
- () => [new Vector36(a.x, a.y, a.z), new Vector36(b.x, b.y, b.z)],
2134
+ const ends = useMemo6(
2135
+ () => [new Vector37(a.x, a.y, a.z), new Vector37(b.x, b.y, b.z)],
1568
2136
  // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1569
2137
  // and a rebuilt array would rebuild the line's geometry with it.
1570
2138
  [a.x, a.y, a.z, b.x, b.y, b.z]
1571
2139
  );
1572
- return /* @__PURE__ */ jsx4(Polyline, { points: ends, color });
2140
+ return /* @__PURE__ */ jsx9(Polyline, { points: ends, color });
1573
2141
  };
1574
2142
  var Polyline = ({ points, color }) => {
1575
2143
  if (points.length < 2) return null;
1576
- return /* @__PURE__ */ jsx4(
2144
+ return /* @__PURE__ */ jsx9(
1577
2145
  Line,
1578
2146
  {
1579
2147
  points,
@@ -1588,8 +2156,8 @@ var Polyline = ({ points, color }) => {
1588
2156
  );
1589
2157
  };
1590
2158
  function useDot() {
1591
- const geometry = useMemo4(() => new SphereGeometry(1, 16, 12), []);
1592
- useEffect3(() => () => geometry.dispose(), [geometry]);
2159
+ const geometry = useMemo6(() => new SphereGeometry(1, 16, 12), []);
2160
+ useEffect5(() => () => geometry.dispose(), [geometry]);
1593
2161
  return geometry;
1594
2162
  }
1595
2163
  function isEditing(target) {
@@ -1640,10 +2208,14 @@ export {
1640
2208
  AXES_PLANE_SCALE,
1641
2209
  AXIS_COLORS,
1642
2210
  Axes,
2211
+ BANANA_GAP,
2212
+ BANANA_MODEL_URL,
2213
+ Banana,
1643
2214
  CAD_CAMERA_UP,
1644
2215
  CANDIDATE_WEIGHT,
1645
2216
  CAP_HATCH,
1646
2217
  CHAMFER,
2218
+ CONTROL_SCHEME_OPTIONS,
1647
2219
  CadCameraControls,
1648
2220
  DEFAULT_FIT_MARGIN,
1649
2221
  DEFAULT_SECTION_NORMAL,
@@ -1662,6 +2234,7 @@ export {
1662
2234
  HIGHLIGHT_COLORS,
1663
2235
  HIGHLIGHT_WEIGHT,
1664
2236
  HOVER_WEIGHT,
2237
+ HoverCard,
1665
2238
  MAX_FRAME_RATIO,
1666
2239
  MEASURE_LABEL_CLASS,
1667
2240
  MEASURE_LINE_PIXELS,
@@ -1686,17 +2259,21 @@ export {
1686
2259
  PartMesh,
1687
2260
  PartReportFormatError,
1688
2261
  REGION_ATTRIBUTE,
2262
+ SECTION_GIZMO_FRAME_SCALE,
1689
2263
  SECTION_RENDER_ORDER,
1690
2264
  SNAP_MARKER_PIXELS,
1691
2265
  SNAP_PIXELS,
1692
2266
  SectionTool,
1693
2267
  SectionView,
2268
+ Stock,
1694
2269
  TAP_SLOP,
1695
2270
  UnsupportedKernelVersionError,
1696
2271
  VIEW_NAMES,
1697
2272
  VIEW_SIGNS,
1698
2273
  ViewCube,
1699
2274
  Viewer,
2275
+ ViewerToolbar,
2276
+ ViewerToolbarProvider,
1700
2277
  adaptedUp,
1701
2278
  angleArc,
1702
2279
  angleArcRadius,
@@ -1707,8 +2284,11 @@ export {
1707
2284
  arrowPlacement,
1708
2285
  aspectRatio,
1709
2286
  axesPlanes,
2287
+ bananaFrameBounds,
2288
+ bananaPosition,
1710
2289
  bestOwner,
1711
2290
  boundsFromBox,
2291
+ boxStockBounds,
1712
2292
  buildPick,
1713
2293
  buildRegionAttribute,
1714
2294
  buildRegionIndex,
@@ -1732,6 +2312,7 @@ export {
1732
2312
  deltaBetween,
1733
2313
  deltaLegs,
1734
2314
  directionColor,
2315
+ directionHighlights,
1735
2316
  directionIndexOf,
1736
2317
  directionLabel,
1737
2318
  distanceBetween,
@@ -1739,6 +2320,7 @@ export {
1739
2320
  excludedFromFrame,
1740
2321
  featureTypeRank,
1741
2322
  fitDistance,
2323
+ fixedBoxStockBounds,
1742
2324
  focusForPick,
1743
2325
  formatDegrees,
1744
2326
  formatMillimetres,
@@ -1781,6 +2363,7 @@ export {
1781
2363
  sectionDepthConstant,
1782
2364
  sectionDepthRange,
1783
2365
  sectionFromPick,
2366
+ sectionMeasurement,
1784
2367
  sectionOffset,
1785
2368
  sectionOptionsFromState,
1786
2369
  sectionPlane,
@@ -1798,6 +2381,7 @@ export {
1798
2381
  useSectionStore,
1799
2382
  useTapGuard,
1800
2383
  useViewerControls,
2384
+ useViewerToolbar,
1801
2385
  viewDirection,
1802
2386
  viewKind,
1803
2387
  viewUp,