@toolpath/viewer 1.2.0 → 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/README.md CHANGED
@@ -594,6 +594,13 @@ convert, so pass `format` to show anything else:
594
594
  <MeasureTool format={(mm) => `${(mm / MM_PER_INCH).toFixed(3)} in`} />
595
595
  ```
596
596
 
597
+ **Measuring a cut part.** Mount the measure tool with a section cut in place, or beside
598
+ `<SectionTool>`, and it measures what is on screen: the half a cut has removed is not there to snap
599
+ to, the part's edges stop at the plane, the outline where the plane passes through solid material
600
+ is an edge with corners of its own, and the capped face is a surface a point can land on. The
601
+ sample follows the cut — drag the handle or set a depth and the next pointer move sees the new cut.
602
+ It reads the plane off the part's own material, so a controlled `section` prop works the same way.
603
+
597
604
  Hold Shift to hold the next point to the X, Y or Z line through the last one — whichever axis the
598
605
  pointer is furthest along — so a length along an edge is measured square rather than slightly across
599
606
  it. The line wears that axis's colour while it is held, and the finished distance has no legs to
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { P as PartModel, V as Vec3, d as ViewerTheme, S as SectionOptions, e as SectionState, f as PartModelRegion, T as TriangleRange, F as FeatureTag, R as RegionIndex, g as PartModelFeature, h as FeatureType } from './normalize-DHOSNiuO.js';
2
2
  export { A as AXES_PLANE_OFFSET, i as AXES_PLANE_SCALE, j as AxesPlane, B as BuildPickInput, C as CANDIDATE_WEIGHT, k as CAP_HATCH, D as DEFAULT_SECTION_NORMAL, l as DEFAULT_THEME, m as DIRECTION_COLORS, o as DISABLED_SECTION, E as EnginePart, p as FeatureHighlight, H as HANDLE_PIXELS, q as HIGHLIGHT_COLORS, r as HIGHLIGHT_WEIGHT, t as HOVER_WEIGHT, u as HighlightLayers, K as KnownFeatureType, v as KnownShapeKind, N as NO_MODIFIERS, O as OUTLINE_SCALE, w as PICKED_SURFACE_LABEL, x as PREVIEW_SCALE, y as PartMesh, z as PartMeshProps, a as PartMeshRefs, G as PartObject, I as PartPick, J as PickModifiers, L as REGION_ATTRIBUTE, Q as RegionHighlight, U as RegionPaint, W as SECTION_RENDER_ORDER, X as SectionAnchor, Y as SectionBounds, Z as SectionPlacement, _ as ShapeKind, $ as SurfaceHit, a0 as applyHighlightLayers, a1 as axesPlanes, a2 as buildPick, a3 as buildRegionAttribute, a4 as buildRegionTexels, a5 as createPart, a6 as directionColor, a7 as dragPlane, a8 as focusForPick, a9 as hitUnderRay, n as normalizePartReport, aa as pickedStartDepth, ab as resolveTheme, ac as sectionAnchor, ad as sectionBounds, ae as sectionConstant, af as sectionDepth, ag as sectionDepthConstant, ah as sectionDepthRange, ai as sectionFromPick, aj as sectionOffset, ak as sectionOptionsFromState, al as sectionPlane, s as smoothRegionNormals, am as surfaceUnderRay, an as themesEqual, ao as viewDirection } from './normalize-DHOSNiuO.js';
3
- import { BufferGeometry, CanvasTexture, Box3, Plane, Vector3, OrthographicCamera, PerspectiveCamera, Object3D, Matrix4, Vector2, ShaderMaterial, Texture, Color, MeshBasicMaterial, WebGLRenderTarget } from 'three';
3
+ import { BufferGeometry, CanvasTexture, Box3, Plane, Vector3, OrthographicCamera, PerspectiveCamera, Object3D, Matrix4, Vector2, Ray, ShaderMaterial, Texture, Color, MeshBasicMaterial, WebGLRenderTarget } from 'three';
4
4
  import * as react from 'react';
5
5
  import { RefObject, ReactNode, CSSProperties } from 'react';
6
6
  import CameraControls from 'camera-controls';
@@ -738,6 +738,71 @@ interface SectionStore {
738
738
  }
739
739
  declare function createSectionStore(): SectionStore;
740
740
 
741
+ /**
742
+ * A cut part, re-sampled for measuring.
743
+ *
744
+ * A clipping plane hides half of a part without changing a triangle, so the
745
+ * mesh and its edges still describe the whole thing — and a tool that snaps
746
+ * to them would offer corners nobody can see, and nothing at all on the cut.
747
+ * This is what the measure tool works against instead: the part's edges
748
+ * trimmed to the half that is left, the outline where the plane passes
749
+ * through solid material, and a test for whether a point on the plane is on
750
+ * the capped face. It is rebuilt whenever the plane moves; see
751
+ * `measure-tool.tsx`.
752
+ *
753
+ * Everything here is in one space — the mesh's own — and the plane is
754
+ * expected in it too. Six floats per segment throughout, the layout
755
+ * `regionEdgesGeometry` uses.
756
+ */
757
+ interface SectionSample {
758
+ /** The edges of the part on the kept side, plus the cut's outline. */
759
+ readonly edges: Float32Array;
760
+ /** The cut's outline alone: where the plane crosses solid material. */
761
+ readonly contour: Float32Array;
762
+ }
763
+ /**
764
+ * Trims segments to the half-space a plane keeps.
765
+ *
766
+ * A segment wholly on the removed side goes; one that crosses is cut at the
767
+ * plane and its kept part stays. `Plane` keeps the side its normal points
768
+ * into — where the signed distance is positive — as the part's own clipping
769
+ * does.
770
+ */
771
+ declare function clipSegments(positions: ArrayLike<number>, plane: Plane): Float32Array;
772
+ /**
773
+ * Where a plane passes through a mesh: one segment per triangle it crosses,
774
+ * then straight runs inside one region stitched into one segment each.
775
+ *
776
+ * The stitching is what makes the outline snappable. A flat face is two
777
+ * triangles or two hundred, and the plane crosses each of them separately,
778
+ * so before stitching every triangle boundary on the face was a corner the
779
+ * pointer would snap to. After it, a corner is where the outline turns —
780
+ * where the plane leaves one surface for the next — and on a curved surface
781
+ * the facets stay, as they do in the part's own edges.
782
+ *
783
+ * `regionOf` says which surface each triangle belongs to, indexed by
784
+ * triangle; without it every triangle is one surface, which stitches a
785
+ * straight run across a fold only if the fold is perfectly flat, which it is
786
+ * not.
787
+ */
788
+ declare function sectionContour(positions: ArrayLike<number>, regionOf: ArrayLike<number> | null, plane: Plane): Float32Array;
789
+ /**
790
+ * Whether a point on the plane lies inside the cut's outline — on the capped
791
+ * face rather than in the air beside it.
792
+ *
793
+ * Even-odd against every outline segment, in the plane's own two axes, so a
794
+ * part cut through a bore is inside the outer loop and outside the hole's.
795
+ */
796
+ declare function pointInContour(point: Vector3, plane: Plane, contour: ArrayLike<number>): boolean;
797
+ /**
798
+ * Where a ray meets the capped face, or `null` when it misses the cap — a
799
+ * ray through the open half that lands beside the part, or one that reaches
800
+ * the plane from behind.
801
+ */
802
+ declare function capHit(ray: Ray, plane: Plane, contour: ArrayLike<number>): Vector3 | null;
803
+ /** The edges and outline of a part cut by `plane`, everything in the mesh's own space. */
804
+ declare function sampleSection(meshPositions: ArrayLike<number>, regionOf: ArrayLike<number> | null, edgePositions: ArrayLike<number> | null, plane: Plane): SectionSample;
805
+
741
806
  type CapUniforms = {
742
807
  uMask: {
743
808
  value: Texture | null;
@@ -1510,4 +1575,4 @@ declare const ORBIT_TARGET_FADE_MS = 1000;
1510
1575
  */
1511
1576
  declare function orbitTargetOpacity(sinceHold: number, fade?: number): number;
1512
1577
 
1513
- export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, CAD_CAMERA_UP, CHAMFER, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type CubeZone, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, type NamedDirection, ORBIT_TARGET_COLOR, ORBIT_TARGET_FADE_MS, ORBIT_TARGET_FLASH_MS, ORBIT_TARGET_PIXELS, ORBIT_TARGET_RING_COLOR, ORBIT_TARGET_RING_OPACITY, ORBIT_TARGET_RING_PIXELS, ORBIT_TARGET_RING_WIDTH, PERSPECTIVE_FOV, POINTS_PER_MEASUREMENT, PartModel, PartModelFeature, PartModelRegion, PartReportFormatError, type Projection, type RankingContext, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, type ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bestOwner, boundsFromBox, buildRegionIndex, cadViewDirections, cameraLimits, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTypeRank, fitDistance, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, screenLength, screenPoint, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useSectionStore, useTapGuard, useViewerControls, viewKind, viewUp, viewVector, visualSurfaces };
1578
+ export { ANGLE_ARC_FRACTION, ANGLE_ARC_SEGMENTS, AXIS_COLORS, type AngleMeasurement, type ArrowPlacement, Axes, type AxesProps, type Axis, type AxisLock, CAD_CAMERA_UP, CHAMFER, CadCameraControls, type CadCameraControlsProps, type CameraLimits, type CapMaterial, type CapUniforms, type ControlScheme, type CubeZone, DEFAULT_FIT_MARGIN, DELTA_DASH_PIXELS, DOUBLE_TAP_MS, type DeltaLeg, DirectionArrows, type DirectionArrowsProps, type DirectionGroup, type DistanceMeasurement, type DoubleTapPoint, type DoubleTapTracker, EXCLUDE_FROM_FRAME, ExtendedCameraControls, type ExtendedCameraControlsOptions, FEATURE_TYPE_RANKS, FeatureTag, FeatureType, Grid, type GridProps, type GridSpec, MAX_FRAME_RATIO, MEASURE_LABEL_CLASS, MEASURE_LINE_PIXELS, MEASURE_MARKER_PIXELS, MEASURE_RENDER_ORDER, MIN_FRAME_RATIO, type MeasureLabelKind, type MeasureMode, MeasureTool, type MeasureToolProps, type Measurement, type NamedDirection, ORBIT_TARGET_COLOR, ORBIT_TARGET_FADE_MS, ORBIT_TARGET_FLASH_MS, ORBIT_TARGET_PIXELS, ORBIT_TARGET_RING_COLOR, ORBIT_TARGET_RING_OPACITY, ORBIT_TARGET_RING_PIXELS, ORBIT_TARGET_RING_WIDTH, PERSPECTIVE_FOV, POINTS_PER_MEASUREMENT, PartModel, PartModelFeature, PartModelRegion, PartReportFormatError, type Projection, type RankingContext, RegionIndex, type Retarget, type RetargetPose, SNAP_MARKER_PIXELS, SNAP_PIXELS, type SceneBounds, SectionOptions, type SectionSample, SectionState, type SectionStore, SectionTool, type SectionToolProps, SectionView, type Snap, type SnapEdges, type SnapHit, type SnapKind, type SurfaceOf, TAP_SLOP, type TapPoint, type TapTracker, TriangleRange, UnsupportedKernelVersionError, VIEW_NAMES, VIEW_SIGNS, Vec3, ViewCube, type ViewCubeProps, type ViewKind, type ViewName, Viewer, type ViewerCamera, type ViewerControls, type ViewerHandle, type ViewerProps, ViewerTheme, type ViewerView, type ViewportSize, adaptedUp, angleArc, angleArcRadius, angleAt, applyCapTheme, applyProjection, arrowPlacement, aspectRatio, bestOwner, boundsFromBox, buildRegionIndex, cadViewDirections, cameraLimits, capHit, clipSegments, contentBounds, createCapMaterial, createMaskMaterial, createMaskTarget, createSectionStore, createStencilMaterials, cubeOutlineGeometry, cubeZones, currentViewDirection, cycleOwner, defaultBounds, deltaBetween, deltaLegs, directionIndexOf, directionLabel, distanceBetween, excludedFromFrame, featureTypeRank, fitDistance, formatDegrees, formatMillimetres, gridGeometry, gridSpec, groupByDirection, labelGeometry, labelTexture, lockToAxis, measurementFromPoints, measurementLabel, measurementLabelAnchor, midpoint, movedFar, nextMeasurementId, orbitTargetOpacity, orthographicHalfHeight, panelGeometry, perspectiveFitDistance, pointInContour, rankOwners, regionAdjacency, regionEdgesGeometry, regionNormals, resolveSectionPlane, retargetPose, sameDirection, sampleSection, screenLength, screenPoint, sectionContour, snapAt, squaredUp, startPosition, targetBoundary, trackDoubleTaps, trackTaps, useContentBox, useRetarget, useSectionStore, useTapGuard, useViewerControls, viewKind, viewUp, viewVector, visualSurfaces };
package/dist/index.js CHANGED
@@ -842,11 +842,185 @@ import { Html, Line } from "@react-three/drei";
842
842
  import { useFrame as useFrame2, useThree as useThree4 } from "@react-three/fiber";
843
843
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
844
844
  import {
845
+ Matrix4,
845
846
  Raycaster as Raycaster2,
846
847
  SphereGeometry,
847
848
  Vector2 as Vector23,
848
- Vector3 as Vector35
849
+ Vector3 as Vector36
849
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
850
1024
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
851
1025
  var FURNITURE4 = { [EXCLUDE_FROM_FRAME]: true };
852
1026
  var MEASURE_LABEL_CLASS = "toolpath-measure-label";
@@ -943,7 +1117,7 @@ var MeasureTool = ({
943
1117
  ] });
944
1118
  };
945
1119
  var NO_POINTER = { pointerEvents: "none" };
946
- var NOWHERE = [new Vector35(), new Vector35()];
1120
+ var NOWHERE = [new Vector36(), new Vector36()];
947
1121
  var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
948
1122
  const camera = useThree4((state) => state.camera);
949
1123
  const scene = useThree4((state) => state.scene);
@@ -967,6 +1141,7 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
967
1141
  useEffect3(() => {
968
1142
  const raycaster = new Raycaster2();
969
1143
  const pointer = new Vector23();
1144
+ const samples = new SampleCache();
970
1145
  const hide = () => {
971
1146
  if (!snap.current) return;
972
1147
  snap.current = null;
@@ -986,22 +1161,16 @@ var Snapper = ({ mode, draft, onPlace, theme, format, label }) => {
986
1161
  -((clientY - rect.top) / rect.height) * 2 + 1
987
1162
  );
988
1163
  raycaster.setFromCamera(pointer, camera);
989
- const hit = hitUnderRay(raycaster, scene);
990
- if (!hit?.face) {
1164
+ const target = targetUnderRay(raycaster, scene, samples);
1165
+ if (!target) {
991
1166
  hide();
992
1167
  return;
993
1168
  }
994
- const normal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
995
1169
  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
- );
1170
+ const snapped = snapAt(target, target.edges, camera, viewport);
1002
1171
  const from = placed[placed.length - 1];
1003
1172
  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;
1173
+ const found = lock ? { ...snapped, point: new Vector36(lock.point.x, lock.point.y, lock.point.z), edge: null } : snapped;
1005
1174
  snap.current = found;
1006
1175
  const node = marker.current;
1007
1176
  if (node) {
@@ -1144,17 +1313,124 @@ function liveLabel(mode, placed, at, format) {
1144
1313
  }
1145
1314
  return null;
1146
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
+ }
1147
1365
  function edgesBeside(object) {
1148
1366
  const siblings = object.parent?.children ?? [];
1149
1367
  for (const sibling of siblings) {
1150
1368
  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 };
1369
+ const positions = sibling.geometry.getAttribute("position");
1370
+ if (positions) return positions.array;
1155
1371
  }
1156
1372
  return null;
1157
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
+ };
1158
1434
  var MeasurementView = ({
1159
1435
  measurement,
1160
1436
  theme,
@@ -1194,7 +1470,7 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1194
1470
  const lines = useRef3([]);
1195
1471
  const centre = useMemo4(() => {
1196
1472
  const at = midpoint(a, b);
1197
- return new Vector35(at.x, at.y, at.z);
1473
+ return new Vector36(at.x, at.y, at.z);
1198
1474
  }, [a, b]);
1199
1475
  useFrame2(() => {
1200
1476
  const dash = screenLength(camera, centre, size, DELTA_DASH_PIXELS);
@@ -1216,8 +1492,8 @@ var DeltaLegs = ({ a, b, id, format, label }) => {
1216
1492
  lines.current[index] = line;
1217
1493
  },
1218
1494
  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)
1495
+ new Vector36(leg.from.x, leg.from.y, leg.from.z),
1496
+ new Vector36(leg.to.x, leg.to.y, leg.to.z)
1221
1497
  ],
1222
1498
  color: AXIS_COLORS[leg.axis],
1223
1499
  lineWidth: MEASURE_LINE_PIXELS.leg,
@@ -1253,7 +1529,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1253
1529
  const invalidate = useThree4((state) => state.invalidate);
1254
1530
  const node = useRef3(null);
1255
1531
  const dot = useDot();
1256
- const position = useMemo4(() => new Vector35(point.x, point.y, point.z), [point]);
1532
+ const position = useMemo4(() => new Vector36(point.x, point.y, point.z), [point]);
1257
1533
  useFrame2(() => {
1258
1534
  const group = node.current;
1259
1535
  if (!group) return;
@@ -1268,7 +1544,7 @@ var ScreenDot = ({ point, pixels, color }) => {
1268
1544
  var Segment = ({ points, color }) => {
1269
1545
  const [a, b] = points;
1270
1546
  const ends = useMemo4(
1271
- () => [new Vector35(a.x, a.y, a.z), new Vector35(b.x, b.y, b.z)],
1547
+ () => [new Vector36(a.x, a.y, a.z), new Vector36(b.x, b.y, b.z)],
1272
1548
  // Keyed on the numbers: a draft's tuple is a fresh array on every render,
1273
1549
  // and a rebuilt array would rebuild the line's geometry with it.
1274
1550
  [a.x, a.y, a.z, b.x, b.y, b.z]
@@ -1419,6 +1695,8 @@ export {
1419
1695
  buildRegionTexels,
1420
1696
  cadViewDirections,
1421
1697
  cameraLimits,
1698
+ capHit,
1699
+ clipSegments,
1422
1700
  contentBounds,
1423
1701
  createCapMaterial,
1424
1702
  createMaskMaterial,
@@ -1463,6 +1741,7 @@ export {
1463
1741
  panelGeometry,
1464
1742
  perspectiveFitDistance,
1465
1743
  pickedStartDepth,
1744
+ pointInContour,
1466
1745
  rankOwners,
1467
1746
  regionAdjacency,
1468
1747
  regionEdgesGeometry,
@@ -1471,11 +1750,13 @@ export {
1471
1750
  resolveTheme,
1472
1751
  retargetPose,
1473
1752
  sameDirection,
1753
+ sampleSection,
1474
1754
  screenLength,
1475
1755
  screenPoint,
1476
1756
  sectionAnchor,
1477
1757
  sectionBounds,
1478
1758
  sectionConstant,
1759
+ sectionContour,
1479
1760
  sectionDepth,
1480
1761
  sectionDepthConstant,
1481
1762
  sectionDepthRange,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/viewer",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "React Three Fiber viewer for Toolpath Engine part reports",
5
5
  "license": "MIT",
6
6
  "engines": {