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