@toolpath/viewer 1.1.0 → 1.2.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";
@@ -155,6 +175,18 @@ function gridGeometry(spec) {
155
175
  geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
156
176
  return geometry;
157
177
  }
178
+ function gridFor(box, options = {}) {
179
+ if (box.isEmpty()) return null;
180
+ const spec = gridSpec(box);
181
+ const cell = options.step ?? spec.step;
182
+ return gridGeometry({
183
+ ...spec,
184
+ step: cell,
185
+ // Snapped outwards to a whole number of cells, so the part sits inside the
186
+ // grid rather than ending part-way through a square.
187
+ extent: options.extent ?? Math.ceil(spec.extent / cell) * cell
188
+ });
189
+ }
158
190
 
159
191
  // src/primitives.tsx
160
192
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -162,18 +194,9 @@ var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
162
194
  var Grid = ({ step, extent, color, opacity = 0.35 }) => {
163
195
  const theme = resolveTheme();
164
196
  const box = useContentBox();
165
- const geometry = useMemo(() => {
166
- const spec = gridSpec(box);
167
- const cell = step ?? spec.step;
168
- return gridGeometry({
169
- ...spec,
170
- step: cell,
171
- // Snapped outwards to a whole number of cells, so the part sits inside
172
- // the grid rather than ending part-way through a square.
173
- extent: extent ?? Math.ceil(spec.extent / cell) * cell
174
- });
175
- }, [box, extent, step]);
176
- useEffect(() => () => geometry.dispose(), [geometry]);
197
+ const geometry = useMemo(() => gridFor(box, { step, extent }), [box, extent, step]);
198
+ useEffect(() => () => geometry?.dispose(), [geometry]);
199
+ if (!geometry) return null;
177
200
  return /* @__PURE__ */ jsx("lineSegments", { geometry, renderOrder: -1, raycast: () => null, userData: FURNITURE, children: /* @__PURE__ */ jsx("lineBasicMaterial", { color: color ?? theme.cubeEdge, transparent: true, opacity }) });
178
201
  };
179
202
  var Axes = ({ size = 25 }) => /* @__PURE__ */ jsx("axesHelper", { args: [size], userData: FURNITURE, raycast: () => null });
@@ -351,6 +374,936 @@ var Arrow = ({ direction, box, color, opacity, onPick, onTop = false }) => {
351
374
  ] });
352
375
  };
