@toolpath/viewer 1.5.0 → 2.1.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
@@ -152,6 +152,7 @@ import {
152
152
  trackDoubleTaps,
153
153
  trackTaps,
154
154
  useContentBox,
155
+ usePartContext,
155
156
  useRetarget,
156
157
  useSectionStore,
157
158
  useTapGuard,
@@ -161,7 +162,50 @@ import {
161
162
  viewUp,
162
163
  viewVector,
163
164
  visualSurfaces
164
- } from "./chunk-4TBLMR4Z.js";
165
+ } from "./chunk-WTTOUG4P.js";
166
+
167
+ // src/render/feature-triangles.ts
168
+ var EMPTY = { positions: new Float32Array(), normals: new Float32Array() };
169
+ function featureTriangles(model, geometry, tag) {
170
+ const position = geometry.getAttribute("position");
171
+ if (!position) return EMPTY;
172
+ const normal = geometry.getAttribute("normal");
173
+ const index = geometry.getIndex();
174
+ const vertex = (corner) => index ? index.getX(corner) : corner;
175
+ const triangleCount = Math.floor((index ? index.count : position.count) / 3);
176
+ const positions = [];
177
+ const normals = [];
178
+ for (const region of new Set(model.regionIndex.regionsForFeature(tag))) {
179
+ const range = model.regionIndex.rangeForRegion(region);
180
+ if (!range) continue;
181
+ for (let triangle = range.start; triangle < Math.min(range.end, triangleCount); triangle++) {
182
+ const corners = [vertex(triangle * 3), vertex(triangle * 3 + 1), vertex(triangle * 3 + 2)];
183
+ const at = corners.map((corner) => [
184
+ position.getX(corner),
185
+ position.getY(corner),
186
+ position.getZ(corner)
187
+ ]);
188
+ const out = normal ? corners.reduce(
189
+ (sum, corner) => [
190
+ sum[0] + normal.getX(corner),
191
+ sum[1] + normal.getY(corner),
192
+ sum[2] + normal.getZ(corner)
193
+ ],
194
+ [0, 0, 0]
195
+ ) : winding(at);
196
+ const length = Math.hypot(out[0], out[1], out[2]);
197
+ if (length === 0) continue;
198
+ positions.push(...at[0], ...at[1], ...at[2]);
199
+ normals.push(out[0] / length, out[1] / length, out[2] / length);
200
+ }
201
+ }
202
+ return { positions: new Float32Array(positions), normals: new Float32Array(normals) };
203
+ }
204
+ var winding = ([a, b, c]) => {
205
+ const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
206
+ const v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
207
+ return [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
208
+ };
165
209
 
166
210
  // src/hover-card.tsx
167
211
  import { useEffect, useState } from "react";
@@ -222,6 +266,7 @@ var StockIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */
222
266
  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
267
  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
268
  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" }) });
269
+ var ToolsIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M9 2h6v7H9zM9.5 9v9.5l2.5 3.5 2.5-3.5V9M9.5 12l5 2.5M9.5 15.5l5 2.5" }) });
225
270
  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
271
  var HoverIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v12H9l-5 4zm4 5h8m-8 3h5" }) });
227
272
  var FocusIcon = () => /* @__PURE__ */ jsx2(SvgIcon, { children: /* @__PURE__ */ jsx2("path", { d: "M4 4h16v16H4zM8 8h8v8H8zM2 12h4m12 0h4M12 2v4m0 12v4" }) });
@@ -236,6 +281,7 @@ var ICONS = {
236
281
  axes: AxesIcon,
237
282
  grid: GridIcon,
238
283
  banana: BananaIcon,
284
+ tools: ToolsIcon,
239
285
  directions: DirectionsIcon,
240
286
  hover: HoverIcon,
241
287
  focus: FocusIcon,
@@ -315,6 +361,14 @@ var BananaButton = () => /* @__PURE__ */ jsx3(
315
361
  label: (pressed) => pressed ? "Banana for scale (on)" : "Banana for scale"
316
362
  }
317
363
  );
364
+ var ToolsButton = () => /* @__PURE__ */ jsx3(
365
+ ToolbarButton,
366
+ {
367
+ action: "tools",
368
+ icon: "tools",
369
+ label: (pressed) => pressed ? "Hide tools on the part" : "Show tools on the part"
370
+ }
371
+ );
318
372
  var DirectionsButton = () => /* @__PURE__ */ jsx3(
319
373
  ToolbarButton,
320
374
  {
@@ -376,7 +430,8 @@ var DefaultControls = () => {
376
430
  controls.stock ? /* @__PURE__ */ jsx3(StockButton, {}, "stock") : null,
377
431
  controls.axes ? /* @__PURE__ */ jsx3(AxesButton, {}, "axes") : null,
378
432
  controls.grid ? /* @__PURE__ */ jsx3(GridButton, {}, "grid") : null,
379
- controls.banana ? /* @__PURE__ */ jsx3(BananaButton, {}, "banana") : null
433
+ controls.banana ? /* @__PURE__ */ jsx3(BananaButton, {}, "banana") : null,
434
+ controls.tools ? /* @__PURE__ */ jsx3(ToolsButton, {}, "tools") : null
380
435
  ].filter(Boolean);
381
436
  const analysis = [
382
437
  controls.directions ? /* @__PURE__ */ jsx3(DirectionsButton, {}, "directions") : null,
@@ -417,6 +472,7 @@ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
417
472
  AxesButton,
418
473
  GridButton,
419
474
  BananaButton,
475
+ ToolsButton,
420
476
  DirectionsButton,
421
477
  HoverButton,
422
478
  FocusButton,
@@ -430,20 +486,105 @@ var ViewerToolbar = Object.assign(ViewerToolbarRoot, {
430
486
 
431
487
  // src/stock.tsx
432
488
  import { useThree } from "@react-three/fiber";
433
- import { useEffect as useEffect2, useLayoutEffect, useMemo } from "react";
434
- import { BoxGeometry, Vector3 as Vector32 } from "three";
489
+ import { useEffect as useEffect2, useLayoutEffect, useMemo, useState as useState2 } from "react";
490
+
491
+ // src/render/stock-mesh.ts
492
+ import { Mesh } from "three";
493
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
494
+ async function parseStockGlb(data) {
495
+ const { scene } = await new GLTFLoader().parseAsync(data, "");
496
+ try {
497
+ return stockMeshGeometries(scene);
498
+ } finally {
499
+ scene.traverse((object) => {
500
+ if (!(object instanceof Mesh)) return;
501
+ object.geometry.dispose();
502
+ for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
503
+ material.dispose();
504
+ }
505
+ });
506
+ }
507
+ }
508
+ function stockMeshGeometries(scene) {
509
+ const geometries = [];
510
+ scene.updateWorldMatrix(true, true);
511
+ scene.traverse((object) => {
512
+ if (!(object instanceof Mesh)) return;
513
+ const geometry = object.geometry.clone().applyMatrix4(object.matrixWorld);
514
+ if (!geometry.hasAttribute("normal")) geometry.computeVertexNormals();
515
+ geometries.push(geometry);
516
+ });
517
+ if (geometries.length === 0) throw new Error("The stock GLB contains no mesh.");
518
+ return geometries;
519
+ }
520
+
521
+ // src/render/stock-mesh-buffer.ts
522
+ function createStockMeshBuffer() {
523
+ const owned = /* @__PURE__ */ new Set();
524
+ let disposed = false;
525
+ return {
526
+ request(decode, ready, failed) {
527
+ disposed = false;
528
+ let active = true;
529
+ void decode().then(
530
+ (geometries) => {
531
+ if (!active || disposed) {
532
+ geometries.forEach((geometry) => geometry.dispose());
533
+ return;
534
+ }
535
+ geometries.forEach((geometry) => owned.add(geometry));
536
+ ready(geometries);
537
+ },
538
+ (cause) => {
539
+ if (active && !disposed) {
540
+ failed(cause);
541
+ }
542
+ }
543
+ );
544
+ return () => {
545
+ active = false;
546
+ };
547
+ },
548
+ commit(geometries) {
549
+ const visible = new Set(geometries);
550
+ for (const geometry of owned) {
551
+ if (!visible.has(geometry)) {
552
+ owned.delete(geometry);
553
+ geometry.dispose();
554
+ }
555
+ }
556
+ },
557
+ dispose() {
558
+ disposed = true;
559
+ owned.forEach((geometry) => geometry.dispose());
560
+ owned.clear();
561
+ }
562
+ };
563
+ }
435
564
 
436
565
  // src/render/stock.ts
437
566
  import {
438
567
  Box3,
568
+ BoxGeometry,
569
+ CylinderGeometry,
439
570
  EdgesGeometry,
440
571
  Group,
441
572
  LineBasicMaterial,
442
573
  LineSegments,
443
- Mesh,
574
+ Matrix4,
575
+ Mesh as Mesh2,
444
576
  MeshLambertMaterial,
577
+ Quaternion,
445
578
  Vector3
446
579
  } from "three";
580
+ function boxFigure(bounds) {
581
+ return {
582
+ lower: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
583
+ upper: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }
584
+ };
585
+ }
586
+ var CYLINDER_SEGMENTS = 64;
587
+ var CYLINDER_AXIS = new Vector3(0, 1, 0);
447
588
  function fixedBoxStockBounds(geometry, dimensions, position = "model_centered", positionOffset = 0, offset = { x: 0, y: 0, z: 0 }) {
448
589
  if (![dimensions.x, dimensions.y, dimensions.z].every(Number.isFinite) || [dimensions.x, dimensions.y, dimensions.z].some((value) => value <= 0)) {
449
590
  throw new RangeError("Fixed stock dimensions must be finite and positive.");
@@ -456,13 +597,97 @@ function fixedBoxStockBounds(geometry, dimensions, position = "model_centered",
456
597
  }
457
598
  const part = partBounds(geometry);
458
599
  const center = part.getCenter(new Vector3());
459
- const z = position === "offset_from_top" ? part.max.z + positionOffset - dimensions.z / 2 : position === "offset_from_bottom" ? part.min.z - positionOffset + dimensions.z / 2 : center.z;
460
- const half = new Vector3(dimensions.x / 2, dimensions.y / 2, dimensions.z / 2);
600
+ const z = fixedStockLowerZ(part, dimensions.z, position, positionOffset);
461
601
  return new Box3(
462
- new Vector3(center.x, center.y, z).sub(half),
463
- new Vector3(center.x, center.y, z).add(half)
602
+ new Vector3(center.x - dimensions.x / 2, center.y - dimensions.y / 2, z),
603
+ new Vector3(center.x + dimensions.x / 2, center.y + dimensions.y / 2, z + dimensions.z)
464
604
  ).translate(new Vector3(offset.x, offset.y, offset.z));
465
605
  }
606
+ function fixedCylinderStock(geometry, size, position = "model_centered", positionOffset = 0) {
607
+ if (![size.diameter, size.length].every((value) => Number.isFinite(value) && value > 0)) {
608
+ throw new RangeError("Fixed stock diameter and length must be finite and positive.");
609
+ }
610
+ if (!Number.isFinite(positionOffset)) {
611
+ throw new RangeError("Fixed stock position offset must be finite.");
612
+ }
613
+ const part = partBounds(geometry);
614
+ const center = part.getCenter(new Vector3());
615
+ return {
616
+ origin: {
617
+ x: center.x,
618
+ y: center.y,
619
+ z: fixedStockLowerZ(part, size.length, position, positionOffset)
620
+ },
621
+ axis: { x: 0, y: 0, z: 1 },
622
+ diameter: size.diameter,
623
+ length: size.length
624
+ };
625
+ }
626
+ function fixedStockLowerZ(part, length, position, positionOffset) {
627
+ if (position === "offset_from_top") return part.max.z + positionOffset - length;
628
+ if (position === "offset_from_bottom") return part.min.z - positionOffset;
629
+ return (part.min.z + part.max.z - length) / 2;
630
+ }
631
+ function cylinderStockGeometry(input) {
632
+ const figure = input.partGeometry ? fixedCylinderStock(input.partGeometry, input, input.position, input.positionOffset) : input;
633
+ const { origin, diameter, length } = figure;
634
+ if (![origin.x, origin.y, origin.z].every(Number.isFinite)) {
635
+ throw new RangeError("Cylinder stock origin must be finite.");
636
+ }
637
+ if (![diameter, length].every((value) => Number.isFinite(value) && value > 0)) {
638
+ throw new RangeError("Cylinder stock diameter and length must be finite and positive.");
639
+ }
640
+ const axis = new Vector3(figure.axis.x, figure.axis.y, figure.axis.z);
641
+ const norm = axis.length();
642
+ if (!Number.isFinite(norm) || norm === 0) {
643
+ throw new RangeError("Cylinder stock axis must be finite and non-zero.");
644
+ }
645
+ axis.divideScalar(norm);
646
+ const center = axis.clone().multiplyScalar(length / 2).add(new Vector3(origin.x, origin.y, origin.z));
647
+ return new CylinderGeometry(diameter / 2, diameter / 2, length, CYLINDER_SEGMENTS).applyQuaternion(new Quaternion().setFromUnitVectors(CYLINDER_AXIS, axis)).translate(center.x, center.y, center.z);
648
+ }
649
+ function boxStockGeometry(input) {
650
+ const figure = input.partGeometry ? boxFigure(
651
+ input.dimensions ? fixedBoxStockBounds(
652
+ input.partGeometry,
653
+ input.dimensions,
654
+ input.position,
655
+ input.positionOffset,
656
+ input.offset
657
+ ) : boxStockBounds(input.partGeometry, input.allowance, input.offset)
658
+ ) : input;
659
+ return orientedBoxStockGeometry(figure);
660
+ }
661
+ function orientedBoxStockGeometry(figure) {
662
+ const { lower, upper } = figure;
663
+ const frame = figure.frame ?? {
664
+ location: { x: 0, y: 0, z: 0 },
665
+ axis: { x: 0, y: 0, z: 1 },
666
+ refDirection: { x: 1, y: 0, z: 0 }
667
+ };
668
+ const { location, axis, refDirection } = frame;
669
+ if (![location, axis, refDirection, lower, upper].every(
670
+ (vector) => [vector.x, vector.y, vector.z].every(Number.isFinite)
671
+ )) {
672
+ throw new RangeError("Box stock frame and corners must be finite.");
673
+ }
674
+ const size = new Vector3(upper.x - lower.x, upper.y - lower.y, upper.z - lower.z);
675
+ if (size.toArray().some((value) => !(value > 0))) {
676
+ throw new RangeError("Box stock upper corner must be above its lower corner on every axis.");
677
+ }
678
+ const z = new Vector3(axis.x, axis.y, axis.z).normalize();
679
+ const x = new Vector3(refDirection.x, refDirection.y, refDirection.z);
680
+ const reference = x.length();
681
+ x.addScaledVector(z, -x.dot(z));
682
+ if (!(z.lengthSq() > 0 && x.length() > 1e-9 * reference)) {
683
+ throw new RangeError(
684
+ "Box stock frame axis and reference direction must be non-zero and not parallel."
685
+ );
686
+ }
687
+ x.normalize();
688
+ const y = new Vector3().crossVectors(z, x);
689
+ 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));
690
+ }
466
691
  function boxStockBounds(geometry, allowance = 0, offset = { x: 0, y: 0, z: 0 }) {
467
692
  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);
468
693
  if (padding.toArray().some((value) => !Number.isFinite(value) || value < 0)) {
@@ -494,81 +719,167 @@ function partBounds(geometry) {
494
719
  }
495
720
  return box;
496
721
  }
497
- function createStock(geometry) {
722
+ function createStock(geometry, legacyWorkpiece = false) {
498
723
  const material = new MeshLambertMaterial({ transparent: true, depthWrite: false });
499
724
  const edgeMaterial = new LineBasicMaterial({ transparent: true, depthWrite: false });
500
- const mesh = new Mesh(geometry, material);
501
- const edgeGeometry = new EdgesGeometry(geometry, 15);
502
- const edges = new LineSegments(edgeGeometry, edgeMaterial);
725
+ const mesh = new Mesh2(geometry, material);
503
726
  const object = new Group();
504
727
  object.userData[STOCK_OBJECT] = true;
505
- mesh.renderOrder = 5;
506
- edges.renderOrder = 6;
728
+ mesh.renderOrder = legacyWorkpiece ? 2 : 5;
507
729
  mesh.raycast = () => {
508
730
  };
509
- edges.raycast = () => {
510
- };
511
- object.add(mesh, edges);
731
+ object.add(mesh);
732
+ if (legacyWorkpiece) {
733
+ const shadow = mesh.clone();
734
+ shadow.raycast = () => {
735
+ };
736
+ object.add(shadow);
737
+ edgeMaterial.depthWrite = true;
738
+ }
739
+ let edges = null;
512
740
  return {
513
741
  object,
514
742
  material,
515
743
  edgeMaterial,
516
- edges,
744
+ /**
745
+ * Shows or hides the outline, building it the first time it is shown: the
746
+ * edge pass reads every triangle, and stock cut by a toolpath has a great
747
+ * many of them.
748
+ */
749
+ showEdges(show) {
750
+ if (show && !edges) {
751
+ edges = new LineSegments(new EdgesGeometry(geometry, 15), edgeMaterial);
752
+ edges.renderOrder = legacyWorkpiece ? 1 : 6;
753
+ edges.raycast = () => {
754
+ };
755
+ object.add(edges);
756
+ }
757
+ if (edges) edges.visible = show;
758
+ },
517
759
  dispose() {
518
760
  material.dispose();
519
761
  edgeMaterial.dispose();
520
- edgeGeometry.dispose();
762
+ edges?.geometry.dispose();
521
763
  }
522
764
  };
523
765
  }
524
766
 
525
767
  // src/stock.tsx
526
768
  import { jsx as jsx4 } from "react/jsx-runtime";
527
- var Stock = ({
769
+ 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 });
770
+ var GeometryStock = ({
528
771
  geometry,
529
- color = 12176354,
772
+ renderStyle = "overlay",
773
+ color = renderStyle === "legacy-workpiece" ? 16777215 : 12176354,
774
+ emissive = renderStyle === "legacy-workpiece" ? 3948625 : 0,
775
+ flatShading = renderStyle === "legacy-workpiece",
530
776
  opacity = 0.2,
531
- edgeColor = 11058648,
532
- edgeOpacity = 0.75,
777
+ edgeColor = renderStyle === "legacy-workpiece" ? 0 : 11058648,
778
+ edgeOpacity = renderStyle === "legacy-workpiece" ? 0.5 : 0.75,
533
779
  showEdges = true
534
780
  }) => {
535
781
  const invalidate = useThree((state) => state.invalidate);
536
- const stock = useMemo(() => createStock(geometry), [geometry]);
782
+ const stock = useMemo(
783
+ () => createStock(geometry, renderStyle === "legacy-workpiece"),
784
+ [geometry, renderStyle]
785
+ );
537
786
  useEffect2(() => () => stock.dispose(), [stock]);
538
787
  useLayoutEffect(() => {
539
788
  stock.material.color.setHex(color);
789
+ stock.material.emissive.setHex(emissive);
790
+ if (stock.material.flatShading !== flatShading) {
791
+ stock.material.flatShading = flatShading;
792
+ stock.material.needsUpdate = true;
793
+ }
540
794
  stock.material.opacity = opacity;
795
+ stock.material.depthWrite = renderStyle === "legacy-workpiece" && opacity === 1;
796
+ stock.material.polygonOffset = renderStyle === "legacy-workpiece" && opacity === 1;
797
+ stock.material.polygonOffsetFactor = 1;
798
+ stock.material.polygonOffsetUnits = 1;
541
799
  stock.edgeMaterial.color.setHex(edgeColor);
542
800
  stock.edgeMaterial.opacity = edgeOpacity;
543
- stock.edges.visible = showEdges;
801
+ stock.showEdges(showEdges);
544
802
  invalidate();
545
- }, [color, edgeColor, edgeOpacity, invalidate, opacity, showEdges, stock]);
803
+ }, [
804
+ color,
805
+ emissive,
806
+ flatShading,
807
+ edgeColor,
808
+ edgeOpacity,
809
+ invalidate,
810
+ opacity,
811
+ renderStyle,
812
+ showEdges,
813
+ stock
814
+ ]);
546
815
  return /* @__PURE__ */ jsx4("primitive", { object: stock.object, dispose: null });
547
816
  };
548
- var BoxStock = ({
549
- partGeometry,
550
- dimensions,
551
- position = "model_centered",
552
- positionOffset = 0,
553
- allowance = 0,
554
- offset,
555
- ...props
817
+ var GlbStock = ({
818
+ glb,
819
+ retainPrevious = false,
820
+ ...appearance
821
+ }) => {
822
+ const buffer = useMemo(() => createStockMeshBuffer(), []);
823
+ const [decoded, setDecoded] = useState2(null);
824
+ useEffect2(() => () => buffer.dispose(), [buffer]);
825
+ useLayoutEffect(() => {
826
+ buffer.commit(decoded?.geometries ?? []);
827
+ }, [buffer, decoded]);
828
+ useEffect2(
829
+ () => buffer.request(
830
+ () => parseStockGlb(glb),
831
+ (geometries) => {
832
+ setDecoded({ source: glb, geometries });
833
+ },
834
+ (cause) => {
835
+ setDecoded({
836
+ source: glb,
837
+ error: cause instanceof Error ? cause : new Error(String(cause))
838
+ });
839
+ }
840
+ ),
841
+ [buffer, glb]
842
+ );
843
+ if (!decoded || !retainPrevious && decoded.source !== glb) return null;
844
+ if (decoded.error) throw decoded.error;
845
+ return decoded.geometries?.map((geometry) => /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance }, geometry.uuid));
846
+ };
847
+ var keepNonFinite = (_key, value) => typeof value === "number" && !Number.isFinite(value) ? String(value) : value;
848
+ var restoreNonFinite = (_key, value) => value === "NaN" || value === "Infinity" || value === "-Infinity" ? Number(value) : value;
849
+ var ShapeStock = ({
850
+ box,
851
+ cylinder,
852
+ ...appearance
556
853
  }) => {
557
- const ox = offset?.x ?? 0;
558
- const oy = offset?.y ?? 0;
559
- const oz = offset?.z ?? 0;
854
+ const isBox = box !== void 0;
855
+ const partGeometry = box?.partGeometry ?? cylinder?.partGeometry;
856
+ const figure = JSON.stringify(
857
+ box ? box.partGeometry ? {
858
+ dimensions: box.dimensions,
859
+ allowance: box.allowance,
860
+ offset: box.offset,
861
+ position: box.position,
862
+ positionOffset: box.positionOffset
863
+ } : { frame: box.frame, lower: box.lower, upper: box.upper } : cylinder.partGeometry ? {
864
+ diameter: cylinder.diameter,
865
+ length: cylinder.length,
866
+ position: cylinder.position,
867
+ positionOffset: cylinder.positionOffset
868
+ } : {
869
+ origin: cylinder.origin,
870
+ axis: cylinder.axis,
871
+ diameter: cylinder.diameter,
872
+ length: cylinder.length
873
+ },
874
+ keepNonFinite
875
+ );
560
876
  const geometry = useMemo(() => {
561
- const box = dimensions ? fixedBoxStockBounds(partGeometry, dimensions, position, positionOffset, {
562
- x: ox,
563
- y: oy,
564
- z: oz
565
- }) : boxStockBounds(partGeometry, allowance, { x: ox, y: oy, z: oz });
566
- const size = box.getSize(new Vector32());
567
- const center = box.getCenter(new Vector32());
568
- return new BoxGeometry(size.x, size.y, size.z).translate(center.x, center.y, center.z);
569
- }, [allowance, dimensions, ox, oy, oz, partGeometry, position, positionOffset]);
877
+ const options = JSON.parse(figure, restoreNonFinite);
878
+ const input = partGeometry ? { ...options, partGeometry } : options;
879
+ return isBox ? boxStockGeometry(input) : cylinderStockGeometry(input);
880
+ }, [figure, isBox, partGeometry]);
570
881
  useEffect2(() => () => geometry.dispose(), [geometry]);
571
- return /* @__PURE__ */ jsx4(Stock, { geometry, ...props });
882
+ return /* @__PURE__ */ jsx4(GeometryStock, { geometry, ...appearance });
572
883
  };
