@toolpath/viewer 1.3.0 → 1.5.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.
@@ -11,7 +11,7 @@ import {
11
11
  useRef as useRef6,
12
12
  useSyncExternalStore
13
13
  } from "react";
14
- import { Vector3 as Vector310 } from "three";
14
+ import { Vector3 as Vector311 } from "three";
15
15
 
16
16
  // src/model/directions.ts
17
17
  var EPSILON = 1e-6;
@@ -193,14 +193,31 @@ function applyHighlightLayers(part, layers, theme) {
193
193
  }
194
194
  }
195
195
 
196
+ // src/render/focus.ts
197
+ var DEFAULT_FOCUS_OPACITY = 0.15;
198
+ function focusOpacity(options) {
199
+ return Math.min(Math.max(options?.opacity ?? DEFAULT_FOCUS_OPACITY, 0), 1);
200
+ }
201
+ function focusStateKey(options, selection) {
202
+ return `${options === void 0 ? "off" : `on:${options.opacity ?? ""}`}|${selection.join(" ")}`;
203
+ }
204
+ function focusedRegions(model, selection) {
205
+ const regions = /* @__PURE__ */ new Set();
206
+ for (const tag of selection) {
207
+ for (const region of model.regionIndex.regionsForFeature(tag)) regions.add(region);
208
+ }
209
+ return regions;
210
+ }
211
+
196
212
  // src/render/section.ts
197
- import { Plane, Vector3 as Vector32 } from "three";
213
+ import { Plane, Vector3 as Vector33 } from "three";
198
214
 
199
215
  // src/render/camera.ts
200
216
  import { OrthographicCamera, Vector3 } from "three";
201
217
  var PERSPECTIVE_FOV = 30;
202
218
  var DEFAULT_FIT_MARGIN = 1.2;
203
219
  var EXCLUDE_FROM_FRAME = "viewerExcludeFromFrame";
220
+ var STOCK_OBJECT = "viewerStock";
204
221
  function defaultBounds() {
205
222
  return { center: new Vector3(0, 0, 0), radius: 1 };
206
223
  }
@@ -221,16 +238,30 @@ function boundsFromBox(box) {
221
238
  return { center, radius: radius > 0 ? radius : 1 };
222
239
  }
223
240
  function contentBounds(root, into) {
241
+ return measureBounds(root, into, false);
242
+ }
243
+ function partBounds(root, into) {
244
+ return measureBounds(root, into, true);
245
+ }
246
+ function measureBounds(root, into, partOnly) {
224
247
  into.makeEmpty();
225
248
  root.updateWorldMatrix(true, true);
226
249
  root.traverse((object) => {
227
- if (excludedFromFrame(object, root)) return;
250
+ if (partOnly ? excludedFromPart(object, root) : excludedFromFrame(object, root)) return;
228
251
  if ("isMesh" in object || "isLine" in object || "isPoints" in object) {
229
252
  into.expandByObject(object);
230
253
  }
231
254
  });
232
255
  return boundsFromBox(into);
233
256
  }
257
+ function excludedFromPart(object, root) {
258
+ let current = object;
259
+ while (current && current !== root) {
260
+ if (current.userData[STOCK_OBJECT]) return true;
261
+ current = current.parent;
262
+ }
263
+ return excludedFromFrame(object, root);
264
+ }
234
265
  function excludedFromFrame(object, root) {
235
266
  let current = object;
236
267
  while (current && current !== root) {
@@ -351,6 +382,206 @@ function currentViewDirection(camera, target, into) {
351
382
  return into.normalize();
352
383
  }
353
384
 
385
+ // src/render/measure.ts
386
+ import { Vector2, Vector3 as Vector32 } from "three";
387
+ var MEASURE_RENDER_ORDER = 7;
388
+ var AXIS_COLORS = { x: 16739179, y: 7331978, z: 7314431 };
389
+ var SNAP_PIXELS = {
390
+ vertex: 14,
391
+ midpoint: 12,
392
+ edge: 9
393
+ };
394
+ var SNAP_MARKER_PIXELS = {
395
+ vertex: 6,
396
+ midpoint: 5,
397
+ edge: 4,
398
+ face: 3.5
399
+ };
400
+ var MEASURE_MARKER_PIXELS = 3.5;
401
+ var MEASURE_LINE_PIXELS = {
402
+ line: 2,
403
+ leg: 1.5,
404
+ edge: 3
405
+ };
406
+ var DELTA_DASH_PIXELS = 6;
407
+ var ANGLE_ARC_FRACTION = 0.3;
408
+ var ANGLE_ARC_SEGMENTS = 24;
409
+ function snapAt(hit, edges, camera, viewport) {
410
+ const face = {
411
+ kind: "face",
412
+ point: hit.point.clone(),
413
+ normal: hit.normal.clone(),
414
+ edge: null
415
+ };
416
+ if (!edges) return face;
417
+ const positions = edges.positions;
418
+ const segments = Math.floor(positions.length / 6);
419
+ if (segments === 0) return face;
420
+ const inverse = edges.matrixWorld.clone().invert();
421
+ const p = hit.point.clone().applyMatrix4(inverse);
422
+ const n = hit.normal.clone().transformDirection(inverse);
423
+ const reach = screenLength(camera, hit.point, viewport, SNAP_PIXELS.vertex) * 1.5;
424
+ const reachSq = reach * reach;
425
+ const pointer = screenPoint(camera, hit.point, viewport);
426
+ const best = {
427
+ vertex: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 },
428
+ midpoint: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 },
429
+ edge: { distance: Number.POSITIVE_INFINITY, index: -1, t: 0 }
430
+ };
431
+ const a = new Vector32();
432
+ const b = new Vector32();
433
+ const ab = new Vector32();
434
+ const ap = new Vector32();
435
+ const q = new Vector32();
436
+ const pixel = new Vector2();
437
+ const consider = (kind, point, index, t) => {
438
+ const distance = screenPoint(
439
+ camera,
440
+ q.copy(point).applyMatrix4(edges.matrixWorld),
441
+ viewport,
442
+ pixel
443
+ ).distanceTo(pointer);
444
+ if (distance <= SNAP_PIXELS[kind] && distance < best[kind].distance) {
445
+ best[kind] = { distance, index, t };
446
+ }
447
+ };
448
+ for (let index = 0; index < segments; index += 1) {
449
+ const offset = index * 6;
450
+ a.set(positions[offset], positions[offset + 1], positions[offset + 2]);
451
+ b.set(positions[offset + 3], positions[offset + 4], positions[offset + 5]);
452
+ if (Math.abs(ap.subVectors(a, p).dot(n)) > reach) continue;
453
+ if (Math.abs(ap.subVectors(b, p).dot(n)) > reach) continue;
454
+ ab.subVectors(b, a);
455
+ ap.subVectors(p, a);
456
+ const lengthSq = ab.lengthSq();
457
+ const t = lengthSq === 0 ? 0 : Math.min(1, Math.max(0, ap.dot(ab) / lengthSq));
458
+ q.copy(a).addScaledVector(ab, t);
459
+ if (q.distanceToSquared(p) > reachSq) continue;
460
+ consider("vertex", a, index, 0);
461
+ consider("vertex", b, index, 1);
462
+ consider("midpoint", q.copy(a).addScaledVector(ab, 0.5), index, 0.5);
463
+ consider("edge", q.copy(a).addScaledVector(ab, t), index, t);
464
+ }
465
+ for (const kind of ["vertex", "midpoint", "edge"]) {
466
+ const found = best[kind];
467
+ if (found.index < 0) continue;
468
+ const offset = found.index * 6;
469
+ const start = new Vector32(positions[offset], positions[offset + 1], positions[offset + 2]);
470
+ const end = new Vector32(positions[offset + 3], positions[offset + 4], positions[offset + 5]);
471
+ const point = start.clone().lerp(end, found.t);
472
+ start.applyMatrix4(edges.matrixWorld);
473
+ end.applyMatrix4(edges.matrixWorld);
474
+ point.applyMatrix4(edges.matrixWorld);
475
+ return {
476
+ kind,
477
+ point,
478
+ normal: hit.normal.clone(),
479
+ edge: kind === "vertex" ? null : [start, end]
480
+ };
481
+ }
482
+ return face;
483
+ }
484
+ function screenPoint(camera, point, viewport, into = new Vector2()) {
485
+ const ndc = point.clone().project(camera);
486
+ return into.set((ndc.x + 1) / 2 * viewport.width, (1 - ndc.y) / 2 * viewport.height);
487
+ }
488
+ var POINTS_PER_MEASUREMENT = {
489
+ distance: 2,
490
+ angle: 3
491
+ };
492
+ function measurementFromPoints(kind, points, id) {
493
+ if (points.length < POINTS_PER_MEASUREMENT[kind]) return null;
494
+ const [a, b, c] = points;
495
+ if (kind === "distance") return { id, kind, points: [copy(a), copy(b)] };
496
+ return { id, kind, points: [copy(a), copy(b), copy(c)] };
497
+ }
498
+ function nextMeasurementId(held) {
499
+ return held.reduce((top, entry) => Math.max(top, entry.id), 0) + 1;
500
+ }
501
+ function distanceBetween(a, b) {
502
+ return Math.hypot(b.x - a.x, b.y - a.y, b.z - a.z);
503
+ }
504
+ function deltaBetween(a, b) {
505
+ return { x: b.x - a.x, y: b.y - a.y, z: b.z - a.z };
506
+ }
507
+ function midpoint(a, b) {
508
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
509
+ }
510
+ function angleAt(vertex, a, b) {
511
+ const u = new Vector32(a.x - vertex.x, a.y - vertex.y, a.z - vertex.z);
512
+ const w = new Vector32(b.x - vertex.x, b.y - vertex.y, b.z - vertex.z);
513
+ if (u.lengthSq() === 0 || w.lengthSq() === 0) return 0;
514
+ return u.angleTo(w) * 180 / Math.PI;
515
+ }
516
+ function lockToAxis(from, to) {
517
+ const delta = deltaBetween(from, to);
518
+ let axis = "x";
519
+ for (const candidate of ["y", "z"]) {
520
+ if (Math.abs(delta[candidate]) > Math.abs(delta[axis])) axis = candidate;
521
+ }
522
+ return { axis, point: { ...from, [axis]: to[axis] } };
523
+ }
524
+ function deltaLegs(a, b) {
525
+ const delta = deltaBetween(a, b);
526
+ const threshold = Math.max(1e-9, distanceBetween(a, b) * 1e-6);
527
+ const legs = [];
528
+ let cursor = copy(a);
529
+ for (const axis of ["x", "y", "z"]) {
530
+ if (Math.abs(delta[axis]) <= threshold) continue;
531
+ const to = { ...cursor, [axis]: b[axis] };
532
+ legs.push({ axis, from: cursor, to, length: Math.abs(delta[axis]) });
533
+ cursor = to;
534
+ }
535
+ return legs.length > 1 ? legs : [];
536
+ }
537
+ function angleArc(vertex, a, b, radius, segments = ANGLE_ARC_SEGMENTS) {
538
+ const v = new Vector32(vertex.x, vertex.y, vertex.z);
539
+ const u = new Vector32(a.x, a.y, a.z).sub(v);
540
+ const w = new Vector32(b.x, b.y, b.z).sub(v);
541
+ if (u.lengthSq() === 0 || w.lengthSq() === 0) return [];
542
+ u.normalize();
543
+ w.normalize();
544
+ const theta = u.angleTo(w);
545
+ const sine = Math.sin(theta);
546
+ if (sine < 1e-6) {
547
+ return [v.clone().addScaledVector(u, radius), v.clone().addScaledVector(w, radius)];
548
+ }
549
+ const points = [];
550
+ for (let i = 0; i <= segments; i += 1) {
551
+ const t = i / segments;
552
+ const direction = u.clone().multiplyScalar(Math.sin((1 - t) * theta) / sine).addScaledVector(w, Math.sin(t * theta) / sine);
553
+ points.push(v.clone().addScaledVector(direction, radius));
554
+ }
555
+ return points;
556
+ }
557
+ function angleArcRadius(vertex, a, b) {
558
+ return Math.min(distanceBetween(vertex, a), distanceBetween(vertex, b)) * ANGLE_ARC_FRACTION;
559
+ }
560
+ function formatMillimetres(mm, digits = 2) {
561
+ return `${mm.toFixed(digits)} mm`;
562
+ }
563
+ function formatDegrees(degrees, digits = 1) {
564
+ return `${degrees.toFixed(digits)}\xB0`;
565
+ }
566
+ function measurementLabel(measurement, format = formatMillimetres) {
567
+ if (measurement.kind === "distance") {
568
+ const [a2, b2] = measurement.points;
569
+ return format(distanceBetween(a2, b2));
570
+ }
571
+ const [a, vertex, b] = measurement.points;
572
+ return formatDegrees(angleAt(vertex, a, b));
573
+ }
574
+ function measurementLabelAnchor(measurement) {
575
+ if (measurement.kind === "distance") return midpoint(measurement.points[0], measurement.points[1]);
576
+ const [a, vertex, b] = measurement.points;
577
+ const arc = angleArc(vertex, a, b, angleArcRadius(vertex, a, b) * 1.6, 2);
578
+ const middle = arc[1] ?? arc[0];
579
+ return middle ? { x: middle.x, y: middle.y, z: middle.z } : copy(vertex);
580
+ }
581
+ function copy(v) {
582
+ return { x: v.x, y: v.y, z: v.z };
583
+ }
584
+
354
585
  // src/render/section.ts