353
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
+ Raycaster as Raycaster2,
846
+ SphereGeometry,
847
+ Vector2 as Vector23,
848
+ Vector3 as Vector35
849
+ } from "three";
850
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
851
+ var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
852
+ var MEASURE_LABEL_CLASS = "toolpath-measure-label";
853
+ var MeasureTool = ({
854
+ mode = "distance",
855
+ measurements,
856
+ onChange,
857
+ showDeltas = true,
858
+ format = formatMillimetres,
859
+ labelClassName,
860
+ theme
861
+ }) => {
862
+ const store = useSectionStore();
863
+ const box = useContentBox();
864
+ const resolved = useMemo4(() => resolveTheme(theme), [theme]);
865
+ const [held, setHeld] = useState3([]);
866
+ const controlled = measurements !== void 0;
867
+ const list = controlled ? measurements : held;
868
+ const [draft, setDraft] = useState3([]);
869
+ useEffect3(() => {
870
+ store.setEngaged(true);
871
+ return () => store.setEngaged(false);
872
+ }, [store]);
873
+ useEffect3(() => {
874
+ setDraft([]);
875
+ }, [mode]);
876
+ const commit = (next) => {
877
+ if (!controlled) setHeld(next);
878
+ onChange?.(next);
879
+ };
880
+ const latest = useRef3({ list, commit });
881
+ latest.current = { list, commit };
882
+ useEffect3(() => {
883
+ const onKey = (event) => {
884
+ if (event.key === "Escape") {
885
+ setDraft([]);
886
+ return;
887
+ }
888
+ if (event.key !== "Delete" && event.key !== "Backspace") return;
889
+ if (isEditing(event.target)) return;
890
+ const { list: current, commit: apply } = latest.current;
891
+ if (current.length === 0) return;
892
+ event.preventDefault();
893
+ apply(current.slice(0, -1));
894
+ };
895
+ window.addEventListener("keydown", onKey);
896
+ return () => window.removeEventListener("keydown", onKey);
897
+ }, []);
898
+ const place = (point) => {
899
+ const points = [...draft, point];
900
+ const complete = measurementFromPoints(mode, points, nextMeasurementId(list));
901
+ if (complete) {
902
+ commit([...list, complete]);
903
+ setDraft([]);
904
+ } else {
905
+ setDraft(points);
906
+ }
907
+ };
908
+ if (box.isEmpty()) return null;
909
+ const label = className(labelClassName);
910
+ return /* @__PURE__ */ jsxs4("group", { userData: FURNITURE4, children: [
911
+ /* @__PURE__ */ jsx4(
912
+ Snapper,
913
+ {
914
+ mode,
915
+ draft,
916
+ onPlace: place,
917
+ theme: resolved,
918
+ format,
919
+ label
920
+ }
921
+ ),
922
+ draft.map((point, index) => /* @__PURE__ */ jsx4(
923
+ ScreenDot,
924
+ {
925
+ point,
926
+ pixels: MEASURE_MARKER_PIXELS,
927
+ color: resolved.measure
928
+ },
929
+ index
930
+ )),
931
+ draft.length === 2 ? /* @__PURE__ */ jsx4(Segment, { points: [draft[0], draft[1]], color: resolved.measure }) : null,
932
+ list.map((measurement) => /* @__PURE__ */ jsx4(
933
+ MeasurementView,
934
+ {
935
+ measurement,
936
+ theme: resolved,
937
+ format,
938
+ showDeltas,
939
+ label
940
+ },
941
+ measurement.id
942
+ ))
943
+ ] });
944
+ };
945
+ var NO_POINTER = { pointerEvents: "none" };
946
+ var NOWHERE = [new Vector35(), new Vector35()];
947
+ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
948
+ const camera = useThree4((state) => state.camera);
949
+ const scene = useThree4((state) => state.scene);
950
+ const size = useThree4((state) => state.size);
951
+ const domElement = useThree4((state) => state.gl.domElement);
952
+ const invalidate = useThree4((state) => state.invalidate);
953
+ const isTap = useTapGuard();
954
+ const marker = useRef3(null);
955
+ const edgeLine = useRef3(null);
956
+ const band = useRef3(null);
957
+ const bandLabel = useRef3(null);
958
+ const bandText = useRef3(null);
959
+ const snap = useRef3(null);
960
+ const dot = useDot();
961
+ useLayoutEffect(() => {
962
+ if (edgeLine.current) edgeLine.current.visible = false;
963
+ if (band.current) band.current.visible = false;
964
+ }, []);
965
+ const current = useRef3({ mode, draft, onPlace, format, size });
966
+ current.current = { mode, draft, onPlace, format, size };
967
+ useEffect3(() => {
968
+ const raycaster = new Raycaster2();
969
+ const pointer = new Vector23();
970
+ const hide = () => {
971
+ if (!snap.current) return;
972
+ snap.current = null;
973
+ if (marker.current) marker.current.visible = false;
974
+ if (edgeLine.current) edgeLine.current.visible = false;
975
+ if (band.current) band.current.visible = false;
976
+ if (bandLabel.current) bandLabel.current.visible = false;
977
+ if (bandText.current) bandText.current.style.display = "none";
978
+ invalidate();
979
+ };
980
+ let last = null;
981
+ const update = (clientX, clientY, shift) => {
982
+ last = { x: clientX, y: clientY };
983
+ const rect = domElement.getBoundingClientRect();
984
+ pointer.set(
985
+ (clientX - rect.left) / rect.width * 2 - 1,
986
+ -((clientY - rect.top) / rect.height) * 2 + 1
987
+ );
988
+ raycaster.setFromCamera(pointer, camera);
989
+ const hit = hitUnderRay(raycaster, scene);
990
+ if (!hit?.face) {
991
+ hide();
992
+ return;
993
+ }
994
+ const normal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
995
+ const { size: viewport, draft: placed, mode: kind, format: write } = current.current;
996
+ const snapped = snapAt(
997
+ { point: hit.point, normal },
998
+ edgesBeside(hit.object),
999
+ camera,
1000
+ viewport
1001
+ );
1002
+ const from = placed[placed.length - 1];
1003
+ const lock = shift && from ? lockToAxis(from, snapped.point) : null;
1004
+ const found = lock ? { ...snapped, point: new Vector35(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1005
+ snap.current = found;
1006
+ const node = marker.current;
1007
+ if (node) {
1008
+ node.position.copy(found.point);
1009
+ node.visible = true;
1010
+ node.scale.setScalar(
1011
+ screenLength(camera, found.point, viewport, SNAP_MARKER_PIXELS[found.kind])
1012
+ );
1013
+ }
1014
+ if (edgeLine.current) {
1015
+ edgeLine.current.visible = found.edge !== null;
1016
+ if (found.edge) moveLine(edgeLine.current, found.edge[0], found.edge[1]);
1017
+ }
1018
+ const text = liveLabel(kind, placed, found.point, write);
1019
+ if (band.current) {
1020
+ band.current.visible = from !== void 0;
1021
+ if (from) moveLine(band.current, from, found.point);
1022
+ setLineColor(band.current, lock ? AXIS_COLORS[lock.axis] : theme.measure);
1023
+ }
1024
+ if (bandLabel.current && from) {
1025
+ const at = midpoint(from, found.point);
1026
+ bandLabel.current.position.set(at.x, at.y, at.z);
1027
+ bandLabel.current.visible = text !== null;
1028
+ }
1029
+ if (bandText.current) {
1030
+ bandText.current.style.display = text === null ? "none" : "";
1031
+ bandText.current.textContent = text ?? "";
1032
+ }
1033
+ invalidate();
1034
+ };
1035
+ const move = (event) => {
1036
+ if (event.buttons !== 0) {
1037
+ hide();
1038
+ return;
1039
+ }
1040
+ update(event.clientX, event.clientY, event.shiftKey);
1041
+ };
1042
+ const key = (event) => {
1043
+ if (event.key !== "Shift" || !last || !snap.current) return;
1044
+ update(last.x, last.y, event.type === "keydown");
1045
+ };
1046
+ const up = (event) => {
1047
+ if (event.button !== 0 || !isTap(event)) return;
1048
+ update(event.clientX, event.clientY, event.shiftKey);
1049
+ const found = snap.current;
1050
+ if (!found) return;
1051
+ current.current.onPlace({ x: found.point.x, y: found.point.y, z: found.point.z });
1052
+ };
1053
+ const leave = () => {
1054
+ last = null;
1055
+ hide();
1056
+ };
1057
+ domElement.addEventListener("pointermove", move);
1058
+ domElement.addEventListener("pointerleave", leave);
1059
+ domElement.addEventListener("pointerup", up);
1060
+ window.addEventListener("keydown", key);
1061
+ window.addEventListener("keyup", key);
1062
+ return () => {
1063
+ domElement.removeEventListener("pointermove", move);
1064
+ domElement.removeEventListener("pointerleave", leave);
1065
+ domElement.removeEventListener("pointerup", up);
1066
+ window.removeEventListener("keydown", key);
1067
+ window.removeEventListener("keyup", key);
1068
+ };
1069
+ }, [camera, domElement, invalidate, isTap, scene, theme.measure]);
1070
+ useEffect3(() => {
1071
+ if (draft.length === 0 && band.current) {
1072
+ band.current.visible = false;
1073
+ if (bandLabel.current) bandLabel.current.visible = false;
1074
+ if (bandText.current) {
1075
+ bandText.current.style.display = "none";
1076
+ bandText.current.textContent = "";
1077
+ }
1078
+ invalidate();
1079
+ }
1080
+ }, [draft, invalidate]);
1081
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1082
+ /* @__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 }) }) }),
1083
+ /* @__PURE__ */ jsx4(
1084
+ Line,
1085
+ {
1086
+ ref: edgeLine,
1087
+ points: NOWHERE,
1088
+ color: theme.measureSnap,
1089
+ lineWidth: MEASURE_LINE_PIXELS.edge,
1090
+ depthTest: false,
1091
+ depthWrite: false,
1092
+ frustumCulled: false,
1093
+ renderOrder: MEASURE_RENDER_ORDER,
1094
+ raycast: () => null
1095
+ }
1096
+ ),
1097
+ /* @__PURE__ */ jsx4(
1098
+ Line,
1099
+ {
1100
+ ref: band,
1101
+ points: NOWHERE,
1102
+ color: theme.measure,
1103
+ lineWidth: MEASURE_LINE_PIXELS.line,
1104
+ depthTest: false,
1105
+ depthWrite: false,
1106
+ frustumCulled: false,
1107
+ renderOrder: MEASURE_RENDER_ORDER,
1108
+ raycast: () => null
1109
+ }
1110
+ ),
1111
+ /* @__PURE__ */ jsx4("group", { ref: bandLabel, visible: false, children: /* @__PURE__ */ jsx4(Html, { center: true, zIndexRange: [0, 0], style: NO_POINTER, children: /* @__PURE__ */ jsx4(
1112
+ "div",
1113
+ {
1114
+ ref: bandText,
1115
+ className: label,
1116
+ "data-measure-label": "live",
1117
+ style: { display: "none" }
1118
+ }
1119
+ ) }) })
1120
+ ] });
1121
+ };
1122
+ function moveLine(line, from, to) {
1123
+ const start = line.geometry.getAttribute("instanceStart");
1124
+ const array = start.data.array;
1125
+ array[0] = from.x;
1126
+ array[1] = from.y;
1127
+ array[2] = from.z;
1128
+ array[3] = to.x;
1129
+ array[4] = to.y;
1130
+ array[5] = to.z;
1131
+ start.data.needsUpdate = true;
1132
+ line.computeLineDistances();
1133
+ }
1134
+ function setLineColor(line, color) {
1135
+ const material = line.material;
1136
+ if (material.color.getHex() !== color) material.color.setHex(color);
1137
+ }
1138
+ function liveLabel(mode, placed, at, format) {
1139
+ if (mode === "distance" && placed.length === 1) {
1140
+ return format(distanceBetween(placed[0], at));
1141
+ }
1142
+ if (mode === "angle" && placed.length === 2) {
1143
+ return formatDegrees(angleAt(placed[1], placed[0], at));
1144
+ }
1145
+ return null;
1146
+ }
1147
+ function edgesBeside(object) {
1148
+ const siblings = object.parent?.children ?? [];
1149
+ for (const sibling of siblings) {
1150
+ if (!("isLineSegments" in sibling)) continue;
1151
+ const geometry = sibling.geometry;
1152
+ const positions = geometry.getAttribute("position");
1153
+ if (!positions) continue;
1154
+ return { positions: positions.array, matrixWorld: sibling.matrixWorld };
1155
+ }
1156
+ return null;
1157
+ }
1158
+ var MeasurementView = ({
1159
+ measurement,
1160
+ theme,
1161
+ format,
1162
+ showDeltas,
1163
+ label
1164
+ }) => {
1165
+ const text = measurementLabel(measurement, format);
1166
+ const anchor = measurementLabelAnchor(measurement);
1167
+ if (measurement.kind === "distance") {
1168
+ const [a2, b2] = measurement.points;
1169
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1170
+ /* @__PURE__ */ jsx4(Segment, { points: [a2, b2], color: theme.measure }),
1171
+ /* @__PURE__ */ jsx4(ScreenDot, { point: a2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1172
+ /* @__PURE__ */ jsx4(ScreenDot, { point: b2, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1173
+ showDeltas ? /* @__PURE__ */ jsx4(DeltaLegs, { a: a2, b: b2, id: measurement.id, format, label }) : null,
1174
+ /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "distance", id: measurement.id, className: label })
1175
+ ] });
1176
+ }
1177
+ const [a, vertex, b] = measurement.points;
1178
+ const arc = angleArc(vertex, a, b, angleArcRadius(vertex, a, b));
1179
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1180
+ /* @__PURE__ */ jsx4(Segment, { points: [vertex, a], color: theme.measure }),
1181
+ /* @__PURE__ */ jsx4(Segment, { points: [vertex, b], color: theme.measure }),
1182
+ /* @__PURE__ */ jsx4(Polyline, { points: arc, color: theme.measure }),
1183
+ /* @__PURE__ */ jsx4(ScreenDot, { point: a, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1184
+ /* @__PURE__ */ jsx4(ScreenDot, { point: vertex, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1185
+ /* @__PURE__ */ jsx4(ScreenDot, { point: b, pixels: MEASURE_MARKER_PIXELS, color: theme.measure }),
1186
+ /* @__PURE__ */ jsx4(Label, { at: anchor, text, kind: "angle", id: measurement.id, className: label })
1187
+ ] });
1188
+ };
1189
+ var DeltaLegs = ({ a, b, id, format, label }) => {
1190
+ const camera = useThree4((state) => state.camera);
1191
+ const size = useThree4((state) => state.size);
1192
+ const invalidate = useThree4((state) => state.invalidate);
1193
+ const legs = useMemo4(() => deltaLegs(a, b), [a, b]);
1194
+ const lines = useRef3([]);
1195
+ const centre = useMemo4(() => {
1196
+ const at = midpoint(a, b);
1197
+ return new Vector35(at.x, at.y, at.z);
1198
+ }, [a, b]);
1199
+ useFrame2(() => {
1200
+ const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
1201
+ let changed = false;
1202
+ for (const line of lines.current) {
1203
+ const material = line?.material;
1204
+ if (!material || Math.abs(material.dashSize - dash) <= dash * 1e-3) continue;
1205
+ material.dashSize = dash;
1206
+ material.gapSize = dash;
1207
+ changed = true;
1208
+ }
1209
+ if (changed) invalidate();
1210
+ });
1211
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1212
+ legs.map((leg, index) => /* @__PURE__ */ jsx4(
1213
+ Line,
1214
+ {
1215
+ ref: (line) => {
1216
+ lines.current[index] = line;
1217
+ },
1218
+ points: [
1219
+ new Vector35(leg.from.x, leg.from.y, leg.from.z),
1220
+ new Vector35(leg.to.x, leg.to.y, leg.to.z)
1221
+ ],
1222
+ color: AXIS_COLORS[leg.axis],
1223
+ lineWidth: MEASURE_LINE_PIXELS.leg,
1224
+ dashed: true,
1225
+ dashSize: 1,
1226
+ gapSize: 1,
1227
+ depthTest: false,
1228
+ depthWrite: false,
1229
+ frustumCulled: false,
1230
+ renderOrder: MEASURE_RENDER_ORDER,
1231
+ raycast: () => null
1232
+ },
1233
+ leg.axis
1234
+ )),
1235
+ legs.map((leg) => /* @__PURE__ */ jsx4(
1236
+ Label,
1237
+ {
1238
+ at: midpoint(leg.from, leg.to),
1239
+ text: `${leg.axis.toUpperCase()} ${format(leg.length)}`,
1240
+ kind: "delta",
1241
+ axis: leg.axis,
1242
+ id,
1243
+ className: label
1244
+ },
1245
+ leg.axis
1246
+ ))
1247
+ ] });
1248
+ };
1249
+ 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 }) }) });
1250
+ var ScreenDot = ({ point, pixels, color }) => {
1251
+ const camera = useThree4((state) => state.camera);
1252
+ const size = useThree4((state) => state.size);
1253
+ const invalidate = useThree4((state) => state.invalidate);
1254
+ const node = useRef3(null);
1255
+ const dot = useDot();
1256
+ const position = useMemo4(() => new Vector35(point.x, point.y, point.z), [point]);
1257
+ useFrame2(() => {
1258
+ const group = node.current;
1259
+ if (!group) return;
1260
+ const length = screenLength(camera, position, size, pixels);
1261
+ if (Math.abs(group.scale.x - length) > length * 1e-3) {
1262
+ group.scale.setScalar(length);
1263
+ invalidate();
1264
+ }
1265
+ });
1266
+ 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 }) }) });
1267
+ };
1268
+ var Segment = ({ points, color }) => {
1269
+ const [a, b] = points;
1270
+ const ends = useMemo4(
1271
+ () => [new Vector35(a.x, a.y, a.z), new Vector35(b.x, b.y, b.z)],
1272
+ // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1273
+ // and a rebuilt array would rebuild the line's geometry with it.
1274
+ [a.x, a.y, a.z, b.x, b.y, b.z]
1275
+ );
1276
+ return /* @__PURE__ */ jsx4(Polyline, { points: ends, color });
1277
+ };
1278
+ var Polyline = ({ points, color }) => {
1279
+ if (points.length < 2) return null;
1280
+ return /* @__PURE__ */ jsx4(
1281
+ Line,
1282
+ {
1283
+ points,
1284
+ color,
1285
+ lineWidth: MEASURE_LINE_PIXELS.line,
1286
+ depthTest: false,
1287
+ depthWrite: false,
1288
+ frustumCulled: false,
1289
+ renderOrder: MEASURE_RENDER_ORDER,
1290
+ raycast: () => null
1291
+ }
1292
+ );
1293
+ };
1294
+ function useDot() {
1295
+ const geometry = useMemo4(() => new SphereGeometry(1, 16, 12), []);
1296
+ useEffect3(() => () => geometry.dispose(), [geometry]);
1297
+ return geometry;
1298
+ }
1299
+ function isEditing(target) {
1300
+ if (!(target instanceof HTMLElement)) return false;
1301
+ return target.isContentEditable || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement;
1302
+ }
1303
+ function className(extra) {
1304
+ return extra ? `${MEASURE_LABEL_CLASS} ${extra}` : MEASURE_LABEL_CLASS;
1305
+ }
1306
+
354
1307
  // src/model/normals.ts
