@toolpath/viewer 1.1.1 → 1.3.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,12 +1,17 @@
1
1
  import {
2
+ AXES_PLANE_OFFSET,
3
+ AXES_PLANE_SCALE,
2
4
  CAD_CAMERA_UP,
3
5
  CANDIDATE_WEIGHT,
6
+ CAP_HATCH,
4
7
  CHAMFER,
5
8
  CONE_AXIS,
6
9
  CadCameraControls,
7
10
  DEFAULT_FIT_MARGIN,
11
+ DEFAULT_SECTION_NORMAL,
8
12
  DEFAULT_THEME,
9
13
  DIRECTION_COLORS,
14
+ DISABLED_SECTION,
10
15
  DOUBLE_TAP_MS,
11
16
  EXCLUDE_FROM_FRAME,
12
17
  EnginePart,
@@ -29,8 +34,10 @@ import {
29
34
  ORBIT_TARGET_RING_OPACITY,
30
35
  ORBIT_TARGET_RING_PIXELS,
31
36
  ORBIT_TARGET_RING_WIDTH,
37
+ OUTLINE_SCALE,
32
38
  PERSPECTIVE_FOV,
33
39
  PICKED_SURFACE_LABEL,
40
+ PREVIEW_SCALE,
34
41
  PartMesh,
35
42
  PartReportFormatError,
36
43
  REGION_ATTRIBUTE,
@@ -43,10 +50,12 @@ import {
43
50
  VIEW_SIGNS,
44
51
  Viewer,
45
52
  adaptedUp,
53
+ applyCapTheme,
46
54
  applyHighlightLayers,
47
55
  applyProjection,
48
56
  arrowPlacement,
49
57
  aspectRatio,
58
+ axesPlanes,
50
59
  bestOwner,
51
60
  boundsFromBox,
52
61
  buildPick,
@@ -56,7 +65,12 @@ import {
56
65
  cadViewDirections,
57
66
  cameraLimits,
58
67
  contentBounds,
68
+ createCapMaterial,
69
+ createMaskMaterial,
70
+ createMaskTarget,
59
71
  createPart,
72
+ createSectionStore,
73
+ createStencilMaterials,
60
74
  cubeOutlineGeometry,
61
75
  cubeZones,
62
76
  currentViewDirection,
@@ -66,10 +80,12 @@ import {
66
80
  directionIndexOf,
67
81
  directionLabel,
68
82
  dragPlane,
83
+ excludedFromFrame,
69
84
  featureTypeRank,
70
85
  fitDistance,
71
86
  focusForPick,
72
87
  groupByDirection,
88
+ hitUnderRay,
73
89
  labelGeometry,
74
90
  labelTexture,
75
91
  movedFar,
@@ -87,6 +103,7 @@ import {
87
103
  retargetPose,
88
104
  sameDirection,
89
105
  screenLength,
106
+ sectionAnchor,
90
107
  sectionBounds,
91
108
  sectionConstant,
92
109
  sectionDepth,
@@ -94,16 +111,19 @@ import {
94
111
  sectionDepthRange,
95
112
  sectionFromPick,
96
113
  sectionOffset,
114
+ sectionOptionsFromState,
97
115
  sectionPlane,
98
116
  smoothRegionNormals,
99
117
  squaredUp,
100
118
  startPosition,
119
+ surfaceUnderRay,
101
120
  targetBoundary,
102
121
  themesEqual,
103
122
  trackDoubleTaps,
104
123
  trackTaps,
105
124
  useContentBox,
106
125
  useRetarget,
126
+ useSectionStore,
107
127
  useTapGuard,
108
128
  useViewerControls,
109
129
  viewDirection,
@@ -111,7 +131,7 @@ import {
111
131
  viewUp,
112
132
  viewVector,
113
133
  visualSurfaces
114
- } from "./chunk-4BU4QZCL.js";
134
+ } from "./chunk-HWAEUJ4H.js";
115
135
 
116
136
  // src/primitives.tsx
117
137
  import { GizmoHelper } from "@react-three/drei";
@@ -354,6 +374,1212 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
354
374
  ] });
355
375
  };
356
376
 
377
+ // 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";
380
+ import {
381
+ BufferGeometry as BufferGeometry2,
382
+ DoubleSide,
383
+ Quaternion as Quaternion2,
384
+ Raycaster,
385
+ Vector2 as Vector22,
386
+ Vector3 as Vector34
387
+ } 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);
593
+ var AXIS_ROTATIONS = {
594
+ x: [0, Math.PI / 2, 0],
595
+ y: [Math.PI / 2, 0, 0],
596
+ z: [0, 0, 0]
597
+ };
598
+ var PREVIEW_LIFT = 2e-3;
599
+ var SHEET_SETBACK = 1e-3;
600
+ var SectionTool = ({ theme }) => {
601
+ const store = useSectionStore();
602
+ const section = useSyncExternalStore(store.subscribe, store.get, store.get);
603
+ const box = useContentBox();
604
+ const resolved = useMemo3(() => resolveTheme(theme), [theme]);
605
+ useEffect2(() => {
606
+ store.setEngaged(true);
607
+ return () => store.setEngaged(false);
608
+ }, [store]);
609
+ useEffect2(() => {
610
+ if (!section) return void 0;
611
+ const clear = (event) => {
612
+ if (event.key === "Escape") store.set(null);
613
+ };
614
+ window.addEventListener("keydown", clear);
615
+ return () => window.removeEventListener("keydown", clear);
616
+ }, [section, store]);
617
+ 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 }) });
619
+ };
620
+ var useSquare = () => {
621
+ const square = useMemo3(
622
+ () => 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)
627
+ ]),
628
+ []
629
+ );
630
+ useEffect2(() => () => square.dispose(), [square]);
631
+ return square;
632
+ };
633
+ var Picker = ({ box, theme }) => {
634
+ 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);
639
+ const isTap = useTapGuard();
640
+ 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]);
646
+ const previewSize = span * PREVIEW_SCALE;
647
+ useEffect2(() => {
648
+ const raycaster = new Raycaster();
649
+ const pointer = new Vector22();
650
+ const aim = new Vector34();
651
+ const hide = () => {
652
+ if (!hover.current) return;
653
+ hover.current = null;
654
+ if (previewRef.current) previewRef.current.visible = false;
655
+ invalidate();
656
+ };
657
+ const move = (event) => {
658
+ const rect = domElement.getBoundingClientRect();
659
+ pointer.set(
660
+ (event.clientX - rect.left) / rect.width * 2 - 1,
661
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
662
+ );
663
+ raycaster.setFromCamera(pointer, camera);
664
+ const hit = surfaceUnderRay(raycaster, scene);
665
+ const preview = previewRef.current;
666
+ if (!hit || !preview) {
667
+ hide();
668
+ return;
669
+ }
670
+ hover.current = hit;
671
+ preview.position.copy(hit.point).addScaledVector(hit.normal, span * PREVIEW_LIFT);
672
+ preview.lookAt(aim.copy(hit.point).add(hit.normal));
673
+ preview.visible = true;
674
+ invalidate();
675
+ };
676
+ const up = (event) => {
677
+ const hit = hover.current;
678
+ if (event.button !== 0 || !hit || !isTap(event)) return;
679
+ store.set({
680
+ enabled: true,
681
+ plane: sectionFromPick({ point: hit.point, normal: hit.normal }),
682
+ depth: pickedStartDepth(box)
683
+ });
684
+ };
685
+ domElement.addEventListener("pointermove", move);
686
+ domElement.addEventListener("pointerleave", hide);
687
+ domElement.addEventListener("pointerup", up);
688
+ return () => {
689
+ domElement.removeEventListener("pointermove", move);
690
+ domElement.removeEventListener("pointerleave", hide);
691
+ domElement.removeEventListener("pointerup", up);
692
+ };
693
+ }, [box, camera, domElement, invalidate, isTap, scene, span, store]);
694
+ const [planes, setPlanes] = useState2(() => axesPlanes(box, camera.position));
695
+ const key = planes.map((plane) => plane.sign).join();
696
+ useFrame(() => {
697
+ 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();
698
+ if (next !== key) setPlanes(axesPlanes(box, camera.position));
699
+ });
700
+ const planeHandlers = (plane) => ({
701
+ // A plane is behind the part, and the part does not stop propagation: a
702
+ // click on a face reaches the plane behind it too. The preview says which
703
+ // one the pointer is really on.
704
+ onPointerOver: (event) => {
705
+ if (hover.current) return;
706
+ event.stopPropagation();
707
+ setHoveredPlane(plane.axis);
708
+ invalidate();
709
+ },
710
+ onPointerOut: () => {
711
+ setHoveredPlane((held) => held === plane.axis ? null : held);
712
+ invalidate();
713
+ },
714
+ onClick: (event) => {
715
+ if (hover.current || !isTap(event.nativeEvent)) return;
716
+ event.stopPropagation();
717
+ store.set(plane.options);
718
+ }
719
+ });
720
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
721
+ /* @__PURE__ */ jsxs3("group", { ref: previewRef, visible: false, children: [
722
+ /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
723
+ /* @__PURE__ */ jsx3("planeGeometry", { args: [previewSize, previewSize] }),
724
+ /* @__PURE__ */ jsx3(
725
+ "meshBasicMaterial",
726
+ {
727
+ color: theme.sectionOutline,
728
+ transparent: true,
729
+ opacity: 0.2,
730
+ side: DoubleSide,
731
+ depthWrite: false
732
+ }
733
+ )
734
+ ] }),
735
+ /* @__PURE__ */ jsx3(
736
+ "lineLoop",
737
+ {
738
+ geometry: square,
739
+ scale: previewSize / 2,
740
+ renderOrder: SECTION_RENDER_ORDER.outline,
741
+ raycast: () => null,
742
+ children: /* @__PURE__ */ jsx3(
743
+ "lineBasicMaterial",
744
+ {
745
+ color: theme.sectionOutline,
746
+ transparent: true,
747
+ opacity: 0.5,
748
+ depthTest: false,
749
+ depthWrite: false
750
+ }
751
+ )
752
+ }
753
+ )
754
+ ] }),
755
+ planes.map((plane) => /* @__PURE__ */ jsxs3(
756
+ "group",
757
+ {
758
+ position: [plane.position.x, plane.position.y, plane.position.z],
759
+ rotation: AXIS_ROTATIONS[plane.axis],
760
+ children: [
761
+ /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, ...planeHandlers(plane), children: [
762
+ /* @__PURE__ */ jsx3("planeGeometry", { args: [plane.size, plane.size] }),
763
+ /* @__PURE__ */ jsx3(
764
+ "meshBasicMaterial",
765
+ {
766
+ color: AXIS_COLORS[plane.axis],
767
+ transparent: true,
768
+ opacity: hoveredPlane === plane.axis ? 0.45 : 0.3,
769
+ side: DoubleSide,
770
+ depthWrite: false
771
+ }
772
+ )
773
+ ] }),
774
+ /* @__PURE__ */ jsx3(
775
+ "lineLoop",
776
+ {
777
+ geometry: square,
778
+ scale: plane.size / 2,
779
+ renderOrder: SECTION_RENDER_ORDER.outline,
780
+ raycast: () => null,
781
+ children: /* @__PURE__ */ jsx3("lineBasicMaterial", { color: AXIS_COLORS[plane.axis], transparent: true, opacity: 0.7 })
782
+ }
783
+ )
784
+ ]
785
+ },
786
+ plane.axis
787
+ ))
788
+ ] });
789
+ };
790
+ var CutSheet = ({ box, section, theme }) => {
791
+ const square = useSquare();
792
+ const cut = useMemo3(() => resolveSectionPlane(section, box), [box, section]);
793
+ const span = useMemo3(() => box.getSize(new Vector34()).length(), [box]);
794
+ const placement = useMemo3(() => {
795
+ if (!cut) return null;
796
+ const position = sectionAnchor(box, cut.plane).addScaledVector(
797
+ cut.plane.normal,
798
+ -span * SHEET_SETBACK
799
+ );
800
+ return { position, quaternion: new Quaternion2().setFromUnitVectors(Z, cut.plane.normal) };
801
+ }, [box, cut, span]);
802
+ if (!placement) return null;
803
+ const size = span * OUTLINE_SCALE;
804
+ return /* @__PURE__ */ jsxs3("group", { position: placement.position, quaternion: placement.quaternion, children: [
805
+ /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.outline, raycast: () => null, children: [
806
+ /* @__PURE__ */ jsx3("planeGeometry", { args: [size, size] }),
807
+ /* @__PURE__ */ jsx3(
808
+ "meshBasicMaterial",
809
+ {
810
+ color: theme.sectionOutline,
811
+ transparent: true,
812
+ opacity: 0.1,
813
+ side: DoubleSide,
814
+ depthWrite: false
815
+ }
816
+ )
817
+ ] }),
818
+ /* @__PURE__ */ jsx3(
819
+ "lineLoop",
820
+ {
821
+ geometry: square,
822
+ scale: size / 2,
823
+ renderOrder: SECTION_RENDER_ORDER.outline,
824
+ raycast: () => null,
825
+ children: /* @__PURE__ */ jsx3(
826
+ "lineBasicMaterial",
827
+ {
828
+ color: theme.sectionOutline,
829
+ transparent: true,
830
+ opacity: 0.5,
831
+ depthTest: false,
832
+ depthWrite: false
833
+ }
834
+ )
835
+ }
836
+ )
837
+ ] });
838
+ };
839
+
840
+ // src/measure-tool.tsx
841
+ import { Html, Line } from "@react-three/drei";
842
+ import { useFrame as useFrame2, useThree as useThree4 } from "@react-three/fiber";
843
+ import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
844
+ import {
845
+ Matrix4,
846
+ Raycaster as Raycaster2,
847
+ SphereGeometry,
848
+ Vector2 as Vector23,
849
+ Vector3 as Vector36
850
+ } from "three";
851
+
852
+ // src/render/resample.ts
853
+ import { Vector3 as Vector35 } from "three";
854
+ var ON_PLANE = 1e-7;
855
+ var STITCH = 1e-5;
856
+ var COLLINEAR = 1e-6;
857
+ function clipSegments(positions, plane) {
858
+ const out = [];
859
+ const a = new Vector35();
860
+ const b = new Vector35();
861
+ const count = Math.floor(positions.length / 6);
862
+ for (let i = 0; i < count; i += 1) {
863
+ a.set(positions[i * 6], positions[i * 6 + 1], positions[i * 6 + 2]);
864
+ b.set(positions[i * 6 + 3], positions[i * 6 + 4], positions[i * 6 + 5]);
865
+ const da = plane.distanceToPoint(a);
866
+ const db = plane.distanceToPoint(b);
867
+ if (da < -ON_PLANE && db < -ON_PLANE) continue;
868
+ if (da < -ON_PLANE) a.lerp(b, da / (da - db));
869
+ else if (db < -ON_PLANE) b.lerp(a, db / (db - da));
870
+ out.push(a.x, a.y, a.z, b.x, b.y, b.z);
871
+ }
872
+ return Float32Array.from(out);
873
+ }
874
+ function sectionContour(positions, regionOf, plane) {
875
+ const a = new Vector35();
876
+ const b = new Vector35();
877
+ const c = new Vector35();
878
+ const p = new Vector35();
879
+ const q = new Vector35();
880
+ const byRegion = /* @__PURE__ */ new Map();
881
+ const triangleCount = Math.floor(positions.length / 9);
882
+ for (let t = 0; t < triangleCount; t += 1) {
883
+ a.set(positions[t * 9], positions[t * 9 + 1], positions[t * 9 + 2]);
884
+ b.set(positions[t * 9 + 3], positions[t * 9 + 4], positions[t * 9 + 5]);
885
+ c.set(positions[t * 9 + 6], positions[t * 9 + 7], positions[t * 9 + 8]);
886
+ const crossings = triangleCrossings(a, b, c, plane, p, q);
887
+ if (!crossings) continue;
888
+ const region = regionOf ? regionOf[t] ?? 0 : 0;
889
+ const list = byRegion.get(region);
890
+ const entry = [p.x, p.y, p.z, q.x, q.y, q.z];
891
+ if (list) list.push(...entry);
892
+ else byRegion.set(region, entry);
893
+ }
894
+ const out = [];
895
+ for (const segments of byRegion.values()) stitch(segments, out);
896
+ return Float32Array.from(out);
897
+ }
898
+ function triangleCrossings(a, b, c, plane, p, q) {
899
+ const da = plane.distanceToPoint(a);
900
+ const db = plane.distanceToPoint(b);
901
+ const dc = plane.distanceToPoint(c);
902
+ const sa = da < -ON_PLANE;
903
+ const sb = db < -ON_PLANE;
904
+ const sc = dc < -ON_PLANE;
905
+ if (sa === sb && sb === sc) return false;
906
+ const found = [];
907
+ const edge = (u, v, du, dv, su, sv) => {
908
+ if (su === sv) return;
909
+ found.push(u.clone().lerp(v, du / (du - dv)));
910
+ };
911
+ edge(a, b, da, db, sa, sb);
912
+ edge(b, c, db, dc, sb, sc);
913
+ edge(c, a, dc, da, sc, sa);
914
+ if (found.length < 2) return false;
915
+ p.copy(found[0]);
916
+ q.copy(found[1]);
917
+ return p.distanceToSquared(q) > ON_PLANE * ON_PLANE;
918
+ }
919
+ function stitch(segments, out) {
920
+ const count = segments.length / 6;
921
+ const key = (x, y, z) => `${Math.round(x / STITCH)},${Math.round(y / STITCH)},${Math.round(z / STITCH)}`;
922
+ const ends = /* @__PURE__ */ new Map();
923
+ for (let i = 0; i < count; i += 1) {
924
+ for (const end of [0, 1]) {
925
+ const o = i * 6 + end * 3;
926
+ const k = key(segments[o], segments[o + 1], segments[o + 2]);
927
+ const list = ends.get(k);
928
+ if (list) list.push(i * 2 + end);
929
+ else ends.set(k, [i * 2 + end]);
930
+ }
931
+ }
932
+ const point = (i, end, into) => {
933
+ const o = i * 6 + end * 3;
934
+ return into.set(segments[o], segments[o + 1], segments[o + 2]);
935
+ };
936
+ const direction = (i, into) => {
937
+ const s = new Vector35();
938
+ const e = new Vector35();
939
+ point(i, 0, s);
940
+ point(i, 1, e);
941
+ return into.subVectors(e, s).normalize();
942
+ };
943
+ const consumed = new Uint8Array(count);
944
+ const d = new Vector35();
945
+ const n = new Vector35();
946
+ const next = (i, end) => {
947
+ const at = point(i, end, new Vector35());
948
+ const list = ends.get(key(at.x, at.y, at.z)) ?? [];
949
+ if (list.length !== 2) return null;
950
+ const other = list.find((entry) => entry >> 1 !== i);
951
+ if (other === void 0) return null;
952
+ const j = other >> 1;
953
+ if (consumed[j]) return null;
954
+ direction(i, d);
955
+ direction(j, n);
956
+ if (Math.abs(Math.abs(d.dot(n)) - 1) > COLLINEAR) return null;
957
+ return { index: j, end: other & 1 };
958
+ };
959
+ for (let i = 0; i < count; i += 1) {
960
+ if (consumed[i]) continue;
961
+ consumed[i] = 1;
962
+ const walk = (end) => {
963
+ let at = { index: i, end };
964
+ let steps = 0;
965
+ for (; ; ) {
966
+ const step = next(at.index, at.end);
967
+ if (!step || steps > count) break;
968
+ consumed[step.index] = 1;
969
+ at = { index: step.index, end: 1 - step.end };
970
+ steps += 1;
971
+ }
972
+ return at;
973
+ };
974
+ const back = walk(0);
975
+ const forward = walk(1);
976
+ const s = point(back.index, back.end, new Vector35());
977
+ const e = point(forward.index, forward.end, new Vector35());
978
+ out.push(s.x, s.y, s.z, e.x, e.y, e.z);
979
+ }
980
+ }
981
+ function pointInContour(point, plane, contour) {
982
+ const [u, v] = planeAxes(plane);
983
+ const px = point.dot(u);
984
+ const py = point.dot(v);
985
+ const a = new Vector35();
986
+ const b = new Vector35();
987
+ let inside = false;
988
+ const count = Math.floor(contour.length / 6);
989
+ for (let i = 0; i < count; i += 1) {
990
+ a.set(contour[i * 6], contour[i * 6 + 1], contour[i * 6 + 2]);
991
+ b.set(contour[i * 6 + 3], contour[i * 6 + 4], contour[i * 6 + 5]);
992
+ const ax = a.dot(u);
993
+ const ay = a.dot(v);
994
+ const bx = b.dot(u);
995
+ const by = b.dot(v);
996
+ if (ay > py === by > py) continue;
997
+ const x = ax + (py - ay) / (by - ay) * (bx - ax);
998
+ if (px < x) inside = !inside;
999
+ }
1000
+ return inside;
1001
+ }
1002
+ function capHit(ray, plane, contour) {
1003
+ const point = ray.intersectPlane(plane, new Vector35());
1004
+ if (!point) return null;
1005
+ return pointInContour(point, plane, contour) ? point : null;
1006
+ }
1007
+ function planeAxes(plane) {
1008
+ const n = plane.normal;
1009
+ const seed = Math.abs(n.x) < 0.9 ? new Vector35(1, 0, 0) : new Vector35(0, 1, 0);
1010
+ const u = new Vector35().crossVectors(n, seed).normalize();
1011
+ const v = new Vector35().crossVectors(n, u).normalize();
1012
+ return [u, v];
1013
+ }
1014
+ function sampleSection(meshPositions, regionOf, edgePositions, plane) {
1015
+ const contour = sectionContour(meshPositions, regionOf, plane);
1016
+ const kept = edgePositions ? clipSegments(edgePositions, plane) : new Float32Array();
1017
+ const edges = new Float32Array(kept.length + contour.length);
1018
+ edges.set(kept, 0);
1019
+ edges.set(contour, kept.length);
1020
+ return { edges, contour };
1021
+ }
1022
+
1023
+ // src/measure-tool.tsx
1024
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1025
+ var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
1026
+ var MEASURE_LABEL_CLASS = "toolpath-measure-label";
1027
+ var MeasureTool = ({
1028
+ mode = "distance",
1029
+ measurements,
1030
+ onChange,
1031
+ showDeltas = true,
1032
+ format = formatMillimetres,
1033
+ labelClassName,
1034
+ theme
1035
+ }) => {
1036
+ const store = useSectionStore();
1037
+ const box = useContentBox();
1038
+ const resolved = useMemo4(() => resolveTheme(theme), [theme]);
1039
+ const [held, setHeld] = useState3([]);
1040
+ const controlled = measurements !== void 0;
1041
+ const list = controlled ? measurements : held;
1042
+ const [draft, setDraft] = useState3([]);
1043
+ useEffect3(() => {
1044
+ store.setEngaged(true);
1045
+ return () => store.setEngaged(false);
1046
+ }, [store]);
1047
+ useEffect3(() => {
1048
+ setDraft([]);
1049
+ }, [mode]);
1050
+ const commit = (next) => {
1051
+ if (!controlled) setHeld(next);
1052
+ onChange?.(next);
1053
+ };
1054
+ const latest = useRef3({ list, commit });
1055
+ latest.current = { list, commit };
1056
+ useEffect3(() => {
1057
+ const onKey = (event) => {
1058
+ if (event.key === "Escape") {
1059
+ setDraft([]);
1060
+ return;
1061
+ }
1062
+ if (event.key !== "Delete" && event.key !== "Backspace") return;
1063
+ if (isEditing(event.target)) return;
1064
+ const { list: current, commit: apply } = latest.current;
1065
+ if (current.length === 0) return;
1066
+ event.preventDefault();
1067
+ apply(current.slice(0, -1));
1068
+ };
1069
+ window.addEventListener("keydown", onKey);
1070
+ return () => window.removeEventListener("keydown", onKey);
1071
+ }, []);
1072
+ const place = (point) => {
1073
+ const points = [...draft, point];
1074
+ const complete = measurementFromPoints(mode, points, nextMeasurementId(list));
1075
+ if (complete) {
1076
+ commit([...list, complete]);
1077
+ setDraft([]);
1078
+ } else {
1079
+ setDraft(points);
1080
+ }
1081
+ };
1082
+ if (box.isEmpty()) return null;
1083
+ const label = className(labelClassName);
1084
+ return /* @__PURE__ */ jsxs4("group", { userData: FURNITURE4, children: [
1085
+ /* @__PURE__ */ jsx4(
1086
+ Snapper,
1087
+ {
1088
+ mode,
1089
+ draft,
1090
+ onPlace: place,
1091
+ theme: resolved,
1092
+ format,
1093
+ label
1094
+ }
1095
+ ),
1096
+ draft.map((point, index) => /* @__PURE__ */ jsx4(
1097
+ ScreenDot,
1098
+ {
1099
+ point,
1100
+ pixels: MEASURE_MARKER_PIXELS,
1101
+ color: resolved.measure
1102
+ },
1103
+ index
1104
+ )),
1105
+ draft.length === 2 ? /* @__PURE__ */ jsx4(Segment, { points: [draft[0], draft[1]], color: resolved.measure }) : null,
1106
+ list.map((measurement) => /* @__PURE__ */ jsx4(
1107
+ MeasurementView,
1108
+ {
1109
+ measurement,
1110
+ theme: resolved,
1111
+ format,
1112
+ showDeltas,
1113
+ label
1114
+ },
1115
+ measurement.id
1116
+ ))
1117
+ ] });
1118
+ };
1119
+ var NO_POINTER = { pointerEvents: "none" };
1120
+ var NOWHERE = [new Vector36(), new Vector36()];
1121
+ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
1122
+ const camera = useThree4((state) => state.camera);
1123
+ const scene = useThree4((state) => state.scene);
1124
+ const size = useThree4((state) => state.size);
1125
+ const domElement = useThree4((state) => state.gl.domElement);
1126
+ const invalidate = useThree4((state) => state.invalidate);
1127
+ const isTap = useTapGuard();
1128
+ const marker = useRef3(null);
1129
+ const edgeLine = useRef3(null);
1130
+ const band = useRef3(null);
1131
+ const bandLabel = useRef3(null);
1132
+ const bandText = useRef3(null);
1133
+ const snap = useRef3(null);
1134
+ const dot = useDot();
1135
+ useLayoutEffect(() => {
1136
+ if (edgeLine.current) edgeLine.current.visible = false;
1137
+ if (band.current) band.current.visible = false;
1138
+ }, []);
1139
+ const current = useRef3({ mode, draft, onPlace, format, size });
1140
+ current.current = { mode, draft, onPlace, format, size };
1141
+ useEffect3(() => {
1142
+ const raycaster = new Raycaster2();
1143
+ const pointer = new Vector23();
1144
+ const samples = new SampleCache();
1145
+ const hide = () => {
1146
+ if (!snap.current) return;
1147
+ snap.current = null;
1148
+ if (marker.current) marker.current.visible = false;
1149
+ if (edgeLine.current) edgeLine.current.visible = false;
1150
+ if (band.current) band.current.visible = false;
1151
+ if (bandLabel.current) bandLabel.current.visible = false;
1152
+ if (bandText.current) bandText.current.style.display = "none";
1153
+ invalidate();
1154
+ };
1155
+ let last = null;
1156
+ const update = (clientX, clientY, shift) => {
1157
+ last = { x: clientX, y: clientY };
1158
+ const rect = domElement.getBoundingClientRect();
1159
+ pointer.set(
1160
+ (clientX - rect.left) / rect.width * 2 - 1,
1161
+ -((clientY - rect.top) / rect.height) * 2 + 1
1162
+ );
1163
+ raycaster.setFromCamera(pointer, camera);
1164
+ const target = targetUnderRay(raycaster, scene, samples);
1165
+ if (!target) {
1166
+ hide();
1167
+ return;
1168
+ }
1169
+ const { size: viewport, draft: placed, mode: kind, format: write } = current.current;
1170
+ const snapped = snapAt(target, target.edges, camera, viewport);
1171
+ const from = placed[placed.length - 1];
1172
+ const lock = shift && from ? lockToAxis(from, snapped.point) : null;
1173
+ const found = lock ? { ...snapped, point: new Vector36(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1174
+ snap.current = found;
1175
+ const node = marker.current;
1176
+ if (node) {
1177
+ node.position.copy(found.point);
1178
+ node.visible = true;
1179
+ node.scale.setScalar(
1180
+ screenLength(camera, found.point, viewport, SNAP_MARKER_PIXELS[found.kind])
1181
+ );
1182
+ }
1183
+ if (edgeLine.current) {
1184
+ edgeLine.current.visible = found.edge !== null;
1185
+ if (found.edge) moveLine(edgeLine.current, found.edge[0], found.edge[1]);
1186
+ }
1187
+ const text = liveLabel(kind, placed, found.point, write);
1188
+ if (band.current) {
1189
+ band.current.visible = from !== void 0;
1190
+ if (from) moveLine(band.current, from, found.point);
1191
+ setLineColor(band.current, lock ? AXIS_COLORS[lock.axis] : theme.measure);
1192
+ }
1193
+ if (bandLabel.current && from) {
1194
+ const at = midpoint(from, found.point);
1195
+ bandLabel.current.position.set(at.x, at.y, at.z);
1196
+ bandLabel.current.visible = text !== null;
1197
+ }
1198
+ if (bandText.current) {
1199
+ bandText.current.style.display = text === null ? "none" : "";
1200
+ bandText.current.textContent = text ?? "";
1201
+ }
1202
+ invalidate();
1203
+ };
1204
+ const move = (event) => {
1205
+ if (event.buttons !== 0) {
1206
+ hide();
1207
+ return;
1208
+ }
1209
+ update(event.clientX, event.clientY, event.shiftKey);
1210
+ };
1211
+ const key = (event) => {
1212
+ if (event.key !== "Shift" || !last || !snap.current) return;
1213
+ update(last.x, last.y, event.type === "keydown");
1214
+ };
1215
+ const up = (event) => {
1216
+ if (event.button !== 0 || !isTap(event)) return;
1217
+ update(event.clientX, event.clientY, event.shiftKey);
1218
+ const found = snap.current;
1219
+ if (!found) return;
1220
+ current.current.onPlace({ x: found.point.x, y: found.point.y, z: found.point.z });
1221
+ };
1222
+ const leave = () => {
1223
+ last = null;
1224
+ hide();
1225
+ };
1226
+ domElement.addEventListener("pointermove", move);
1227
+ domElement.addEventListener("pointerleave", leave);
1228
+ domElement.addEventListener("pointerup", up);
1229
+ window.addEventListener("keydown", key);
1230
+ window.addEventListener("keyup", key);
1231
+ return () => {
1232
+ domElement.removeEventListener("pointermove", move);
1233
+ domElement.removeEventListener("pointerleave", leave);
1234
+ domElement.removeEventListener("pointerup", up);
1235
+ window.removeEventListener("keydown", key);
1236
+ window.removeEventListener("keyup", key);
1237
+ };
1238
+ }, [camera, domElement, invalidate, isTap, scene, theme.measure]);
1239
+ useEffect3(() => {
1240
+ if (draft.length === 0 && band.current) {
1241
+ band.current.visible = false;
1242
+ if (bandLabel.current) bandLabel.current.visible = false;
1243
+ if (bandText.current) {
1244
+ bandText.current.style.display = "none";
1245
+ bandText.current.textContent = "";
1246
+ }
1247
+ invalidate();
1248
+ }
1249
+ }, [draft, invalidate]);
1250
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1251
+ /* @__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 }) }) }),
1252
+ /* @__PURE__ */ jsx4(
1253
+ Line,
1254
+ {
1255
+ ref: edgeLine,
1256
+ points: NOWHERE,
1257
+ color: theme.measureSnap,
1258
+ lineWidth: MEASURE_LINE_PIXELS.edge,
1259
+ depthTest: false,
1260
+ depthWrite: false,
1261
+ frustumCulled: false,
1262
+ renderOrder: MEASURE_RENDER_ORDER,
1263
+ raycast: () => null
1264
+ }
1265
+ ),
1266
+ /* @__PURE__ */ jsx4(
1267
+ Line,
1268
+ {
1269
+ ref: band,
1270
+ points: NOWHERE,
1271
+ color: theme.measure,
1272
+ lineWidth: MEASURE_LINE_PIXELS.line,
1273
+ depthTest: false,
1274
+ depthWrite: false,
1275
+ frustumCulled: false,
1276
+ renderOrder: MEASURE_RENDER_ORDER,
1277
+ raycast: () => null
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ jsx4("group", { ref: bandLabel, visible: false, children: /* @__PURE__ */ jsx4(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx4(
1281
+ "div",
1282
+ {
1283
+ ref: bandText,
1284
+ className: label,
1285
+ "data-measure-label": "live",
1286
+ style: { display: "none" }
1287
+ }
1288
+ ) }) })
1289
+ ] });
1290
+ };
1291
+ function moveLine(line, from, to) {
1292
+ const start = line.geometry.getAttribute("instanceStart");
1293
+ const array = start.data.array;
1294
+ array[0] = from.x;
1295
+ array[1] = from.y;
1296
+ array[2] = from.z;
1297
+ array[3] = to.x;
1298
+ array[4] = to.y;
1299
+ array[5] = to.z;
1300
+ start.data.needsUpdate = true;
1301
+ line.computeLineDistances();
1302
+ }
1303
+ function setLineColor(line, color) {
1304
+ const material = line.material;
1305
+ if (material.color.getHex() !== color) material.color.setHex(color);
1306
+ }
1307
+ function liveLabel(mode, placed, at, format) {
1308
+ if (mode === "distance" && placed.length === 1) {
1309
+ return format(distanceBetween(placed[0], at));
1310
+ }
1311
+ if (mode === "angle" && placed.length === 2) {
1312
+ return formatDegrees(angleAt(placed[1], placed[0], at));
1313
+ }
1314
+ return null;
1315
+ }
1316
+ function targetUnderRay(raycaster, scene, samples) {
1317
+ let best = null;
1318
+ const hit = hitUnderRay(raycaster, scene);
1319
+ if (hit?.face) {
1320
+ best = {
1321
+ point: hit.point,
1322
+ normal: hit.face.normal.clone().transformDirection(hit.object.matrixWorld),
1323
+ distance: hit.distance,
1324
+ edges: samples.edgesFor(hit.object)
1325
+ };
1326
+ }
1327
+ const local = new Matrix4();
1328
+ for (const mesh of cutMeshes(scene)) {
1329
+ const planes = clippingPlanesOf(mesh);
1330
+ const sample = samples.for(mesh, planes);
1331
+ local.copy(mesh.matrixWorld).invert();
1332
+ const ray = raycaster.ray.clone().applyMatrix4(local);
1333
+ for (const [index, plane] of planes.entries()) {
1334
+ const contour = sample.contours[index];
1335
+ if (!contour) continue;
1336
+ const point = capHit(ray, plane.clone().applyMatrix4(local), contour);
1337
+ if (!point) continue;
1338
+ const world = point.applyMatrix4(mesh.matrixWorld);
1339
+ if (planes.some((other, i) => i !== index && other.distanceToPoint(world) < -1e-7)) continue;
1340
+ const distance = world.distanceTo(raycaster.ray.origin);
1341
+ if (best && distance >= best.distance) continue;
1342
+ best = {
1343
+ point: world,
1344
+ // The cap faces the half that was taken away.
1345
+ normal: plane.normal.clone().negate(),
1346
+ distance,
1347
+ edges: { positions: sample.edges, matrixWorld: mesh.matrixWorld }
1348
+ };
1349
+ }
1350
+ }
1351
+ return best;
1352
+ }
1353
+ function cutMeshes(scene) {
1354
+ const found = [];
1355
+ scene.traverse((object) => {
1356
+ if (!("isMesh" in object) || !object.visible || excludedFromFrame(object, scene)) return;
1357
+ if (clippingPlanesOf(object).length) found.push(object);
1358
+ });
1359
+ return found;
1360
+ }
1361
+ function clippingPlanesOf(mesh) {
1362
+ const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
1363
+ return material?.clippingPlanes ?? [];
1364
+ }
1365
+ function edgesBeside(object) {
1366
+ const siblings = object.parent?.children ?? [];
1367
+ for (const sibling of siblings) {
1368
+ if (!("isLineSegments" in sibling)) continue;
1369
+ const positions = sibling.geometry.getAttribute("position");
1370
+ if (positions) return positions.array;
1371
+ }
1372
+ return null;
1373
+ }
1374
+ var SampleCache = class {
1375
+ held = /* @__PURE__ */ new Map();
1376
+ regions = /* @__PURE__ */ new Map();
1377
+ /** The edges to snap to on `mesh`: its own, or the trimmed set with the cut's outline. */
1378
+ edgesFor(mesh) {
1379
+ const planes = clippingPlanesOf(mesh);
1380
+ if (planes.length === 0) {
1381
+ const positions = edgesBeside(mesh);
1382
+ return positions ? { positions, matrixWorld: mesh.matrixWorld } : null;
1383
+ }
1384
+ return { positions: this.for(mesh, planes).edges, matrixWorld: mesh.matrixWorld };
1385
+ }
1386
+ for(mesh, planes) {
1387
+ const key = planes.map((plane) => `${plane.normal.x},${plane.normal.y},${plane.normal.z},${plane.constant}`).join("|");
1388
+ const cached = this.held.get(mesh);
1389
+ if (cached && cached.key === key) return cached;
1390
+ const local = new Matrix4().copy(mesh.matrixWorld).invert();
1391
+ const localPlanes = planes.map((plane) => plane.clone().applyMatrix4(local));
1392
+ const positions = mesh.geometry.getAttribute("position")?.array ?? [];
1393
+ const regionOf = this.regionsOf(mesh.geometry);
1394
+ const contours = localPlanes.map(
1395
+ (plane, index) => localPlanes.reduce(
1396
+ (outline, other, i) => i === index ? outline : clipSegments(outline, other),
1397
+ sectionContour(positions, regionOf, plane)
1398
+ )
1399
+ );
1400
+ const own = edgesBeside(mesh) ?? [];
1401
+ const trimmed = localPlanes.reduce(
1402
+ (kept, plane) => clipSegments(kept, plane),
1403
+ own
1404
+ );
1405
+ const total = contours.reduce((sum, contour) => sum + contour.length, trimmed.length);
1406
+ const edges = new Float32Array(total);
1407
+ edges.set(trimmed, 0);
1408
+ let offset = trimmed.length;
1409
+ for (const contour of contours) {
1410
+ edges.set(contour, offset);
1411
+ offset += contour.length;
1412
+ }
1413
+ const sample = { key, contours, edges };
1414
+ this.held.set(mesh, sample);
1415
+ return sample;
1416
+ }
1417
+ /**
1418
+ * Which surface each triangle belongs to, read off the region attribute
1419
+ * `createPart` gives the mesh. A mesh without one is sampled as a single
1420
+ * surface, which still stitches a flat face's outline into one run.
1421
+ */
1422
+ regionsOf(geometry) {
1423
+ if (this.regions.has(geometry)) return this.regions.get(geometry) ?? null;
1424
+ const attribute = geometry.getAttribute(REGION_ATTRIBUTE);
1425
+ let regions = null;
1426
+ if (attribute && !geometry.index) {
1427
+ regions = new Int32Array(Math.floor(attribute.count / 3));
1428
+ for (let t = 0; t < regions.length; t += 1) regions[t] = attribute.getX(t * 3);
1429
+ }
1430
+ this.regions.set(geometry, regions);
1431
+ return regions;
1432
+ }
1433
+ };
1434
+ var MeasurementView = ({
1435
+ measurement,
1436
+ theme,
1437
+ format,
1438
+ showDeltas,
1439
+ label
1440
+ }) => {
1441
+ const text = measurementLabel(measurement, format);
1442
+ const anchor = measurementLabelAnchor(measurement);
1443
+ if (measurement.kind === "distance") {
1444
+ const [a2, b2] = measurement.points;
1445
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1446
+ /* @__PURE__ */ jsx4(Segment, { points: [a2, b2], color: theme.measure }),
1447
+ /* @__PURE__ */ jsx4(ScreenDot, { point: a2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1448
+ /* @__PURE__ */ jsx4(ScreenDot, { point: b2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1449
+ showDeltas ? /* @__PURE__ */ jsx4(DeltaLegs, { a: a2, b: b2, id: measurement.id, format, label }) : null,
1450
+ /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "distance", id: measurement.id, className: label })
1451
+ ] });
1452
+ }
1453
+ const [a, vertex, b] = measurement.points;
1454
+ const arc = angleArc(vertex, a, b, angleArcRadius(vertex, a, b));
1455
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1456
+ /* @__PURE__ */ jsx4(Segment, { points: [vertex, a], color: theme.measure }),
1457
+ /* @__PURE__ */ jsx4(Segment, { points: [vertex, b], color: theme.measure }),
1458
+ /* @__PURE__ */ jsx4(Polyline, { points: arc, color: theme.measure }),
1459
+ /* @__PURE__ */ jsx4(ScreenDot, { point: a, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1460
+ /* @__PURE__ */ jsx4(ScreenDot, { point: vertex, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1461
+ /* @__PURE__ */ jsx4(ScreenDot, { point: b, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1462
+ /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "angle", id: measurement.id, className: label })
1463
+ ] });
1464
+ };
1465
+ var DeltaLegs = ({ a, b, id, format, label }) => {
1466
+ const camera = useThree4((state) => state.camera);
1467
+ const size = useThree4((state) => state.size);
1468
+ const invalidate = useThree4((state) => state.invalidate);
1469
+ const legs = useMemo4(() => deltaLegs(a, b), [a, b]);
1470
+ const lines = useRef3([]);
1471
+ const centre = useMemo4(() => {
1472
+ const at = midpoint(a, b);
1473
+ return new Vector36(at.x, at.y, at.z);
1474
+ }, [a, b]);
1475
+ useFrame2(() => {
1476
+ const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
1477
+ let changed = false;
1478
+ for (const line of lines.current) {
1479
+ const material = line?.material;
1480
+ if (!material || Math.abs(material.dashSize - dash) <= dash * 1e-3) continue;
1481
+ material.dashSize = dash;
1482
+ material.gapSize = dash;
1483
+ changed = true;
1484
+ }
1485
+ if (changed) invalidate();
1486
+ });
1487
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1488
+ legs.map((leg, index) => /* @__PURE__ */ jsx4(
1489
+ Line,
1490
+ {
1491
+ ref: (line) => {
1492
+ lines.current[index] = line;
1493
+ },
1494
+ points: [
1495
+ new Vector36(leg.from.x, leg.from.y, leg.from.z),
1496
+ new Vector36(leg.to.x, leg.to.y, leg.to.z)
1497
+ ],
1498
+ color: AXIS_COLORS[leg.axis],
1499
+ lineWidth: MEASURE_LINE_PIXELS.leg,
1500
+ dashed: true,
1501
+ dashSize: 1,
1502
+ gapSize: 1,
1503
+ depthTest: false,
1504
+ depthWrite: false,
1505
+ frustumCulled: false,
1506
+ renderOrder: MEASURE_RENDER_ORDER,
1507
+ raycast: () => null
1508
+ },
1509
+ leg.axis
1510
+ )),
1511
+ legs.map((leg) => /* @__PURE__ */ jsx4(
1512
+ Label,
1513
+ {
1514
+ at: midpoint(leg.from, leg.to),
1515
+ text: `${leg.axis.toUpperCase()} ${format(leg.length)}`,
1516
+ kind: "delta",
1517
+ axis: leg.axis,
1518
+ id,
1519
+ className: label
1520
+ },
1521
+ leg.axis
1522
+ ))
1523
+ ] });
1524
+ };
1525
+ 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 }) }) });
1526
+ var ScreenDot = ({ point, pixels, color }) => {
1527
+ const camera = useThree4((state) => state.camera);
1528
+ const size = useThree4((state) => state.size);
1529
+ const invalidate = useThree4((state) => state.invalidate);
1530
+ const node = useRef3(null);
1531
+ const dot = useDot();
1532
+ const position = useMemo4(() => new Vector36(point.x, point.y, point.z), [point]);
1533
+ useFrame2(() => {
1534
+ const group = node.current;
1535
+ if (!group) return;
1536
+ const length = screenLength(camera, position, size, pixels);
1537
+ if (Math.abs(group.scale.x - length) > length * 1e-3) {
1538
+ group.scale.setScalar(length);
1539
+ invalidate();
1540
+ }
1541
+ });
1542
+ 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 }) }) });
1543
+ };
1544
+ var Segment = ({ points, color }) => {
1545
+ const [a, b] = points;
1546
+ const ends = useMemo4(
1547
+ () => [new Vector36(a.x, a.y, a.z), new Vector36(b.x, b.y, b.z)],
1548
+ // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1549
+ // and a rebuilt array would rebuild the line's geometry with it.
1550
+ [a.x, a.y, a.z, b.x, b.y, b.z]
1551
+ );
1552
+ return /* @__PURE__ */ jsx4(Polyline, { points: ends, color });
1553
+ };
1554
+ var Polyline = ({ points, color }) => {
1555
+ if (points.length < 2) return null;
1556
+ return /* @__PURE__ */ jsx4(
1557
+ Line,
1558
+ {
1559
+ points,
1560
+ color,
1561
+ lineWidth: MEASURE_LINE_PIXELS.line,
1562
+ depthTest: false,
1563
+ depthWrite: false,
1564
+ frustumCulled: false,
1565
+ renderOrder: MEASURE_RENDER_ORDER,
1566
+ raycast: () => null
1567
+ }
1568
+ );
1569
+ };
1570
+ function useDot() {
1571
+ const geometry = useMemo4(() => new SphereGeometry(1, 16, 12), []);
1572
+ useEffect3(() => () => geometry.dispose(), [geometry]);
1573
+ return geometry;
1574
+ }
1575
+ function isEditing(target) {
1576
+ if (!(target instanceof HTMLElement)) return false;
1577
+ return target.isContentEditable || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement;
1578
+ }
1579
+ function className(extra) {
1580
+ return extra ? `${MEASURE_LABEL_CLASS} ${extra}` : MEASURE_LABEL_CLASS;
1581
+ }
1582
+
357
1583
  // src/model/normals.ts