573
884
 
574
885
  // src/banana.tsx
@@ -577,13 +888,13 @@ import { useThree as useThree2 } from "@react-three/fiber";
577
888
  import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef } from "react";
578
889
 
579
890
  // src/render/banana.ts
580
- import { Vector3 as Vector33 } from "three";
891
+ import { Vector3 as Vector32 } from "three";
581
892
  var BANANA_GAP = 0.1;
582
893
  function bananaPosition(part, banana) {
583
- const size = part.getSize(new Vector33());
894
+ const size = part.getSize(new Vector32());
584
895
  const reach = Math.max(size.x, size.y, size.z);
585
- const centre = part.getCenter(new Vector33());
586
- return new Vector33(
896
+ const centre = part.getCenter(new Vector32());
897
+ return new Vector32(
587
898
  part.max.x + reach * BANANA_GAP - banana.min.x,
588
899
  centre.y - (banana.min.y + banana.max.y) / 2,
589
900
  part.min.z - banana.min.z
@@ -650,16 +961,16 @@ function directionHighlights(model, activeDirection = null) {
650
961
  // src/primitives.tsx
651
962
  import { GizmoHelper } from "@react-three/drei";
652
963
  import { useThree as useThree3 } from "@react-three/fiber";
653
- import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
964
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
654
965
 
655
966
  // src/render/grid.ts
656
- import { BufferGeometry, Float32BufferAttribute, Vector3 as Vector34 } from "three";
967
+ import { BufferGeometry, Float32BufferAttribute, Vector3 as Vector33 } from "three";
657
968
  var STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1e3];
658
969
  var TARGET_CELLS = 10;
659
970
  var OVERHANG = 1.6;
660
971
  function gridSpec(box) {
661
- const size = box.getSize(new Vector34());
662
- const center = box.getCenter(new Vector34());
972
+ const size = box.getSize(new Vector33());
973
+ const center = box.getCenter(new Vector33());
663
974
  const largest = Math.max(size.x, size.y, 1e-6);
664
975
  const ideal = largest / TARGET_CELLS;
665
976
  const step = [...STEPS].reverse().find((candidate) => candidate <= ideal) ?? STEPS[0];
@@ -732,7 +1043,7 @@ var CubePanels = ({
732
1043
  const controls = useViewerControls();
733
1044
  const invalidate = useThree3((state) => state.invalidate);
734
1045
  const groupRef = useRef2(null);
735
- const [hovered, setHovered] = useState2(null);
1046
+ const [hovered, setHovered] = useState3(null);
736
1047
  const panels = useMemo3(
737
1048
  () => zones.map((zone) => ({ zone, geometry: panelGeometry(zone) })),
738
1049
  [zones]
@@ -801,7 +1112,7 @@ var CubePanels = ({
801
1112
  // src/direction-arrows.tsx
802
1113
  import { useThree as useThree4 } from "@react-three/fiber";
803
1114
  import { useMemo as useMemo4 } from "react";
804
- import { Quaternion, Vector3 as Vector35 } from "three";
1115
+ import { Quaternion as Quaternion2, Vector3 as Vector34 } from "three";
805
1116
  import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
806
1117
  var FURNITURE3 = { [EXCLUDE_FROM_FRAME]: true };
807
1118
  var DirectionArrows = ({
@@ -861,9 +1172,9 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
861
1172
  const invalidate = useThree4((state) => state.invalidate);
862
1173
  const { tip, length, quaternion } = useMemo4(() => {
863
1174
  const placement = arrowPlacement(direction, box);
864
- const aim = new Quaternion().setFromUnitVectors(
1175
+ const aim = new Quaternion2().setFromUnitVectors(
865
1176
  CONE_AXIS,
866
- new Vector35(direction.x, direction.y, direction.z).normalize()
1177
+ new Vector34(direction.x, direction.y, direction.z).normalize()
867
1178
  );
868
1179
  return { tip: placement.tip, length: placement.length, quaternion: aim };
869
1180
  }, [box, direction]);
@@ -890,18 +1201,18 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
890
1201
 
891
1202
  // src/section-tool.tsx
892
1203
  import { useFrame, useThree as useThree5 } from "@react-three/fiber";
893
- import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
1204
+ import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState4, useSyncExternalStore } from "react";
894
1205
  import {
895
1206
  BufferGeometry as BufferGeometry2,
896
1207
  DoubleSide,
897
- Quaternion as Quaternion2,
1208
+ Quaternion as Quaternion3,
898
1209
  Raycaster,
899
1210
  Vector2,
900
- Vector3 as Vector36
1211
+ Vector3 as Vector35
901
1212
  } from "three";
902
1213
  import { Fragment, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
903
1214
  var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
904
- var Z = new Vector36(0, 0, 1);
1215
+ var Z = new Vector35(0, 0, 1);
905
1216
  var AXIS_ROTATIONS = {
906
1217
  x: [0, Math.PI / 2, 0],
907
1218
  y: [Math.PI / 2, 0, 0],
@@ -932,10 +1243,10 @@ var SectionTool = ({ theme }) => {
932
1243
  var useSquare = () => {
933
1244
  const square = useMemo5(
934
1245
  () => new BufferGeometry2().setFromPoints([
935
- new Vector36(-1, 1, 0),
936
- new Vector36(1, 1, 0),
937
- new Vector36(1, -1, 0),
938
- new Vector36(-1, -1, 0)
1246
+ new Vector35(-1, 1, 0),
1247
+ new Vector35(1, 1, 0),
1248
+ new Vector35(1, -1, 0),
1249
+ new Vector35(-1, -1, 0)
939
1250
  ]),
940
1251
  []
941
1252
  );
@@ -952,9 +1263,9 @@ var Picker = ({ box, theme }) => {
952
1263
  const square = useSquare();
953
1264
  const previewRef = useRef3(null);
954
1265
  const hover = useRef3(null);
955
- const [hoveredPlane, setHoveredPlane] = useState3(null);
956
- const span = useMemo5(() => box.getSize(new Vector36()).length(), [box]);
957
- const centre = useMemo5(() => box.getCenter(new Vector36()), [box]);
1266
+ const [hoveredPlane, setHoveredPlane] = useState4(null);
1267
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1268
+ const centre = useMemo5(() => box.getCenter(new Vector35()), [box]);
958
1269
  const previewSize = span * PREVIEW_SCALE;
959
1270
  useEffect4(() => {
960
1271
  store.setPicking(true);
@@ -963,7 +1274,7 @@ var Picker = ({ box, theme }) => {
963
1274
  useEffect4(() => {
964
1275
  const raycaster = new Raycaster();
965
1276
  const pointer = new Vector2();
966
- const aim = new Vector36();
1277
+ const aim = new Vector35();
967
1278
  const hide = () => {
968
1279
  if (!hover.current) return;
969
1280
  hover.current = null;
@@ -1008,7 +1319,7 @@ var Picker = ({ box, theme }) => {
1008
1319
  domElement.removeEventListener("pointerup", up);
1009
1320
  };
1010
1321
  }, [box, camera, domElement, invalidate, isTap, scene, span, store]);
1011
- const [planes, setPlanes] = useState3(() => axesPlanes(box, camera.position));
1322
+ const [planes, setPlanes] = useState4(() => axesPlanes(box, camera.position));
1012
1323
  const key = planes.map((plane) => plane.sign).join();
1013
1324
  useFrame(() => {
1014
1325
  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();
@@ -1107,14 +1418,14 @@ var Picker = ({ box, theme }) => {
1107
1418
  var CutSheet = ({ box, section, theme }) => {
1108
1419
  const square = useSquare();
1109
1420
  const cut = useMemo5(() => resolveSectionPlane(section, box), [box, section]);
1110
- const span = useMemo5(() => box.getSize(new Vector36()).length(), [box]);
1421
+ const span = useMemo5(() => box.getSize(new Vector35()).length(), [box]);
1111
1422
  const placement = useMemo5(() => {
1112
1423
  if (!cut) return null;
1113
1424
  const position = sectionAnchor(box, cut.plane).addScaledVector(
1114
1425
  cut.plane.normal,
1115
1426
  -span * SHEET_SETBACK
1116
1427
  );
1117
- return { position, quaternion: new Quaternion2().setFromUnitVectors(Z, cut.plane.normal) };
1428
+ return { position, quaternion: new Quaternion3().setFromUnitVectors(Z, cut.plane.normal) };
1118
1429
  }, [box, cut, span]);
1119
1430
  if (!placement) return null;
1120
1431
  const size = span * OUTLINE_SCALE;
@@ -1162,26 +1473,26 @@ import {
1162
1473
  useLayoutEffect as useLayoutEffect3,
1163
1474
  useMemo as useMemo6,
1164
1475
  useRef as useRef4,
1165
- useState as useState4,
1476
+ useState as useState5,
1166
1477
  useSyncExternalStore as useSyncExternalStore2
1167
1478
  } from "react";
1168
1479
  import {
1169
- Matrix4,
1480
+ Matrix4 as Matrix42,
1170
1481
  Raycaster as Raycaster2,
1171
1482
  SphereGeometry,
1172
1483
  Vector2 as Vector22,
1173
- Vector3 as Vector38
1484
+ Vector3 as Vector37
1174
1485
  } from "three";
1175
1486
 
1176
1487
  // src/render/resample.ts
1177
- import { Vector3 as Vector37 } from "three";
1488
+ import { Vector3 as Vector36 } from "three";
1178
1489
  var ON_PLANE = 1e-7;
1179
1490
  var STITCH = 1e-5;
1180
1491
  var COLLINEAR = 1e-6;
1181
1492
  function clipSegments(positions, plane) {
1182
1493
  const out = [];
1183
- const a = new Vector37();
1184
- const b = new Vector37();
1494
+ const a = new Vector36();
1495
+ const b = new Vector36();
1185
1496
  const count = Math.floor(positions.length / 6);
1186
1497
  for (let i = 0; i < count; i += 1) {
1187
1498
  a.set(positions[i * 6], positions[i * 6 + 1], positions[i * 6 + 2]);
@@ -1196,11 +1507,11 @@ function clipSegments(positions, plane) {
1196
1507
  return Float32Array.from(out);
1197
1508
  }
1198
1509
  function sectionContour(positions, regionOf, plane) {
1199
- const a = new Vector37();
1200
- const b = new Vector37();
1201
- const c = new Vector37();
1202
- const p = new Vector37();
1203
- const q = new Vector37();
1510
+ const a = new Vector36();
1511
+ const b = new Vector36();
1512
+ const c = new Vector36();
1513
+ const p = new Vector36();
1514
+ const q = new Vector36();
1204
1515
  const byRegion = /* @__PURE__ */ new Map();
1205
1516
  const triangleCount = Math.floor(positions.length / 9);
1206
1517
  for (let t = 0; t < triangleCount; t += 1) {
@@ -1258,17 +1569,17 @@ function stitch(segments, out) {
1258
1569
  return into.set(segments[o], segments[o + 1], segments[o + 2]);
1259
1570
  };
1260
1571
  const direction = (i, into) => {
1261
- const s = new Vector37();
1262
- const e = new Vector37();
1572
+ const s = new Vector36();
1573
+ const e = new Vector36();
1263
1574
  point(i, 0, s);
1264
1575
  point(i, 1, e);
1265
1576
  return into.subVectors(e, s).normalize();
1266
1577
  };
1267
1578
  const consumed = new Uint8Array(count);
1268
- const d = new Vector37();
1269
- const n = new Vector37();
1579
+ const d = new Vector36();
1580
+ const n = new Vector36();
1270
1581
  const next = (i, end) => {
1271
- const at = point(i, end, new Vector37());
1582
+ const at = point(i, end, new Vector36());
1272
1583
  const list = ends.get(key(at.x, at.y, at.z)) ?? [];
1273
1584
  if (list.length !== 2) return null;
1274
1585
  const other = list.find((entry) => entry >> 1 !== i);
@@ -1297,8 +1608,8 @@ function stitch(segments, out) {
1297
1608
  };
1298
1609
  const back = walk(0);
1299
1610
  const forward = walk(1);
1300
- const s = point(back.index, back.end, new Vector37());
1301
- const e = point(forward.index, forward.end, new Vector37());
1611
+ const s = point(back.index, back.end, new Vector36());
1612
+ const e = point(forward.index, forward.end, new Vector36());
1302
1613
  out.push(s.x, s.y, s.z, e.x, e.y, e.z);
1303
1614
  }
1304
1615
  }
@@ -1306,8 +1617,8 @@ function pointInContour(point, plane, contour) {
1306
1617
  const [u, v] = planeAxes(plane);
1307
1618
  const px = point.dot(u);
1308
1619
  const py = point.dot(v);
1309
- const a = new Vector37();
1310
- const b = new Vector37();
1620
+ const a = new Vector36();
1621
+ const b = new Vector36();
1311
1622
  let inside = false;
1312
1623
  const count = Math.floor(contour.length / 6);
1313
1624
  for (let i = 0; i < count; i += 1) {
@@ -1324,15 +1635,15 @@ function pointInContour(point, plane, contour) {
1324
1635
  return inside;
1325
1636
  }
1326
1637
  function capHit(ray, plane, contour) {
1327
- const point = ray.intersectPlane(plane, new Vector37());
1638
+ const point = ray.intersectPlane(plane, new Vector36());
1328
1639
  if (!point) return null;
1329
1640
  return pointInContour(point, plane, contour) ? point : null;
1330
1641
  }
1331
1642
  function planeAxes(plane) {
1332
1643
  const n = plane.normal;
1333
- const seed = Math.abs(n.x) < 0.9 ? new Vector37(1, 0, 0) : new Vector37(0, 1, 0);
1334
- const u = new Vector37().crossVectors(n, seed).normalize();
1335
- const v = new Vector37().crossVectors(n, u).normalize();
1644
+ const seed = Math.abs(n.x) < 0.9 ? new Vector36(1, 0, 0) : new Vector36(0, 1, 0);
1645
+ const u = new Vector36().crossVectors(n, seed).normalize();
1646
+ const v = new Vector36().crossVectors(n, u).normalize();
1336
1647
  return [u, v];
1337
1648
  }
1338
1649
  function sampleSection(meshPositions, regionOf, edgePositions, plane) {
@@ -1361,10 +1672,10 @@ var MeasureTool = ({
1361
1672
  const box = useContentBox();
1362
1673
  const resolved = useMemo6(() => resolveTheme(theme), [theme]);
1363
1674
  const picking = useSyncExternalStore2(store.subscribe, store.isPicking, store.isPicking);
1364
- const [held, setHeld] = useState4([]);
1675
+ const [held, setHeld] = useState5([]);
1365
1676
  const controlled = measurements !== void 0;
1366
1677
  const list = controlled ? measurements : held;
1367
- const [draft, setDraft] = useState4([]);
1678
+ const [draft, setDraft] = useState5([]);
1368
1679
  useEffect5(() => {
1369
1680
  store.setEngaged(true);
1370
1681
  return () => store.setEngaged(false);
@@ -1442,7 +1753,7 @@ var MeasureTool = ({
1442
1753
  ] });
1443
1754
  };
1444
1755
  var NO_POINTER = { pointerEvents: "none" };
1445
- var NOWHERE = [new Vector38(), new Vector38()];
1756
+ var NOWHERE = [new Vector37(), new Vector37()];
1446
1757
  var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1447
1758
  const camera = useThree6((state) => state.camera);
1448
1759
  const scene = useThree6((state) => state.scene);
@@ -1495,7 +1806,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1495
1806
  const snapped = snapAt(target, target.edges, camera, viewport);
1496
1807
  const from = placed[placed.length - 1];
1497
1808
  const lock = shift && from ? lockToAxis(from, snapped.point) : null;
1498
- const found = lock ? { ...snapped, point: new Vector38(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1809
+ const found = lock ? { ...snapped, point: new Vector37(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1499
1810
  snap.current = found;
1500
1811
  const node = marker.current;
1501
1812
  if (node) {
@@ -1657,7 +1968,7 @@ function targetUnderRay(raycaster, scene, samples) {
1657
1968
  edges: samples.edgesFor(hit.object)
1658
1969
  };
1659
1970
  }
1660
- const local = new Matrix4();
1971
+ const local = new Matrix42();
1661
1972
  for (const mesh of cutMeshes(scene)) {
1662
1973
  const planes = clippingPlanesOf(mesh);
1663
1974
  const sample = samples.for(mesh, planes);
@@ -1720,7 +2031,7 @@ var SampleCache = class {
1720
2031
  const key = planes.map((plane) => `${plane.normal.x},${plane.normal.y},${plane.normal.z},${plane.constant}`).join("|");
1721
2032
  const cached = this.held.get(mesh);
1722
2033
  if (cached && cached.key === key) return cached;
1723
- const local = new Matrix4().copy(mesh.matrixWorld).invert();
2034
+ const local = new Matrix42().copy(mesh.matrixWorld).invert();
1724
2035
  const localPlanes = planes.map((plane) => plane.clone().applyMatrix4(local));
1725
2036
  const positions = mesh.geometry.getAttribute("position")?.array ?? [];
1726
2037
  const regionOf = this.regionsOf(mesh.geometry);
@@ -1803,7 +2114,7 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1803
2114
  const lines = useRef4([]);
1804
2115
  const centre = useMemo6(() => {
1805
2116
  const at = midpoint(a, b);
1806
- return new Vector38(at.x, at.y, at.z);
2117
+ return new Vector37(at.x, at.y, at.z);
1807
2118
  }, [a, b]);
1808
2119
  useFrame2(() => {
1809
2120
  const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
@@ -1825,8 +2136,8 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1825
2136
  lines.current[index] = line;
1826
2137
  },
1827
2138
  points: [
1828
- new Vector38(leg.from.x, leg.from.y, leg.from.z),
1829
- new Vector38(leg.to.x, leg.to.y, leg.to.z)
2139
+ new Vector37(leg.from.x, leg.from.y, leg.from.z),
2140
+ new Vector37(leg.to.x, leg.to.y, leg.to.z)
1830
2141
  ],
1831
2142
  color: AXIS_COLORS[leg.axis],
1832
2143
  lineWidth: MEASURE_LINE_PIXELS.leg,
@@ -1862,7 +2173,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1862
2173
  const invalidate = useThree6((state) => state.invalidate);
1863
2174
  const node = useRef4(null);
1864
2175
  const dot = useDot();
1865
- const position = useMemo6(() => new Vector38(point.x, point.y, point.z), [point]);
2176
+ const position = useMemo6(() => new Vector37(point.x, point.y, point.z), [point]);
1866
2177
  useFrame2(() => {
1867
2178
  const group = node.current;
1868
2179
  if (!group) return;
@@ -1877,7 +2188,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1877
2188
  var Segment = ({ points, color }) => {
1878
2189
  const [a, b] = points;
1879
2190
  const ends = useMemo6(
1880
- () => [new Vector38(a.x, a.y, a.z), new Vector38(b.x, b.y, b.z)],
2191
+ () => [new Vector37(a.x, a.y, a.z), new Vector37(b.x, b.y, b.z)],
1881
2192
  // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1882
2193
  // and a rebuilt array would rebuild the line's geometry with it.
1883
2194
  [a.x, a.y, a.z, b.x, b.y, b.z]
@@ -1913,6 +2224,145 @@ function className(extra) {
1913
2224
  return extra ? `${MEASURE_LABEL_CLASS} ${extra}` : MEASURE_LABEL_CLASS;
1914
2225
  }
1915
2226
 
2227
+ // src/tool-marks.tsx
2228
+ import { Html as Html2, Line as Line2 } from "@react-three/drei";
2229
+ import { useMemo as useMemo7 } from "react";
2230
+ import { DoubleSide as DoubleSide2 } from "three";
2231
+
2232
+ // src/render/tool-marks.ts
2233
+ import { Quaternion as Quaternion4, Vector2 as Vector23, Vector3 as Vector38 } from "three";
2234
+ var TOOL_CORNER_STEPS = 12;
2235
+ var TOOL_ROUND = 64;
2236
+ function toolProfile(radius, corner, height) {
2237
+ const rounded = Math.min(Math.max(corner, 0), radius);
2238
+ const flat = radius - rounded;
2239
+ const points = [new Vector23(0, 0)];
2240
+ if (rounded <= 0) {
2241
+ points.push(new Vector23(radius, 0));
2242
+ } else {
2243
+ for (let step = 0; step <= TOOL_CORNER_STEPS; step++) {
2244
+ const angle = step / TOOL_CORNER_STEPS * (Math.PI / 2);
2245
+ points.push(
2246
+ new Vector23(flat + Math.sin(angle) * rounded, rounded - Math.cos(angle) * rounded)
2247
+ );
2248
+ }
2249
+ }
2250
+ points.push(new Vector23(radius, Math.max(height, rounded)));
2251
+ return points;
2252
+ }
2253
+ var UP = new Vector38(0, 1, 0);
2254
+ var AXES = [new Vector38(1, 0, 0), new Vector38(0, 1, 0), new Vector38(0, 0, 1)];
2255
+ var toVector = ({ x, y, z }) => new Vector38(x, y, z);
2256
+ var squareTo = (w, along) => {
2257
+ const wanted = along ? toVector(along) : null;
2258
+ const flat = wanted?.clone().addScaledVector(w, -wanted.dot(w));
2259
+ if (flat && flat.lengthSq() > 1e-12) return flat.normalize();
2260
+ const least = AXES.reduce(
2261
+ (best, axis) => Math.abs(axis.dot(w)) < Math.abs(best.dot(w)) ? axis : best
2262
+ );
2263
+ return least.clone().addScaledVector(w, -least.dot(w)).normalize();
2264
+ };
2265
+ var circle = (centre, u, v, radius) => Array.from({ length: TOOL_ROUND + 1 }, (_, step) => {
2266
+ const angle = step / TOOL_ROUND * Math.PI * 2;
2267
+ return centre.clone().addScaledVector(u, Math.cos(angle) * radius).addScaledVector(v, Math.sin(angle) * radius);
2268
+ });
2269
+ var tickLength = (diameter) => Math.min(Math.max(diameter * 0.15, 0.3), 3);
2270
+ function toolMarkShape(mark) {
2271
+ const axis = toVector(mark.axis);
2272
+ if (!(mark.diameter > 0) || axis.lengthSq() === 0) return null;
2273
+ const w = axis.normalize();
2274
+ const u = squareTo(w, mark.across);
2275
+ const v = new Vector38().crossVectors(w, u);
2276
+ const radius = mark.diameter / 2;
2277
+ const corner = Math.min(Math.max(mark.cornerRadius ?? 0, 0), radius);
2278
+ const height = Math.max(mark.height, corner);
2279
+ const base = toVector(mark.base);
2280
+ const top = base.clone().addScaledVector(w, height);
2281
+ const dimension = [
2282
+ top.clone().addScaledVector(u, radius),
2283
+ top.clone().addScaledVector(u, -radius)
2284
+ ];
2285
+ const half = tickLength(mark.diameter) / 2;
2286
+ const tick = (end) => [
2287
+ end.clone().addScaledVector(w, -half),
2288
+ end.clone().addScaledVector(w, half)
2289
+ ];
2290
+ return {
2291
+ base,
2292
+ turn: new Quaternion4().setFromUnitVectors(UP, w),
2293
+ profile: mark.profile ? mark.profile.map(({ r, z }) => new Vector23(r, z)) : toolProfile(radius, corner, height),
2294
+ rims: [
2295
+ ...radius - corner > 1e-9 ? [circle(base, u, v, radius - corner)] : [],
2296
+ circle(top, u, v, radius)
2297
+ ],
2298
+ dimension,
2299
+ ticks: [tick(dimension[0]), tick(dimension[1])],
2300
+ labelAt: top
2301
+ };
2302
+ }
2303
+
2304
+ // src/tool-marks.tsx
2305
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2306
+ var BODY = { color: 16317180, opacity: 0.32 };
2307
+ var NO_POINTER2 = { pointerEvents: "none" };
2308
+ var ignoreRay = () => void 0;
2309
+ var ToolMarks = ({ marks, visible = true, labelClassName, theme }) => {
2310
+ const color = resolveTheme(theme).measure;
2311
+ const shapes = useMemo7(
2312
+ () => marks.flatMap((mark) => {
2313
+ const shape = toolMarkShape(mark);
2314
+ return shape ? [{ mark, shape }] : [];
2315
+ }),
2316
+ [marks]
2317
+ );
2318
+ const label = labelClassName ? `${MEASURE_LABEL_CLASS} ${labelClassName}` : MEASURE_LABEL_CLASS;
2319
+ return /* @__PURE__ */ jsx10("group", { visible, userData: { [EXCLUDE_FROM_FRAME]: true }, children: shapes.map(({ mark, shape }, index) => /* @__PURE__ */ jsxs6("group", { children: [
2320
+ /* @__PURE__ */ jsx10(ToolBody, { shape }),
2321
+ [...shape.rims, shape.dimension, ...shape.ticks].map((points, line) => /* @__PURE__ */ jsx10(
2322
+ Line2,
2323
+ {
2324
+ points,
2325
+ color,
2326
+ lineWidth: MEASURE_LINE_PIXELS.line,
2327
+ depthTest: false,
2328
+ depthWrite: false,
2329
+ renderOrder: MEASURE_RENDER_ORDER,
2330
+ raycast: ignoreRay
2331
+ },
2332
+ line
2333
+ )),
2334
+ mark.label && visible ? /* @__PURE__ */ jsx10(Html2, { position: shape.labelAt, center: true, zIndexRange: [0, 0], style: NO_POINTER2, children: /* @__PURE__ */ jsxs6("div", { className: label, "data-measure-label": "tool", children: [
2335
+ /* @__PURE__ */ jsx10("span", { "data-measure-value": true, children: mark.label }),
2336
+ mark.note ? /* @__PURE__ */ jsxs6("span", { "data-measure-note": true, children: [
2337
+ " ",
2338
+ mark.note
2339
+ ] }) : null
2340
+ ] }) }) : null
2341
+ ] }, index)) });
2342
+ };
2343
+ var ToolBody = ({ shape }) => /* @__PURE__ */ jsxs6(
2344
+ "mesh",
2345
+ {
2346
+ position: shape.base,
2347
+ quaternion: shape.turn,
2348
+ raycast: ignoreRay,
2349
+ renderOrder: MEASURE_RENDER_ORDER - 1,
2350
+ children: [
2351
+ /* @__PURE__ */ jsx10("latheGeometry", { args: [shape.profile, 64] }),
2352
+ /* @__PURE__ */ jsx10(
2353
+ "meshBasicMaterial",
2354
+ {
2355
+ color: BODY.color,
2356
+ transparent: true,
2357
+ opacity: BODY.opacity,
2358
+ depthWrite: false,
2359
+ side: DoubleSide2
2360
+ }
2361
+ )
2362
+ ]
2363
+ }
2364
+ );
2365
+
1916
2366
  // src/model/normals.ts
1917
2367
  function regionNormals(geometry, regions) {
1918
2368
  const position = geometry.getAttribute("position");
@@ -1956,7 +2406,6 @@ export {
1956
2406
  BANANA_GAP,
1957
2407
  BANANA_MODEL_URL,
1958
2408
  Banana,
1959
- BoxStock,
1960
2409
  CAD_CAMERA_UP,
1961
2410
  CANDIDATE_WEIGHT,
1962
2411
  CAP_HATCH,
@@ -2013,6 +2462,7 @@ export {
2013
2462
  SectionView,
2014
2463
  Stock,
2015
2464
  TAP_SLOP,
2465
+ ToolMarks,
2016
2466
  UnsupportedKernelVersionError,
2017
2467
  VIEW_NAMES,
2018
2468
  VIEW_SIGNS,
@@ -2064,6 +2514,7 @@ export {
2064
2514
  distanceBetween,
2065
2515
  dragPlane,
2066
2516
  excludedFromFrame,
2517
+ featureTriangles,
2067
2518
  featureTypeRank,
2068
2519
  fitDistance,
2069
2520
  fixedBoxStockBounds,
@@ -2123,6 +2574,7 @@ export {
2123
2574
  trackDoubleTaps,
2124
2575
  trackTaps,
2125
2576
  useContentBox,
2577
+ usePartContext,
2126
2578
  useRetarget,
2127
2579
  useSectionStore,
2128
2580
  useTapGuard,