@toolpath/viewer 0.4.0 → 1.0.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.
@@ -1,10 +1,10 @@
1
1
  // src/engine/engine-part.tsx
2
- import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
2
+ import { useEffect as useEffect6, useMemo as useMemo7 } from "react";
3
3
 
4
4
  // src/part-mesh.tsx
5
- import { useThree as useThree6 } from "@react-three/fiber";
6
- import { useCallback as useCallback3, useEffect as useEffect4, useLayoutEffect, useMemo as useMemo5, useRef as useRef5 } from "react";
7
- import { Vector3 as Vector39 } from "three";
5
+ import { useThree as useThree7 } from "@react-three/fiber";
6
+ import { useCallback as useCallback3, useEffect as useEffect5, useLayoutEffect, useMemo as useMemo6, useRef as useRef6 } from "react";
7
+ import { Vector3 as Vector310 } from "three";
8
8
 
9
9
  // src/model/directions.ts
10
10
  var EPSILON = 1e-6;
@@ -178,7 +178,7 @@ function applyHighlightLayers(part, layers, theme) {
178
178
  }
179
179
 
180
180
  // src/render/section.ts
181
- import { OrthographicCamera, Plane, Vector3 } from "three";
181
+ import { Plane, Vector3 } from "three";
182
182
  var SECTION_RENDER_ORDER = {
183
183
  stencil: 1,
184
184
  cap: 2,
@@ -247,12 +247,6 @@ function sectionPlane(box, normal, offset, into = new Plane()) {
247
247
  const axis = unit(normal);
248
248
  return into.set(axis, sectionConstant(sectionBounds(box, normal), offset));
249
249
  }
250
- function screenLength(camera, point, viewport, pixels) {
251
- const height = viewport.height > 0 ? viewport.height : 1;
252
- const zoom = camera.zoom || 1;
253
- const visible = camera instanceof OrthographicCamera ? (camera.top - camera.bottom) / zoom : 2 * Math.tan(camera.fov / 2 * Math.PI / 180) * Math.max(camera.position.distanceTo(point), EPSILON2) / zoom;
254
- return visible / height * pixels;
255
- }
256
250
  function dragPlane(axis, view, point, into = new Plane()) {
257
251
  const along = axis.lengthSq() === 0 ? ARROW_AXIS.clone() : axis.clone().normalize();
258
252
  const side = new Vector3().crossVectors(view, along);
@@ -266,7 +260,13 @@ function perpendicular(axis) {
266
260
 
267
261
  // src/tap.tsx
268
262
  import { useThree } from "@react-three/fiber";
269
- import { useEffect, useMemo, useRef } from "react";
263
+ import {
264
+ createContext,
265
+ useContext,
266
+ useEffect,
267
+ useMemo,
268
+ useRef
269
+ } from "react";
270
270
 
271
271
  // src/render/tap.ts
272
272
  var TAP_SLOP = 4;
@@ -294,23 +294,35 @@ function trackDoubleTaps(within = DOUBLE_TAP_MS, slop = TAP_SLOP) {
294
294
  const paired = last !== null && event.timeStamp - last.timeStamp <= within && !movedFar(last, event, slop);
295
295
  last = paired ? null : event;
296
296
  return paired;
297
+ },
298
+ reset: () => {
299
+ last = null;
297
300
  }
298
301
  };
299
302
  }
300
303
 
301
304
  // src/tap.tsx
305
+ import { jsx } from "react/jsx-runtime";
306
+ var ViewerTapContext = createContext(null);
307
+ var ViewerTapProvider = ({ value, children }) => /* @__PURE__ */ jsx(ViewerTapContext.Provider, { value, children });
302
308
  function useTapGuard() {
309
+ const shared = useContext(ViewerTapContext);
303
310
  const domElement = useThree((state) => state.gl.domElement);
304
311
  const tracker = useRef(null);
305
312
  useEffect(() => {
313
+ if (shared) return;
306
314
  const tracked = trackTaps(domElement);
307
315
  tracker.current = tracked;
308
316
  return () => {
309
317
  tracked.dispose();
310
318
  if (tracker.current === tracked) tracker.current = null;
311
319
  };
312
- }, [domElement]);
313
- return useMemo(() => (event) => tracker.current?.isTap(event) ?? true, []);
320
+ }, [domElement, shared]);
321
+ const own = useMemo(
322
+ () => (event) => tracker.current?.isTap(event) ?? true,
323
+ []
324
+ );
325
+ return shared ?? own;
314
326
  }
315
327
 
316
328
  // src/render/part.ts
@@ -687,7 +699,8 @@ function buildPick(input) {
687
699
  triangleIndex: input.triangleIndex,
688
700
  point: input.point,
689
701
  normal: input.normal,
690
- modifiers: input.modifiers ?? NO_MODIFIERS
702
+ modifiers: input.modifiers ?? NO_MODIFIERS,
703
+ doubled: input.doubled ?? false
691
704
  };
692
705
  }