355
586
  var SECTION_RENDER_ORDER = {
356
587
  stencil: 1,
@@ -361,6 +592,7 @@ var SECTION_RENDER_ORDER = {
361
592
  };
362
593
  var START_DEPTH = 5e-3;
363
594
  var HANDLE_PIXELS = 78;
595
+ var SECTION_GIZMO_FRAME_SCALE = 1.1;
364
596
  var CAP_HATCH = {
365
597
  pitch: 9,
366
598
  line: 1.25,
@@ -371,7 +603,7 @@ var AXES_PLANE_SCALE = 1.2;
371
603
  var PREVIEW_SCALE = 0.125;
372
604
  var OUTLINE_SCALE = 1.2;
373
605
  var EPSILON2 = 1e-9;
374
- var ARROW_AXIS = new Vector32(0, 1, 0);
606
+ var ARROW_AXIS = new Vector33(0, 1, 0);
375
607
  var DEFAULT_SECTION_NORMAL = { x: 0, y: 0, z: 1 };
376
608
  var DISABLED_SECTION = {
377
609
  enabled: false,
@@ -380,8 +612,25 @@ var DISABLED_SECTION = {
380
612
  constant: 0,
381
613
  plane: null,
382
614
  depth: null,
615
+ cutDistance: 0,
383
616
  depthRange: null
384
617
  };
618
+ function sectionMeasurement(state) {
619
+ return `${(state.depth ?? state.cutDistance ?? 0).toFixed(2)} mm`;
620
+ }
621
+ function sectionDirectionColor(normal) {
622
+ const x = Math.abs(normal.x);
623
+ const y = Math.abs(normal.y);
624
+ const z = Math.abs(normal.z);
625
+ const total = x + y + z || 1;
626
+ const channel = (shift) => Math.round(
627
+ ((AXIS_COLORS.x >> shift & 255) * x + (AXIS_COLORS.y >> shift & 255) * y + (AXIS_COLORS.z >> shift & 255) * z) / total
628
+ );
629
+ return channel(16) << 16 | channel(8) << 8 | channel(0);
630
+ }
631
+ function sectionHandleColor(themeColor, directionColor2) {
632
+ return themeColor ?? directionColor2;
633
+ }
385
634
  function sectionOptionsFromState(state) {
386
635
  if (state.plane) {
387
636
  return { enabled: state.enabled, plane: state.plane, depth: state.depth ?? 0 };
@@ -389,8 +638,8 @@ function sectionOptionsFromState(state) {
389
638
  return { enabled: state.enabled, normal: state.normal, offset: state.offset };
390
639
  }
391
640
  function axesPlanes(box, eye) {
392
- const centre = box.getCenter(new Vector32());
393
- const extent = box.getSize(new Vector32());
641
+ const centre = box.getCenter(new Vector33());
642
+ const extent = box.getSize(new Vector33());
394
643
  const largest = Math.max(extent.x, extent.y, extent.z);
395
644
  const size = largest * AXES_PLANE_SCALE;
396
645
  const offset = largest * AXES_PLANE_OFFSET;
@@ -425,7 +674,7 @@ function axesPlanes(box, eye) {
425
674
  function hitUnderRay(raycaster, root) {
426
675
  for (const hit of raycaster.intersectObjects(root.children, true)) {
427
676
  if (!("isMesh" in hit.object) || !hit.face) continue;
428
- if (!hit.object.visible || excludedFromFrame(hit.object, root)) continue;
677
+ if (!hit.object.visible || excludedFromPart(hit.object, root)) continue;
429
678
  if (clippedAway(hit)) continue;
430
679
  return hit;
431
680
  }
@@ -447,21 +696,21 @@ function surfaceUnderRay(raycaster, root) {
447
696
  const normal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
448
697
  return { point: hit.point, normal };
449
698
  }
450
- function sectionAnchor(box, plane, into = new Vector32()) {
699
+ function sectionAnchor(box, plane, into = new Vector33()) {
451
700
  const centre = box.getCenter(into);
452
701
  return centre.addScaledVector(plane.normal, -(plane.constant + plane.normal.dot(centre)));
453
702
  }
454
703
  var PICKED_SURFACE_LABEL = "Part surface";
455
704
  function unit(v) {
456
- const vector = new Vector32(v.x, v.y, v.z);
705
+ const vector = new Vector33(v.x, v.y, v.z);
457
706
  return vector.lengthSq() === 0 ? vector.set(0, 0, 1) : vector.normalize();
458
707
  }
459
708
  function clamp01(value) {
460
709
  return Math.min(1, Math.max(0, value));
461
710
  }
462
- function sectionBounds(box, normal) {
711
+ function projectedBounds(box, normal) {
463
712
  const axis = unit(normal);
464
- const corner = new Vector32();
713
+ const corner = new Vector33();
465
714
  let low = Number.POSITIVE_INFINITY;
466
715
  let high = Number.NEGATIVE_INFINITY;
467
716
  for (let i = 0; i < 8; i += 1) {
@@ -474,9 +723,17 @@ function sectionBounds(box, normal) {
474
723
  low = Math.min(low, distance);
475
724
  high = Math.max(high, distance);
476
725
  }
726
+ return { low, high };
727
+ }
728
+ function sectionBounds(box, normal) {
729
+ const { low, high } = projectedBounds(box, normal);
477
730
  const margin = Math.max((high - low) * 5e-3, 1e-6);
478
731
  return { min: -(high + margin), max: -(low - margin) };
479
732
  }
733
+ function sectionCutDistance(box, normal, constant) {
734
+ const { low, high } = projectedBounds(box, normal);
735
+ return Math.max(0, Math.min(high - low, -constant - low));
736
+ }
480
737
  function sectionConstant(bounds, t) {
481
738
  return bounds.max + clamp01(t) * (bounds.min - bounds.max);
482
739
  }
@@ -485,7 +742,7 @@ function sectionOffset(bounds, constant) {
485
742
  return span === 0 ? 0 : clamp01((constant - bounds.max) / span);
486
743
  }
487
744
  function sectionDepth(normal, anchor, constant) {
488
- return -unit(normal).dot(new Vector32(anchor.x, anchor.y, anchor.z)) - constant;
745
+ return -unit(normal).dot(new Vector33(anchor.x, anchor.y, anchor.z)) - constant;
489
746
  }
490
747
  function sectionDepthConstant(normal, anchor, depth) {
491
748
  return sectionDepth(normal, anchor, depth);
@@ -504,7 +761,7 @@ function sectionFromPick(surface, label = PICKED_SURFACE_LABEL) {
504
761
  };
505
762
  }
506
763
  function pickedStartDepth(box) {
507
- return box.getSize(new Vector32()).length() * START_DEPTH;
764
+ return box.getSize(new Vector33()).length() * START_DEPTH;
508
765
  }
509
766
  function sectionPlane(box, normal, offset, into = new Plane()) {
510
767
  const axis = unit(normal);
@@ -512,12 +769,12 @@ function sectionPlane(box, normal, offset, into = new Plane()) {
512
769
  }
513
770
  function dragPlane(axis, view, point, into = new Plane()) {
514
771
  const along = axis.lengthSq() === 0 ? ARROW_AXIS.clone() : axis.clone().normalize();
515
- const side = new Vector32().crossVectors(view, along);
772
+ const side = new Vector33().crossVectors(view, along);
516
773
  if (side.lengthSq() < EPSILON2) side.copy(perpendicular(along));
517
774
  return into.setFromNormalAndCoplanarPoint(side.cross(along).normalize(), point);
518
775
  }
519
776
  function perpendicular(axis) {
520
- const candidate = Math.abs(axis.x) < 0.9 ? new Vector32(1, 0, 0) : new Vector32(0, 1, 0);
777
+ const candidate = Math.abs(axis.x) < 0.9 ? new Vector33(1, 0, 0) : new Vector33(0, 1, 0);
521
778
  return candidate.cross(axis).normalize();
522
779
  }
523
780
 
@@ -593,15 +850,17 @@ import {
593
850
  Box3,
594
851
  Color,
595
852
  DataTexture,
853
+ DoubleSide,
596
854
  Float32BufferAttribute as Float32BufferAttribute2,
597
855
  Group,
598
856
  LineBasicMaterial,
599
857
  LineSegments,
600
858
  Mesh,
601
859
  MeshLambertMaterial,
860
+ RedFormat,
602
861
  RGBAFormat,
603
862
  UnsignedByteType,
604
- Vector3 as Vector33
863
+ Vector3 as Vector34
605
864
  } from "three";
606
865
 
607
866
  // src/render/edges.ts
@@ -700,6 +959,13 @@ function createPart(model, geometry, theme) {
700
959
  const state = new Uint8Array(width * 4);
701
960
  const stateTexture = new DataTexture(state, width, 1, RGBAFormat, UnsignedByteType);
702
961
  stateTexture.needsUpdate = true;
962
+ const opacity = new Uint8Array(width).fill(255);
963
+ const opacityTexture = new DataTexture(opacity, width, 1, RedFormat, UnsignedByteType);
964
+ opacityTexture.needsUpdate = true;
965
+ const focusState = { value: false };
966
+ const wireframe = { value: false };
967
+ let focused = false;
968
+ let currentTheme = theme;
703
969
  const material = new MeshLambertMaterial({
704
970
  color: theme.part,
705
971
  emissive: theme.partEmissive,
@@ -709,40 +975,64 @@ function createPart(model, geometry, theme) {
709
975
  polygonOffsetFactor: 1,
710
976
  polygonOffsetUnits: 1
711
977
  });
712
- material.onBeforeCompile = (shader) => {
713
- shader.uniforms["uRegionState"] = { value: stateTexture };
714
- shader.vertexShader = shader.vertexShader.replace(
715
- "#include <common>",
716
- `#include <common>
978
+ const configureShader = (target, opaqueFocusPass) => {
979
+ target.onBeforeCompile = (shader) => {
980
+ shader.uniforms["uRegionState"] = { value: stateTexture };
981
+ shader.uniforms["uRegionOpacity"] = { value: opacityTexture };
982
+ shader.uniforms["uWireframe"] = wireframe;
983
+ shader.uniforms["uFocus"] = focusState;
984
+ shader.uniforms["uOpaqueFocusPass"] = { value: opaqueFocusPass };
985
+ shader.vertexShader = shader.vertexShader.replace(
986
+ "#include <common>",
987
+ `#include <common>
717
988
  attribute float ${REGION_ATTRIBUTE};
718
989
  varying float vRegion;`
719
- ).replace(
720
- "#include <begin_vertex>",
721
- `#include <begin_vertex>
990
+ ).replace(
991
+ "#include <begin_vertex>",
992
+ `#include <begin_vertex>
722
993
  vRegion = ${REGION_ATTRIBUTE};`
723
- );
724
- shader.fragmentShader = shader.fragmentShader.replace(
725
- "#include <common>",
726
- `#include <common>
994
+ );
995
+ shader.fragmentShader = shader.fragmentShader.replace(
996
+ "#include <common>",
997
+ `#include <common>
727
998
  uniform sampler2D uRegionState;
999
+ uniform sampler2D uRegionOpacity;
1000
+ uniform bool uWireframe;
1001
+ uniform bool uFocus;
1002
+ uniform bool uOpaqueFocusPass;
728
1003
  varying float vRegion;
729
1004
  vec4 regionState;`
730
- ).replace(
731
- "#include <color_fragment>",
732
- `#include <color_fragment>
1005
+ ).replace(
1006
+ "#include <color_fragment>",
1007
+ `#include <color_fragment>
733
1008
  regionState = texelFetch(uRegionState, ivec2(int(vRegion + 0.5), 0), 0);
734
1009
  diffuseColor.rgb = mix(diffuseColor.rgb, regionState.rgb, regionState.a);
1010
+ float regionOpacity = texelFetch(uRegionOpacity, ivec2(int(vRegion + 0.5), 0), 0).r;
1011
+ if (uFocus && uOpaqueFocusPass && regionOpacity < 0.999) discard;
1012
+ if (uFocus && !uOpaqueFocusPass && regionOpacity >= 0.999) discard;
1013
+ diffuseColor.a *= regionOpacity;
1014
+ if (uWireframe) diffuseColor.a *= regionState.a;
735
1015
  `
736
- ).replace(
737
- "#include <emissivemap_fragment>",
738
- `#include <emissivemap_fragment>
1016
+ ).replace(
1017
+ "#include <emissivemap_fragment>",
1018
+ `#include <emissivemap_fragment>
739
1019
  totalEmissiveRadiance = mix(
740
1020
  totalEmissiveRadiance,
741
1021
  regionState.rgb * ${EMISSIVE_MIX.toFixed(2)},
742
1022
  regionState.a
743
1023
  );`
744
- );
1024
+ );
1025
+ };
745
1026
  };
1027
+ const focusMaterial = new MeshLambertMaterial({
1028
+ color: theme.part,
1029
+ emissive: theme.partEmissive,
1030
+ // A selected face can be viewed through the part from the side its normal
1031
+ // faces away from.
1032
+ side: DoubleSide
1033
+ });
1034
+ configureShader(focusMaterial, true);
1035
+ configureShader(material, false);
746
1036
  const mesh = new Mesh(geometry, material);
747
1037
  mesh.renderOrder = 3;
748
1038
  const edgeGeometry = regionEdgesGeometry(geometry, model);
@@ -755,10 +1045,25 @@ function createPart(model, geometry, theme) {
755
1045
  edges.renderOrder = 4;
756
1046
  edges.raycast = () => {
757
1047
  };
1048
+ const focusMesh = new Mesh(geometry, focusMaterial);
1049
+ focusMesh.renderOrder = 3;
1050
+ focusMesh.visible = false;
1051
+ focusMesh.raycast = () => {
1052
+ };
1053
+ const syncTransparency = () => {
1054
+ const transparent = wireframe.value || focused;
1055
+ if (material.transparent !== transparent || material.depthWrite !== !transparent) {
1056
+ material.transparent = transparent;
1057
+ material.depthWrite = !transparent;
1058
+ material.needsUpdate = true;
1059
+ }
1060
+ focusState.value = focused;
1061
+ focusMesh.visible = focused;
1062
+ };
758
1063
  const object = new Group();
759
- object.add(mesh, edges);
1064
+ object.add(focusMesh, mesh, edges);
760
1065
  const scratchColor = new Color();
761
- const scratchVector = new Vector33();
1066
+ const scratchVector = new Vector34();
762
1067
  const paintRegion = (region, color, weight) => {
763
1068
  const column = texels.get(region);
764
1069
  if (column === void 0) return;
@@ -796,6 +1101,27 @@ function createPart(model, geometry, theme) {
796
1101
  state.fill(0);
797
1102
  stateTexture.needsUpdate = true;
798
1103
  },
1104
+ setFocus(selection, focus) {
1105
+ const regions = focus === void 0 ? null : focusedRegions(model, selection);
1106
+ const outside = focusOpacity(focus);
1107
+ let changed = false;
1108
+ let hasTransparency = false;
1109
+ for (const [region, column] of texels) {
1110
+ const value = regions === null || regions.size === 0 || regions.has(region) ? 255 : Math.round(outside * 255);
1111
+ if (opacity[column] !== value) {
1112
+ opacity[column] = value;
1113
+ changed = true;
1114
+ }
1115
+ hasTransparency ||= value < 255;
1116
+ }
1117
+ focused = hasTransparency;
1118
+ syncTransparency();
1119
+ if (changed) opacityTexture.needsUpdate = true;
1120
+ },
1121
+ regionOpacity(region) {
1122
+ const column = texels.get(region);
1123
+ return column === void 0 ? null : (opacity[column] ?? 0) / 255;
1124
+ },
799
1125
  boxForFeature(tag) {
800
1126
  const regions = model.regionIndex.regionsForFeature(tag);
801
1127
  if (regions.length === 0) return null;
@@ -812,13 +1138,27 @@ function createPart(model, geometry, theme) {
812
1138
  setClippingPlanes(planes) {
813
1139
  const value = planes === null ? null : [...planes];
814
1140
  material.clippingPlanes = value;
1141
+ focusMaterial.clippingPlanes = value;
815
1142
  edgeMaterial.clippingPlanes = value;
816
1143
  },
817
1144
  setTheme(next) {
1145
+ currentTheme = next;
818
1146
  material.color.setHex(next.part);
819
1147
  material.emissive.setHex(next.partEmissive);
820
- edgeMaterial.color.setHex(next.edge);
821
- edgeMaterial.opacity = next.edgeOpacity;
1148
+ focusMaterial.color.setHex(next.part);
1149
+ focusMaterial.emissive.setHex(next.partEmissive);
1150
+ edgeMaterial.color.setHex(wireframe.value ? next.part : next.edge);
1151
+ edgeMaterial.opacity = wireframe.value ? 1 : next.edgeOpacity;
1152
+ },
1153
+ setDisplay(display, showEdges = true) {
1154
+ const enabled = display === "wireframe";
1155
+ if (wireframe.value !== enabled) {
1156
+ wireframe.value = enabled;
1157
+ syncTransparency();
1158
+ }
1159
+ edges.visible = enabled || showEdges;
1160
+ edgeMaterial.color.setHex(enabled ? currentTheme.part : currentTheme.edge);
1161
+ edgeMaterial.opacity = enabled ? 1 : currentTheme.edgeOpacity;
822
1162
  },
823
1163
  dispose() {
824
1164
  object.clear();
@@ -826,9 +1166,11 @@ function createPart(model, geometry, theme) {
826
1166
  geometry.deleteAttribute(REGION_ATTRIBUTE);
827
1167
  }
828
1168
  material.dispose();
1169
+ focusMaterial.dispose();
829
1170
  edgeGeometry.dispose();
830
1171
  edgeMaterial.dispose();
831
1172
  stateTexture.dispose();
1173
+ opacityTexture.dispose();
832
1174
  }
833
1175
  };
834
1176
  }
@@ -962,6 +1304,7 @@ function buildPick(input) {
962
1304
  triangleIndex: input.triangleIndex,
963
1305
  point: input.point,
964
1306
  normal: input.normal,
1307
+ ...input.pointer === void 0 ? {} : { pointer: input.pointer },
965
1308
  modifiers: input.modifiers ?? NO_MODIFIERS,
966
1309
  doubled: input.doubled ?? false
967
1310
  };
@@ -985,7 +1328,7 @@ import {
985
1328
  useMemo as useMemo4,
986
1329
  useRef as useRef3
987
1330
  } from "react";
988
- import { Box3 as Box33, Vector3 as Vector37 } from "three";
1331
+ import { Box3 as Box33, Vector3 as Vector38 } from "three";
989
1332
 
990
1333
  // src/camera.tsx
991
1334
  import { useFrame, useThree as useThree2 } from "@react-three/fiber";
@@ -1001,10 +1344,96 @@ import {
1001
1344
  Raycaster,
1002
1345
  Sphere,
1003
1346
  Spherical,
1004
- Vector2,
1005
- Vector3 as Vector34,
1347
+ Vector2 as Vector22,
1348
+ Vector3 as Vector35,
1006
1349
  Vector4
1007
1350
  } from "three";
1351
+
1352
+ // src/render/control-schemes.ts
1353
+ var CONTROL_SCHEME_OPTIONS = [
1354
+ { value: "toolpath", label: "Toolpath" },
1355
+ { value: "fusion", label: "Fusion" },
1356
+ { value: "alias", label: "Alias" },
1357
+ { value: "inventor", label: "Inventor" },
1358
+ { value: "solidworks", label: "SolidWorks" },
1359
+ { value: "tinkercad", label: "Tinkercad" },
1360
+ { value: "powermill", label: "PowerMill" },
1361
+ { value: "onshape", label: "Onshape" }
1362
+ ];
1363
+ var DEFAULT_TOUCHES = {
1364
+ one: "rotate",
1365
+ two: "dolly-truck",
1366
+ three: "truck"
1367
+ };
1368
+ var wheelFor = (orthographic) => orthographic ? "zoom" : "dolly";
1369
+ var mapping = (environment, mouse, options, touches = DEFAULT_TOUCHES) => ({
1370
+ mouse: {
1371
+ left: "none",
1372
+ middle: "none",
1373
+ right: "none",
1374
+ wheel: wheelFor(environment.orthographic),
1375
+ ...mouse
1376
+ },
1377
+ touches,
1378
+ ...options
1379
+ });
1380
+ var resolveControlScheme = (scheme, environment) => {
1381
+ const { shift, ctrl } = environment.modifiers;
1382
+ switch (scheme) {
1383
+ case "fusion":
1384
+ case "inventor": {
1385
+ const rotating = shift;
1386
+ return mapping(
1387
+ environment,
1388
+ { middle: rotating ? "rotate" : "truck", wheel: "none" },
1389
+ { usesCadWheel: true, immediate: true },
1390
+ {
1391
+ one: "rotate",
1392
+ two: rotating ? "rotate" : "truck",
1393
+ three: "truck"
1394
+ }
1395
+ );
1396
+ }
1397
+ case "solidworks":
1398
+ return mapping(
1399
+ environment,
1400
+ { middle: shift ? "zoom" : ctrl ? "truck" : "rotate" },
1401
+ { usesCadWheel: false, immediate: false }
1402
+ );
1403
+ case "alias":
1404
+ return mapping(
1405
+ environment,
1406
+ { left: "rotate", middle: "truck" },
1407
+ { usesCadWheel: false, immediate: false }
1408
+ );
1409
+ case "tinkercad":
1410
+ return mapping(
1411
+ environment,
1412
+ { right: shift ? "truck" : "rotate" },
1413
+ { usesCadWheel: false, immediate: false }
1414
+ );
1415
+ case "powermill":
1416
+ return mapping(
1417
+ environment,
1418
+ { middle: shift ? "truck" : "rotate" },
1419
+ { usesCadWheel: false, immediate: false }
1420
+ );
1421
+ case "onshape":
1422
+ return mapping(
1423
+ environment,
1424
+ { middle: "truck", right: "rotate" },
1425
+ { usesCadWheel: false, immediate: false }
1426
+ );
1427
+ case "toolpath":
1428
+ return mapping(
1429
+ environment,
1430
+ { left: "rotate", right: "truck" },
1431
+ { usesCadWheel: false, immediate: false }
1432
+ );
1433
+ }
1434
+ };
1435
+
1436
+ // src/render/controls.ts
1008
1437
  CameraControls.install({
1009
1438
  THREE: {
1010
1439
  Box3: Box32,
@@ -1013,8 +1442,8 @@ CameraControls.install({
1013
1442
  Raycaster,
1014
1443
  Sphere,
1015
1444
  Spherical,
1016
- Vector2,
1017
- Vector3: Vector34,
1445
+ Vector2: Vector22,
1446
+ Vector3: Vector35,
1018
1447
  Vector4
1019
1448
  }
1020
1449
  });
@@ -1032,13 +1461,14 @@ var ExtendedCameraControls = class extends CameraControls {
1032
1461
  #attached = false;
1033
1462
  #autoUpEnabled = false;
1034
1463
  #shiftPressed = false;
1464
+ #ctrlPressed = false;
1035
1465
  #wheelHandler = null;
1036
1466
  // Scratch objects — `#onPointerMove` and `#adaptUpVector` run at pointer and
1037
1467
  // frame rate respectively, so neither may allocate.
1038
1468
  #spherical = new Spherical();
1039
- #scratchA = new Vector34();
1040
- #scratchB = new Vector34();
1041
- #scratchC = new Vector34();
1469
+ #scratchA = new Vector35();
1470
+ #scratchB = new Vector35();
1471
+ #scratchC = new Vector35();
1042
1472
  constructor(camera, domElement, options = {}) {
1043
1473
  super(camera, domElement);
1044
1474
  this.#domElement = domElement;
@@ -1101,7 +1531,7 @@ var ExtendedCameraControls = class extends CameraControls {
1101
1531
  view?.removeEventListener("keyup", this.#onModifierChange);
1102
1532
  view?.removeEventListener("blur", this.#onWindowBlur);
1103
1533
  this.#disableAutoUp();
1104
- this.#disableFusionWheel();
1534
+ this.#disableCadWheel();
1105
1535
  }
1106
1536
  dispose() {
1107
1537
  this.detach();
@@ -1114,29 +1544,38 @@ var ExtendedCameraControls = class extends CameraControls {
1114
1544
  */
1115
1545
  applyScheme(scheme) {
1116
1546
  this.#scheme = scheme;
1117
- this.mouseButtons.left = CameraControls.ACTION.NONE;
1118
- this.mouseButtons.middle = CameraControls.ACTION.NONE;
1119
- this.mouseButtons.right = CameraControls.ACTION.NONE;
1120
- this.mouseButtons.wheel = CameraControls.ACTION.NONE;
1121
- this.touches.one = CameraControls.ACTION.TOUCH_ROTATE;
1122
- this.touches.two = CameraControls.ACTION.TOUCH_DOLLY_TRUCK;
1123
- this.touches.three = CameraControls.ACTION.TOUCH_TRUCK;
1124
- this.smoothTime = DEFAULT_SMOOTH_TIME;
1125
- this.draggingSmoothTime = DEFAULT_SMOOTH_TIME;
1126
- this.#disableFusionWheel();
1127
- if (scheme === "fusion") {
1128
- const rotating = this.#shiftPressed;
1129
- this.mouseButtons.middle = rotating ? CameraControls.ACTION.ROTATE : CameraControls.ACTION.TRUCK;
1130
- this.touches.two = rotating ? CameraControls.ACTION.TOUCH_ROTATE : CameraControls.ACTION.TOUCH_TRUCK;
1131
- this.smoothTime = 0;
1132
- this.draggingSmoothTime = 0;
1133
- this.#enableFusionWheel();
1134
- return;
1547
+ const mapping2 = resolveControlScheme(scheme, {
1548
+ orthographic: this.camera instanceof OrthographicCamera2,
1549
+ modifiers: { shift: this.#shiftPressed, ctrl: this.#ctrlPressed }
1550
+ });
1551
+ this.#applyMapping(mapping2);
1552
+ }
1553
+ #applyMapping(mapping2) {
1554
+ const action = {
1555
+ none: CameraControls.ACTION.NONE,
1556
+ rotate: CameraControls.ACTION.ROTATE,
1557
+ truck: CameraControls.ACTION.TRUCK,
1558
+ zoom: CameraControls.ACTION.ZOOM,
1559
+ dolly: CameraControls.ACTION.DOLLY
1560
+ };
1561
+ const touchAction = {
1562
+ rotate: CameraControls.ACTION.TOUCH_ROTATE,
1563
+ truck: CameraControls.ACTION.TOUCH_TRUCK,
1564
+ "dolly-truck": CameraControls.ACTION.TOUCH_DOLLY_TRUCK
1565
+ };
1566
+ this.#disableCadWheel();
1567
+ this.mouseButtons.left = action[mapping2.mouse.left];
1568
+ this.mouseButtons.middle = action[mapping2.mouse.middle];
1569
+ this.mouseButtons.right = action[mapping2.mouse.right];
1570
+ this.mouseButtons.wheel = action[mapping2.mouse.wheel];
1571
+ this.touches.one = touchAction[mapping2.touches.one];
1572
+ this.touches.two = touchAction[mapping2.touches.two];
1573
+ this.touches.three = touchAction[mapping2.touches.three];
1574
+ this.smoothTime = mapping2.immediate ? 0 : DEFAULT_SMOOTH_TIME;
1575
+ this.draggingSmoothTime = mapping2.immediate ? 0 : DEFAULT_SMOOTH_TIME;
1576
+ if (mapping2.usesCadWheel) {
1577
+ this.#enableCadWheel();
1135
1578
  }
1136
- this.mouseButtons.left = CameraControls.ACTION.ROTATE;
1137
- this.mouseButtons.right = CameraControls.ACTION.TRUCK;
1138
- this.mouseButtons.middle = CameraControls.ACTION.TRUCK;
1139
- this.mouseButtons.wheel = this.camera instanceof OrthographicCamera2 ? CameraControls.ACTION.ZOOM : CameraControls.ACTION.DOLLY;
1140
1579
  }
1141
1580
  setFreeOrbit(freeOrbit) {
1142
1581
  if (this.#freeOrbit === freeOrbit) {
@@ -1174,13 +1613,16 @@ var ExtendedCameraControls = class extends CameraControls {
1174
1613
  this.#autoUpEnabled = false;
1175
1614
  this.removeEventListener("update", this.#adaptUpVector);
1176
1615
  }
1177
- #enableFusionWheel() {
1178
- this.#wheelHandler = (event) => this.#onFusionWheel(event);
1616
+ #enableCadWheel() {
1617
+ if (!this.#attached || this.#wheelHandler) {
1618
+ return;
1619
+ }
1620
+ this.#wheelHandler = (event) => this.#onCadWheel(event);
1179
1621
  this.#domElement.addEventListener("wheel", this.#wheelHandler, {
1180
1622
  passive: false
1181
1623
  });
1182
1624
  }
1183
- #disableFusionWheel() {
1625
+ #disableCadWheel() {
1184
1626
  if (!this.#wheelHandler) {
1185
1627
  return;
1186
1628
  }
@@ -1223,7 +1665,7 @@ var ExtendedCameraControls = class extends CameraControls {
1223
1665
  void this.rotateTo(azimuth, polar, false);
1224
1666
  this.update(0);
1225
1667
  };
1226
- #onFusionWheel = (event) => {
1668
+ #onCadWheel = (event) => {
1227
1669
  event.preventDefault();
1228
1670
  if (event.ctrlKey) {
1229
1671
  void this.zoom(-event.deltaY * FUSION_PINCH_ZOOM_SCALE, false);
@@ -1241,19 +1683,18 @@ var ExtendedCameraControls = class extends CameraControls {
1241
1683
  this.dispatchEvent({ type: "control" });
1242
1684
  };
1243
1685
  #onModifierChange = (event) => {
1244
- this.#setShiftPressed(event.shiftKey);
1686
+ this.#setModifiers(event.shiftKey, event.ctrlKey);
1245
1687
  };
1246
1688
  #onWindowBlur = () => {
1247
- this.#setShiftPressed(false);
1689
+ this.#setModifiers(false, false);
1248
1690
  };
1249
- #setShiftPressed(pressed) {
1250
- if (this.#shiftPressed === pressed) {
1691
+ #setModifiers(shift, ctrl) {
1692
+ if (this.#shiftPressed === shift && this.#ctrlPressed === ctrl) {
1251
1693
  return;
1252
1694
  }
1253
- this.#shiftPressed = pressed;
1254
- if (this.#scheme === "fusion") {
1255
- this.applyScheme(this.#scheme);
1256
- }
1695
+ this.#shiftPressed = shift;
1696
+ this.#ctrlPressed = ctrl;
1697
+ this.applyScheme(this.#scheme);
1257
1698
  }
1258
1699
  };
1259
1700
 
@@ -1328,11 +1769,12 @@ function retargetPose(cameraPosition, currentTarget, hitPoint) {
1328
1769
  // src/render/section-store.ts
1329
1770
  function createSectionStore() {
1330
1771
  let current = null;
1331
- let engaged = 0;
1332
1772
  const listeners = /* @__PURE__ */ new Set();
1333
1773
  const notify = () => {
1334
1774
  for (const listener of listeners) listener();
1335
1775
  };
1776
+ const engaged = counted(notify);
1777
+ const picking = counted(notify);
1336
1778
  return {
1337
1779
  get: () => current,
1338
1780
  set: (next) => {
@@ -1340,12 +1782,10 @@ function createSectionStore() {
1340
1782
  current = next;
1341
1783
  notify();
1342
1784
  },
1343
- isEngaged: () => engaged > 0,
1344
- setEngaged: (next) => {
1345
- const was = engaged > 0;
1346
- engaged = Math.max(0, engaged + (next ? 1 : -1));
1347
- if (engaged > 0 !== was) notify();
1348
- },
1785
+ isEngaged: engaged.is,
1786
+ setEngaged: engaged.set,
1787
+ isPicking: picking.is,
1788
+ setPicking: picking.set,
1349
1789
  subscribe: (listener) => {
1350
1790
  listeners.add(listener);
1351
1791
  return () => {
@@ -1354,15 +1794,26 @@ function createSectionStore() {
1354
1794
  }
1355
1795
  };
1356
1796
  }
1797
+ function counted(notify) {
1798
+ let held = 0;
1799
+ return {
1800
+ is: () => held > 0,
1801
+ set: (next) => {
1802
+ const was = held > 0;
1803
+ held = Math.max(0, held + (next ? 1 : -1));
1804
+ if (held > 0 !== was) notify();
1805
+ }
1806
+ };
1807
+ }
1357
1808
 
1358
1809
  // src/target-marker.tsx
1359
1810
  import { useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
1360
1811
  import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
1361
1812
  import {
1362
- DoubleSide,
1813
+ DoubleSide as DoubleSide2,
1363
1814
  RingGeometry,
1364
1815
  SphereGeometry,
1365
- Vector3 as Vector35
1816
+ Vector3 as Vector36
1366
1817
  } from "three";
1367
1818
 
1368
1819
  // src/render/target.ts
@@ -1405,8 +1856,8 @@ var TargetMarker = ({ controlsRef }) => {
1405
1856
  const heldUntil = useRef2(0);
1406
1857
  const painted = useRef2(-1);
1407
1858
  const placed = useRef2(false);
1408
- const lastTarget = useMemo3(() => new Vector35(), []);
1409
- const scratch = useMemo3(() => new Vector35(), []);
1859
+ const lastTarget = useMemo3(() => new Vector36(), []);
1860
+ const scratch = useMemo3(() => new Vector36(), []);
1410
1861
  useEffect3(() => {
1411
1862
  const controls = controlsRef.current;
1412
1863
  if (!controls) return void 0;
@@ -1472,7 +1923,7 @@ var TargetMarker = ({ controlsRef }) => {
1472
1923
  opacity: 0,
1473
1924
  depthTest: false,
1474
1925
  depthWrite: false,
1475
- side: DoubleSide
1926
+ side: DoubleSide2
1476
1927
  }
1477
1928
  )
1478
1929
  }
@@ -1506,7 +1957,7 @@ import {
1506
1957
  CanvasTexture,
1507
1958
  Float32BufferAttribute as Float32BufferAttribute3,
1508
1959
  SRGBColorSpace,
1509
- Vector3 as Vector36
1960
+ Vector3 as Vector37
1510
1961
  } from "three";
1511
1962
  var VIEW_NAMES = [
1512
1963
  // The six faces.
@@ -1590,12 +2041,12 @@ function viewUp(direction) {
1590
2041
  return { x: 0, y: 0, z: 1 };
1591
2042
  }
1592
2043
  function squaredUp(direction, currentUp) {
1593
- const forward = new Vector36(direction.x, direction.y, direction.z).normalize();
2044
+ const forward = new Vector37(direction.x, direction.y, direction.z).normalize();
1594
2045
  const canonical = viewUp(direction);
1595
- const zero = new Vector36(canonical.x, canonical.y, canonical.z);
2046
+ const zero = new Vector37(canonical.x, canonical.y, canonical.z);
1596
2047
  zero.addScaledVector(forward, -forward.dot(zero)).normalize();
1597
- const quarter = new Vector36().crossVectors(forward, zero);
1598
- const current = new Vector36(currentUp.x, currentUp.y, currentUp.z);
2048
+ const quarter = new Vector37().crossVectors(forward, zero);
2049
+ const current = new Vector37(currentUp.x, currentUp.y, currentUp.z);
1599
2050
  const rolls = [zero, quarter, zero.clone().negate(), quarter.clone().negate()];
1600
2051
  const nearest = rolls.reduce(
1601
2052
  (best, roll) => roll.dot(current) > best.dot(current) ? roll : best
@@ -1613,11 +2064,11 @@ function cubeZones(chamfer = CHAMFER) {
1613
2064
  const direction = viewVector(name);
1614
2065
  const [sx, sy, sz] = VIEW_SIGNS[name];
1615
2066
  const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
1616
- const normal = new Vector36(direction.x, direction.y, direction.z);
2067
+ const normal = new Vector37(direction.x, direction.y, direction.z);
1617
2068
  let polygon;
1618
2069
  if (kind === "face") {
1619
2070
  const up = viewUp(direction);
1620
- const upVector = new Vector36(up.x, up.y, up.z);
2071
+ const upVector = new Vector37(up.x, up.y, up.z);
1621
2072
  const right = normal.clone().negate().cross(upVector).normalize();
1622
2073
  const square = [
1623
2074
  [-1, -1],
@@ -1639,7 +2090,7 @@ function cubeZones(chamfer = CHAMFER) {
1639
2090
  [chamfer, 1, -chamfer]
1640
2091
  ];
1641
2092
  polygon = rectangle.map(
1642
- ([onA, onB, onC]) => new Vector36().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2093
+ ([onA, onB, onC]) => new Vector37().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
1643
2094
  );
1644
2095
  } else {
1645
2096
  const triangle = [
@@ -1647,11 +2098,11 @@ function cubeZones(chamfer = CHAMFER) {
1647
2098
  [chamfer, 1, chamfer],
1648
2099
  [chamfer, chamfer, 1]
1649
2100
  ];
1650
- polygon = triangle.map(([onX, onY, onZ]) => new Vector36(sx * onX, sy * onY, sz * onZ));
2101
+ polygon = triangle.map(([onX, onY, onZ]) => new Vector37(sx * onX, sy * onY, sz * onZ));
1651
2102
  }
1652
2103
  const [first, second, third] = polygon;
1653
2104
  if (first && second && third) {
1654
- const facing = new Vector36().subVectors(second, first).cross(new Vector36().subVectors(third, first));
2105
+ const facing = new Vector37().subVectors(second, first).cross(new Vector37().subVectors(third, first));
1655
2106
  if (facing.dot(normal) < 0) polygon.reverse();
1656
2107
  }
1657
2108
  return { name, kind, direction, polygon: polygon.map(vec) };
@@ -1781,10 +2232,10 @@ var ViewerScene = ({
1781
2232
  const framedRef = useRef3(null);
1782
2233
  const scratchBox = useMemo4(() => new Box33(), []);
1783
2234
  const scratchBoundary = useMemo4(() => new Box33(), []);
1784
- const scratchDirection = useMemo4(() => new Vector37(), []);
1785
- const scratchView = useMemo4(() => new Vector37(), []);
1786
- const scratchTarget = useMemo4(() => new Vector37(), []);
1787
- const scratchPosition = useMemo4(() => new Vector37(), []);
2235
+ const scratchDirection = useMemo4(() => new Vector38(), []);
2236
+ const scratchView = useMemo4(() => new Vector38(), []);
2237
+ const scratchTarget = useMemo4(() => new Vector38(), []);
2238
+ const scratchPosition = useMemo4(() => new Vector38(), []);
1788
2239
  const applyLimits = useCallback(
1789
2240
  (bounds, framed) => {
1790
2241
  controlsRef.current?.applyLimits(
@@ -1833,8 +2284,8 @@ var ViewerScene = ({
1833
2284
  (bounds) => {
1834
2285
  const controls = controlsRef.current;
1835
2286
  if (!controls) return false;
1836
- const target = controls.getTarget(new Vector37());
1837
- const direction = currentViewDirection(camera, target, new Vector37());
2287
+ const target = controls.getTarget(new Vector38());
2288
+ const direction = currentViewDirection(camera, target, new Vector38());
1838
2289
  const distance = fitDistance(projection, size, bounds, DEFAULT_FIT_MARGIN);
1839
2290
  const position = direction.multiplyScalar(distance).add(bounds.center);
1840
2291
  void controls.setLookAt(
@@ -1859,8 +2310,8 @@ var ViewerScene = ({
1859
2310
  const fitContent = useCallback(() => {
1860
2311
  const controls = controlsRef.current;
1861
2312
  if (!controls) return false;
1862
- const target = controls.getTarget(new Vector37());
1863
- return frame(currentViewDirection(camera, target, new Vector37()), true);
2313
+ const target = controls.getTarget(new Vector38());
2314
+ return frame(currentViewDirection(camera, target, new Vector38()), true);
1864
2315
  }, [camera, frame]);
1865
2316
  const resetContent = useCallback(() => {
1866
2317
  const bounds = measure();
@@ -2015,7 +2466,8 @@ var Viewer = forwardRef(function Viewer2({
2015
2466
  return /* @__PURE__ */ jsx3(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx3(SectionStoreContext.Provider, { value: sectionStore, children: /* @__PURE__ */ jsx3(
2016
2467
  "div",
2017
2468
  {
2018
- className,
2469
+ className: ["viewer-root", className].filter(Boolean).join(" "),
2470
+ "data-viewer-root": "true",
2019
2471
  ref: hold,
2020
2472
  onMouseDown: (event) => {
2021
2473
  if (event.target instanceof HTMLCanvasElement) event.preventDefault();
@@ -2024,11 +2476,20 @@ var Viewer = forwardRef(function Viewer2({
2024
2476
  children: /* @__PURE__ */ jsx3(
2025
2477
  Canvas,
2026
2478
  {
2479
+ className: "viewer-canvas-container",
2480
+ "data-viewer-canvas-container": "true",
2027
2481
  orthographic: projection === "orthographic",
2028
2482
  camera: { fov: PERSPECTIVE_FOV, up: [0, 0, 1], position: [1, -1, 1] },
2029
2483
  dpr: [1, 2],
2030
2484
  frameloop: "demand",
2031
2485
  gl: { antialias: true, alpha: true, stencil: true, localClippingEnabled: true },
2486
+ onCreated: ({ gl }) => {
2487
+ const canvas = gl.domElement;
2488
+ canvas.classList.add("viewer-canvas");
2489
+ canvas.dataset.viewerCanvas = "true";
2490
+ canvas.parentElement?.classList.add("viewer-canvas-frame");
2491
+ canvas.parentElement?.setAttribute("data-viewer-canvas-frame", "true");
2492
+ },
2032
2493
  onPointerMissed: (event) => {
2033
2494
  if (event.button !== 0) return;
2034
2495
  if (tracker.current?.isTap(event) ?? true) onPointerMissed?.();
@@ -2060,29 +2521,31 @@ import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber
2060
2521
  import { useCallback as useCallback2, useEffect as useEffect5, useLayoutEffect, useMemo as useMemo5, useRef as useRef4, useState } from "react";
2061
2522
  import {
2062
2523
  Color as Color3,
2524
+ DoubleSide as DoubleSide4,
2525
+ EdgesGeometry,
2063
2526
  Mesh as Mesh3,
2064
2527
  Plane as Plane2,
2065
2528
  PlaneGeometry,
2066
2529
  Quaternion as Quaternion2,
2067
2530
  Raycaster as Raycaster2,
2068
2531
  Scene,
2069
- Vector2 as Vector23,
2070
- Vector3 as Vector39
2532
+ Vector2 as Vector24,
2533
+ Vector3 as Vector310
2071
2534
  } from "three";
2072
2535
 
2073
2536
  // src/render/directions.ts
2074
- import { Vector3 as Vector38 } from "three";
2075
- var CONE_AXIS = new Vector38(0, 1, 0);
2537
+ import { Vector3 as Vector39 } from "three";
2538
+ var CONE_AXIS = new Vector39(0, 1, 0);
2076
2539
  var LENGTH = 0.45;
2077
2540
  var HEAD = 0.45;
2078
2541
  var HEAD_RADIUS = 0.3;
2079
2542
  var SHAFT_RADIUS = 0.09;
2080
2543
  var GAP = 0.2;
2081
2544
  function arrowPlacement(direction, box) {
2082
- const center = box.getCenter(new Vector38());
2083
- const half = box.getSize(new Vector38()).multiplyScalar(0.5);
2545
+ const center = box.getCenter(new Vector39());
2546
+ const half = box.getSize(new Vector39()).multiplyScalar(0.5);
2084
2547
  const radius = half.length() || 1;
2085
- const axis = new Vector38(direction.x, direction.y, direction.z);
2548
+ const axis = new Vector39(direction.x, direction.y, direction.z);
2086
2549
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
2087
2550
  axis.normalize();
2088
2551
  let exit = Number.POSITIVE_INFINITY;
@@ -2103,7 +2566,7 @@ import {
2103
2566
  BackSide,
2104
2567
  Color as Color2,
2105
2568
  DecrementWrapStencilOp,
2106
- DoubleSide as DoubleSide2,
2569
+ DoubleSide as DoubleSide3,
2107
2570
  FrontSide,
2108
2571
  IncrementWrapStencilOp,
2109
2572
  LinearFilter,
@@ -2111,7 +2574,7 @@ import {
2111
2574
  NotEqualStencilFunc,
2112
2575
  ReplaceStencilOp,
2113
2576
  ShaderMaterial,
2114
- Vector2 as Vector22,
2577
+ Vector2 as Vector23,
2115
2578
  WebGLRenderTarget
2116
2579
  } from "three";
2117
2580
  var VERTEX = (
@@ -2170,7 +2633,7 @@ function createMaskTarget() {
2170
2633
  function createCapMaterial(mask) {
2171
2634
  const uniforms = {
2172
2635
  uMask: { value: mask },
2173
- uResolution: { value: new Vector22(1, 1) },
2636
+ uResolution: { value: new Vector23(1, 1) },
2174
2637
  uFill: { value: new Color2() },
2175
2638
  uHatch: { value: new Color2() },
2176
2639
  uOutline: { value: new Color2() },
@@ -2182,7 +2645,7 @@ function createCapMaterial(mask) {
2182
2645
  vertexShader: VERTEX,
2183
2646
  fragmentShader: FRAGMENT,
2184
2647
  uniforms,
2185
- side: DoubleSide2,
2648
+ side: DoubleSide3,
2186
2649
  stencilWrite: true,
2187
2650
  stencilRef: 0,
2188
2651
  stencilFunc: NotEqualStencilFunc,
@@ -2228,7 +2691,7 @@ function createStencilMaterials(clip) {
2228
2691
  function createMaskMaterial() {
2229
2692
  return new MeshBasicMaterial2({
2230
2693
  color: 16777215,
2231
- side: DoubleSide2,
2694
+ side: DoubleSide3,
2232
2695
  stencilWrite: true,
2233
2696
  stencilRef: 0,
2234
2697
  stencilFunc: NotEqualStencilFunc,
@@ -2239,12 +2702,12 @@ function createMaskMaterial() {
2239
2702
  }
2240
2703
 
2241
2704
  // src/section-view.tsx
2242
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2705
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2243
2706
  var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
2244
2707
  function resolveSectionPlane(options, box) {
2245
2708
  if (!options?.enabled || box.isEmpty()) return null;
2246
2709
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_SECTION_NORMAL;
2247
- const axis = new Vector39(normal.x, normal.y, normal.z);
2710
+ const axis = new Vector310(normal.x, normal.y, normal.z);
2248
2711
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
2249
2712
  axis.normalize();
2250
2713
  const bounds = sectionBounds(box, normal);
@@ -2260,16 +2723,18 @@ function resolveSectionPlane(options, box) {
2260
2723
  constant,
2261
2724
  plane: options.plane ?? null,
2262
2725
  depth: anchor === null ? null : sectionDepth(normal, anchor, constant),
2726
+ cutDistance: sectionCutDistance(box, normal, constant),
2263
2727
  depthRange: anchor === null ? null : sectionDepthRange(bounds, normal, anchor)
2264
2728
  }
2265
2729
  };
2266
2730
  }
2267
- var Z = new Vector39(0, 0, 1);
2731
+ var Z = new Vector310(0, 0, 1);
2268
2732
  var SectionView = ({
2269
2733
  geometry,
2270
2734
  box,
2271
2735
  plane,
2272
2736
  theme,
2737
+ handleColor,
2273
2738
  showHandle,
2274
2739
  onDrag
2275
2740
  }) => {
@@ -2283,7 +2748,10 @@ var SectionView = ({
2283
2748
  const handleRef = useRef4(null);
2284
2749
  const [hovered, setHovered] = useState(false);
2285
2750
  const dragging = useRef4(null);
2286
- const span = useMemo5(() => box.getSize(new Vector39()).length(), [box]);
2751
+ const span = useMemo5(() => box.getSize(new Vector310()).length(), [box]);
2752
+ const frameSize = span * SECTION_GIZMO_FRAME_SCALE;
2753
+ const directionColor2 = useMemo5(() => sectionDirectionColor(plane.normal), [plane]);
2754
+ const resolvedHandleColor = sectionHandleColor(handleColor, directionColor2);
2287
2755
  const clip = useMemo5(() => [plane], [plane]);
2288
2756
  const capPosition = useMemo5(() => sectionAnchor(box, plane), [box, plane]);
2289
2757
  const capQuaternion = useMemo5(() => new Quaternion2().setFromUnitVectors(Z, plane.normal), [plane]);
@@ -2291,6 +2759,10 @@ var SectionView = ({
2291
2759
  () => new Quaternion2().setFromUnitVectors(CONE_AXIS, plane.normal.clone().negate()),
2292
2760
  [plane]
2293
2761
  );
2762
+ const frameSurface = useMemo5(() => new PlaneGeometry(frameSize, frameSize), [frameSize]);
2763
+ useEffect5(() => () => frameSurface.dispose(), [frameSurface]);
2764
+ const frameEdges = useMemo5(() => new EdgesGeometry(frameSurface), [frameSurface]);
2765
+ useEffect5(() => () => frameEdges.dispose(), [frameEdges]);
2294
2766
  const stencils = useMemo5(() => createStencilMaterials([]), []);
2295
2767
  useEffect5(() => () => stencils.forEach((material) => material.dispose()), [stencils]);
2296
2768
  useLayoutEffect(() => {
@@ -2320,7 +2792,7 @@ var SectionView = ({
2320
2792
  maskScene.maskMesh.quaternion.copy(capQuaternion);
2321
2793
  }, [capPosition, capQuaternion, maskScene]);
2322
2794
  useLayoutEffect(() => {
2323
- const buffer = gl.getDrawingBufferSize(new Vector23());
2795
+ const buffer = gl.getDrawingBufferSize(new Vector24());
2324
2796
  mask.setSize(buffer.x, buffer.y);
2325
2797
  cap.uniforms.uResolution.value.copy(buffer);
2326
2798
  applyCapTheme(cap, theme, dpr);
@@ -2358,7 +2830,7 @@ var SectionView = ({
2358
2830
  event.stopPropagation();
2359
2831
  const view = camera.position.clone().sub(capPosition).normalize();
2360
2832
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
2361
- const hit = event.ray.intersectPlane(surface, new Vector39());
2833
+ const hit = event.ray.intersectPlane(surface, new Vector310());
2362
2834
  const start = {
2363
2835
  plane: surface,
2364
2836
  from: hit ? hit.dot(plane.normal) : 0,
@@ -2367,7 +2839,7 @@ var SectionView = ({
2367
2839
  dragging.current = start;
2368
2840
  setControlsEnabled(false);
2369
2841
  const raycaster = new Raycaster2();
2370
- const pointer = new Vector23();
2842
+ const pointer = new Vector24();
2371
2843
  const move = (native) => {
2372
2844
  const rect = domElement.getBoundingClientRect();
2373
2845
  pointer.set(
@@ -2375,7 +2847,7 @@ var SectionView = ({
2375
2847
  -((native.clientY - rect.top) / rect.height) * 2 + 1
2376
2848
  );
2377
2849
  raycaster.setFromCamera(pointer, camera);
2378
- const point = raycaster.ray.intersectPlane(start.plane, new Vector39());
2850
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector310());
2379
2851
  if (!point) return;
2380
2852
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
2381
2853
  invalidate();
@@ -2437,37 +2909,81 @@ var SectionView = ({
2437
2909
  raycast: () => null
2438
2910
  }
2439
2911
  ),
2440
- showHandle && onDrag ? /* @__PURE__ */ jsxs3(
2441
- "group",
2442
- {
2443
- ref: handleRef,
2444
- position: capPosition,
2445
- quaternion: handleQuaternion,
2446
- renderOrder: SECTION_RENDER_ORDER.handle,
2447
- children: [
2448
- /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.28, 0], ...grab, children: [
2449
- /* @__PURE__ */ jsx4("coneGeometry", { args: [0.16, 0.34, 20] }),
2450
- /* @__PURE__ */ jsx4(
2451
- "meshBasicMaterial",
2452
- {
2453
- color: hovered ? theme.hover : theme.sectionHandle,
2454
- depthTest: false
2455
- }
2456
- )
2457
- ] }),
2458
- /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.08, 0], ...grab, children: [
2459
- /* @__PURE__ */ jsx4("cylinderGeometry", { args: [0.045, 0.045, 0.4, 12] }),
2460
- /* @__PURE__ */ jsx4(
2912
+ showHandle && onDrag ? /* @__PURE__ */ jsxs3(Fragment, { children: [
2913
+ /* @__PURE__ */ jsxs3(
2914
+ "group",
2915
+ {
2916
+ position: capPosition,
2917
+ quaternion: capQuaternion,
2918
+ renderOrder: SECTION_RENDER_ORDER.outline,
2919
+ children: [
2920
+ /* @__PURE__ */ jsx4("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx4(
2461
2921
  "meshBasicMaterial",
2462
2922
  {
2463
- color: hovered ? theme.hover : theme.sectionHandle,
2464
- depthTest: false
2923
+ color: directionColor2,
2924
+ transparent: true,
2925
+ opacity: 0.18,
2926
+ side: DoubleSide4,
2927
+ depthTest: false,
2928
+ depthWrite: false
2465
2929
  }
2466
- )
2467
- ] })
2468
- ]
2469
- }
2470
- ) : null
2930
+ ) }),
2931
+ /* @__PURE__ */ jsx4("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx4("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
2932
+ ]
2933
+ }
2934
+ ),
2935
+ /* @__PURE__ */ jsxs3(
2936
+ "group",
2937
+ {
2938
+ ref: handleRef,
2939
+ position: capPosition,
2940
+ quaternion: handleQuaternion,
2941
+ renderOrder: SECTION_RENDER_ORDER.handle,
2942
+ children: [
2943
+ /* @__PURE__ */ jsxs3("mesh", { ...grab, children: [
2944
+ /* @__PURE__ */ jsx4("sphereGeometry", { args: [0.1, 16, 12] }),
2945
+ /* @__PURE__ */ jsx4(
2946
+ "meshBasicMaterial",
2947
+ {
2948
+ color: hovered ? theme.hover : resolvedHandleColor,
2949
+ depthTest: false
2950
+ }
2951
+ )
2952
+ ] }),
2953
+ /* @__PURE__ */ jsxs3("mesh", { ...grab, children: [
2954
+ /* @__PURE__ */ jsx4("cylinderGeometry", { args: [0.035, 0.035, 0.9, 12] }),
2955
+ /* @__PURE__ */ jsx4(
2956
+ "meshBasicMaterial",
2957
+ {
2958
+ color: hovered ? theme.hover : resolvedHandleColor,
2959
+ depthTest: false
2960
+ }
2961
+ )
2962
+ ] }),
2963
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.61, 0], ...grab, children: [
2964
+ /* @__PURE__ */ jsx4("coneGeometry", { args: [0.15, 0.3, 20] }),
2965
+ /* @__PURE__ */ jsx4(
2966
+ "meshBasicMaterial",
2967
+ {
2968
+ color: hovered ? theme.hover : resolvedHandleColor,
2969
+ depthTest: false
2970
+ }
2971
+ )
2972
+ ] }),
2973
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, -0.61, 0], rotation: [Math.PI, 0, 0], ...grab, children: [
2974
+ /* @__PURE__ */ jsx4("coneGeometry", { args: [0.15, 0.3, 20] }),
2975
+ /* @__PURE__ */ jsx4(
2976
+ "meshBasicMaterial",
2977
+ {
2978
+ color: hovered ? theme.hover : resolvedHandleColor,
2979
+ depthTest: false
2980
+ }
2981
+ )
2982
+ ] })
2983
+ ]
2984
+ }
2985
+ )
2986
+ ] }) : null
2471
2987
  ] })
2472
2988
  );
2473
2989
  };
@@ -2483,7 +2999,7 @@ function useContentBox() {
2483
2999
  useFrame5(() => {
2484
3000
  if (measured.current) return;
2485
3001
  const next = new Box34();
2486
- contentBounds(scene, next);
3002
+ partBounds(scene, next);
2487
3003
  if (next.isEmpty()) return;
2488
3004
  measured.current = true;
2489
3005
  setBox(next);
@@ -2492,16 +3008,18 @@ function useContentBox() {
2492
3008
  }
2493
3009
 
2494
3010
  // src/part-mesh.tsx
2495
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3011
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2496
3012
  var PartMesh = ({
2497
3013
  model,
2498
3014
  geometry,
2499
3015
  selection = [],
3016
+ focus,
2500
3017
  candidates = [],
2501
3018
  highlights = [],
2502
3019
  regionHighlights = [],
2503
3020
  pickedRegions = [],
2504
3021
  hoveredFeatureIds = [],
3022
+ hover = true,
2505
3023
  activeDirection = null,
2506
3024
  section,
2507
3025
  onSectionChange,
@@ -2510,7 +3028,8 @@ var PartMesh = ({
2510
3028
  onHover,
2511
3029
  onPick,
2512
3030
  theme,
2513
- showEdges = true
3031
+ showEdges = true,
3032
+ display = "solid"
2514
3033
  }) => {
2515
3034
  const { camera, controls, invalidate } = useThree7();
2516
3035
  const viewerControls = useViewerControls();
@@ -2519,6 +3038,8 @@ var PartMesh = ({
2519
3038
  currentTheme.current = resolved;
2520
3039
  const part = useMemo6(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
2521
3040
  const hoverRegion = useRef6(null);
3041
+ const onHoverRef = useRef6(onHover);
3042
+ onHoverRef.current = onHover;
2522
3043
  const box = useContentBox();
2523
3044
  const store = useSectionStore();
2524
3045
  const shared = useSyncExternalStore(store.subscribe, store.get, store.get);
@@ -2568,10 +3089,19 @@ var PartMesh = ({
2568
3089
  part.setTheme(resolved);
2569
3090
  repaint();
2570
3091
  }, [part, repaint, resolved]);
3092
+ const focusRef = useRef6(focus);
3093
+ focusRef.current = focus;
3094
+ const selectionRef = useRef6(selection);
3095
+ selectionRef.current = selection;
3096
+ const focusKey = focusStateKey(focus, selection);
2571
3097
  useLayoutEffect2(() => {
2572
- part.edges.visible = showEdges;
3098
+ part.setFocus(selectionRef.current, focusRef.current);
2573
3099
  invalidate();
2574
- }, [invalidate, part, showEdges]);
3100
+ }, [focusKey, invalidate, part]);
3101
+ useLayoutEffect2(() => {
3102
+ part.setDisplay(display, showEdges);
3103
+ invalidate();
3104
+ }, [display, invalidate, part, showEdges]);
2575
3105
  useLayoutEffect2(() => {
2576
3106
  part.setClippingPlanes(cut ? [cut.plane] : null);
2577
3107
  invalidate();
@@ -2614,6 +3144,7 @@ var PartMesh = ({
2614
3144
  triangleIndex,
2615
3145
  point: [event.point.x, event.point.y, event.point.z],
2616
3146
  normal: [normal.x, normal.y, normal.z],
3147
+ pointer: { clientX: source.clientX, clientY: source.clientY },
2617
3148
  activeDirection,
2618
3149
  doubled,
2619
3150
  viewDirection: viewDirection(camera, target),
@@ -2633,8 +3164,12 @@ var PartMesh = ({
2633
3164
  repaint();
2634
3165
  onHover?.(next);
2635
3166
  };
2636
- const onHoverRef = useRef6(onHover);
2637
- onHoverRef.current = onHover;
3167
+ useLayoutEffect2(() => {
3168
+ if (hover || hoverRegion.current === null) return;
3169
+ hoverRegion.current = null;
3170
+ repaint();
3171
+ onHoverRef.current?.(null);
3172
+ }, [hover, repaint]);
2638
3173
  useLayoutEffect2(() => {
2639
3174
  if (!engaged || hoverRegion.current === null) return;
2640
3175
  hoverRegion.current = null;
@@ -2645,18 +3180,21 @@ var PartMesh = ({
2645
3180
  (constant) => {
2646
3181
  if (!cut) return;
2647
3182
  const anchor = options?.plane?.point;
3183
+ const bounds = sectionBounds(box, cut.state.normal);
3184
+ const offset = sectionOffset(bounds, constant);
2648
3185
  const state = {
2649
3186
  ...cut.state,
2650
3187
  constant,
2651
- offset: sectionOffset(sectionBounds(box, cut.state.normal), constant),
2652
- depth: anchor ? sectionDepth(cut.state.normal, anchor, constant) : null
3188
+ offset,
3189
+ depth: anchor ? sectionDepth(cut.state.normal, anchor, constant) : null,
3190
+ cutDistance: sectionCutDistance(box, cut.state.normal, constant)
2653
3191
  };
2654
3192
  if (!controlled) store.set(sectionOptionsFromState(state));
2655
3193
  onSectionChange?.(state);
2656
3194
  },
2657
3195
  [box, controlled, cut, onSectionChange, options, store]
2658
3196
  );
2659
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
3197
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
2660
3198
  /* @__PURE__ */ jsx5(
2661
3199
  "primitive",
2662
3200
  {
@@ -2665,7 +3203,7 @@ var PartMesh = ({
2665
3203
  pressedWhileEngaged.current = engaged;
2666
3204
  },
2667
3205
  onPointerMove: (event) => {
2668
- if (!engaged) emitHover(pickFor(event));
3206
+ if (!engaged && hover) emitHover(pickFor(event));
2669
3207
  },
2670
3208
  onPointerOut: () => {
2671
3209
  emitHover(null);
@@ -2701,15 +3239,16 @@ var PartMesh = ({
2701
3239
  box,
2702
3240
  plane: cut.plane,
2703
3241
  theme: resolved,
3242
+ handleColor: theme?.sectionHandle,
2704
3243
  showHandle: !controlled || onSectionChange !== void 0,
2705
3244
  onDrag: !controlled || onSectionChange ? dragSection : void 0
2706
3245
  }
2707
3246
  ) : null
2708
3247
  ] });
2709
3248
  };
2710
- var ORIGIN = new Vector310();
2711
- var UP = new Vector310(0, 0, 1);
2712
- var TARGET = new Vector310();
3249
+ var ORIGIN = new Vector311();
3250
+ var UP = new Vector311(0, 0, 1);
3251
+ var TARGET = new Vector311();
2713
3252
  function readTarget(controls) {
2714
3253
  if (controls && typeof controls.getTarget === "function") {
2715
3254
  return controls.getTarget(TARGET);
@@ -3368,6 +3907,7 @@ export {
3368
3907
  PERSPECTIVE_FOV,
3369
3908
  DEFAULT_FIT_MARGIN,
3370
3909
  EXCLUDE_FROM_FRAME,
3910
+ STOCK_OBJECT,
3371
3911
  defaultBounds,
3372
3912
  aspectRatio,
3373
3913
  screenLength,
@@ -3387,8 +3927,35 @@ export {
3387
3927
  adaptedUp,
3388
3928
  cadViewDirections,
3389
3929
  currentViewDirection,
3930
+ MEASURE_RENDER_ORDER,
3931
+ AXIS_COLORS,
3932
+ SNAP_PIXELS,
3933
+ SNAP_MARKER_PIXELS,
3934
+ MEASURE_MARKER_PIXELS,
3935
+ MEASURE_LINE_PIXELS,
3936
+ DELTA_DASH_PIXELS,
3937
+ ANGLE_ARC_FRACTION,
3938
+ ANGLE_ARC_SEGMENTS,
3939
+ snapAt,
3940
+ screenPoint,
3941
+ POINTS_PER_MEASUREMENT,
3942
+ measurementFromPoints,
3943
+ nextMeasurementId,
3944
+ distanceBetween,
3945
+ deltaBetween,
3946
+ midpoint,
3947
+ angleAt,
3948
+ lockToAxis,
3949
+ deltaLegs,
3950
+ angleArc,
3951
+ angleArcRadius,
3952
+ formatMillimetres,
3953
+ formatDegrees,
3954
+ measurementLabel,
3955
+ measurementLabelAnchor,
3390
3956
  SECTION_RENDER_ORDER,
3391
3957
  HANDLE_PIXELS,
3958
+ SECTION_GIZMO_FRAME_SCALE,
3392
3959
  CAP_HATCH,
3393
3960
  AXES_PLANE_OFFSET,
3394
3961
  AXES_PLANE_SCALE,
@@ -3396,6 +3963,7 @@ export {
3396
3963
  OUTLINE_SCALE,
3397
3964
  DEFAULT_SECTION_NORMAL,
3398
3965
  DISABLED_SECTION,
3966
+ sectionMeasurement,
3399
3967
  sectionOptionsFromState,
3400
3968
  axesPlanes,
3401
3969
  hitUnderRay,
@@ -3434,6 +4002,7 @@ export {
3434
4002
  viewDirection,
3435
4003
  buildPick,
3436
4004
  focusForPick,
4005
+ CONTROL_SCHEME_OPTIONS,
3437
4006
  ExtendedCameraControls,
3438
4007
  CadCameraControls,
3439
4008
  retargetPose,