@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 +7 -0
- package/dist/index.d.ts +67 -2
- package/dist/index.js +302 -21
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
990
|
-
if (!
|
|
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
|
|
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
|
|
1152
|
-
|
|
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
|
|
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
|
|
1220
|
-
new
|
|
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
|
|
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
|
|
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,
|