693
706
  function focusForPick(pick, previousRegion, previousFocus) {
@@ -698,25 +711,25 @@ function focusForPick(pick, previousRegion, previousFocus) {
698
711
  }
699
712
 
700
713
  // src/viewer.tsx
701
- import { Canvas, useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
714
+ import { Canvas, useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
702
715
  import {
703
- createContext,
716
+ createContext as createContext2,
704
717
  forwardRef,
705
718
  useCallback,
706
- useContext,
707
- useEffect as useEffect3,
719
+ useContext as useContext2,
720
+ useEffect as useEffect4,
708
721
  useImperativeHandle,
709
- useMemo as useMemo3,
710
- useRef as useRef2
722
+ useMemo as useMemo4,
723
+ useRef as useRef3
711
724
  } from "react";
712
- import { Box3 as Box33, Vector3 as Vector36 } from "three";
725
+ import { Box3 as Box33, Vector3 as Vector37 } from "three";
713
726
 
714
727
  // src/camera.tsx
715
728
  import { useFrame, useThree as useThree2 } from "@react-three/fiber";
716
729
  import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
717
730
 
718
731
  // src/render/camera.ts
719
- import { OrthographicCamera as OrthographicCamera2, Vector3 as Vector33 } from "three";
732
+ import { OrthographicCamera, Vector3 as Vector33 } from "three";
720
733
  var PERSPECTIVE_FOV = 30;
721
734
  var DEFAULT_FIT_MARGIN = 1.2;
722
735
  var EXCLUDE_FROM_FRAME = "viewerExcludeFromFrame";
@@ -726,6 +739,13 @@ function defaultBounds() {
726
739
  function aspectRatio(size) {
727
740
  return size.width > 0 && size.height > 0 ? size.width / size.height : 1;
728
741
  }
742
+ var SCREEN_LENGTH_EPSILON = 1e-9;
743
+ function screenLength(camera, point, viewport, pixels) {
744
+ const height = viewport.height > 0 ? viewport.height : 1;
745
+ const zoom = camera.zoom || 1;
746
+ const visible = camera instanceof OrthographicCamera ? (camera.top - camera.bottom) / zoom : 2 * Math.tan(camera.fov / 2 * Math.PI / 180) * Math.max(camera.position.distanceTo(point), SCREEN_LENGTH_EPSILON) / zoom;
747
+ return visible / height * pixels;
748
+ }
729
749
  function boundsFromBox(box) {
730
750
  if (box.isEmpty()) return defaultBounds();
731
751
  const center = box.getCenter(new Vector33());
@@ -774,10 +794,53 @@ function startPosition(projection, size, bounds, margin = DEFAULT_FIT_MARGIN) {
774
794
  const [x, y, z] = START_DIRECTION[projection];
775
795
  return new Vector33(x, y, z).normalize().multiplyScalar(fitDistance(projection, size, bounds, margin)).add(bounds.center);
776
796
  }
797
+ var MIN_FRAME_RATIO = 0.25;
798
+ var MAX_FRAME_RATIO = 10;
799
+ var TARGET_BOUNDARY_RATIO = 4;
800
+ function cameraLimits(projection, size, bounds, margin = DEFAULT_FIT_MARGIN, framed) {
801
+ const fit = fitDistance(projection, size, bounds, margin);
802
+ const framedRadius = framed && framed.radius > 0 ? framed.radius : bounds.radius;
803
+ if (projection === "orthographic") {
804
+ const reach = bounds.radius > 0 ? bounds.radius / framedRadius : 1;
805
+ return {
806
+ minZoom: Math.min(MIN_FRAME_RATIO, MIN_FRAME_RATIO * reach),
807
+ maxZoom: Math.max(MAX_FRAME_RATIO, MAX_FRAME_RATIO * reach),
808
+ // Distance does not change apparent size under an orthographic camera, so
809
+ // these are not the zoom clamps in another form — they only keep the
810
+ // camera clear of the geometry it is looking at, and bounded.
811
+ minDistance: bounds.radius * margin,
812
+ maxDistance: fit / MIN_FRAME_RATIO
813
+ };
814
+ }
815
+ const framedFit = fitDistance(
816
+ projection,
817
+ size,
818
+ framedRadius === bounds.radius ? bounds : { ...bounds, radius: framedRadius },
819
+ margin
820
+ );
821
+ return {
822
+ // The wheel is `ACTION.DOLLY` under a perspective camera, so nothing here
823
+ // moves `camera.zoom`. They are set to the same pair anyway: a consumer
824
+ // that does zoom should not find the two cameras disagreeing about how far
825
+ // in is too far.
826
+ minZoom: MIN_FRAME_RATIO,
827
+ maxZoom: MAX_FRAME_RATIO,
828
+ minDistance: Math.min(fit, framedFit) / MAX_FRAME_RATIO,
829
+ maxDistance: Math.max(fit, framedFit) / MIN_FRAME_RATIO
830
+ };
831
+ }
832
+ var scratchBoundarySize = new Vector33();
833
+ function targetBoundary(bounds, into, margin = DEFAULT_FIT_MARGIN) {
834
+ const reach = bounds.radius * margin * TARGET_BOUNDARY_RATIO;
835
+ return into.setFromCenterAndSize(
836
+ bounds.center,
837
+ scratchBoundarySize.set(reach * 2, reach * 2, reach * 2)
838
+ );
839
+ }
777
840
  function applyProjection(camera, size, bounds, margin = DEFAULT_FIT_MARGIN) {
778
841
  const aspect = aspectRatio(size);
779
842
  const radius = bounds.radius * margin;
780
- if (camera instanceof OrthographicCamera2) {
843
+ if (camera instanceof OrthographicCamera) {
781
844
  const halfHeight = orthographicHalfHeight(aspect, radius);
782
845
  const halfWidth = halfHeight * aspect;
783
846
  camera.left = -halfWidth;
@@ -797,6 +860,11 @@ function applyProjection(camera, size, bounds, margin = DEFAULT_FIT_MARGIN) {
797
860
  camera.updateProjectionMatrix();
798
861
  }
799
862
  var CAD_CAMERA_UP = new Vector33(0, 0, 1);
863
+ var scratchSide = new Vector33();
864
+ function adaptedUp(view, up, into) {
865
+ const side = scratchSide.crossVectors(view, up).normalize();
866
+ return into.crossVectors(side, view).normalize();
867
+ }
800
868
  var cadViewDirections = {
801
869
  front: new Vector33(0, -1, 0),
802
870
  back: new Vector33(0, 1, 0),
@@ -817,7 +885,7 @@ import CameraControls from "camera-controls";
817
885
  import {
818
886
  Box3 as Box32,
819
887
  Matrix4,
820
- OrthographicCamera as OrthographicCamera3,
888
+ OrthographicCamera as OrthographicCamera2,
821
889
  Quaternion,
822
890
  Raycaster,
823
891
  Sphere,
@@ -844,6 +912,8 @@ var FUSION_PINCH_ZOOM_SCALE = 0.04;
844
912
  var FUSION_ROTATE_SCALE = 2.2;
845
913
  var FUSION_TRUCK_SCALE = 0.33;
846
914
  var DEFAULT_SMOOTH_TIME = 1e-3;
915
+ var DOLLY_SPEED = 1.15;
916
+ var REST_THRESHOLD = 5e-3;
847
917
  var ExtendedCameraControls = class extends CameraControls {
848
918
  #domElement;
849
919
  #freeOrbit;
@@ -858,13 +928,34 @@ var ExtendedCameraControls = class extends CameraControls {
858
928
  #scratchA = new Vector34();
859
929
  #scratchB = new Vector34();
860
930
  #scratchC = new Vector34();
861
- #scratchD = new Vector34();
862
931
  constructor(camera, domElement, options = {}) {
863
932
  super(camera, domElement);
864
933
  this.#domElement = domElement;
865
934
  this.#freeOrbit = options.freeOrbit ?? true;
866
935
  this.azimuthRotateSpeed = 0;
867
936
  this.polarRotateSpeed = 0;
937
+ this.dollySpeed = DOLLY_SPEED;
938
+ this.restThreshold = REST_THRESHOLD;
939
+ }
940
+ /**
941
+ * Bounds how far the wheel may travel, and where the orbit target may go.
942
+ *
943
+ * Re-applied whenever the scene is re-measured rather than set once: the
944
+ * limits are derived from the part's bounds, and a viewer that loads a second
945
+ * part would otherwise keep the first part's idea of far.
946
+ *
947
+ * The boundary confines the *target*. Zoom-to-cursor moves it, and goes on
948
+ * moving it after the zoom clamp bites — which is a runaway no zoom clamp can
949
+ * catch. `boundaryEnclosesCamera` stays off: under an orthographic camera the
950
+ * camera sits well outside the framing it is looking at, so enclosing it would
951
+ * drag the target back in on every frame.
952
+ */
953
+ applyLimits(limits, boundary) {
954
+ this.minZoom = limits.minZoom;
955
+ this.maxZoom = limits.maxZoom;
956
+ this.minDistance = limits.minDistance;
957
+ this.maxDistance = limits.maxDistance;
958
+ this.setBoundary(boundary);
868
959
  }
869
960
  get freeOrbit() {
870
961
  return this.#freeOrbit;
@@ -934,7 +1025,7 @@ var ExtendedCameraControls = class extends CameraControls {
934
1025
  this.mouseButtons.left = CameraControls.ACTION.ROTATE;
935
1026
  this.mouseButtons.right = CameraControls.ACTION.TRUCK;
936
1027
  this.mouseButtons.middle = CameraControls.ACTION.TRUCK;
937
- this.mouseButtons.wheel = this.camera instanceof OrthographicCamera3 ? CameraControls.ACTION.ZOOM : CameraControls.ACTION.DOLLY;
1028
+ this.mouseButtons.wheel = this.camera instanceof OrthographicCamera2 ? CameraControls.ACTION.ZOOM : CameraControls.ACTION.DOLLY;
938
1029
  }
939
1030
  setFreeOrbit(freeOrbit) {
940
1031
  if (this.#freeOrbit === freeOrbit) {
@@ -992,9 +1083,7 @@ var ExtendedCameraControls = class extends CameraControls {
992
1083
  #adaptUpVector = () => {
993
1084
  const target = this.getTarget(this.#scratchA);
994
1085
  const view = this.#scratchB.subVectors(target, this.camera.position).normalize();
995
- const side = this.#scratchC.crossVectors(view, this.camera.up).normalize();
996
- const up = this.#scratchD.crossVectors(side, view).normalize();
997
- this.camera.up.copy(up);
1086
+ this.camera.up.copy(adaptedUp(view, this.camera.up, this.#scratchC));
998
1087
  const position = this.getPosition(this.#scratchA);
999
1088
  this.updateCameraUp();
1000
1089
  void this.setPosition(position.x, position.y, position.z, false);
@@ -1110,13 +1199,173 @@ var CadCameraControls = ({
1110
1199
  return null;
1111
1200
  };
1112
1201
 
1202
+ // src/render/retarget.ts
1203
+ function retargetPose(cameraPosition, currentTarget, hitPoint) {
1204
+ const dx = hitPoint.x - currentTarget.x;
1205
+ const dy = hitPoint.y - currentTarget.y;
1206
+ const dz = hitPoint.z - currentTarget.z;
1207
+ return {
1208
+ position: {
1209
+ x: cameraPosition.x + dx,
1210
+ y: cameraPosition.y + dy,
1211
+ z: cameraPosition.z + dz
1212
+ },
1213
+ target: { x: hitPoint.x, y: hitPoint.y, z: hitPoint.z }
1214
+ };
1215
+ }
1216
+
1217
+ // src/target-marker.tsx
1218
+ import { useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
1219
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
1220
+ import {
1221
+ DoubleSide,
1222
+ RingGeometry,
1223
+ SphereGeometry,
1224
+ Vector3 as Vector35
1225
+ } from "three";
1226
+
1227
+ // src/render/target.ts
1228
+ var ORBIT_TARGET_PIXELS = 3;
1229
+ var ORBIT_TARGET_RING_PIXELS = 14;
1230
+ var ORBIT_TARGET_RING_WIDTH = 0.16;
1231
+ var ORBIT_TARGET_COLOR = 3948625;
1232
+ var ORBIT_TARGET_RING_COLOR = 7057587;
1233
+ var ORBIT_TARGET_RING_OPACITY = 0.9;
1234
+ var ORBIT_TARGET_FLASH_MS = 100;
1235
+ var ORBIT_TARGET_FADE_MS = 1e3;
1236
+ function orbitTargetOpacity(sinceHold, fade = ORBIT_TARGET_FADE_MS) {
1237
+ if (sinceHold <= 0) return 1;
1238
+ if (fade <= 0 || sinceHold >= fade) return 0;
1239
+ return 1 - sinceHold / fade;
1240
+ }
1241
+
1242
+ // src/target-marker.tsx
1243
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1244
+ var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
1245
+ var TargetMarker = ({ controlsRef }) => {
1246
+ const camera = useThree3((state) => state.camera);
1247
+ const size = useThree3((state) => state.size);
1248
+ const invalidate = useThree3((state) => state.invalidate);
1249
+ const registered = useThree3((state) => state.controls);
1250
+ const group = useRef2(null);
1251
+ const ring = useRef2(null);
1252
+ const dotMaterial = useRef2(null);
1253
+ const ringMaterial = useRef2(null);
1254
+ const dotGeometry = useMemo3(() => new SphereGeometry(1, 16, 12), []);
1255
+ const ringGeometry = useMemo3(() => new RingGeometry(1 - ORBIT_TARGET_RING_WIDTH, 1, 48), []);
1256
+ useEffect3(
1257
+ () => () => {
1258
+ dotGeometry.dispose();
1259
+ ringGeometry.dispose();
1260
+ },
1261
+ [dotGeometry, ringGeometry]
1262
+ );
1263
+ const dragging = useRef2(false);
1264
+ const heldUntil = useRef2(0);
1265
+ const painted = useRef2(-1);
1266
+ const placed = useRef2(false);
1267
+ const lastTarget = useMemo3(() => new Vector35(), []);
1268
+ const scratch = useMemo3(() => new Vector35(), []);
1269
+ useEffect3(() => {
1270
+ const controls = controlsRef.current;
1271
+ if (!controls) return void 0;
1272
+ const start = () => {
1273
+ dragging.current = true;
1274
+ invalidate();
1275
+ };
1276
+ const end = () => {
1277
+ dragging.current = false;
1278
+ heldUntil.current = performance.now();
1279
+ invalidate();
1280
+ };
1281
+ controls.addEventListener("controlstart", start);
1282
+ controls.addEventListener("controlend", end);
1283
+ return () => {
1284
+ controls.removeEventListener("controlstart", start);
1285
+ controls.removeEventListener("controlend", end);
1286
+ };
1287
+ }, [controlsRef, invalidate, registered]);
1288
+ useFrame2(() => {
1289
+ const controls = controlsRef.current;
1290
+ const node = group.current;
1291
+ if (!controls || !node) return;
1292
+ const now = performance.now();
1293
+ const target = controls.getTarget(scratch);
1294
+ if (!placed.current) {
1295
+ placed.current = true;
1296
+ lastTarget.copy(target);
1297
+ node.position.copy(target);
1298
+ } else if (!target.equals(lastTarget)) {
1299
+ lastTarget.copy(target);
1300
+ node.position.copy(target);
1301
+ heldUntil.current = Math.max(heldUntil.current, now + ORBIT_TARGET_FLASH_MS);
1302
+ }
1303
+ const opacity = dragging.current ? 1 : orbitTargetOpacity(now - heldUntil.current);
1304
+ if (opacity !== painted.current) {
1305
+ painted.current = opacity;
1306
+ node.visible = opacity > 0;
1307
+ if (dotMaterial.current) dotMaterial.current.opacity = opacity;
1308
+ if (ringMaterial.current) ringMaterial.current.opacity = opacity * ORBIT_TARGET_RING_OPACITY;
1309
+ invalidate();
1310
+ }
1311
+ if (node.visible) {
1312
+ node.scale.setScalar(screenLength(camera, target, size, 1));
1313
+ ring.current?.quaternion.copy(camera.quaternion);
1314
+ }
1315
+ });
1316
+ return /* @__PURE__ */ jsxs("group", { ref: group, visible: false, userData: FURNITURE, children: [
1317
+ /* @__PURE__ */ jsx2(
1318
+ "mesh",
1319
+ {
1320
+ ref: ring,
1321
+ geometry: ringGeometry,
1322
+ scale: ORBIT_TARGET_RING_PIXELS,
1323
+ renderOrder: 998,
1324
+ raycast: () => null,
1325
+ children: /* @__PURE__ */ jsx2(
1326
+ "meshBasicMaterial",
1327
+ {
1328
+ ref: ringMaterial,
1329
+ color: ORBIT_TARGET_RING_COLOR,
1330
+ transparent: true,
1331
+ opacity: 0,
1332
+ depthTest: false,
1333
+ depthWrite: false,
1334
+ side: DoubleSide
1335
+ }
1336
+ )
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsx2(
1340
+ "mesh",
1341
+ {
1342
+ geometry: dotGeometry,
1343
+ scale: ORBIT_TARGET_PIXELS,
1344
+ renderOrder: 999,
1345
+ raycast: () => null,
1346
+ children: /* @__PURE__ */ jsx2(
1347
+ "meshBasicMaterial",
1348
+ {
1349
+ ref: dotMaterial,
1350
+ color: ORBIT_TARGET_COLOR,
1351
+ transparent: true,
1352
+ opacity: 0,
1353
+ depthTest: false,
1354
+ depthWrite: false
1355
+ }
1356
+ )
1357
+ }
1358
+ )
1359
+ ] });
1360
+ };
1361
+
1113
1362
  // src/render/view-cube.ts
1114
1363
  import {
1115
1364
  BufferGeometry,
1116
1365
  CanvasTexture,
1117
1366
  Float32BufferAttribute as Float32BufferAttribute3,
1118
1367
  SRGBColorSpace,
1119
- Vector3 as Vector35
1368
+ Vector3 as Vector36
1120
1369
  } from "three";
1121
1370
  var VIEW_NAMES = [
1122
1371
  // The six faces.
@@ -1200,12 +1449,12 @@ function viewUp(direction) {
1200
1449
  return { x: 0, y: 0, z: 1 };
1201
1450
  }
1202
1451
  function squaredUp(direction, currentUp) {
1203
- const forward = new Vector35(direction.x, direction.y, direction.z).normalize();
1452
+ const forward = new Vector36(direction.x, direction.y, direction.z).normalize();
1204
1453
  const canonical = viewUp(direction);
1205
- const zero = new Vector35(canonical.x, canonical.y, canonical.z);
1454
+ const zero = new Vector36(canonical.x, canonical.y, canonical.z);
1206
1455
  zero.addScaledVector(forward, -forward.dot(zero)).normalize();
1207
- const quarter = new Vector35().crossVectors(forward, zero);
1208
- const current = new Vector35(currentUp.x, currentUp.y, currentUp.z);
1456
+ const quarter = new Vector36().crossVectors(forward, zero);
1457
+ const current = new Vector36(currentUp.x, currentUp.y, currentUp.z);
1209
1458
  const rolls = [zero, quarter, zero.clone().negate(), quarter.clone().negate()];
1210
1459
  const nearest = rolls.reduce(
1211
1460
  (best, roll) => roll.dot(current) > best.dot(current) ? roll : best
@@ -1223,11 +1472,11 @@ function cubeZones(chamfer = CHAMFER) {
1223
1472
  const direction = viewVector(name);
1224
1473
  const [sx, sy, sz] = VIEW_SIGNS[name];
1225
1474
  const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
1226
- const normal = new Vector35(direction.x, direction.y, direction.z);
1475
+ const normal = new Vector36(direction.x, direction.y, direction.z);
1227
1476
  let polygon;
1228
1477
  if (kind === "face") {
1229
1478
  const up = viewUp(direction);
1230
- const upVector = new Vector35(up.x, up.y, up.z);
1479
+ const upVector = new Vector36(up.x, up.y, up.z);
1231
1480
  const right = normal.clone().negate().cross(upVector).normalize();
1232
1481
  const square = [
1233
1482
  [-1, -1],
@@ -1249,7 +1498,7 @@ function cubeZones(chamfer = CHAMFER) {
1249
1498
  [chamfer, 1, -chamfer]
1250
1499
  ];
1251
1500
  polygon = rectangle.map(
1252
- ([onA, onB, onC]) => new Vector35().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
1501
+ ([onA, onB, onC]) => new Vector36().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
1253
1502
  );
1254
1503
  } else {
1255
1504
  const triangle = [
@@ -1257,11 +1506,11 @@ function cubeZones(chamfer = CHAMFER) {
1257
1506
  [chamfer, 1, chamfer],
1258
1507
  [chamfer, chamfer, 1]
1259
1508
  ];
1260
- polygon = triangle.map(([onX, onY, onZ]) => new Vector35(sx * onX, sy * onY, sz * onZ));
1509
+ polygon = triangle.map(([onX, onY, onZ]) => new Vector36(sx * onX, sy * onY, sz * onZ));
1261
1510
  }
1262
1511
  const [first, second, third] = polygon;
1263
1512
  if (first && second && third) {
1264
- const facing = new Vector35().subVectors(second, first).cross(new Vector35().subVectors(third, first));
1513
+ const facing = new Vector36().subVectors(second, first).cross(new Vector36().subVectors(third, first));
1265
1514
  if (facing.dot(normal) < 0) polygon.reverse();
1266
1515
  }
1267
1516
  return { name, kind, direction, polygon: polygon.map(vec) };
@@ -1351,13 +1600,15 @@ function labelTexture(label, color) {
1351
1600
  }
1352
1601
 
1353
1602
  // src/viewer.tsx
1354
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1355
- var ViewerControlsContext = createContext(null);
1603
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1604
+ var ViewerControlsContext = createContext2(null);
1356
1605
  var useViewerControls = () => {
1357
- const controls = useContext(ViewerControlsContext);
1606
+ const controls = useContext2(ViewerControlsContext);
1358
1607
  if (!controls) throw new Error("useViewerControls must be used inside <Viewer>");
1359
1608
  return controls;
1360
1609
  };
1610
+ var ViewerRetargetContext = createContext2(null);
1611
+ var useRetarget = () => useContext2(ViewerRetargetContext);
1361
1612
  var ViewerScene = ({
1362
1613
  children,
1363
1614
  setControls,
@@ -1366,20 +1617,36 @@ var ViewerScene = ({
1366
1617
  freeOrbit,
1367
1618
  zoomTo,
1368
1619
  recentreOnDoubleClick,
1620
+ retargetOnDoubleClick,
1621
+ showOrbitTarget,
1369
1622
  theme
1370
1623
  }) => {
1371
- const camera = useThree3((state) => state.camera);
1372
- const size = useThree3((state) => state.size);
1373
- const invalidate = useThree3((state) => state.invalidate);
1374
- const domElement = useThree3((state) => state.gl.domElement);
1375
- const controlsRef = useRef2(null);
1376
- const contentRef = useRef2(null);
1377
- const lightsRef = useRef2(null);
1378
- const initialFrameComplete = useRef2(false);
1379
- const boundsRef = useRef2(defaultBounds());
1380
- const scratchBox = useMemo3(() => new Box33(), []);
1381
- const scratchDirection = useMemo3(() => new Vector36(), []);
1382
- const scratchView = useMemo3(() => new Vector36(), []);
1624
+ const camera = useThree4((state) => state.camera);
1625
+ const size = useThree4((state) => state.size);
1626
+ const invalidate = useThree4((state) => state.invalidate);
1627
+ const domElement = useThree4((state) => state.gl.domElement);
1628
+ const isTap = useTapGuard();
1629
+ const controlsRef = useRef3(null);
1630
+ const contentRef = useRef3(null);
1631
+ const lightsRef = useRef3(null);
1632
+ const initialFrameComplete = useRef3(false);
1633
+ const boundsRef = useRef3(defaultBounds());
1634
+ const framedRef = useRef3(null);
1635
+ const scratchBox = useMemo4(() => new Box33(), []);
1636
+ const scratchBoundary = useMemo4(() => new Box33(), []);
1637
+ const scratchDirection = useMemo4(() => new Vector37(), []);
1638
+ const scratchView = useMemo4(() => new Vector37(), []);
1639
+ const scratchTarget = useMemo4(() => new Vector37(), []);
1640
+ const scratchPosition = useMemo4(() => new Vector37(), []);
1641
+ const applyLimits = useCallback(
1642
+ (bounds, framed) => {
1643
+ controlsRef.current?.applyLimits(
1644
+ cameraLimits(projection, size, bounds, DEFAULT_FIT_MARGIN, framed),
1645
+ targetBoundary(bounds, scratchBoundary, DEFAULT_FIT_MARGIN)
1646
+ );
1647
+ },
1648
+ [projection, scratchBoundary, size]
1649
+ );
1383
1650
  const measure = useCallback(() => {
1384
1651
  const content = contentRef.current;
1385
1652
  boundsRef.current = content ? contentBounds(content, scratchBox) : defaultBounds();
@@ -1408,17 +1675,19 @@ var ViewerScene = ({
1408
1675
  transition
1409
1676
  );
1410
1677
  if (projection === "orthographic") void controls.zoomTo(1, transition);
1678
+ framedRef.current = null;
1679
+ applyLimits(bounds);
1411
1680
  invalidate();
1412
1681
  return true;
1413
1682
  },
1414
- [camera, invalidate, measure, projection, scratchDirection, size]
1683
+ [applyLimits, camera, invalidate, measure, projection, scratchDirection, size]
1415
1684
  );
1416
1685
  const frameBounds = useCallback(
1417
1686
  (bounds) => {
1418
1687
  const controls = controlsRef.current;
1419
1688
  if (!controls) return false;
1420
- const target = controls.getTarget(new Vector36());
1421
- const direction = currentViewDirection(camera, target, new Vector36());
1689
+ const target = controls.getTarget(new Vector37());
1690
+ const direction = currentViewDirection(camera, target, new Vector37());
1422
1691
  const distance = fitDistance(projection, size, bounds, DEFAULT_FIT_MARGIN);
1423
1692
  const position = direction.multiplyScalar(distance).add(bounds.center);
1424
1693
  void controls.setLookAt(
@@ -1430,36 +1699,65 @@ var ViewerScene = ({
1430
1699
  bounds.center.z,
1431
1700
  true
1432
1701
  );
1702
+ framedRef.current = bounds;
1703
+ applyLimits(boundsRef.current, bounds);
1433
1704
  if (projection === "orthographic") {
1434
1705
  void controls.zoomTo(boundsRef.current.radius / bounds.radius, true);
1435
1706
  }
1436
1707
  invalidate();
1437
1708
  return true;
1438
1709
  },
1439
- [camera, invalidate, projection, size]
1710
+ [applyLimits, camera, invalidate, projection, size]
1440
1711
  );
1441
1712
  const fitContent = useCallback(() => {
1442
1713
  const controls = controlsRef.current;
1443
1714
  if (!controls) return false;
1444
- const target = controls.getTarget(new Vector36());
1445
- return frame(currentViewDirection(camera, target, new Vector36()), true);
1715
+ const target = controls.getTarget(new Vector37());
1716
+ return frame(currentViewDirection(camera, target, new Vector37()), true);
1446
1717
  }, [camera, frame]);
1447
1718
  const resetContent = useCallback(() => {
1448
1719
  const bounds = measure();
1449
1720
  const start = startPosition(projection, size, bounds).sub(bounds.center);
1450
- return frame(start, true);
1721
+ return frame(start, true, CAD_CAMERA_UP);
1451
1722
  }, [frame, measure, projection, size]);
1452
- useEffect3(() => {
1723
+ const retarget = useCallback(
1724
+ (point) => {
1725
+ const controls = controlsRef.current;
1726
+ if (!controls) return;
1727
+ const pose = retargetPose(
1728
+ controls.getPosition(scratchPosition),
1729
+ controls.getTarget(scratchTarget),
1730
+ point
1731
+ );
1732
+ void controls.setLookAt(
1733
+ pose.position.x,
1734
+ pose.position.y,
1735
+ pose.position.z,
1736
+ pose.target.x,
1737
+ pose.target.y,
1738
+ pose.target.z,
1739
+ true
1740
+ );
1741
+ invalidate();
1742
+ },
1743
+ [invalidate, scratchPosition, scratchTarget]
1744
+ );
1745
+ const retargetOn = useMemo4(
1746
+ () => retargetOnDoubleClick ? retarget : null,
1747
+ [retarget, retargetOnDoubleClick]
1748
+ );
1749
+ useEffect4(() => {
1453
1750
  if (!recentreOnDoubleClick) return void 0;
1454
1751
  const doubles = trackDoubleTaps();
1455
1752
  const recentre = (event) => {
1456
1753
  if (event.button !== 1) return;
1754
+ if (!isTap(event)) return;
1457
1755
  if (doubles.isDouble(event)) fitContent();
1458
1756
  };
1459
1757
  domElement.addEventListener("auxclick", recentre);
1460
1758
  return () => domElement.removeEventListener("auxclick", recentre);
1461
- }, [domElement, fitContent, recentreOnDoubleClick]);
1462
- useEffect3(() => {
1759
+ }, [domElement, fitContent, isTap, recentreOnDoubleClick]);
1760
+ useEffect4(() => {
1463
1761
  setControls({
1464
1762
  fit: () => {
1465
1763
  fitContent();
@@ -1482,37 +1780,38 @@ var ViewerScene = ({
1482
1780
  }
1483
1781
  });
1484
1782
  }, [camera, fitContent, frame, frameBounds, resetContent, scratchView, setControls]);
1485
- useEffect3(() => {
1783
+ useEffect4(() => {
1486
1784
  if (initialFrameComplete.current) resetContent();
1487
1785
  }, [projection]);
1488
- useEffect3(() => {
1786
+ useEffect4(() => {
1489
1787
  applyProjection(camera, size, boundsRef.current, DEFAULT_FIT_MARGIN);
1788
+ if (initialFrameComplete.current) applyLimits(boundsRef.current, framedRef.current ?? void 0);
1490
1789
  invalidate();
1491
- }, [camera, invalidate, size]);
1492
- useFrame2(() => {
1790
+ }, [applyLimits, camera, invalidate, size]);
1791
+ useFrame3(() => {
1493
1792
  const lights = lightsRef.current;
1494
1793
  if (lights && !lights.quaternion.equals(camera.quaternion)) {
1495
1794
  lights.quaternion.copy(camera.quaternion);
1496
1795
  invalidate();
1497
1796
  }
1498
1797
  });
1499
- useFrame2(() => {
1798
+ useFrame3(() => {
1500
1799
  if (!initialFrameComplete.current && contentRef.current?.children.length) {
1501
1800
  initialFrameComplete.current = resetContent();
1502
1801
  }
1503
1802
  });
1504
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1505
- /* @__PURE__ */ jsxs("group", { ref: lightsRef, children: [
1506
- /* @__PURE__ */ jsx("ambientLight", { color: theme.ambient, intensity: theme.ambientIntensity }),
1507
- /* @__PURE__ */ jsx(
1803
+ return /* @__PURE__ */ jsx3(ViewerTapProvider, { value: isTap, children: /* @__PURE__ */ jsxs2(ViewerRetargetContext.Provider, { value: retargetOn, children: [
1804
+ /* @__PURE__ */ jsxs2("group", { ref: lightsRef, children: [
1805
+ /* @__PURE__ */ jsx3("ambientLight", { color: theme.ambient, intensity: theme.ambientIntensity }),
1806
+ /* @__PURE__ */ jsx3(
1508
1807
  "hemisphereLight",
1509
1808
  {
1510
1809
  args: [theme.hemisphereSky, theme.hemisphereGround, theme.hemisphereIntensity]
1511
1810
  }
1512
1811
  )
1513
1812
  ] }),
1514
- /* @__PURE__ */ jsx("group", { ref: contentRef, children }),
1515
- /* @__PURE__ */ jsx(
1813
+ /* @__PURE__ */ jsx3("group", { ref: contentRef, children }),
1814
+ /* @__PURE__ */ jsx3(
1516
1815
  CadCameraControls,
1517
1816
  {
1518
1817
  controlsRef,
@@ -1520,24 +1819,27 @@ var ViewerScene = ({
1520
1819
  freeOrbit,
1521
1820
  zoomTo
1522
1821
  }
1523
- )
1524
- ] });
1822
+ ),
1823
+ showOrbitTarget ? /* @__PURE__ */ jsx3(TargetMarker, { controlsRef }) : null
1824
+ ] }) });
1525
1825
  };
1526
1826
  var Viewer = forwardRef(function Viewer2({
1527
1827
  children,
1528
1828
  className,
1529
1829
  style,
1530
- projection = "perspective",
1830
+ projection = "orthographic",
1531
1831
  controls = "toolpath",
1532
1832
  freeOrbit = true,
1533
1833
  zoomTo = "cursor",
1534
1834
  recentreOnDoubleClick = true,
1835
+ retargetOnDoubleClick = true,
1836
+ showOrbitTarget = false,
1535
1837
  theme,
1536
1838
  onPointerMissed
1537
1839
  }, ref) {
1538
- const actionsRef = useRef2(null);
1539
- const resolved = useMemo3(() => resolveTheme(theme), [theme]);
1540
- const proxy = useMemo3(
1840
+ const actionsRef = useRef3(null);
1841
+ const resolved = useMemo4(() => resolveTheme(theme), [theme]);
1842
+ const proxy = useMemo4(
1541
1843
  () => ({
1542
1844
  fit: () => actionsRef.current?.fit(),
1543
1845
  reset: () => actionsRef.current?.reset(),
@@ -1551,12 +1853,12 @@ var Viewer = forwardRef(function Viewer2({
1551
1853
  const setControls = useCallback((next) => {
1552
1854
  actionsRef.current = next;
1553
1855
  }, []);
1554
- const tracker = useRef2(null);
1856
+ const tracker = useRef3(null);
1555
1857
  const hold = useCallback((element) => {
1556
1858
  tracker.current?.dispose();
1557
1859
  tracker.current = element ? trackTaps(element) : null;
1558
1860
  }, []);
1559
- return /* @__PURE__ */ jsx(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx(
1861
+ return /* @__PURE__ */ jsx3(ViewerControlsContext.Provider, { value: proxy, children: /* @__PURE__ */ jsx3(
1560
1862
  "div",
1561
1863
  {
1562
1864
  className,
@@ -1565,7 +1867,7 @@ var Viewer = forwardRef(function Viewer2({
1565
1867
  if (event.target instanceof HTMLCanvasElement) event.preventDefault();
1566
1868
  },
1567
1869
  style: { height: "100%", width: "100%", ...style },
1568
- children: /* @__PURE__ */ jsx(
1870
+ children: /* @__PURE__ */ jsx3(
1569
1871
  Canvas,
1570
1872
  {
1571
1873
  orthographic: projection === "orthographic",
@@ -1577,7 +1879,7 @@ var Viewer = forwardRef(function Viewer2({
1577
1879
  if (event.button !== 0) return;
1578
1880
  if (tracker.current?.isTap(event) ?? true) onPointerMissed?.();
1579
1881
  },
1580
- children: /* @__PURE__ */ jsx(
1882
+ children: /* @__PURE__ */ jsx3(
1581
1883
  ViewerScene,
1582
1884
  {
1583
1885
  setControls,
@@ -1586,6 +1888,8 @@ var Viewer = forwardRef(function Viewer2({
1586
1888
  freeOrbit,
1587
1889
  zoomTo,
1588
1890
  recentreOnDoubleClick,
1891
+ retargetOnDoubleClick,
1892
+ showOrbitTarget,
1589
1893
  theme: resolved,
1590
1894
  children
1591
1895
  }
@@ -1598,13 +1902,13 @@ var Viewer = forwardRef(function Viewer2({
1598
1902
  });
1599
1903
 
1600
1904
  // src/section-view.tsx
1601
- import { useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
1602
- import { useCallback as useCallback2, useMemo as useMemo4, useRef as useRef3, useState } from "react";
1905
+ import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
1906
+ import { useCallback as useCallback2, useMemo as useMemo5, useRef as useRef4, useState } from "react";
1603
1907
  import {
1604
1908
  AlwaysStencilFunc,
1605
1909
  BackSide,
1606
1910
  DecrementWrapStencilOp,
1607
- DoubleSide,
1911
+ DoubleSide as DoubleSide2,
1608
1912
  FrontSide,
1609
1913
  IncrementWrapStencilOp,
1610
1914
  NotEqualStencilFunc,
@@ -1613,22 +1917,22 @@ import {
1613
1917
  Raycaster as Raycaster2,
1614
1918
  ReplaceStencilOp,
1615
1919
  Vector2 as Vector22,
1616
- Vector3 as Vector38
1920
+ Vector3 as Vector39
1617
1921
  } from "three";
1618
1922
 
1619
1923
  // src/render/directions.ts
1620
- import { Vector3 as Vector37 } from "three";
1621
- var CONE_AXIS = new Vector37(0, 1, 0);
1924
+ import { Vector3 as Vector38 } from "three";
1925
+ var CONE_AXIS = new Vector38(0, 1, 0);
1622
1926
  var LENGTH = 0.45;
1623
1927
  var HEAD = 0.45;
1624
1928
  var HEAD_RADIUS = 0.3;
1625
1929
  var SHAFT_RADIUS = 0.09;
1626
1930
  var GAP = 0.2;
1627
1931
  function arrowPlacement(direction, box) {
1628
- const center = box.getCenter(new Vector37());
1629
- const half = box.getSize(new Vector37()).multiplyScalar(0.5);
1932
+ const center = box.getCenter(new Vector38());
1933
+ const half = box.getSize(new Vector38()).multiplyScalar(0.5);
1630
1934
  const radius = half.length() || 1;
1631
- const axis = new Vector37(direction.x, direction.y, direction.z);
1935
+ const axis = new Vector38(direction.x, direction.y, direction.z);
1632
1936
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
1633
1937
  axis.normalize();
1634
1938
  let exit = Number.POSITIVE_INFINITY;
@@ -1644,13 +1948,13 @@ function arrowPlacement(direction, box) {
1644
1948
  }
1645
1949
 
1646
1950
  // src/section-view.tsx
1647
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1951
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1648
1952
  var DEFAULT_NORMAL = { x: 0, y: 0, z: 1 };
1649
- var FURNITURE = { [EXCLUDE_FROM_FRAME]: true };
1953
+ var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
1650
1954
  function resolveSectionPlane(options, box) {
1651
1955
  if (!options?.enabled || box.isEmpty()) return null;
1652
1956
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_NORMAL;
1653
- const axis = new Vector38(normal.x, normal.y, normal.z);
1957
+ const axis = new Vector39(normal.x, normal.y, normal.z);
1654
1958
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
1655
1959
  axis.normalize();
1656
1960
  const bounds = sectionBounds(box, normal);
@@ -1678,31 +1982,31 @@ var SectionView = ({
1678
1982
  showHandle,
1679
1983
  onDrag
1680
1984
  }) => {
1681
- const camera = useThree4((state) => state.camera);
1682
- const size = useThree4((state) => state.size);
1683
- const invalidate = useThree4((state) => state.invalidate);
1684
- const controls = useThree4((state) => state.controls);
1685
- const domElement = useThree4((state) => state.gl.domElement);
1686
- const capRef = useRef3(null);
1687
- const handleRef = useRef3(null);
1985
+ const camera = useThree5((state) => state.camera);
1986
+ const size = useThree5((state) => state.size);
1987
+ const invalidate = useThree5((state) => state.invalidate);
1988
+ const controls = useThree5((state) => state.controls);
1989
+ const domElement = useThree5((state) => state.gl.domElement);
1990
+ const capRef = useRef4(null);
1991
+ const handleRef = useRef4(null);
1688
1992
  const [hovered, setHovered] = useState(false);
1689
- const dragging = useRef3(null);
1690
- const centre = useMemo4(() => box.getCenter(new Vector38()), [box]);
1691
- const span = useMemo4(() => box.getSize(new Vector38()).length(), [box]);
1692
- const clip = useMemo4(() => [plane], [plane]);
1693
- const capPosition = useMemo4(() => {
1993
+ const dragging = useRef4(null);
1994
+ const centre = useMemo5(() => box.getCenter(new Vector39()), [box]);
1995
+ const span = useMemo5(() => box.getSize(new Vector39()).length(), [box]);
1996
+ const clip = useMemo5(() => [plane], [plane]);
1997
+ const capPosition = useMemo5(() => {
1694
1998
  const point = centre.clone();
1695
1999
  return point.addScaledVector(plane.normal, -(plane.constant + plane.normal.dot(centre)));
1696
2000
  }, [centre, plane]);
1697
- const capQuaternion = useMemo4(
1698
- () => new Quaternion2().setFromUnitVectors(new Vector38(0, 0, 1), plane.normal),
2001
+ const capQuaternion = useMemo5(
2002
+ () => new Quaternion2().setFromUnitVectors(new Vector39(0, 0, 1), plane.normal),
1699
2003
  [plane]
1700
2004
  );
1701
- const handleQuaternion = useMemo4(
2005
+ const handleQuaternion = useMemo5(
1702
2006
  () => new Quaternion2().setFromUnitVectors(CONE_AXIS, plane.normal.clone().negate()),
1703
2007
  [plane]
1704
2008
  );
1705
- useFrame3(() => {
2009
+ useFrame4(() => {
1706
2010
  const handle = handleRef.current;
1707
2011
  if (!handle) return;
1708
2012
  const length = screenLength(camera, capPosition, size, HANDLE_PIXELS);
@@ -1723,7 +2027,7 @@ var SectionView = ({
1723
2027
  event.stopPropagation();
1724
2028
  const view = camera.position.clone().sub(capPosition).normalize();
1725
2029
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
1726
- const hit = event.ray.intersectPlane(surface, new Vector38());
2030
+ const hit = event.ray.intersectPlane(surface, new Vector39());
1727
2031
  const start = {
1728
2032
  plane: surface,
1729
2033
  from: hit ? hit.dot(plane.normal) : 0,
@@ -1740,7 +2044,7 @@ var SectionView = ({
1740
2044
  -((native.clientY - rect.top) / rect.height) * 2 + 1
1741
2045
  );
1742
2046
  raycaster.setFromCamera(pointer, camera);
1743
- const point = raycaster.ray.intersectPlane(start.plane, new Vector38());
2047
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector39());
1744
2048
  if (!point) return;
1745
2049
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
1746
2050
  invalidate();
@@ -1772,8 +2076,8 @@ var SectionView = ({
1772
2076
  // Excluded from framing, all of it. The cap is a quad half again as wide as
1773
2077
  // the part's diagonal, so a Fit that measured it would frame the cut rather
1774
2078
  // than the part and leave the part a speck in the middle of it.
1775
- /* @__PURE__ */ jsxs2("group", { userData: FURNITURE, children: [
1776
- /* @__PURE__ */ jsx2("mesh", { geometry, renderOrder: SECTION_RENDER_ORDER.stencil, raycast: () => null, children: /* @__PURE__ */ jsx2(
2079
+ /* @__PURE__ */ jsxs3("group", { userData: FURNITURE2, children: [
2080
+ /* @__PURE__ */ jsx4("mesh", { geometry, renderOrder: SECTION_RENDER_ORDER.stencil, raycast: () => null, children: /* @__PURE__ */ jsx4(
1777
2081
  "meshBasicMaterial",
1778
2082
  {
1779
2083
  side: BackSide,
@@ -1788,7 +2092,7 @@ var SectionView = ({
1788
2092
  clippingPlanes: clip
1789
2093
  }
1790
2094
  ) }),
1791
- /* @__PURE__ */ jsx2("mesh", { geometry, renderOrder: SECTION_RENDER_ORDER.stencil, raycast: () => null, children: /* @__PURE__ */ jsx2(
2095
+ /* @__PURE__ */ jsx4("mesh", { geometry, renderOrder: SECTION_RENDER_ORDER.stencil, raycast: () => null, children: /* @__PURE__ */ jsx4(
1792
2096
  "meshBasicMaterial",
1793
2097
  {
1794
2098
  side: FrontSide,
@@ -1803,13 +2107,13 @@ var SectionView = ({
1803
2107
  clippingPlanes: clip
1804
2108
  }
1805
2109
  ) }),
1806
- /* @__PURE__ */ jsx2("group", { ref: capRef, position: capPosition, quaternion: capQuaternion, children: /* @__PURE__ */ jsxs2("mesh", { renderOrder: SECTION_RENDER_ORDER.cap, raycast: () => null, children: [
1807
- /* @__PURE__ */ jsx2("planeGeometry", { args: [span * 1.5, span * 1.5] }),
1808
- /* @__PURE__ */ jsx2(
2110
+ /* @__PURE__ */ jsx4("group", { ref: capRef, position: capPosition, quaternion: capQuaternion, children: /* @__PURE__ */ jsxs3("mesh", { renderOrder: SECTION_RENDER_ORDER.cap, raycast: () => null, children: [
2111
+ /* @__PURE__ */ jsx4("planeGeometry", { args: [span * 1.5, span * 1.5] }),
2112
+ /* @__PURE__ */ jsx4(
1809
2113
  "meshBasicMaterial",
1810
2114
  {
1811
2115
  color: theme.sectionCap,
1812
- side: DoubleSide,
2116
+ side: DoubleSide2,
1813
2117
  stencilWrite: true,
1814
2118
  stencilRef: 0,
1815
2119
  stencilFunc: NotEqualStencilFunc,
@@ -1819,7 +2123,7 @@ var SectionView = ({
1819
2123
  }
1820
2124
  )
1821
2125
  ] }) }),
1822
- showHandle && onDrag ? /* @__PURE__ */ jsxs2(
2126
+ showHandle && onDrag ? /* @__PURE__ */ jsxs3(
1823
2127
  "group",
1824
2128
  {
1825
2129
  ref: handleRef,
@@ -1827,9 +2131,9 @@ var SectionView = ({
1827
2131
  quaternion: handleQuaternion,
1828
2132
  renderOrder: SECTION_RENDER_ORDER.handle,
1829
2133
  children: [
1830
- /* @__PURE__ */ jsxs2("mesh", { position: [0, 0.28, 0], ...grab, children: [
1831
- /* @__PURE__ */ jsx2("coneGeometry", { args: [0.16, 0.34, 20] }),
1832
- /* @__PURE__ */ jsx2(
2134
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.28, 0], ...grab, children: [
2135
+ /* @__PURE__ */ jsx4("coneGeometry", { args: [0.16, 0.34, 20] }),
2136
+ /* @__PURE__ */ jsx4(
1833
2137
  "meshBasicMaterial",
1834
2138
  {
1835
2139
  color: hovered ? theme.hover : theme.sectionHandle,
@@ -1837,9 +2141,9 @@ var SectionView = ({
1837
2141
  }
1838
2142
  )
1839
2143
  ] }),
1840
- /* @__PURE__ */ jsxs2("mesh", { position: [0, 0.08, 0], ...grab, children: [
1841
- /* @__PURE__ */ jsx2("cylinderGeometry", { args: [0.045, 0.045, 0.4, 12] }),
1842
- /* @__PURE__ */ jsx2(
2144
+ /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.08, 0], ...grab, children: [
2145
+ /* @__PURE__ */ jsx4("cylinderGeometry", { args: [0.045, 0.045, 0.4, 12] }),
2146
+ /* @__PURE__ */ jsx4(
1843
2147
  "meshBasicMaterial",
1844
2148
  {
1845
2149
  color: hovered ? theme.hover : theme.sectionHandle,
@@ -1855,14 +2159,14 @@ var SectionView = ({
1855
2159
  };
1856
2160
 
1857
2161
  // src/content-box.ts
1858
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
1859
- import { useRef as useRef4, useState as useState2 } from "react";
2162
+ import { useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
2163
+ import { useRef as useRef5, useState as useState2 } from "react";
1860
2164
  import { Box3 as Box34 } from "three";
1861
2165
  function useContentBox() {
1862
- const scene = useThree5((state) => state.scene);
2166
+ const scene = useThree6((state) => state.scene);
1863
2167
  const [box, setBox] = useState2(() => new Box34());
1864
- const measured = useRef4(false);
1865
- useFrame4(() => {
2168
+ const measured = useRef5(false);
2169
+ useFrame5(() => {
1866
2170
  if (measured.current) return;
1867
2171
  const next = new Box34();
1868
2172
  contentBounds(scene, next);
@@ -1874,7 +2178,7 @@ function useContentBox() {
1874
2178
  }
1875
2179
 
1876
2180
  // src/part-mesh.tsx
1877
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2181
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1878
2182
  var PartMesh = ({
1879
2183
  model,
1880
2184
  geometry,
@@ -1894,16 +2198,16 @@ var PartMesh = ({
1894
2198
  theme,
1895
2199
  showEdges = true
1896
2200
  }) => {
1897
- const { camera, controls, invalidate } = useThree6();
2201
+ const { camera, controls, invalidate } = useThree7();
1898
2202
  const viewerControls = useViewerControls();
1899
2203
  const resolved = useStableTheme(theme);
1900
- const currentTheme = useRef5(resolved);
2204
+ const currentTheme = useRef6(resolved);
1901
2205
  currentTheme.current = resolved;
1902
- const part = useMemo5(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
1903
- const hoverRegion = useRef5(null);
2206
+ const part = useMemo6(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
2207
+ const hoverRegion = useRef6(null);
1904
2208
  const box = useContentBox();
1905
- const cut = useMemo5(() => resolveSectionPlane(section, box), [box, section]);
1906
- const layers = useRef5({
2209
+ const cut = useMemo6(() => resolveSectionPlane(section, box), [box, section]);
2210
+ const layers = useRef6({
1907
2211
  selection,
1908
2212
  candidates,
1909
2213
  highlights,
@@ -1928,13 +2232,13 @@ var PartMesh = ({
1928
2232
  );
1929
2233
  invalidate();
1930
2234
  }, [invalidate, part]);
1931
- useEffect4(() => () => part.dispose(), [part]);
1932
- const adjacency = useMemo5(() => regionAdjacency(model, geometry), [model, geometry]);
1933
- useEffect4(() => {
2235
+ useEffect5(() => () => part.dispose(), [part]);
2236
+ const adjacency = useMemo6(() => regionAdjacency(model, geometry), [model, geometry]);
2237
+ useEffect5(() => {
1934
2238
  onAdjacency?.(adjacency);
1935
2239
  }, [adjacency, onAdjacency]);
1936
- const framed = useRef5(null);
1937
- useEffect4(() => {
2240
+ const framed = useRef6(null);
2241
+ useEffect5(() => {
1938
2242
  if (focusFeature === null || focusFeature === framed.current) return;
1939
2243
  framed.current = focusFeature;
1940
2244
  const box2 = part.boxForFeature(focusFeature);
@@ -1952,7 +2256,7 @@ var PartMesh = ({
1952
2256
  part.setClippingPlanes(cut ? [cut.plane] : null);
1953
2257
  invalidate();
1954
2258
  }, [cut, invalidate, part]);
1955
- const reportedSection = useRef5("");
2259
+ const reportedSection = useRef6("");
1956
2260
  useLayoutEffect(() => {
1957
2261
  const state = cut?.state ?? null;
1958
2262
  const key = state ? `${state.constant}|${state.normal.x},${state.normal.y},${state.normal.z}` : "";
@@ -1972,7 +2276,9 @@ var PartMesh = ({
1972
2276
  repaint();
1973
2277
  }, [layerKey, repaint]);
1974
2278
  const isTap = useTapGuard();
1975
- const pickFor = (event) => {
2279
+ const retarget = useRetarget();
2280
+ const doubles = useMemo6(() => trackDoubleTaps(), []);
2281
+ const pickFor = (event, doubled = false) => {
1976
2282
  const triangleIndex = event.faceIndex;
1977
2283
  if (triangleIndex == null) return null;
1978
2284
  const region = model.regionIndex.regionForTriangle(triangleIndex);
@@ -1987,6 +2293,7 @@ var PartMesh = ({
1987
2293
  point: [event.point.x, event.point.y, event.point.z],
1988
2294
  normal: [normal.x, normal.y, normal.z],
1989
2295
  activeDirection,
2296
+ doubled,
1990
2297
  viewDirection: viewDirection(camera, target),
1991
2298
  modifiers: {
1992
2299
  alt: source.altKey,
@@ -2017,16 +2324,24 @@ var PartMesh = ({
2017
2324
  },
2018
2325
  [box, cut, onSectionChange, section]
2019
2326
  );
2020
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
2021
- /* @__PURE__ */ jsx3(
2327
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
2328
+ /* @__PURE__ */ jsx5(
2022
2329
  "primitive",
2023
2330
  {
2024
2331
  object: part.object,
2025
2332
  onPointerMove: (event) => emitHover(pickFor(event)),
2026
- onPointerOut: () => emitHover(null),
2333
+ onPointerOut: () => {
2334
+ emitHover(null);
2335
+ doubles.reset();
2336
+ },
2027
2337
  onClick: (event) => {
2028
- if (!isTap(event.nativeEvent)) return;
2029
- const pick = pickFor(event);
2338
+ if (!isTap(event.nativeEvent)) {
2339
+ doubles.reset();
2340
+ return;
2341
+ }
2342
+ const doubled = doubles.isDouble(event.nativeEvent);
2343
+ const pick = pickFor(event, doubled);
2344
+ if (doubled && retarget) retarget(event.point);
2030
2345
  if (pick) onPick?.(pick);
2031
2346
  },
2032
2347
  onPointerUp: (event) => {
@@ -2040,7 +2355,7 @@ var PartMesh = ({
2040
2355
  }
2041
2356
  }
2042
2357
  ),
2043
- cut ? /* @__PURE__ */ jsx3(
2358
+ cut ? /* @__PURE__ */ jsx5(
2044
2359
  SectionView,
2045
2360
  {
2046
2361
  geometry,
@@ -2053,9 +2368,9 @@ var PartMesh = ({
2053
2368
  ) : null
2054
2369
  ] });
2055
2370
  };
2056
- var ORIGIN = new Vector39();
2057
- var UP = new Vector39(0, 0, 1);
2058
- var TARGET = new Vector39();
2371
+ var ORIGIN = new Vector310();
2372
+ var UP = new Vector310(0, 0, 1);
2373
+ var TARGET = new Vector310();
2059
2374
  function readTarget(controls) {
2060
2375
  if (controls && typeof controls.getTarget === "function") {
2061
2376
  return controls.getTarget(TARGET);
@@ -2064,14 +2379,14 @@ function readTarget(controls) {
2064
2379
  return target ?? ORIGIN;
2065
2380
  }
2066
2381
  function useStableTheme(theme) {
2067
- const held = useRef5(null);
2382
+ const held = useRef6(null);
2068
2383
  const next = resolveTheme(theme);
2069
2384
  if (held.current === null || !themesEqual(held.current, next)) held.current = next;
2070
2385
  return held.current;
2071
2386
  }
2072
2387
 
2073
2388
  // src/engine/geometry.ts
2074
- import { Matrix4 as Matrix42, Mesh as Mesh2 } from "three";
2389
+ import { Matrix4 as Matrix42, Mesh as Mesh3 } from "three";
2075
2390
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
2076
2391
  import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
2077
2392
  var PartMeshError = class extends Error {
@@ -2146,7 +2461,7 @@ function parseStl(data, mesh) {
2146
2461
  function singleMesh(scene) {
2147
2462
  const meshes = [];
2148
2463
  scene.traverse((object) => {
2149
- if (object instanceof Mesh2) meshes.push(object);
2464
+ if (object instanceof Mesh3) meshes.push(object);
2150
2465
  });
2151
2466
  const [mesh] = meshes;
2152
2467
  if (mesh === void 0 || meshes.length !== 1) {
@@ -2182,7 +2497,7 @@ function assertCounts(geometry, expected) {
2182
2497
  }
2183
2498
  function disposeMaterials(scene) {
2184
2499
  scene.traverse((object) => {
2185
- if (!(object instanceof Mesh2)) return;
2500
+ if (!(object instanceof Mesh3)) return;
2186
2501
  for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
2187
2502
  material.dispose();
2188
2503
  }
@@ -2679,15 +2994,15 @@ function compareVersions(a, b) {
2679
2994
  }
2680
2995
 
2681
2996
  // src/engine/engine-part.tsx
2682
- import { jsx as jsx4 } from "react/jsx-runtime";
2997
+ import { jsx as jsx6 } from "react/jsx-runtime";
2683
2998
  var EnginePart = ({ report, ...props }) => {
2684
- const model = useMemo6(() => normalizePartReport(report), [report]);
2999
+ const model = useMemo7(() => normalizePartReport(report), [report]);
2685
3000
  const resource = useGeometryResource(model);
2686
- useEffect5(() => {
3001
+ useEffect6(() => {
2687
3002
  engineGeometryCache.retain(resource);
2688
3003
  return () => engineGeometryCache.release(resource);
2689
3004
  }, [resource]);
2690
- return /* @__PURE__ */ jsx4(PartMesh, { model, geometry: resource.geometry, ...props });
3005
+ return /* @__PURE__ */ jsx6(PartMesh, { model, geometry: resource.geometry, ...props });
2691
3006
  };
2692
3007
  function useGeometryResource(part) {
2693
3008
  const resource = engineGeometryCache.get(part);
@@ -2723,7 +3038,6 @@ export {
2723
3038
  sectionFromPick,
2724
3039
  pickedStartDepth,
2725
3040
  sectionPlane,
2726
- screenLength,
2727
3041
  dragPlane,
2728
3042
  TAP_SLOP,
2729
3043
  movedFar,
@@ -2752,18 +3066,34 @@ export {
2752
3066
  EXCLUDE_FROM_FRAME,
2753
3067
  defaultBounds,
2754
3068
  aspectRatio,
3069
+ screenLength,
2755
3070
  boundsFromBox,
2756
3071
  contentBounds,
2757
3072
  perspectiveFitDistance,
2758
3073
  orthographicHalfHeight,
2759
3074
  fitDistance,
2760
3075
  startPosition,
3076
+ MIN_FRAME_RATIO,
3077
+ MAX_FRAME_RATIO,
3078
+ cameraLimits,
3079
+ targetBoundary,
2761
3080
  applyProjection,
2762
3081
  CAD_CAMERA_UP,
3082
+ adaptedUp,
2763
3083
  cadViewDirections,
2764
3084
  currentViewDirection,
2765
3085
  ExtendedCameraControls,
2766
3086
  CadCameraControls,
3087
+ retargetPose,
3088
+ ORBIT_TARGET_PIXELS,
3089
+ ORBIT_TARGET_RING_PIXELS,
3090
+ ORBIT_TARGET_RING_WIDTH,
3091
+ ORBIT_TARGET_COLOR,
3092
+ ORBIT_TARGET_RING_COLOR,
3093
+ ORBIT_TARGET_RING_OPACITY,
3094
+ ORBIT_TARGET_FLASH_MS,
3095
+ ORBIT_TARGET_FADE_MS,
3096
+ orbitTargetOpacity,
2767
3097
  VIEW_NAMES,
2768
3098
  VIEW_SIGNS,
2769
3099
  viewKind,
@@ -2777,6 +3107,7 @@ export {
2777
3107
  labelGeometry,
2778
3108
  labelTexture,
2779
3109
  useViewerControls,
3110
+ useRetarget,
2780
3111
  Viewer,
2781
3112
  CONE_AXIS,
2782
3113
  HEAD,