358
1584
  function regionNormals(geometry, regions) {
359
1585
  const position = geometry.getAttribute("position");
@@ -388,14 +1614,23 @@ function regionNormals(geometry, regions) {
388
1614
  return normals;
389
1615
  }
390
1616
  export {
1617
+ ANGLE_ARC_FRACTION,
1618
+ ANGLE_ARC_SEGMENTS,
1619
+ AXES_PLANE_OFFSET,
1620
+ AXES_PLANE_SCALE,
1621
+ AXIS_COLORS,
391
1622
  Axes,
392
1623
  CAD_CAMERA_UP,
393
1624
  CANDIDATE_WEIGHT,
1625
+ CAP_HATCH,
394
1626
  CHAMFER,
395
1627
  CadCameraControls,
396
1628
  DEFAULT_FIT_MARGIN,
1629
+ DEFAULT_SECTION_NORMAL,
397
1630
  DEFAULT_THEME,
1631
+ DELTA_DASH_PIXELS,
398
1632
  DIRECTION_COLORS,
1633
+ DISABLED_SECTION,
399
1634
  DOUBLE_TAP_MS,
400
1635
  DirectionArrows,
401
1636
  EXCLUDE_FROM_FRAME,
@@ -408,7 +1643,12 @@ export {
408
1643
  HIGHLIGHT_WEIGHT,
409
1644
  HOVER_WEIGHT,
410
1645
  MAX_FRAME_RATIO,
1646
+ MEASURE_LABEL_CLASS,
1647
+ MEASURE_LINE_PIXELS,
1648
+ MEASURE_MARKER_PIXELS,
1649
+ MEASURE_RENDER_ORDER,
411
1650
  MIN_FRAME_RATIO,
1651
+ MeasureTool,
412
1652
  NO_MODIFIERS,
413
1653
  ORBIT_TARGET_COLOR,
414
1654
  ORBIT_TARGET_FADE_MS,
@@ -418,12 +1658,18 @@ export {
418
1658
  ORBIT_TARGET_RING_OPACITY,
419
1659
  ORBIT_TARGET_RING_PIXELS,
420
1660
  ORBIT_TARGET_RING_WIDTH,
1661
+ OUTLINE_SCALE,
421
1662
  PERSPECTIVE_FOV,
422
1663
  PICKED_SURFACE_LABEL,
1664
+ POINTS_PER_MEASUREMENT,
1665
+ PREVIEW_SCALE,
423
1666
  PartMesh,
424
1667
  PartReportFormatError,
425
1668
  REGION_ATTRIBUTE,
426
1669
  SECTION_RENDER_ORDER,
1670
+ SNAP_MARKER_PIXELS,
1671
+ SNAP_PIXELS,
1672
+ SectionTool,
427
1673
  SectionView,
428
1674
  TAP_SLOP,
429
1675
  UnsupportedKernelVersionError,
@@ -432,10 +1678,15 @@ export {
432
1678
  ViewCube,
433
1679
  Viewer,
434
1680
  adaptedUp,
1681
+ angleArc,
1682
+ angleArcRadius,
1683
+ angleAt,
1684
+ applyCapTheme,
435
1685
  applyHighlightLayers,
436
1686
  applyProjection,
437
1687
  arrowPlacement,
438
1688
  aspectRatio,
1689
+ axesPlanes,
439
1690
  bestOwner,
440
1691
  boundsFromBox,
441
1692
  buildPick,
@@ -444,32 +1695,53 @@ export {
444
1695
  buildRegionTexels,
445
1696
  cadViewDirections,
446
1697
  cameraLimits,
1698
+ capHit,
1699
+ clipSegments,
447
1700
  contentBounds,
1701
+ createCapMaterial,
1702
+ createMaskMaterial,
1703
+ createMaskTarget,
448
1704
  createPart,
1705
+ createSectionStore,
1706
+ createStencilMaterials,
449
1707
  cubeOutlineGeometry,
450
1708
  cubeZones,
451
1709
  currentViewDirection,
452
1710
  cycleOwner,
453
1711
  defaultBounds,
1712
+ deltaBetween,
1713
+ deltaLegs,
454
1714
  directionColor,
455
1715
  directionIndexOf,
456
1716
  directionLabel,
1717
+ distanceBetween,
457
1718
  dragPlane,
1719
+ excludedFromFrame,
458
1720
  featureTypeRank,
459
1721
  fitDistance,
460
1722
  focusForPick,
1723
+ formatDegrees,
1724
+ formatMillimetres,
461
1725
  gridGeometry,
462
1726
  gridSpec,
463
1727
  groupByDirection,
1728
+ hitUnderRay,
464
1729
  labelGeometry,
465
1730
  labelTexture,
1731
+ lockToAxis,
1732
+ measurementFromPoints,
1733
+ measurementLabel,
1734
+ measurementLabelAnchor,
1735
+ midpoint,
466
1736
  movedFar,
1737
+ nextMeasurementId,
467
1738
  normalizePartReport,
468
1739
  orbitTargetOpacity,
469
1740
  orthographicHalfHeight,
470
1741
  panelGeometry,
471
1742
  perspectiveFitDistance,
472
1743
  pickedStartDepth,
1744
+ pointInContour,
473
1745
  rankOwners,
474
1746
  regionAdjacency,
475
1747
  regionEdgesGeometry,
@@ -478,24 +1750,32 @@ export {
478
1750
  resolveTheme,
479
1751
  retargetPose,
480
1752
  sameDirection,
1753
+ sampleSection,
481
1754
  screenLength,
1755
+ screenPoint,
1756
+ sectionAnchor,
482
1757
  sectionBounds,
483
1758
  sectionConstant,
1759
+ sectionContour,
484
1760
  sectionDepth,
485
1761
  sectionDepthConstant,
486
1762
  sectionDepthRange,
487
1763
  sectionFromPick,
488
1764
  sectionOffset,
1765
+ sectionOptionsFromState,
489
1766
  sectionPlane,
490
1767
  smoothRegionNormals,
1768
+ snapAt,
491
1769
  squaredUp,
492
1770
  startPosition,
1771
+ surfaceUnderRay,
493
1772
  targetBoundary,
494
1773
  themesEqual,
495
1774
  trackDoubleTaps,
496
1775
  trackTaps,
497
1776
  useContentBox,
498
1777
  useRetarget,
1778
+ useSectionStore,
499
1779
  useTapGuard,
500
1780
  useViewerControls,
501
1781
  viewDirection,