355
1308
  function regionNormals(geometry, regions) {
356
1309
  const position = geometry.getAttribute("position");
@@ -385,14 +1338,23 @@ function regionNormals(geometry, regions) {
385
1338
  return normals;
386
1339
  }
387
1340
  export {
1341
+ ANGLE_ARC_FRACTION,
1342
+ ANGLE_ARC_SEGMENTS,
1343
+ AXES_PLANE_OFFSET,
1344
+ AXES_PLANE_SCALE,
1345
+ AXIS_COLORS,
388
1346
  Axes,
389
1347
  CAD_CAMERA_UP,
390
1348
  CANDIDATE_WEIGHT,
1349
+ CAP_HATCH,
391
1350
  CHAMFER,
392
1351
  CadCameraControls,
393
1352
  DEFAULT_FIT_MARGIN,
1353
+ DEFAULT_SECTION_NORMAL,
394
1354
  DEFAULT_THEME,
1355
+ DELTA_DASH_PIXELS,
395
1356
  DIRECTION_COLORS,
1357
+ DISABLED_SECTION,
396
1358
  DOUBLE_TAP_MS,
397
1359
  DirectionArrows,
398
1360
  EXCLUDE_FROM_FRAME,
@@ -405,7 +1367,12 @@ export {
405
1367
  HIGHLIGHT_WEIGHT,
406
1368
  HOVER_WEIGHT,
407
1369
  MAX_FRAME_RATIO,
1370
+ MEASURE_LABEL_CLASS,
1371
+ MEASURE_LINE_PIXELS,
1372
+ MEASURE_MARKER_PIXELS,
1373
+ MEASURE_RENDER_ORDER,
408
1374
  MIN_FRAME_RATIO,
1375
+ MeasureTool,
409
1376
  NO_MODIFIERS,
410
1377
  ORBIT_TARGET_COLOR,
411
1378
  ORBIT_TARGET_FADE_MS,
@@ -415,12 +1382,18 @@ export {
415
1382
  ORBIT_TARGET_RING_OPACITY,
416
1383
  ORBIT_TARGET_RING_PIXELS,
417
1384
  ORBIT_TARGET_RING_WIDTH,
1385
+ OUTLINE_SCALE,
418
1386
  PERSPECTIVE_FOV,
419
1387
  PICKED_SURFACE_LABEL,
1388
+ POINTS_PER_MEASUREMENT,
1389
+ PREVIEW_SCALE,
420
1390
  PartMesh,
421
1391
  PartReportFormatError,
422
1392
  REGION_ATTRIBUTE,
423
1393
  SECTION_RENDER_ORDER,
1394
+ SNAP_MARKER_PIXELS,
1395
+ SNAP_PIXELS,
1396
+ SectionTool,
424
1397
  SectionView,
425
1398
  TAP_SLOP,
426
1399
  UnsupportedKernelVersionError,
@@ -429,10 +1402,15 @@ export {
429
1402
  ViewCube,
430
1403
  Viewer,
431
1404
  adaptedUp,
1405
+ angleArc,
1406
+ angleArcRadius,
1407
+ angleAt,
1408
+ applyCapTheme,
432
1409
  applyHighlightLayers,
433
1410
  applyProjection,
434
1411
  arrowPlacement,
435
1412
  aspectRatio,
1413
+ axesPlanes,
436
1414
  bestOwner,
437
1415
  boundsFromBox,
438
1416
  buildPick,
@@ -442,25 +1420,43 @@ export {
442
1420
  cadViewDirections,
443
1421
  cameraLimits,
444
1422
  contentBounds,
1423
+ createCapMaterial,
1424
+ createMaskMaterial,
1425
+ createMaskTarget,
445
1426
  createPart,
1427
+ createSectionStore,
1428
+ createStencilMaterials,
446
1429
  cubeOutlineGeometry,
447
1430
  cubeZones,
448
1431
  currentViewDirection,
449
1432
  cycleOwner,
450
1433
  defaultBounds,
1434
+ deltaBetween,
1435
+ deltaLegs,
451
1436
  directionColor,
452
1437
  directionIndexOf,
453
1438
  directionLabel,
1439
+ distanceBetween,
454
1440
  dragPlane,
1441
+ excludedFromFrame,
455
1442
  featureTypeRank,
456
1443
  fitDistance,
457
1444
  focusForPick,
1445
+ formatDegrees,
1446
+ formatMillimetres,
458
1447
  gridGeometry,
459
1448
  gridSpec,
460
1449
  groupByDirection,
1450
+ hitUnderRay,
461
1451
  labelGeometry,
462
1452
  labelTexture,
1453
+ lockToAxis,
1454
+ measurementFromPoints,
1455
+ measurementLabel,
1456
+ measurementLabelAnchor,
1457
+ midpoint,
463
1458
  movedFar,
1459
+ nextMeasurementId,
464
1460
  normalizePartReport,
465
1461
  orbitTargetOpacity,
466
1462
  orthographicHalfHeight,
@@ -476,6 +1472,8 @@ export {
476
1472
  retargetPose,
477
1473
  sameDirection,
478
1474
  screenLength,
1475
+ screenPoint,
1476
+ sectionAnchor,
479
1477
  sectionBounds,
480
1478
  sectionConstant,
481
1479
  sectionDepth,
@@ -483,16 +1481,20 @@ export {
483
1481
  sectionDepthRange,
484
1482
  sectionFromPick,
485
1483
  sectionOffset,
1484
+ sectionOptionsFromState,
486
1485
  sectionPlane,
487
1486
  smoothRegionNormals,
1487
+ snapAt,
488
1488
  squaredUp,
489
1489
  startPosition,
1490
+ surfaceUnderRay,
490
1491
  targetBoundary,
491
1492
  themesEqual,
492
1493
  trackDoubleTaps,
493
1494
  trackTaps,
494
1495
  useContentBox,
495
1496
  useRetarget,
1497
+ useSectionStore,
496
1498
  useTapGuard,
497
1499
  useViewerControls,
498
1500
  viewDirection,