@liveroom-tech/react-immersive 1.1.8 → 1.2.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.
Files changed (3) hide show
  1. package/dist/index.js +300 -184
  2. package/dist/index.mjs +323 -203
  3. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import {
5
5
  useRef as useRef11,
6
6
  useState as useState10,
7
- useEffect as useEffect14,
7
+ useEffect as useEffect15,
8
8
  useCallback as useCallback4,
9
9
  useMemo as useMemo13,
10
10
  Suspense as Suspense2
@@ -1536,6 +1536,7 @@ function Viewer({
1536
1536
  dpr: [1, effectiveMaxDpr],
1537
1537
  shadows: effectiveShadows ? "soft" : false,
1538
1538
  resize: { debounce: 0, scroll: false },
1539
+ raycaster: { firstHitOnly: true },
1539
1540
  style: backgroundStyle,
1540
1541
  camera: camera ?? {
1541
1542
  fov: 50
@@ -1583,7 +1584,7 @@ function Controls({ controlsRef, onCameraChange }) {
1583
1584
  }
1584
1585
 
1585
1586
  // src/components/Lights.tsx
1586
- import { useRef as useRef4, useMemo as useMemo5 } from "react";
1587
+ import { useRef as useRef4, useMemo as useMemo5, useEffect as useEffect6 } from "react";
1587
1588
  import { useFrame as useFrame3, useThree as useThree5 } from "@react-three/fiber";
1588
1589
  import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1589
1590
  function rotatePositionY(pos, deg) {
@@ -1593,6 +1594,24 @@ function rotatePositionY(pos, deg) {
1593
1594
  const s = Math.sin(rad);
1594
1595
  return [pos[0] * c + pos[2] * s, pos[1], -pos[0] * s + pos[2] * c];
1595
1596
  }
1597
+ function fitShadowCamera(bounds, position) {
1598
+ const [cx, cy, cz] = bounds.center;
1599
+ const radius = bounds.radius;
1600
+ const length = Math.hypot(position[0], position[1], position[2]);
1601
+ const dir = length > 1e-6 ? [position[0] / length, position[1] / length, position[2] / length] : [0, 1, 0];
1602
+ const distance = radius * 2;
1603
+ return {
1604
+ position: [
1605
+ cx + dir[0] * distance,
1606
+ cy + dir[1] * distance,
1607
+ cz + dir[2] * distance
1608
+ ],
1609
+ target: bounds.center,
1610
+ extent: radius,
1611
+ near: Math.max(radius / 100, 1e-3),
1612
+ far: distance + radius * 2
1613
+ };
1614
+ }
1596
1615
  var DEFAULT_LIGHTING = {
1597
1616
  ambient: { intensity: 0.25, color: "#ffffff" },
1598
1617
  lights: [
@@ -1642,7 +1661,8 @@ function LightInstance({
1642
1661
  light,
1643
1662
  shadows,
1644
1663
  shadowsEnabled,
1645
- environmentRotation
1664
+ environmentRotation,
1665
+ modelBounds
1646
1666
  }) {
1647
1667
  const dirRef = useRef4(null);
1648
1668
  const pointRef = useRef4(null);
@@ -1658,8 +1678,19 @@ function LightInstance({
1658
1678
  if (pointRef.current) pointRef.current.position.copy(camera.position);
1659
1679
  if (spotRef.current) spotRef.current.position.copy(camera.position);
1660
1680
  });
1661
- if (!light.visible || light.type === "none") return null;
1662
1681
  const castShadow = shadowsEnabled && shadows.enabled && light.castShadow;
1682
+ const fit = useMemo5(
1683
+ () => light.type === "directional" && castShadow && modelBounds && !light.attachedToCamera ? fitShadowCamera(modelBounds, position) : null,
1684
+ [light.type, light.attachedToCamera, castShadow, modelBounds, position]
1685
+ );
1686
+ useEffect6(() => {
1687
+ const dirLight = dirRef.current;
1688
+ if (!dirLight) return;
1689
+ const [tx, ty, tz] = fit?.target ?? [0, 0, 0];
1690
+ dirLight.target.position.set(tx, ty, tz);
1691
+ dirLight.target.updateMatrixWorld();
1692
+ }, [fit]);
1693
+ if (!light.visible || light.type === "none") return null;
1663
1694
  if (light.type === "directional") {
1664
1695
  return /* @__PURE__ */ jsx7(
1665
1696
  "directionalLight",
@@ -1667,19 +1698,19 @@ function LightInstance({
1667
1698
  ref: dirRef,
1668
1699
  color: light.color,
1669
1700
  intensity: light.intensity,
1670
- position,
1701
+ position: fit?.position ?? position,
1671
1702
  castShadow,
1672
1703
  "shadow-mapSize-width": shadows.mapSize,
1673
1704
  "shadow-mapSize-height": shadows.mapSize,
1674
1705
  "shadow-bias": light.shadowBias,
1675
1706
  "shadow-normalBias": shadows.normalBias,
1676
1707
  "shadow-radius": shadows.radius,
1677
- "shadow-camera-near": 0.5,
1678
- "shadow-camera-far": 40,
1679
- "shadow-camera-left": -8,
1680
- "shadow-camera-right": 8,
1681
- "shadow-camera-top": 8,
1682
- "shadow-camera-bottom": -8
1708
+ "shadow-camera-near": fit?.near ?? 0.5,
1709
+ "shadow-camera-far": fit?.far ?? 40,
1710
+ "shadow-camera-left": -(fit?.extent ?? 8),
1711
+ "shadow-camera-right": fit?.extent ?? 8,
1712
+ "shadow-camera-top": fit?.extent ?? 8,
1713
+ "shadow-camera-bottom": -(fit?.extent ?? 8)
1683
1714
  }
1684
1715
  );
1685
1716
  }
@@ -1715,7 +1746,8 @@ function Lights({
1715
1746
  shadowConfig,
1716
1747
  shadowsEnabled = true,
1717
1748
  environmentRotation = 0,
1718
- environmentConfig
1749
+ environmentConfig,
1750
+ modelBounds
1719
1751
  }) {
1720
1752
  const lighting = lightingConfig ?? DEFAULT_LIGHTING;
1721
1753
  const shadows = shadowConfig ?? DEFAULT_SHADOWS;
@@ -1735,35 +1767,36 @@ function Lights({
1735
1767
  light,
1736
1768
  shadows,
1737
1769
  shadowsEnabled,
1738
- environmentRotation
1770
+ environmentRotation,
1771
+ modelBounds
1739
1772
  },
1740
1773
  light.id
1741
1774
  )),
1742
1775
  sl?.enabled && /* @__PURE__ */ jsx7(
1743
- "directionalLight",
1776
+ LightInstance,
1744
1777
  {
1745
- color: "#ffffff",
1746
- intensity: sl.intensity,
1747
- position: envShadowPosition,
1748
- castShadow: shadowsEnabled && shadows.enabled,
1749
- "shadow-mapSize-width": shadows.mapSize,
1750
- "shadow-mapSize-height": shadows.mapSize,
1751
- "shadow-bias": sl.bias,
1752
- "shadow-normalBias": shadows.normalBias,
1753
- "shadow-radius": shadows.radius,
1754
- "shadow-camera-near": 0.5,
1755
- "shadow-camera-far": 40,
1756
- "shadow-camera-left": -8,
1757
- "shadow-camera-right": 8,
1758
- "shadow-camera-top": 8,
1759
- "shadow-camera-bottom": -8
1778
+ light: {
1779
+ id: "light-env-shadow",
1780
+ type: "directional",
1781
+ color: "#ffffff",
1782
+ intensity: sl.intensity,
1783
+ position: envShadowPosition,
1784
+ attachedToCamera: false,
1785
+ castShadow: true,
1786
+ shadowBias: sl.bias,
1787
+ visible: true
1788
+ },
1789
+ shadows,
1790
+ shadowsEnabled,
1791
+ environmentRotation: 0,
1792
+ modelBounds
1760
1793
  }
1761
1794
  )
1762
1795
  ] });
1763
1796
  }
1764
1797
 
1765
1798
  // src/components/Model.tsx
1766
- import { useState as useState3, useEffect as useEffect6, useRef as useRef5, useMemo as useMemo7 } from "react";
1799
+ import { useState as useState3, useEffect as useEffect7, useRef as useRef5, useMemo as useMemo7 } from "react";
1767
1800
  import { useFrame as useFrame4, useThree as useThree7 } from "@react-three/fiber";
1768
1801
 
1769
1802
  // src/utils/gltfLoader.ts
@@ -2479,6 +2512,72 @@ function refreshSkinnedBounds(root) {
2479
2512
  });
2480
2513
  }
2481
2514
 
2515
+ // src/utils/bvh.ts
2516
+ import { Mesh as Mesh4 } from "three";
2517
+ import {
2518
+ acceleratedRaycast,
2519
+ computeBoundsTree,
2520
+ disposeBoundsTree
2521
+ } from "three-mesh-bvh";
2522
+ var raycastPatched = false;
2523
+ function enableAcceleratedRaycast() {
2524
+ if (raycastPatched) return;
2525
+ Mesh4.prototype.raycast = acceleratedRaycast;
2526
+ raycastPatched = true;
2527
+ }
2528
+ function collectGeometries(root) {
2529
+ const seen = /* @__PURE__ */ new Set();
2530
+ root.traverse((node) => {
2531
+ const geometry = node.geometry;
2532
+ if (!geometry || !geometry.attributes?.position) return;
2533
+ if (geometry.boundsTree) return;
2534
+ seen.add(geometry);
2535
+ });
2536
+ return [...seen];
2537
+ }
2538
+ async function buildSceneBVH(root, signal) {
2539
+ const geometries = collectGeometries(root);
2540
+ if (geometries.length === 0) return;
2541
+ enableAcceleratedRaycast();
2542
+ let worker = null;
2543
+ if (typeof window !== "undefined" && typeof Worker !== "undefined") {
2544
+ try {
2545
+ const mod = await import("three-mesh-bvh/src/workers/ParallelMeshBVHWorker.js");
2546
+ worker = new mod.ParallelMeshBVHWorker();
2547
+ } catch {
2548
+ worker = null;
2549
+ }
2550
+ }
2551
+ try {
2552
+ for (const geometry of geometries) {
2553
+ if (signal?.aborted) return;
2554
+ let tree;
2555
+ if (worker) {
2556
+ try {
2557
+ tree = await worker.generate(geometry);
2558
+ } catch {
2559
+ }
2560
+ }
2561
+ if (tree === void 0) {
2562
+ tree = computeBoundsTree.call(geometry);
2563
+ await new Promise((resolve) => setTimeout(resolve, 0));
2564
+ }
2565
+ if (signal?.aborted) return;
2566
+ geometry.boundsTree = tree;
2567
+ }
2568
+ } finally {
2569
+ worker?.dispose?.();
2570
+ }
2571
+ }
2572
+ function disposeSceneBVH(root) {
2573
+ root.traverse((node) => {
2574
+ const geometry = node.geometry;
2575
+ if (!geometry?.boundsTree) return;
2576
+ disposeBoundsTree.call(geometry);
2577
+ geometry.boundsTree = void 0;
2578
+ });
2579
+ }
2580
+
2482
2581
  // src/components/Model.tsx
2483
2582
  import { jsx as jsx8 } from "react/jsx-runtime";
2484
2583
  function firstMaterial(node) {
@@ -2511,14 +2610,14 @@ function Model({
2511
2610
  const invalidate = useThree7((state) => state.invalidate);
2512
2611
  const [hoveredItem, setHoveredItem] = useState3(null);
2513
2612
  const groupRef = useRef5(null);
2514
- useEffect6(() => {
2613
+ useEffect7(() => {
2515
2614
  if (!highlightOnHover) {
2516
2615
  setHoveredItem(null);
2517
2616
  onItemHover?.(null);
2518
2617
  document.body.style.cursor = "default";
2519
2618
  }
2520
2619
  }, [highlightOnHover, onItemHover]);
2521
- useEffect6(() => {
2620
+ useEffect7(() => {
2522
2621
  return () => {
2523
2622
  document.body.style.cursor = "default";
2524
2623
  };
@@ -2554,7 +2653,7 @@ function Model({
2554
2653
  }
2555
2654
  return shell;
2556
2655
  }, [nodeMap]);
2557
- useEffect6(() => {
2656
+ useEffect7(() => {
2558
2657
  scene.traverse((node) => {
2559
2658
  if (!isMeshNode(node)) return;
2560
2659
  const mesh = node;
@@ -2562,9 +2661,17 @@ function Model({
2562
2661
  mesh.receiveShadow = true;
2563
2662
  });
2564
2663
  }, [scene, shellNodeNames]);
2565
- useEffect6(() => {
2664
+ useEffect7(() => {
2566
2665
  refreshSkinnedBounds(scene);
2567
2666
  }, [scene]);
2667
+ useEffect7(() => {
2668
+ const controller = new AbortController();
2669
+ void buildSceneBVH(scene, controller.signal);
2670
+ return () => {
2671
+ controller.abort();
2672
+ disposeSceneBVH(scene);
2673
+ };
2674
+ }, [scene]);
2568
2675
  const bindingKeyByNodeName = useMemo7(() => {
2569
2676
  const map = {};
2570
2677
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2593,10 +2700,10 @@ function Model({
2593
2700
  }
2594
2701
  return map;
2595
2702
  }, [nodeMap, nodeBindingSignature]);
2596
- useEffect6(() => {
2703
+ useEffect7(() => {
2597
2704
  if (materialRefsOut) materialRefsOut.current = perNodeMaterials;
2598
2705
  }, [perNodeMaterials, materialRefsOut]);
2599
- useEffect6(() => {
2706
+ useEffect7(() => {
2600
2707
  return () => {
2601
2708
  for (const material of Object.values(perNodeMaterials)) {
2602
2709
  material.dispose();
@@ -2647,7 +2754,7 @@ function Model({
2647
2754
  }
2648
2755
  return map;
2649
2756
  }, [perNodeMaterials, shadingMode, rendererMode]);
2650
- useEffect6(() => {
2757
+ useEffect7(() => {
2651
2758
  return () => {
2652
2759
  for (const material of Object.values(perNodeBasicMaterials))
2653
2760
  material.dispose();
@@ -2663,10 +2770,10 @@ function Model({
2663
2770
  if (rendererMode === "matcap") return perNodeMatcapMaterials[nodeName];
2664
2771
  return perNodeMaterials[nodeName];
2665
2772
  };
2666
- useEffect6(() => {
2773
+ useEffect7(() => {
2667
2774
  onModelLoaded?.(scene);
2668
2775
  }, [scene, onModelLoaded]);
2669
- useEffect6(() => {
2776
+ useEffect7(() => {
2670
2777
  if (!nodeRefsOut) return;
2671
2778
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
2672
2779
  const glbNodeName = binding.modelObjectId ?? nodeName;
@@ -2675,7 +2782,7 @@ function Model({
2675
2782
  }
2676
2783
  }, [nodeMap, objectBindings, nodeRefsOut]);
2677
2784
  const appliedTextureUrls = useRef5({});
2678
- useEffect6(() => {
2785
+ useEffect7(() => {
2679
2786
  return () => {
2680
2787
  for (const nodeTextures of Object.values(appliedTextureUrls.current)) {
2681
2788
  for (const [key, appliedUrl] of Object.entries(nodeTextures)) {
@@ -2690,7 +2797,7 @@ function Model({
2690
2797
  appliedTextureUrls.current = {};
2691
2798
  };
2692
2799
  }, []);
2693
- useEffect6(() => {
2800
+ useEffect7(() => {
2694
2801
  const seenNodeNames = /* @__PURE__ */ new Set();
2695
2802
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
2696
2803
  seenNodeNames.add(nodeName);
@@ -2797,7 +2904,7 @@ function Model({
2797
2904
  rendererMode,
2798
2905
  invalidate
2799
2906
  ]);
2800
- useEffect6(() => {
2907
+ useEffect7(() => {
2801
2908
  if (!wireframeEnabled) return;
2802
2909
  const added = [];
2803
2910
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2931,7 +3038,7 @@ function Model({
2931
3038
  emitPlaybackState();
2932
3039
  invalidate();
2933
3040
  };
2934
- useEffect6(() => {
3041
+ useEffect7(() => {
2935
3042
  const mixer = new AnimationMixer(scene);
2936
3043
  mixerRef.current = mixer;
2937
3044
  const actions = {};
@@ -2986,7 +3093,7 @@ function Model({
2986
3093
  emitPlaybackState();
2987
3094
  };
2988
3095
  }, [scene, resolvedAnimationClips]);
2989
- useEffect6(() => {
3096
+ useEffect7(() => {
2990
3097
  const currentClip = activeClipNameRef.current;
2991
3098
  const activeAction = activeActionRef.current;
2992
3099
  if (!currentClip || !activeAction) return;
@@ -3045,7 +3152,7 @@ function Model({
3045
3152
  const explodeStrengthRef = useRef5(0);
3046
3153
  const currentExplodeRef = useRef5(0);
3047
3154
  const explodeTmp = useMemo7(() => new Vector32(), []);
3048
- useEffect6(() => {
3155
+ useEffect7(() => {
3049
3156
  for (const part of explodePartsRef.current) {
3050
3157
  part.node.position.copy(part.restLocalPos);
3051
3158
  }
@@ -3094,7 +3201,7 @@ function Model({
3094
3201
  explodePartsRef.current = parts;
3095
3202
  invalidate();
3096
3203
  }, [scene, nodeMap, nodeBindingSignature, invalidate]);
3097
- useEffect6(() => {
3204
+ useEffect7(() => {
3098
3205
  invalidate();
3099
3206
  }, [explodeFactor, invalidate]);
3100
3207
  useFrame4((_, delta) => {
@@ -3172,7 +3279,7 @@ import {
3172
3279
  useMemo as useMemo8,
3173
3280
  useRef as useRef6,
3174
3281
  useState as useState4,
3175
- useEffect as useEffect7,
3282
+ useEffect as useEffect8,
3176
3283
  useCallback
3177
3284
  } from "react";
3178
3285
  import { Html } from "@react-three/drei";
@@ -3202,7 +3309,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3202
3309
  const worldPosition = useMemo8(() => new Vector33(), []);
3203
3310
  const biasedWorldPosition = useMemo8(() => new Vector33(), []);
3204
3311
  const toCamera = useMemo8(() => new Vector33(), []);
3205
- useEffect7(() => {
3312
+ useEffect8(() => {
3206
3313
  anchorNodeRef.current = null;
3207
3314
  }, [annotation.anchorNodeName]);
3208
3315
  useFrame5(({ camera }) => {
@@ -3248,7 +3355,7 @@ function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick,
3248
3355
  const [zIndex, setZIndex] = useState4(1);
3249
3356
  const clickTimeoutRef = useRef6(null);
3250
3357
  const markerRef = useRef6(null);
3251
- useEffect7(() => {
3358
+ useEffect8(() => {
3252
3359
  return () => {
3253
3360
  if (clickTimeoutRef.current) {
3254
3361
  clearTimeout(clickTimeoutRef.current);
@@ -3430,13 +3537,13 @@ function AnnotationsLayer({
3430
3537
  };
3431
3538
  invalidateFrameRef.current = window.requestAnimationFrame(step);
3432
3539
  }, [invalidate]);
3433
- useEffect7(() => {
3540
+ useEffect8(() => {
3434
3541
  requestAnnotationRefresh();
3435
3542
  }, [annotations, requestAnnotationRefresh]);
3436
- useEffect7(() => {
3543
+ useEffect8(() => {
3437
3544
  requestAnnotationRefresh();
3438
3545
  }, [activeAnnotation, requestAnnotationRefresh]);
3439
- useEffect7(() => {
3546
+ useEffect8(() => {
3440
3547
  return () => {
3441
3548
  if (invalidateFrameRef.current !== null) {
3442
3549
  window.cancelAnimationFrame(invalidateFrameRef.current);
@@ -3499,7 +3606,7 @@ function AnnotationsLayer({
3499
3606
  onCloseAnnotation?.();
3500
3607
  }
3501
3608
  }, [isLocked, onCloseAnnotation]);
3502
- useEffect7(() => {
3609
+ useEffect8(() => {
3503
3610
  const canvas = canvasRootRef.current;
3504
3611
  if (canvas) {
3505
3612
  canvas.addEventListener("click", handleCanvasClick);
@@ -3568,7 +3675,7 @@ function AnnotationsLayer({
3568
3675
  }
3569
3676
 
3570
3677
  // src/components/ModelViewer/MeasurementLayer.tsx
3571
- import { useEffect as useEffect8, useMemo as useMemo9 } from "react";
3678
+ import { useEffect as useEffect9, useMemo as useMemo9 } from "react";
3572
3679
  import { Html as Html2, Line } from "@react-three/drei";
3573
3680
  import { BoxGeometry, EdgesGeometry, Vector3 as Vector34 } from "three";
3574
3681
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -3603,7 +3710,7 @@ function DimensionsBox({ box, unit }) {
3603
3710
  );
3604
3711
  return geometry;
3605
3712
  }, [box.size]);
3606
- useEffect8(() => {
3713
+ useEffect9(() => {
3607
3714
  return () => {
3608
3715
  edges.dispose();
3609
3716
  };
@@ -3659,7 +3766,7 @@ function MeasurementLayer({
3659
3766
  import {
3660
3767
  forwardRef as forwardRef2,
3661
3768
  useCallback as useCallback2,
3662
- useEffect as useEffect9,
3769
+ useEffect as useEffect10,
3663
3770
  useImperativeHandle as useImperativeHandle2,
3664
3771
  useMemo as useMemo10,
3665
3772
  useRef as useRef7
@@ -3767,7 +3874,7 @@ var XRModelPresenter = forwardRef2(
3767
3874
  }),
3768
3875
  [cancelPlacementMode, finishPlacementMode, placeCurrent, startPlacementMode]
3769
3876
  );
3770
- useEffect9(() => {
3877
+ useEffect10(() => {
3771
3878
  if (!inSession) {
3772
3879
  placedPosition.current.set(0, 0, -FORWARD_DISTANCE);
3773
3880
  userScale.current = 1;
@@ -3815,7 +3922,7 @@ var XRModelPresenter = forwardRef2(
3815
3922
  },
3816
3923
  [isAR, placeCurrent]
3817
3924
  );
3818
- useEffect9(() => {
3925
+ useEffect10(() => {
3819
3926
  if (!isAR) return;
3820
3927
  const onTouchStart = (event) => {
3821
3928
  if (event.touches.length !== 2) return;
@@ -3900,7 +4007,7 @@ var XRModelPresenter = forwardRef2(
3900
4007
  var XRModelPresenter_default = XRModelPresenter;
3901
4008
 
3902
4009
  // src/components/ModelViewer/CinematicCamera.tsx
3903
- import { useEffect as useEffect10, useMemo as useMemo11, useRef as useRef8 } from "react";
4010
+ import { useEffect as useEffect11, useMemo as useMemo11, useRef as useRef8 } from "react";
3904
4011
  import { useFrame as useFrame7, useThree as useThree9 } from "@react-three/fiber";
3905
4012
  import {
3906
4013
  Box3 as Box32,
@@ -3951,7 +4058,7 @@ function CinematicCamera({
3951
4058
  const orbitRef = useRef8(null);
3952
4059
  const tmpPos = useMemo11(() => new Vector36(), []);
3953
4060
  const tmpTarget = useMemo11(() => new Vector36(), []);
3954
- useEffect10(() => {
4061
+ useEffect11(() => {
3955
4062
  if (!playing) {
3956
4063
  startedRef.current = false;
3957
4064
  return;
@@ -3976,7 +4083,7 @@ function CinematicCamera({
3976
4083
  startedRef.current = true;
3977
4084
  invalidate();
3978
4085
  }, [playing, hasPath, controlsRef, sceneRef, invalidate]);
3979
- useEffect10(() => {
4086
+ useEffect11(() => {
3980
4087
  if (!playing || !onUserInteract) return;
3981
4088
  const controls = controlsRef.current;
3982
4089
  if (!controls) return;
@@ -4034,7 +4141,7 @@ function CinematicCamera({
4034
4141
  }
4035
4142
 
4036
4143
  // src/hooks/useLicense.ts
4037
- import { useEffect as useEffect11, useState as useState5 } from "react";
4144
+ import { useEffect as useEffect12, useState as useState5 } from "react";
4038
4145
  var LOCAL_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
4039
4146
  var isLocalHostname = (hostname) => LOCAL_HOSTNAMES.includes(hostname) || hostname.endsWith(".local") || hostname.endsWith(".localhost");
4040
4147
  var LICENSE_SERVER = "https://react-immersive.liveroom.dev/api/validate-license";
@@ -4209,7 +4316,7 @@ var useLicense = (licenseKey) => {
4209
4316
  // window isn't available during SSR — useEffect below recomputes this on mount
4210
4317
  isLocalDomain: false
4211
4318
  });
4212
- useEffect11(() => {
4319
+ useEffect12(() => {
4213
4320
  if (!canUseBrowserAPI()) {
4214
4321
  return;
4215
4322
  }
@@ -4260,7 +4367,7 @@ var useLicense = (licenseKey) => {
4260
4367
  };
4261
4368
 
4262
4369
  // src/components/MouseController.tsx
4263
- import { useRef as useRef9, useState as useState6, useCallback as useCallback3, useEffect as useEffect12 } from "react";
4370
+ import { useRef as useRef9, useState as useState6, useCallback as useCallback3, useEffect as useEffect13 } from "react";
4264
4371
 
4265
4372
  // src/styles/MouseController.css
4266
4373
  styleInject(".mc__wrapper {\n position: absolute;\n bottom: -10px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: column;\n align-items: center;\n z-index: 100;\n}\n.mc__dpad {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n margin-bottom: 20px;\n}\n.mc__middle-row {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n.mc__joystick {\n width: 60px;\n height: 60px;\n flex-shrink: 0;\n background:\n radial-gradient(\n circle at 40% 35%,\n rgba(255, 255, 255, 0.07),\n rgba(0, 0, 0, 0.55));\n border-radius: 50%;\n border: 1.5px solid rgba(255, 255, 255, 0.15);\n position: relative;\n cursor: grab;\n touch-action: none;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n margin: 4px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), 0 0 0 4px rgba(255, 255, 255, 0.03);\n}\n.mc__joystick:hover {\n border-color: rgba(255, 255, 255, 0.28);\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.55), 0 0 0 4px rgba(66, 133, 244, 0.08);\n}\n.mc__joystick:active {\n cursor: grabbing;\n border-color: rgba(66, 133, 244, 0.6);\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5), 0 0 4px rgba(66, 133, 244, 0.15);\n}\n.mc__knob {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 34px;\n height: 34px;\n margin-left: -17px;\n margin-top: -17px;\n background:\n linear-gradient(\n 145deg,\n #5a9bf5,\n #1a66d9);\n border-radius: 50%;\n cursor: grab;\n box-shadow: 0 3px 10px rgba(26, 102, 217, 0.5), 0 1px 0 rgba(255, 255, 255, 0.25) inset;\n transition: transform 0.04s linear, box-shadow 0.1s ease;\n border: 1.5px solid rgba(255, 255, 255, 0.45);\n}\n.mc__joystick:active .mc__knob {\n box-shadow: 0 2px 16px rgba(66, 133, 244, 0.7), 0 1px 0 rgba(255, 255, 255, 0.25) inset;\n animation: pulse 0.6s ease infinite;\n}\n.mc__arrow {\n padding: 0;\n margin: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n box-shadow: none;\n outline: none;\n -webkit-appearance: none;\n appearance: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n transition: transform 0.1s ease, filter 0.15s ease;\n overflow: visible;\n}\n.mc__arrow svg {\n display: block !important;\n pointer-events: none !important;\n user-select: none !important;\n -webkit-user-select: none !important;\n overflow: visible !important;\n}\n.mc__arrow--up {\n margin-bottom: 2px;\n}\n.mc__arrow--up:hover {\n transform: scale(1.15) translateY(-2px);\n filter: drop-shadow(0 4px 12px rgba(66, 133, 244, 0.8));\n}\n.mc__arrow--up:active {\n transform: scale(0.92) translateY(2px);\n}\n.mc__arrow--down {\n margin-top: 2px;\n}\n.mc__arrow--down:hover {\n transform: scale(1.15) translateY(2px);\n filter: drop-shadow(0 -4px 12px rgba(66, 133, 244, 0.8));\n}\n.mc__arrow--down:active {\n transform: scale(0.92) translateY(-2px);\n}\n.mc__arrow--left {\n margin-right: 2px;\n}\n.mc__arrow--left:hover {\n transform: scale(1.15) translateX(-2px);\n filter: drop-shadow(4px 0 12px rgba(66, 133, 244, 0.8));\n}\n.mc__arrow--left:active {\n transform: scale(0.92) translateX(2px);\n}\n.mc__arrow--right {\n margin-left: 2px;\n}\n.mc__arrow--right:hover {\n transform: scale(1.15) translateX(2px);\n filter: drop-shadow(-4px 0 12px rgba(66, 133, 244, 0.8));\n}\n.mc__arrow--right:active {\n transform: scale(0.92) translateX(-2px);\n}\n@keyframes pulse {\n 0%, 100% {\n box-shadow: 0 2px 16px rgba(66, 133, 244, 0.6), 0 1px 0 rgba(255, 255, 255, 0.25) inset;\n opacity: 0.85;\n }\n 50% {\n box-shadow: 0 4px 24px rgba(66, 133, 244, 0.95), 0 1px 0 rgba(255, 255, 255, 0.25) inset;\n opacity: 1;\n }\n}\n@media (max-width: 768px) {\n .mc__wrapper {\n bottom: -5px;\n }\n .mc__joystick {\n width: 68px;\n height: 68px;\n }\n .mc__knob {\n width: 30px;\n height: 30px;\n margin-left: -15px;\n margin-top: -15px;\n }\n .mc__arrow--up:hover {\n transform: scale(1.1) translateY(-1px);\n }\n .mc__arrow--down:hover {\n transform: scale(1.1) translateY(1px);\n }\n .mc__arrow--left:hover {\n transform: scale(1.1) translateX(-1px);\n }\n .mc__arrow--right:hover {\n transform: scale(1.1) translateX(1px);\n }\n}\n@media (max-width: 480px) {\n .mc__joystick {\n width: 56px;\n height: 56px;\n }\n .mc__knob {\n width: 26px;\n height: 26px;\n margin-left: -13px;\n margin-top: -13px;\n }\n .mc__arrow--up {\n margin-bottom: 1px;\n }\n .mc__arrow--down {\n margin-top: 1px;\n }\n .mc__arrow--left {\n margin-right: 1px;\n }\n .mc__arrow--right {\n margin-left: 1px;\n }\n}\n@media (hover: none) {\n .mc__arrow:hover {\n transform: none !important;\n filter: none !important;\n }\n .mc__arrow:active {\n transform: scale(0.92) !important;\n }\n}\n");
@@ -4464,7 +4571,7 @@ var MouseController = ({
4464
4571
  lastRepeatTimeRef.current = 0;
4465
4572
  }
4466
4573
  }, []);
4467
- useEffect12(() => {
4574
+ useEffect13(() => {
4468
4575
  window.addEventListener("mousemove", onJoystickMove);
4469
4576
  window.addEventListener("mouseup", onJoystickEnd);
4470
4577
  window.addEventListener("touchmove", onJoystickMove, { passive: false });
@@ -4695,7 +4802,7 @@ var MaterialPicker = ({
4695
4802
  };
4696
4803
 
4697
4804
  // src/components/ModelViewer/ObjectBindingDataPanel.tsx
4698
- import { Fragment as Fragment5, useEffect as useEffect13, useRef as useRef10, useState as useState7 } from "react";
4805
+ import { Fragment as Fragment5, useEffect as useEffect14, useRef as useRef10, useState as useState7 } from "react";
4699
4806
 
4700
4807
  // src/styles/ObjectBindingDataPanel.css
4701
4808
  styleInject('.left-panel {\n width: 280px;\n min-height: 0;\n background: var(--ri-databinding-bg, #111318);\n border-right: 1px solid var(--ri-databinding-border, rgba(255, 255, 255, 0.06));\n display: flex;\n flex-direction: column;\n padding: 24px 18px;\n overflow-y: auto;\n font-family: var( --ri-databinding-font-family, "Inter", "Segoe UI", sans-serif );\n flex-shrink: 0;\n transition: margin-left 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: var(--ri-databinding-shadow, 4px 0 24px rgba(0, 0, 0, 0.35));\n}\n.left-panel--open {\n margin-left: 0;\n}\n.left-panel--closed {\n margin-left: -280px;\n}\n.left-panel::-webkit-scrollbar {\n width: 4px;\n}\n.left-panel::-webkit-scrollbar-track {\n background: transparent;\n}\n.left-panel::-webkit-scrollbar-thumb {\n background: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n border-radius: 4px;\n}\n.left-panel__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n padding-bottom: 14px;\n border-bottom: 1px solid var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.left-panel__title {\n font-size: 14px;\n font-weight: 600;\n color: var(--ri-databinding-text, #f1f5f9);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n padding-right: 8px;\n margin: 0;\n letter-spacing: 0.02em;\n}\n.left-panel__close-btn {\n width: 26px;\n height: 26px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border: 1px solid var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n border-radius: 6px;\n color: var(--ri-databinding-text-subtle, #64748b);\n font-size: 13px;\n flex-shrink: 0;\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease;\n}\n.left-panel__close-btn:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n color: var(--ri-databinding-text-hover, #cbd5e1);\n border-color: var(--ri-databinding-border-hover, rgba(255, 255, 255, 0.15));\n}\n.left-panel__data {\n padding: 12px 6px;\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border: 1px solid var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n border-radius: 10px;\n font-size: 11px;\n font-family: var( --ri-databinding-mono-font-family, "JetBrains Mono", "Fira Code", "Menlo", monospace );\n color: var(--ri-databinding-text-muted, #94a3b8);\n line-height: 1.7;\n overflow-x: auto;\n margin-bottom: 18px;\n}\n.left-panel__divider {\n border: none;\n border-top: 1px solid var(--ri-databinding-border, rgba(255, 255, 255, 0.06));\n margin-bottom: 16px;\n}\n.left-panel__actions-label {\n font-size: 10px;\n font-weight: 600;\n color: var(--ri-databinding-text-faint, #475569);\n text-transform: uppercase;\n letter-spacing: 0.12em;\n margin-bottom: 10px;\n}\n.left-panel__actions-list {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.left-panel__action-extra {\n display: grid;\n grid-template-rows: 0fr;\n opacity: 0;\n margin-top: 0;\n transition:\n grid-template-rows 0.28s cubic-bezier(0.4, 0, 0.2, 1),\n opacity 0.2s ease,\n margin-top 0.28s cubic-bezier(0.4, 0, 0.2, 1);\n}\n.left-panel__action-extra--open {\n grid-template-rows: 1fr;\n opacity: 1;\n margin-top: 4px;\n}\n.left-panel__action-extra-inner {\n overflow: hidden;\n min-height: 0;\n}\n.left-panel__action-btn {\n position: relative;\n padding: 9px 14px;\n border-radius: 8px;\n text-align: left;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n overflow: hidden;\n border: 1px solid var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n color: var(--ri-databinding-text-muted, #94a3b8);\n letter-spacing: 0.01em;\n}\n.left-panel__action-btn:active {\n transform: scale(0.97);\n}\n.left-panel__action-btn--active::before {\n content: "";\n position: absolute;\n left: 0;\n top: 20%;\n bottom: 20%;\n width: 3px;\n background: var(--ri-databinding-accent, #93c5fd);\n border-radius: 0 3px 3px 0;\n box-shadow: 0 0 8px var(--ri-databinding-accent-glow, rgba(147, 197, 253, 0.8));\n}\n.left-panel__action-btn--active:hover {\n background: var( --ri-databinding-accent-gradient, linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 60%, #2563eb 100%) );\n border-color: var(--ri-databinding-accent-border, rgba(96, 165, 250, 0.55));\n}\n.left-panel__action-btn--active:active {\n transform: scale(0.97);\n background: var( --ri-databinding-accent-gradient-active, linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%) );\n}\n.left-panel__data {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-bottom: 18px;\n}\n.left-panel__data-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n padding: 7px 10px;\n border-radius: 7px;\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border: 1px solid transparent;\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.left-panel__data-row:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.left-panel__data-key {\n font-size: 11px;\n font-weight: 600;\n color: var(--ri-databinding-text-faint, #475569);\n text-transform: uppercase;\n letter-spacing: 0.07em;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.left-panel__data-value {\n font-size: 12px;\n font-family: var( --ri-databinding-mono-font-family, "JetBrains Mono", "Fira Code", "Menlo", monospace );\n text-align: right;\n overflow-wrap: anywhere;\n word-break: break-word;\n min-width: 0;\n}\n.left-panel__data-group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 7px 10px;\n border-radius: 7px;\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border: 1px solid transparent;\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.left-panel__data-group:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.left-panel__data-children {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: 4px;\n padding-left: 10px;\n border-left: 1px solid var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.left-panel__data-children .left-panel__data-row,\n.left-panel__data-children .left-panel__data-group {\n padding: 3px 0;\n background: transparent;\n border-color: transparent;\n}\n.left-panel__data-children .left-panel__data-row:hover,\n.left-panel__data-children .left-panel__data-group:hover {\n background: transparent;\n}\n@media (max-width: 900px) {\n .left-panel {\n width: 50vw;\n }\n .left-panel--open {\n margin-left: 0;\n }\n .left-panel--closed {\n margin-left: -50vw;\n }\n .left-panel {\n padding: 18px 12px;\n }\n .left-panel__data-row {\n flex-direction: column;\n align-items: stretch;\n gap: 2px;\n padding: 6px 8px;\n }\n .left-panel__data-group {\n padding: 6px 8px;\n }\n .left-panel__data-value {\n text-align: left;\n font-size: 11.5px;\n }\n .left-panel__data-children {\n padding-left: 8px;\n }\n}\n');
@@ -4730,7 +4837,7 @@ function ActionExtra({ content }) {
4730
4837
  const [expanded, setExpanded] = useState7(hasContent);
4731
4838
  const cachedRef = useRef10(content);
4732
4839
  if (hasContent) cachedRef.current = content;
4733
- useEffect13(() => {
4840
+ useEffect14(() => {
4734
4841
  if (hasContent) {
4735
4842
  setMounted(true);
4736
4843
  return;
@@ -4741,7 +4848,7 @@ function ActionExtra({ content }) {
4741
4848
  return () => clearTimeout(timer);
4742
4849
  }
4743
4850
  }, [hasContent, mounted]);
4744
- useEffect13(() => {
4851
+ useEffect14(() => {
4745
4852
  if (mounted && hasContent) {
4746
4853
  const frame = requestAnimationFrame(() => setExpanded(true));
4747
4854
  return () => cancelAnimationFrame(frame);
@@ -4765,7 +4872,7 @@ function ObjectBindingDataPanel({
4765
4872
  renderActionExtra
4766
4873
  }) {
4767
4874
  const [displayedObject, setDisplayedObject] = useState7(selectedObject);
4768
- useEffect13(() => {
4875
+ useEffect14(() => {
4769
4876
  if (selectedObject) setDisplayedObject(selectedObject);
4770
4877
  }, [selectedObject]);
4771
4878
  const objectToRender = selectedObject ?? displayedObject;
@@ -5170,7 +5277,7 @@ function GuidedTourCameraFollow({
5170
5277
  const goal = useMemo13(() => new Vector37(), []);
5171
5278
  const nodeName = annotation?.anchorNodeName;
5172
5279
  const localPosition = annotation?.anchorLocalPosition;
5173
- useEffect14(() => {
5280
+ useEffect15(() => {
5174
5281
  nodeRef.current = null;
5175
5282
  hasPrev.current = false;
5176
5283
  }, [nodeName, annotation?.id]);
@@ -5215,6 +5322,14 @@ function computeMeasureBox(scene) {
5215
5322
  size: [size.x, size.y, size.z]
5216
5323
  };
5217
5324
  }
5325
+ function computeModelBounds(scene) {
5326
+ const box = new Box33().setFromObject(scene);
5327
+ if (box.isEmpty()) return null;
5328
+ const center = box.getCenter(new Vector37());
5329
+ const radius = box.getSize(new Vector37()).length() / 2;
5330
+ if (!Number.isFinite(radius) || radius <= 0) return null;
5331
+ return { center: [center.x, center.y, center.z], radius };
5332
+ }
5218
5333
  function fitClippingPlanesToScene(scene, controls) {
5219
5334
  const camera = controls?.camera;
5220
5335
  if (!(camera instanceof PerspectiveCamera3)) return;
@@ -5315,7 +5430,7 @@ function ModelViewer({
5315
5430
  [performanceProfile]
5316
5431
  );
5317
5432
  const controlsRef = useRef11(null);
5318
- useEffect14(() => {
5433
+ useEffect15(() => {
5319
5434
  if (!controlsRef.current) return;
5320
5435
  controlsRef.current.dollySpeed = Math.max(zoomSensitivity, 0);
5321
5436
  }, [zoomSensitivity]);
@@ -5325,7 +5440,7 @@ function ModelViewer({
5325
5440
  const pendingObjectId = useRef11(null);
5326
5441
  const rendererCaptureRef = useRef11(null);
5327
5442
  const createdBlobUrls = useRef11({});
5328
- useEffect14(() => {
5443
+ useEffect15(() => {
5329
5444
  return () => {
5330
5445
  for (const url of Object.values(createdBlobUrls.current)) {
5331
5446
  URL.revokeObjectURL(url);
@@ -5384,13 +5499,14 @@ function ModelViewer({
5384
5499
  const [measurePoints, setMeasurePoints] = useState10([]);
5385
5500
  const [showDimensions, setShowDimensions] = useState10(false);
5386
5501
  const [measureBox, setMeasureBox] = useState10(null);
5502
+ const [modelBounds, setModelBounds] = useState10(null);
5387
5503
  const measureModeRef = useRef11(false);
5388
5504
  measureModeRef.current = measureMode;
5389
5505
  const measurePointCounter = useRef11(0);
5390
5506
  const measureDownRef = useRef11(null);
5391
5507
  const [explodeFactor, setExplodeFactor] = useState10(0);
5392
5508
  const [explodeOpen, setExplodeOpen] = useState10(false);
5393
- useEffect14(() => {
5509
+ useEffect15(() => {
5394
5510
  setExplodeFactor(0);
5395
5511
  setExplodeOpen(false);
5396
5512
  }, [modelUrl]);
@@ -5411,11 +5527,11 @@ function ModelViewer({
5411
5527
  }, [cinematic, resolvedSceneConfig]);
5412
5528
  const [cinematicPlaying, setCinematicPlaying] = useState10(false);
5413
5529
  const cinematicAutoStartedRef = useRef11(false);
5414
- useEffect14(() => {
5530
+ useEffect15(() => {
5415
5531
  setCinematicPlaying(false);
5416
5532
  cinematicAutoStartedRef.current = false;
5417
5533
  }, [modelUrl]);
5418
- useEffect14(() => {
5534
+ useEffect15(() => {
5419
5535
  if (!cinematicConfig?.autoPlay || cinematicAutoStartedRef.current) return;
5420
5536
  if (modelLoaded && cameraPositioned && !isAutoFraming) {
5421
5537
  cinematicAutoStartedRef.current = true;
@@ -5443,7 +5559,7 @@ function ModelViewer({
5443
5559
  const [isHandheld, setIsHandheld] = useState10(true);
5444
5560
  const [resolvedMobileHandoffUrl, setResolvedMobileHandoffUrl] = useState10(null);
5445
5561
  const [isMobileHandoffOpen, setIsMobileHandoffOpen] = useState10(false);
5446
- useEffect14(() => {
5562
+ useEffect15(() => {
5447
5563
  setIsHandheld(isHandheldDevice());
5448
5564
  }, []);
5449
5565
  const resolveMobileHandoffUrl = useCallback4(() => {
@@ -5458,7 +5574,7 @@ function ModelViewer({
5458
5574
  return null;
5459
5575
  }
5460
5576
  }, [mobileHandoffUrl]);
5461
- useEffect14(() => {
5577
+ useEffect15(() => {
5462
5578
  if (!enableXR || isHandheld) {
5463
5579
  setResolvedMobileHandoffUrl(null);
5464
5580
  setIsMobileHandoffOpen(false);
@@ -5477,17 +5593,17 @@ function ModelViewer({
5477
5593
  DEFAULT_AR_OVERLAY_STATE
5478
5594
  );
5479
5595
  const [relocatePressed, setRelocatePressed] = useState10(false);
5480
- useEffect14(() => {
5596
+ useEffect15(() => {
5481
5597
  if (!xrStore) return;
5482
5598
  return xrStore.subscribe(
5483
5599
  (state) => setInARSession(state.session != null && state.mode === "immersive-ar")
5484
5600
  );
5485
5601
  }, [xrStore]);
5486
- useEffect14(() => {
5602
+ useEffect15(() => {
5487
5603
  setRelocatePressed(false);
5488
5604
  setArOverlayState(DEFAULT_AR_OVERLAY_STATE);
5489
5605
  }, [inARSession]);
5490
- useEffect14(() => {
5606
+ useEffect15(() => {
5491
5607
  if (!enableXR) return;
5492
5608
  const xr = navigator.xr;
5493
5609
  if (!xr?.isSessionSupported) return;
@@ -5522,7 +5638,7 @@ function ModelViewer({
5522
5638
  setRelocatePressed(true);
5523
5639
  xrPresenterRef.current?.startPlacementMode();
5524
5640
  }, []);
5525
- useEffect14(() => {
5641
+ useEffect15(() => {
5526
5642
  if (!relocatePressed) return;
5527
5643
  const onPointerUp = () => finishRelocation(true);
5528
5644
  const onPointerCancel = () => finishRelocation(false);
@@ -5585,7 +5701,7 @@ function ModelViewer({
5585
5701
  const [annotations, setAnnotations] = useState10(
5586
5702
  () => sceneConfig?.annotations ?? []
5587
5703
  );
5588
- useEffect14(() => {
5704
+ useEffect15(() => {
5589
5705
  if (annotations.length > 0) {
5590
5706
  const maxId = annotations.reduce((max, ann) => Math.max(max, ann.id), 0);
5591
5707
  annotationCounterRef.current = maxId;
@@ -5751,23 +5867,23 @@ function ModelViewer({
5751
5867
  );
5752
5868
  }
5753
5869
  }, [setActiveAnnotation]);
5754
- useEffect14(() => {
5870
+ useEffect15(() => {
5755
5871
  setCurrentAnnotationIndex(-1);
5756
5872
  setIsGuidedTourActive(false);
5757
5873
  }, [sortedAnnotations.length]);
5758
- useEffect14(() => {
5874
+ useEffect15(() => {
5759
5875
  return () => {
5760
5876
  if (annotationSmoothRestoreTimeoutRef.current) {
5761
5877
  clearTimeout(annotationSmoothRestoreTimeoutRef.current);
5762
5878
  }
5763
5879
  };
5764
5880
  }, []);
5765
- useEffect14(() => {
5881
+ useEffect15(() => {
5766
5882
  if (currentAnnotation && isGuidedTourActive) {
5767
5883
  setActiveAnnotation(currentAnnotation);
5768
5884
  }
5769
5885
  }, [currentAnnotation, setActiveAnnotation, isGuidedTourActive]);
5770
- useEffect14(() => {
5886
+ useEffect15(() => {
5771
5887
  if (sceneConfig?.annotations === void 0) {
5772
5888
  return;
5773
5889
  }
@@ -5796,7 +5912,7 @@ function ModelViewer({
5796
5912
  );
5797
5913
  return annotation ?? null;
5798
5914
  }, [activeAnnotation, annotations]);
5799
- useEffect14(() => {
5915
+ useEffect15(() => {
5800
5916
  if (activeAnnotation && !activeViewingAnnotation) {
5801
5917
  closeViewingAnnotation();
5802
5918
  }
@@ -5848,12 +5964,12 @@ function ModelViewer({
5848
5964
  );
5849
5965
  const isPanelOpen = showObjectBindingDataPanel && currentSelectedObject !== null && currentSelectedObjectKey !== null;
5850
5966
  const currentPanelObject = (currentSelectedObjectKey ? currentObjectBindings[currentSelectedObjectKey] : null) ?? currentSelectedObject;
5851
- useEffect14(() => {
5967
+ useEffect15(() => {
5852
5968
  if (!isControlledBindings) {
5853
5969
  setInternalObjectBindings(objectBindings);
5854
5970
  }
5855
5971
  }, [isControlledBindings, objectBindings]);
5856
- useEffect14(() => {
5972
+ useEffect15(() => {
5857
5973
  onHiddenObjectsChange?.(currentHiddenObjects);
5858
5974
  }, [currentHiddenObjects, onHiddenObjectsChange]);
5859
5975
  const applySelection = useCallback4(
@@ -6035,24 +6151,24 @@ function ModelViewer({
6035
6151
  bindingKeyIndex
6036
6152
  ]
6037
6153
  );
6038
- useEffect14(() => {
6154
+ useEffect15(() => {
6039
6155
  if (!colorPicker) return;
6040
6156
  window.addEventListener("pointerdown", closeColorPicker);
6041
6157
  return () => window.removeEventListener("pointerdown", closeColorPicker);
6042
6158
  }, [colorPicker, closeColorPicker]);
6043
- useEffect14(() => {
6159
+ useEffect15(() => {
6044
6160
  if (!materialPopup) return;
6045
6161
  const close = () => setMaterialPopup(null);
6046
6162
  window.addEventListener("pointerdown", close);
6047
6163
  return () => window.removeEventListener("pointerdown", close);
6048
6164
  }, [materialPopup]);
6049
- useEffect14(() => {
6165
+ useEffect15(() => {
6050
6166
  if (!downloadMenuOpen) return;
6051
6167
  const close = () => setDownloadMenuOpen(false);
6052
6168
  window.addEventListener("pointerdown", close);
6053
6169
  return () => window.removeEventListener("pointerdown", close);
6054
6170
  }, [downloadMenuOpen]);
6055
- useEffect14(() => {
6171
+ useEffect15(() => {
6056
6172
  if (!activeViewingAnnotation) return;
6057
6173
  const handleClickOutside = (event) => {
6058
6174
  if (viewAnnotationPopupRef.current && !viewAnnotationPopupRef.current.contains(event.target)) {
@@ -6070,7 +6186,7 @@ function ModelViewer({
6070
6186
  document.removeEventListener("touchstart", handleClickOutside);
6071
6187
  };
6072
6188
  }, [activeViewingAnnotation, closeViewingAnnotation]);
6073
- useEffect14(() => {
6189
+ useEffect15(() => {
6074
6190
  if (!pendingAnnotation) return;
6075
6191
  const handleClickOutside = (event) => {
6076
6192
  const target = event.target;
@@ -6217,7 +6333,7 @@ function ModelViewer({
6217
6333
  },
6218
6334
  [captureInitialCameraState, notifyViewerReady]
6219
6335
  );
6220
- useEffect14(() => {
6336
+ useEffect15(() => {
6221
6337
  fitAttemptedRef.current = false;
6222
6338
  fitInFlightRef.current = false;
6223
6339
  initialCameraStateRef.current = null;
@@ -6289,7 +6405,7 @@ function ModelViewer({
6289
6405
  notifyViewerReady,
6290
6406
  onAutoFit
6291
6407
  ]);
6292
- useEffect14(() => {
6408
+ useEffect15(() => {
6293
6409
  if (!refitOnResize) {
6294
6410
  return;
6295
6411
  }
@@ -6398,7 +6514,7 @@ function ModelViewer({
6398
6514
  },
6399
6515
  [applySelection, resolveBindingKey2, disableZoom]
6400
6516
  );
6401
- useEffect14(() => {
6517
+ useEffect15(() => {
6402
6518
  notifyViewerReady();
6403
6519
  }, [notifyViewerReady]);
6404
6520
  const handleCameraChangeInternal = useCallback4(
@@ -6412,6 +6528,7 @@ function ModelViewer({
6412
6528
  (scene) => {
6413
6529
  sceneRef.current = scene;
6414
6530
  setMeasureBox(computeMeasureBox(scene));
6531
+ setModelBounds(computeModelBounds(scene));
6415
6532
  setModelLoaded(true);
6416
6533
  setModelLoadFailed(false);
6417
6534
  setMismatchDismissed(false);
@@ -6429,7 +6546,7 @@ function ModelViewer({
6429
6546
  },
6430
6547
  [onLoadError]
6431
6548
  );
6432
- useEffect14(() => {
6549
+ useEffect15(() => {
6433
6550
  const scene = sceneRef.current;
6434
6551
  if (!modelLoaded || !scene) {
6435
6552
  setBindingsMismatch(false);
@@ -6490,7 +6607,7 @@ function ModelViewer({
6490
6607
  },
6491
6608
  []
6492
6609
  );
6493
- useEffect14(() => {
6610
+ useEffect15(() => {
6494
6611
  setMeasurePoints([]);
6495
6612
  }, [modelUrl, resolvedSceneConfig.model.rotation]);
6496
6613
  const handleDownloadMesh = useCallback4(() => {
@@ -6767,7 +6884,8 @@ function ModelViewer({
6767
6884
  shadowConfig: resolvedSceneConfig.shadows,
6768
6885
  shadowsEnabled: shadows,
6769
6886
  environmentRotation: resolvedSceneConfig.environment.rotation,
6770
- environmentConfig: resolvedSceneConfig.environment
6887
+ environmentConfig: resolvedSceneConfig.environment,
6888
+ modelBounds
6771
6889
  }
6772
6890
  ),
6773
6891
  /* @__PURE__ */ jsx19(
@@ -7327,7 +7445,7 @@ function ModelViewer({
7327
7445
  }
7328
7446
 
7329
7447
  // src/components/BindingBuilder/BindingBuilder.tsx
7330
- import { useCallback as useCallback8, useEffect as useEffect19, useMemo as useMemo16, useRef as useRef18, useState as useState24 } from "react";
7448
+ import { useCallback as useCallback8, useEffect as useEffect20, useMemo as useMemo16, useRef as useRef18, useState as useState24 } from "react";
7331
7449
  import {
7332
7450
  Redo2,
7333
7451
  Undo2,
@@ -7579,7 +7697,7 @@ function useViewerCamera() {
7579
7697
  }
7580
7698
 
7581
7699
  // src/hooks/useViewerAnimations.ts
7582
- import { useCallback as useCallback6, useEffect as useEffect15, useRef as useRef13, useState as useState12 } from "react";
7700
+ import { useCallback as useCallback6, useEffect as useEffect16, useRef as useRef13, useState as useState12 } from "react";
7583
7701
  function useViewerAnimations() {
7584
7702
  const controlsRef = useRef13(null);
7585
7703
  const [clips, setClips] = useState12([]);
@@ -7590,7 +7708,7 @@ function useViewerAnimations() {
7590
7708
  const [speed, setSpeedState] = useState12(1);
7591
7709
  const [time, setTime] = useState12(0);
7592
7710
  const [duration, setDuration] = useState12(0);
7593
- useEffect15(
7711
+ useEffect16(
7594
7712
  () => () => {
7595
7713
  unsubscribeRef.current?.();
7596
7714
  },
@@ -7852,7 +7970,7 @@ function inferMimeType(path) {
7852
7970
  }
7853
7971
 
7854
7972
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
7855
- import { useState as useState19, useEffect as useEffect17 } from "react";
7973
+ import { useState as useState19, useEffect as useEffect18 } from "react";
7856
7974
 
7857
7975
  // src/components/BindingBuilder/SceneSettings/GeneralSceneSettings.tsx
7858
7976
  import { useState as useState14 } from "react";
@@ -7880,9 +7998,9 @@ function getContrastTextColor(hex) {
7880
7998
  }
7881
7999
 
7882
8000
  // src/hooks/useClickOutside.ts
7883
- import { useEffect as useEffect16 } from "react";
8001
+ import { useEffect as useEffect17 } from "react";
7884
8002
  function useClickOutside(refs, onOutside, active) {
7885
- useEffect16(() => {
8003
+ useEffect17(() => {
7886
8004
  if (!active) return;
7887
8005
  const refList = Array.isArray(refs) ? refs : [refs];
7888
8006
  const handlePointerDown = (event) => {
@@ -11664,7 +11782,7 @@ function SceneSettingsTab({
11664
11782
  const { tier } = useBindingBuilderContext();
11665
11783
  const canUseStarterFeatures = hasTier(tier, "starter");
11666
11784
  const canUseGrowthFeatures = hasTier(tier, "growth");
11667
- useEffect17(() => {
11785
+ useEffect18(() => {
11668
11786
  if (controlledActiveSubTab !== void 0) {
11669
11787
  setInternalActiveSubTab(controlledActiveSubTab);
11670
11788
  }
@@ -13152,7 +13270,7 @@ function MaterialPresetGallery({
13152
13270
  }
13153
13271
 
13154
13272
  // src/components/BindingBuilder/components/ToolbarExportMenu.tsx
13155
- import { useEffect as useEffect18, useRef as useRef16, useState as useState22 } from "react";
13273
+ import { useEffect as useEffect19, useRef as useRef16, useState as useState22 } from "react";
13156
13274
  import { ChevronDown as ChevronDown5, Download as Download2, FileCode2, FileJson } from "lucide-react";
13157
13275
  import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
13158
13276
  function ToolbarExportMenu({
@@ -13163,7 +13281,7 @@ function ToolbarExportMenu({
13163
13281
  }) {
13164
13282
  const [open, setOpen] = useState22(false);
13165
13283
  const wrapperRef = useRef16(null);
13166
- useEffect18(() => {
13284
+ useEffect19(() => {
13167
13285
  if (!open) return;
13168
13286
  const close = (event) => {
13169
13287
  if (!wrapperRef.current?.contains(event.target)) {
@@ -13603,8 +13721,6 @@ function CameraPositionPanel({
13603
13721
  import { useMemo as useMemo15, useState as useState23 } from "react";
13604
13722
 
13605
13723
  // src/components/BindingBuilder/utils/bindingFactory.ts
13606
- import { GLTFLoader } from "three-stdlib";
13607
- import { WebGLRenderer as WebGLRenderer2 } from "three";
13608
13724
  function slugify(value) {
13609
13725
  return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
13610
13726
  }
@@ -13670,51 +13786,15 @@ function buildBindingsFromNodes(nodeNames) {
13670
13786
  nodeNames.map((nodeName) => [nodeName, createDefaultBinding(nodeName)])
13671
13787
  );
13672
13788
  }
13673
- function collectModelMetadata(modelUrl) {
13674
- const loader = new GLTFLoader();
13675
- let renderer = null;
13676
- if (typeof document !== "undefined") {
13677
- try {
13678
- renderer = new WebGLRenderer2({
13679
- canvas: document.createElement("canvas"),
13680
- antialias: false
13681
- });
13682
- configureGLTFLoader(loader, {}, renderer);
13683
- } catch {
13684
- renderer = null;
13685
- configureGLTFLoader(loader, {});
13789
+ function collectNodeNames(scene) {
13790
+ const names = /* @__PURE__ */ new Set();
13791
+ scene.traverse((child) => {
13792
+ const mesh = child;
13793
+ if (mesh.isMesh && mesh.geometry && child.name) {
13794
+ names.add(child.name);
13686
13795
  }
13687
- } else {
13688
- configureGLTFLoader(loader, {});
13689
- }
13690
- return new Promise((resolve, reject) => {
13691
- loader.load(
13692
- modelUrl,
13693
- (gltf) => {
13694
- const names = /* @__PURE__ */ new Set();
13695
- gltf.scene.traverse((child) => {
13696
- if (child?.isMesh && child.geometry && child.name) {
13697
- names.add(child.name);
13698
- }
13699
- });
13700
- resolve({
13701
- nodeNames: Array.from(names).sort((a, b) => a.localeCompare(b)),
13702
- animations: resolveAnimationClips(gltf.animations).map(
13703
- ({ sourceName, duration }) => ({
13704
- sourceName,
13705
- duration
13706
- })
13707
- )
13708
- });
13709
- renderer?.dispose();
13710
- },
13711
- void 0,
13712
- (error) => {
13713
- renderer?.dispose();
13714
- reject(error);
13715
- }
13716
- );
13717
13796
  });
13797
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
13718
13798
  }
13719
13799
 
13720
13800
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
@@ -14260,6 +14340,8 @@ function BindingBuilder({ licenseKey }) {
14260
14340
  const uploadedModelRef = useRef18(null);
14261
14341
  const previewDragDepthRef = useRef18(0);
14262
14342
  const loadRequestRef = useRef18(0);
14343
+ const pendingLoadRef = useRef18(null);
14344
+ const loadedSceneRef = useRef18(null);
14263
14345
  const {
14264
14346
  cameraState,
14265
14347
  resetView,
@@ -14317,7 +14399,7 @@ function BindingBuilder({ licenseKey }) {
14317
14399
  },
14318
14400
  []
14319
14401
  );
14320
- useEffect19(() => {
14402
+ useEffect20(() => {
14321
14403
  if (!selectedBinding) {
14322
14404
  setMetricsDraft("{}");
14323
14405
  setMetadataDraft("{}");
@@ -14330,7 +14412,7 @@ function BindingBuilder({ licenseKey }) {
14330
14412
  setMetricsError(null);
14331
14413
  setMetadataError(null);
14332
14414
  }, [selectedBinding]);
14333
- useEffect19(() => {
14415
+ useEffect20(() => {
14334
14416
  const derived = createMaterialPanelEnabledState(selectedMaterial);
14335
14417
  if (materialPanelItemRef.current !== selectedItem) {
14336
14418
  materialPanelItemRef.current = selectedItem;
@@ -14349,12 +14431,12 @@ function BindingBuilder({ licenseKey }) {
14349
14431
  return changed ? next : prev;
14350
14432
  });
14351
14433
  }, [selectedItem, selectedMaterial]);
14352
- useEffect19(() => {
14434
+ useEffect20(() => {
14353
14435
  return () => {
14354
14436
  uploadedModelRef.current?.cleanup();
14355
14437
  };
14356
14438
  }, []);
14357
- useEffect19(() => {
14439
+ useEffect20(() => {
14358
14440
  const handleKeyDown = (event) => {
14359
14441
  if (event.key.toLowerCase() !== "z" || !(event.metaKey || event.ctrlKey)) {
14360
14442
  return;
@@ -14375,7 +14457,7 @@ function BindingBuilder({ licenseKey }) {
14375
14457
  window.addEventListener("keydown", handleKeyDown);
14376
14458
  return () => window.removeEventListener("keydown", handleKeyDown);
14377
14459
  }, [activeEditorTab, bindingsHistory, sceneConfigHistory]);
14378
- useEffect19(() => {
14460
+ useEffect20(() => {
14379
14461
  if (loading || statusTone === "default" || statusTone === "success") return;
14380
14462
  setStatusBannerVisible(true);
14381
14463
  const timeout = window.setTimeout(
@@ -14428,42 +14510,76 @@ function BindingBuilder({ licenseKey }) {
14428
14510
  });
14429
14511
  }
14430
14512
  };
14431
- const generateBindingsForModel = async (nextModelUrl, nextModelName) => {
14513
+ const applyBindingsFromScene = useCallback8(
14514
+ (scene, nextModelName) => {
14515
+ const nodeNames = collectNodeNames(scene);
14516
+ bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
14517
+ setSelectedItem(nodeNames[0] ?? null);
14518
+ setSceneConfig((prev) => ({ ...prev, annotations: [] }));
14519
+ setStatusMessage(
14520
+ `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14521
+ );
14522
+ setStatusTone("success");
14523
+ setLoading(false);
14524
+ },
14525
+ [bindingsHistory, setSceneConfig]
14526
+ );
14527
+ const generateBindingsForModel = (nextModelUrl, nextModelName) => {
14432
14528
  const requestId = ++loadRequestRef.current;
14433
14529
  setLoading(true);
14434
14530
  setStatusMessage(
14435
14531
  `Reading ${nextModelName} and generating starter bindings...`
14436
14532
  );
14437
14533
  setStatusTone("default");
14438
- try {
14439
- const { nodeNames, animations } = await collectModelMetadata(nextModelUrl);
14440
- if (requestId !== loadRequestRef.current) return;
14441
- const nextBindings = buildBindingsFromNodes(nodeNames);
14442
- setModelUrl(nextModelUrl);
14443
- setModelName(nextModelName);
14444
- bindingsHistory.reset(nextBindings);
14445
- setSelectedItem(nodeNames[0] ?? null);
14446
- setActiveAnnotation(null);
14534
+ setModelName(nextModelName);
14535
+ setActiveAnnotation(null);
14536
+ const loaded = loadedSceneRef.current;
14537
+ if (nextModelUrl === modelUrl && loaded?.url === nextModelUrl) {
14538
+ pendingLoadRef.current = null;
14539
+ applyBindingsFromScene(loaded.scene, nextModelName);
14540
+ return;
14541
+ }
14542
+ pendingLoadRef.current = {
14543
+ requestId,
14544
+ modelName: nextModelName,
14545
+ modelUrl: nextModelUrl
14546
+ };
14547
+ setModelUrl(nextModelUrl);
14548
+ bindingsHistory.reset({});
14549
+ setSelectedItem(null);
14550
+ };
14551
+ const handleViewerModelLoaded = useCallback8(
14552
+ (scene) => {
14553
+ const pending = pendingLoadRef.current;
14554
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14555
+ pendingLoadRef.current = null;
14556
+ loadedSceneRef.current = { url: pending.modelUrl, scene };
14557
+ applyBindingsFromScene(scene, pending.modelName);
14558
+ },
14559
+ [applyBindingsFromScene]
14560
+ );
14561
+ const handleViewerLoadError = useCallback8((error) => {
14562
+ const pending = pendingLoadRef.current;
14563
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14564
+ pendingLoadRef.current = null;
14565
+ console.error(error);
14566
+ setStatusMessage(
14567
+ `Could not read ${pending.modelName}. Make sure the file is a valid GLB/GLTF asset.`
14568
+ );
14569
+ setStatusTone("error");
14570
+ setLoading(false);
14571
+ }, []);
14572
+ const handleAnimationsReady = useCallback8(
14573
+ (controls) => {
14574
+ viewerAnimations.handleAnimationsReady(controls);
14575
+ const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
14447
14576
  setSceneConfig((prev) => ({
14448
14577
  ...prev,
14449
- animations: mergeAnimationConfig(prev, animations),
14450
- annotations: []
14578
+ animations: mergeAnimationConfig(prev, details)
14451
14579
  }));
14452
- setStatusMessage(
14453
- `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14454
- );
14455
- setStatusTone("success");
14456
- } catch (error) {
14457
- if (requestId !== loadRequestRef.current) return;
14458
- console.error(error);
14459
- setStatusMessage(
14460
- `Could not read ${nextModelName}. Make sure the file is a valid GLB/GLTF asset.`
14461
- );
14462
- setStatusTone("error");
14463
- } finally {
14464
- if (requestId === loadRequestRef.current) setLoading(false);
14465
- }
14466
- };
14580
+ },
14581
+ [viewerAnimations, setSceneConfig]
14582
+ );
14467
14583
  const handleLoadDemo = async () => {
14468
14584
  uploadedModelRef.current?.cleanup();
14469
14585
  uploadedModelRef.current = null;
@@ -14493,6 +14609,8 @@ function BindingBuilder({ licenseKey }) {
14493
14609
  const clearModel = () => {
14494
14610
  uploadedModelRef.current?.cleanup();
14495
14611
  uploadedModelRef.current = null;
14612
+ loadedSceneRef.current = null;
14613
+ pendingLoadRef.current = null;
14496
14614
  previewDragDepthRef.current = 0;
14497
14615
  setIsPreviewDragActive(false);
14498
14616
  setModelUrl(null);
@@ -15291,7 +15409,9 @@ function BindingBuilder({ licenseKey }) {
15291
15409
  onAnnotationsChange: handleAnnotationsChange,
15292
15410
  activeAnnotation: activeAnnotation?.annotation ?? null,
15293
15411
  onActiveAnnotationChange: handleActiveAnnotationChange,
15294
- onAnimationsReady: viewerAnimations.handleAnimationsReady,
15412
+ onAnimationsReady: handleAnimationsReady,
15413
+ onModelLoaded: handleViewerModelLoaded,
15414
+ onLoadError: handleViewerLoadError,
15295
15415
  licenseKey,
15296
15416
  sceneConfig,
15297
15417
  disableZoom: false,
@@ -15374,7 +15494,7 @@ import {
15374
15494
  useRef as useRef19,
15375
15495
  useState as useState25,
15376
15496
  useCallback as useCallback9,
15377
- useEffect as useEffect20,
15497
+ useEffect as useEffect21,
15378
15498
  useMemo as useMemo17,
15379
15499
  Component as Component3,
15380
15500
  Suspense as Suspense3
@@ -15492,14 +15612,14 @@ function InnerModel({
15492
15612
  }
15493
15613
  return map;
15494
15614
  }, [meshMap]);
15495
- useEffect20(
15615
+ useEffect21(
15496
15616
  () => () => {
15497
15617
  Object.values(perKeyMaterials).forEach((m) => m.dispose());
15498
15618
  },
15499
15619
  [perKeyMaterials]
15500
15620
  );
15501
15621
  const notifiedUrl = useRef19(null);
15502
- useEffect20(() => {
15622
+ useEffect21(() => {
15503
15623
  if (notifiedUrl.current === url) return;
15504
15624
  notifiedUrl.current = url;
15505
15625
  scene.updateWorldMatrix(true, true);
@@ -15507,22 +15627,22 @@ function InnerModel({
15507
15627
  onMeshesReady(meshMap);
15508
15628
  onModelLoaded(scene);
15509
15629
  }, [url, scene, meshMap, onMeshesReady, onModelLoaded]);
15510
- useEffect20(() => {
15630
+ useEffect21(() => {
15511
15631
  for (const [key, { ref }] of Object.entries(meshMap)) {
15512
15632
  nodeRefsOut.current[key] = ref;
15513
15633
  }
15514
15634
  }, [meshMap, nodeRefsOut]);
15515
- useEffect20(() => {
15635
+ useEffect21(() => {
15516
15636
  if (!highlightOnHover && hoveredKey !== null) {
15517
15637
  setHoveredKey(null);
15518
15638
  }
15519
15639
  }, [highlightOnHover, hoveredKey]);
15520
- useEffect20(() => {
15640
+ useEffect21(() => {
15521
15641
  return () => {
15522
15642
  document.body.style.cursor = "default";
15523
15643
  };
15524
15644
  }, []);
15525
- useEffect20(() => {
15645
+ useEffect21(() => {
15526
15646
  for (const [key, { ref }] of Object.entries(meshMap)) {
15527
15647
  ref.visible = !hiddenKeys.has(key);
15528
15648
  const mat = perKeyMaterials[key];
@@ -15599,7 +15719,7 @@ function CameraFitter({
15599
15719
  const invalidate = useThree11((state) => state.invalidate);
15600
15720
  const size = useThree11((state) => state.size);
15601
15721
  const resizeTimerRef = useRef19(null);
15602
- useEffect20(() => {
15722
+ useEffect21(() => {
15603
15723
  fitted.current = false;
15604
15724
  invalidate();
15605
15725
  }, [invalidate, scene]);
@@ -15619,7 +15739,7 @@ function CameraFitter({
15619
15739
  }).catch(() => {
15620
15740
  }).finally(onInitialFitComplete);
15621
15741
  });
15622
- useEffect20(() => {
15742
+ useEffect21(() => {
15623
15743
  if (!refitOnResize || !fitted.current || !scene || !controlsRef.current) {
15624
15744
  return;
15625
15745
  }
@@ -15647,7 +15767,7 @@ function CameraFitter({
15647
15767
  function ExposureController({ exposure }) {
15648
15768
  const gl = useThree11((state) => state.gl);
15649
15769
  const invalidate = useThree11((state) => state.invalidate);
15650
- useEffect20(() => {
15770
+ useEffect21(() => {
15651
15771
  gl.toneMappingExposure = exposure;
15652
15772
  invalidate();
15653
15773
  }, [gl, exposure, invalidate]);
@@ -16150,7 +16270,7 @@ function SimpleModelViewer({
16150
16270
  },
16151
16271
  [handleUploadedModel]
16152
16272
  );
16153
- useEffect20(() => {
16273
+ useEffect21(() => {
16154
16274
  return () => {
16155
16275
  revokeUploadedModelUrl();
16156
16276
  };
@@ -16161,10 +16281,10 @@ function SimpleModelViewer({
16161
16281
  },
16162
16282
  []
16163
16283
  );
16164
- useEffect20(() => {
16284
+ useEffect21(() => {
16165
16285
  setEnvSettings((prev) => ({ ...prev, backgroundColor }));
16166
16286
  }, [backgroundColor]);
16167
- useEffect20(() => {
16287
+ useEffect21(() => {
16168
16288
  resetModelState();
16169
16289
  }, [activeModelUrl, resetModelState]);
16170
16290
  const handleMeshesReady = useCallback9((meshes) => {
@@ -16726,7 +16846,7 @@ function useViewerActions(objectBindings, options = {}) {
16726
16846
  }
16727
16847
 
16728
16848
  // src/hooks/useShareableViewerState.ts
16729
- import { useCallback as useCallback15, useEffect as useEffect21, useRef as useRef20, useState as useState29 } from "react";
16849
+ import { useCallback as useCallback15, useEffect as useEffect22, useRef as useRef20, useState as useState29 } from "react";
16730
16850
  function serializeState(cameraState, objectBindings, selectedObjectBinding) {
16731
16851
  const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
16732
16852
  return {
@@ -16780,7 +16900,7 @@ function useShareableViewerState({
16780
16900
  };
16781
16901
  const [restoreAttempted, setRestoreAttempted] = useState29(false);
16782
16902
  const lastSyncedHashRef = useRef20(null);
16783
- useEffect21(() => {
16903
+ useEffect22(() => {
16784
16904
  if (!autoSync || !restoreAttempted) return;
16785
16905
  const tick = () => {
16786
16906
  const current = latestStateRef.current;
@@ -16857,7 +16977,7 @@ function useShareableViewerState({
16857
16977
  setCameraState,
16858
16978
  setSelectedObjectBinding
16859
16979
  ]);
16860
- useEffect21(() => {
16980
+ useEffect22(() => {
16861
16981
  const handleHashChange = () => {
16862
16982
  void restoreFromUrl();
16863
16983
  };