@liveroom-tech/react-immersive 1.1.8 → 1.2.1

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 +288 -184
  2. package/dist/index.mjs +311 -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,60 @@ 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
+ function countTriangles(geometries) {
2539
+ let total = 0;
2540
+ for (const geometry of geometries) {
2541
+ const count = geometry.index ? geometry.index.count : geometry.attributes.position.count;
2542
+ total += count / 3;
2543
+ }
2544
+ return total;
2545
+ }
2546
+ var MAX_BVH_TRIANGLES = 12e6;
2547
+ async function buildSceneBVH(root, signal) {
2548
+ const geometries = collectGeometries(root);
2549
+ if (geometries.length === 0) return;
2550
+ if (countTriangles(geometries) > MAX_BVH_TRIANGLES) return;
2551
+ enableAcceleratedRaycast();
2552
+ for (const geometry of geometries) {
2553
+ if (signal?.aborted) return;
2554
+ const tree = computeBoundsTree.call(geometry);
2555
+ await new Promise((resolve) => setTimeout(resolve, 0));
2556
+ if (signal?.aborted) return;
2557
+ geometry.boundsTree = tree;
2558
+ }
2559
+ }
2560
+ function disposeSceneBVH(root) {
2561
+ root.traverse((node) => {
2562
+ const geometry = node.geometry;
2563
+ if (!geometry?.boundsTree) return;
2564
+ disposeBoundsTree.call(geometry);
2565
+ geometry.boundsTree = void 0;
2566
+ });
2567
+ }
2568
+
2482
2569
  // src/components/Model.tsx
2483
2570
  import { jsx as jsx8 } from "react/jsx-runtime";
2484
2571
  function firstMaterial(node) {
@@ -2511,14 +2598,14 @@ function Model({
2511
2598
  const invalidate = useThree7((state) => state.invalidate);
2512
2599
  const [hoveredItem, setHoveredItem] = useState3(null);
2513
2600
  const groupRef = useRef5(null);
2514
- useEffect6(() => {
2601
+ useEffect7(() => {
2515
2602
  if (!highlightOnHover) {
2516
2603
  setHoveredItem(null);
2517
2604
  onItemHover?.(null);
2518
2605
  document.body.style.cursor = "default";
2519
2606
  }
2520
2607
  }, [highlightOnHover, onItemHover]);
2521
- useEffect6(() => {
2608
+ useEffect7(() => {
2522
2609
  return () => {
2523
2610
  document.body.style.cursor = "default";
2524
2611
  };
@@ -2554,7 +2641,7 @@ function Model({
2554
2641
  }
2555
2642
  return shell;
2556
2643
  }, [nodeMap]);
2557
- useEffect6(() => {
2644
+ useEffect7(() => {
2558
2645
  scene.traverse((node) => {
2559
2646
  if (!isMeshNode(node)) return;
2560
2647
  const mesh = node;
@@ -2562,9 +2649,17 @@ function Model({
2562
2649
  mesh.receiveShadow = true;
2563
2650
  });
2564
2651
  }, [scene, shellNodeNames]);
2565
- useEffect6(() => {
2652
+ useEffect7(() => {
2566
2653
  refreshSkinnedBounds(scene);
2567
2654
  }, [scene]);
2655
+ useEffect7(() => {
2656
+ const controller = new AbortController();
2657
+ void buildSceneBVH(scene, controller.signal);
2658
+ return () => {
2659
+ controller.abort();
2660
+ disposeSceneBVH(scene);
2661
+ };
2662
+ }, [scene]);
2568
2663
  const bindingKeyByNodeName = useMemo7(() => {
2569
2664
  const map = {};
2570
2665
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2593,10 +2688,10 @@ function Model({
2593
2688
  }
2594
2689
  return map;
2595
2690
  }, [nodeMap, nodeBindingSignature]);
2596
- useEffect6(() => {
2691
+ useEffect7(() => {
2597
2692
  if (materialRefsOut) materialRefsOut.current = perNodeMaterials;
2598
2693
  }, [perNodeMaterials, materialRefsOut]);
2599
- useEffect6(() => {
2694
+ useEffect7(() => {
2600
2695
  return () => {
2601
2696
  for (const material of Object.values(perNodeMaterials)) {
2602
2697
  material.dispose();
@@ -2647,7 +2742,7 @@ function Model({
2647
2742
  }
2648
2743
  return map;
2649
2744
  }, [perNodeMaterials, shadingMode, rendererMode]);
2650
- useEffect6(() => {
2745
+ useEffect7(() => {
2651
2746
  return () => {
2652
2747
  for (const material of Object.values(perNodeBasicMaterials))
2653
2748
  material.dispose();
@@ -2663,10 +2758,10 @@ function Model({
2663
2758
  if (rendererMode === "matcap") return perNodeMatcapMaterials[nodeName];
2664
2759
  return perNodeMaterials[nodeName];
2665
2760
  };
2666
- useEffect6(() => {
2761
+ useEffect7(() => {
2667
2762
  onModelLoaded?.(scene);
2668
2763
  }, [scene, onModelLoaded]);
2669
- useEffect6(() => {
2764
+ useEffect7(() => {
2670
2765
  if (!nodeRefsOut) return;
2671
2766
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
2672
2767
  const glbNodeName = binding.modelObjectId ?? nodeName;
@@ -2675,7 +2770,7 @@ function Model({
2675
2770
  }
2676
2771
  }, [nodeMap, objectBindings, nodeRefsOut]);
2677
2772
  const appliedTextureUrls = useRef5({});
2678
- useEffect6(() => {
2773
+ useEffect7(() => {
2679
2774
  return () => {
2680
2775
  for (const nodeTextures of Object.values(appliedTextureUrls.current)) {
2681
2776
  for (const [key, appliedUrl] of Object.entries(nodeTextures)) {
@@ -2690,7 +2785,7 @@ function Model({
2690
2785
  appliedTextureUrls.current = {};
2691
2786
  };
2692
2787
  }, []);
2693
- useEffect6(() => {
2788
+ useEffect7(() => {
2694
2789
  const seenNodeNames = /* @__PURE__ */ new Set();
2695
2790
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
2696
2791
  seenNodeNames.add(nodeName);
@@ -2797,7 +2892,7 @@ function Model({
2797
2892
  rendererMode,
2798
2893
  invalidate
2799
2894
  ]);
2800
- useEffect6(() => {
2895
+ useEffect7(() => {
2801
2896
  if (!wireframeEnabled) return;
2802
2897
  const added = [];
2803
2898
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2931,7 +3026,7 @@ function Model({
2931
3026
  emitPlaybackState();
2932
3027
  invalidate();
2933
3028
  };
2934
- useEffect6(() => {
3029
+ useEffect7(() => {
2935
3030
  const mixer = new AnimationMixer(scene);
2936
3031
  mixerRef.current = mixer;
2937
3032
  const actions = {};
@@ -2986,7 +3081,7 @@ function Model({
2986
3081
  emitPlaybackState();
2987
3082
  };
2988
3083
  }, [scene, resolvedAnimationClips]);
2989
- useEffect6(() => {
3084
+ useEffect7(() => {
2990
3085
  const currentClip = activeClipNameRef.current;
2991
3086
  const activeAction = activeActionRef.current;
2992
3087
  if (!currentClip || !activeAction) return;
@@ -3045,7 +3140,7 @@ function Model({
3045
3140
  const explodeStrengthRef = useRef5(0);
3046
3141
  const currentExplodeRef = useRef5(0);
3047
3142
  const explodeTmp = useMemo7(() => new Vector32(), []);
3048
- useEffect6(() => {
3143
+ useEffect7(() => {
3049
3144
  for (const part of explodePartsRef.current) {
3050
3145
  part.node.position.copy(part.restLocalPos);
3051
3146
  }
@@ -3094,7 +3189,7 @@ function Model({
3094
3189
  explodePartsRef.current = parts;
3095
3190
  invalidate();
3096
3191
  }, [scene, nodeMap, nodeBindingSignature, invalidate]);
3097
- useEffect6(() => {
3192
+ useEffect7(() => {
3098
3193
  invalidate();
3099
3194
  }, [explodeFactor, invalidate]);
3100
3195
  useFrame4((_, delta) => {
@@ -3172,7 +3267,7 @@ import {
3172
3267
  useMemo as useMemo8,
3173
3268
  useRef as useRef6,
3174
3269
  useState as useState4,
3175
- useEffect as useEffect7,
3270
+ useEffect as useEffect8,
3176
3271
  useCallback
3177
3272
  } from "react";
3178
3273
  import { Html } from "@react-three/drei";
@@ -3202,7 +3297,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3202
3297
  const worldPosition = useMemo8(() => new Vector33(), []);
3203
3298
  const biasedWorldPosition = useMemo8(() => new Vector33(), []);
3204
3299
  const toCamera = useMemo8(() => new Vector33(), []);
3205
- useEffect7(() => {
3300
+ useEffect8(() => {
3206
3301
  anchorNodeRef.current = null;
3207
3302
  }, [annotation.anchorNodeName]);
3208
3303
  useFrame5(({ camera }) => {
@@ -3248,7 +3343,7 @@ function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick,
3248
3343
  const [zIndex, setZIndex] = useState4(1);
3249
3344
  const clickTimeoutRef = useRef6(null);
3250
3345
  const markerRef = useRef6(null);
3251
- useEffect7(() => {
3346
+ useEffect8(() => {
3252
3347
  return () => {
3253
3348
  if (clickTimeoutRef.current) {
3254
3349
  clearTimeout(clickTimeoutRef.current);
@@ -3430,13 +3525,13 @@ function AnnotationsLayer({
3430
3525
  };
3431
3526
  invalidateFrameRef.current = window.requestAnimationFrame(step);
3432
3527
  }, [invalidate]);
3433
- useEffect7(() => {
3528
+ useEffect8(() => {
3434
3529
  requestAnnotationRefresh();
3435
3530
  }, [annotations, requestAnnotationRefresh]);
3436
- useEffect7(() => {
3531
+ useEffect8(() => {
3437
3532
  requestAnnotationRefresh();
3438
3533
  }, [activeAnnotation, requestAnnotationRefresh]);
3439
- useEffect7(() => {
3534
+ useEffect8(() => {
3440
3535
  return () => {
3441
3536
  if (invalidateFrameRef.current !== null) {
3442
3537
  window.cancelAnimationFrame(invalidateFrameRef.current);
@@ -3499,7 +3594,7 @@ function AnnotationsLayer({
3499
3594
  onCloseAnnotation?.();
3500
3595
  }
3501
3596
  }, [isLocked, onCloseAnnotation]);
3502
- useEffect7(() => {
3597
+ useEffect8(() => {
3503
3598
  const canvas = canvasRootRef.current;
3504
3599
  if (canvas) {
3505
3600
  canvas.addEventListener("click", handleCanvasClick);
@@ -3568,7 +3663,7 @@ function AnnotationsLayer({
3568
3663
  }
3569
3664
 
3570
3665
  // src/components/ModelViewer/MeasurementLayer.tsx
3571
- import { useEffect as useEffect8, useMemo as useMemo9 } from "react";
3666
+ import { useEffect as useEffect9, useMemo as useMemo9 } from "react";
3572
3667
  import { Html as Html2, Line } from "@react-three/drei";
3573
3668
  import { BoxGeometry, EdgesGeometry, Vector3 as Vector34 } from "three";
3574
3669
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -3603,7 +3698,7 @@ function DimensionsBox({ box, unit }) {
3603
3698
  );
3604
3699
  return geometry;
3605
3700
  }, [box.size]);
3606
- useEffect8(() => {
3701
+ useEffect9(() => {
3607
3702
  return () => {
3608
3703
  edges.dispose();
3609
3704
  };
@@ -3659,7 +3754,7 @@ function MeasurementLayer({
3659
3754
  import {
3660
3755
  forwardRef as forwardRef2,
3661
3756
  useCallback as useCallback2,
3662
- useEffect as useEffect9,
3757
+ useEffect as useEffect10,
3663
3758
  useImperativeHandle as useImperativeHandle2,
3664
3759
  useMemo as useMemo10,
3665
3760
  useRef as useRef7
@@ -3767,7 +3862,7 @@ var XRModelPresenter = forwardRef2(
3767
3862
  }),
3768
3863
  [cancelPlacementMode, finishPlacementMode, placeCurrent, startPlacementMode]
3769
3864
  );
3770
- useEffect9(() => {
3865
+ useEffect10(() => {
3771
3866
  if (!inSession) {
3772
3867
  placedPosition.current.set(0, 0, -FORWARD_DISTANCE);
3773
3868
  userScale.current = 1;
@@ -3815,7 +3910,7 @@ var XRModelPresenter = forwardRef2(
3815
3910
  },
3816
3911
  [isAR, placeCurrent]
3817
3912
  );
3818
- useEffect9(() => {
3913
+ useEffect10(() => {
3819
3914
  if (!isAR) return;
3820
3915
  const onTouchStart = (event) => {
3821
3916
  if (event.touches.length !== 2) return;
@@ -3900,7 +3995,7 @@ var XRModelPresenter = forwardRef2(
3900
3995
  var XRModelPresenter_default = XRModelPresenter;
3901
3996
 
3902
3997
  // src/components/ModelViewer/CinematicCamera.tsx
3903
- import { useEffect as useEffect10, useMemo as useMemo11, useRef as useRef8 } from "react";
3998
+ import { useEffect as useEffect11, useMemo as useMemo11, useRef as useRef8 } from "react";
3904
3999
  import { useFrame as useFrame7, useThree as useThree9 } from "@react-three/fiber";
3905
4000
  import {
3906
4001
  Box3 as Box32,
@@ -3951,7 +4046,7 @@ function CinematicCamera({
3951
4046
  const orbitRef = useRef8(null);
3952
4047
  const tmpPos = useMemo11(() => new Vector36(), []);
3953
4048
  const tmpTarget = useMemo11(() => new Vector36(), []);
3954
- useEffect10(() => {
4049
+ useEffect11(() => {
3955
4050
  if (!playing) {
3956
4051
  startedRef.current = false;
3957
4052
  return;
@@ -3976,7 +4071,7 @@ function CinematicCamera({
3976
4071
  startedRef.current = true;
3977
4072
  invalidate();
3978
4073
  }, [playing, hasPath, controlsRef, sceneRef, invalidate]);
3979
- useEffect10(() => {
4074
+ useEffect11(() => {
3980
4075
  if (!playing || !onUserInteract) return;
3981
4076
  const controls = controlsRef.current;
3982
4077
  if (!controls) return;
@@ -4034,7 +4129,7 @@ function CinematicCamera({
4034
4129
  }
4035
4130
 
4036
4131
  // src/hooks/useLicense.ts
4037
- import { useEffect as useEffect11, useState as useState5 } from "react";
4132
+ import { useEffect as useEffect12, useState as useState5 } from "react";
4038
4133
  var LOCAL_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
4039
4134
  var isLocalHostname = (hostname) => LOCAL_HOSTNAMES.includes(hostname) || hostname.endsWith(".local") || hostname.endsWith(".localhost");
4040
4135
  var LICENSE_SERVER = "https://react-immersive.liveroom.dev/api/validate-license";
@@ -4209,7 +4304,7 @@ var useLicense = (licenseKey) => {
4209
4304
  // window isn't available during SSR — useEffect below recomputes this on mount
4210
4305
  isLocalDomain: false
4211
4306
  });
4212
- useEffect11(() => {
4307
+ useEffect12(() => {
4213
4308
  if (!canUseBrowserAPI()) {
4214
4309
  return;
4215
4310
  }
@@ -4260,7 +4355,7 @@ var useLicense = (licenseKey) => {
4260
4355
  };
4261
4356
 
4262
4357
  // src/components/MouseController.tsx
4263
- import { useRef as useRef9, useState as useState6, useCallback as useCallback3, useEffect as useEffect12 } from "react";
4358
+ import { useRef as useRef9, useState as useState6, useCallback as useCallback3, useEffect as useEffect13 } from "react";
4264
4359
 
4265
4360
  // src/styles/MouseController.css
4266
4361
  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 +4559,7 @@ var MouseController = ({
4464
4559
  lastRepeatTimeRef.current = 0;
4465
4560
  }
4466
4561
  }, []);
4467
- useEffect12(() => {
4562
+ useEffect13(() => {
4468
4563
  window.addEventListener("mousemove", onJoystickMove);
4469
4564
  window.addEventListener("mouseup", onJoystickEnd);
4470
4565
  window.addEventListener("touchmove", onJoystickMove, { passive: false });
@@ -4695,7 +4790,7 @@ var MaterialPicker = ({
4695
4790
  };
4696
4791
 
4697
4792
  // src/components/ModelViewer/ObjectBindingDataPanel.tsx
4698
- import { Fragment as Fragment5, useEffect as useEffect13, useRef as useRef10, useState as useState7 } from "react";
4793
+ import { Fragment as Fragment5, useEffect as useEffect14, useRef as useRef10, useState as useState7 } from "react";
4699
4794
 
4700
4795
  // src/styles/ObjectBindingDataPanel.css
4701
4796
  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 +4825,7 @@ function ActionExtra({ content }) {
4730
4825
  const [expanded, setExpanded] = useState7(hasContent);
4731
4826
  const cachedRef = useRef10(content);
4732
4827
  if (hasContent) cachedRef.current = content;
4733
- useEffect13(() => {
4828
+ useEffect14(() => {
4734
4829
  if (hasContent) {
4735
4830
  setMounted(true);
4736
4831
  return;
@@ -4741,7 +4836,7 @@ function ActionExtra({ content }) {
4741
4836
  return () => clearTimeout(timer);
4742
4837
  }
4743
4838
  }, [hasContent, mounted]);
4744
- useEffect13(() => {
4839
+ useEffect14(() => {
4745
4840
  if (mounted && hasContent) {
4746
4841
  const frame = requestAnimationFrame(() => setExpanded(true));
4747
4842
  return () => cancelAnimationFrame(frame);
@@ -4765,7 +4860,7 @@ function ObjectBindingDataPanel({
4765
4860
  renderActionExtra
4766
4861
  }) {
4767
4862
  const [displayedObject, setDisplayedObject] = useState7(selectedObject);
4768
- useEffect13(() => {
4863
+ useEffect14(() => {
4769
4864
  if (selectedObject) setDisplayedObject(selectedObject);
4770
4865
  }, [selectedObject]);
4771
4866
  const objectToRender = selectedObject ?? displayedObject;
@@ -5170,7 +5265,7 @@ function GuidedTourCameraFollow({
5170
5265
  const goal = useMemo13(() => new Vector37(), []);
5171
5266
  const nodeName = annotation?.anchorNodeName;
5172
5267
  const localPosition = annotation?.anchorLocalPosition;
5173
- useEffect14(() => {
5268
+ useEffect15(() => {
5174
5269
  nodeRef.current = null;
5175
5270
  hasPrev.current = false;
5176
5271
  }, [nodeName, annotation?.id]);
@@ -5215,6 +5310,14 @@ function computeMeasureBox(scene) {
5215
5310
  size: [size.x, size.y, size.z]
5216
5311
  };
5217
5312
  }
5313
+ function computeModelBounds(scene) {
5314
+ const box = new Box33().setFromObject(scene);
5315
+ if (box.isEmpty()) return null;
5316
+ const center = box.getCenter(new Vector37());
5317
+ const radius = box.getSize(new Vector37()).length() / 2;
5318
+ if (!Number.isFinite(radius) || radius <= 0) return null;
5319
+ return { center: [center.x, center.y, center.z], radius };
5320
+ }
5218
5321
  function fitClippingPlanesToScene(scene, controls) {
5219
5322
  const camera = controls?.camera;
5220
5323
  if (!(camera instanceof PerspectiveCamera3)) return;
@@ -5315,7 +5418,7 @@ function ModelViewer({
5315
5418
  [performanceProfile]
5316
5419
  );
5317
5420
  const controlsRef = useRef11(null);
5318
- useEffect14(() => {
5421
+ useEffect15(() => {
5319
5422
  if (!controlsRef.current) return;
5320
5423
  controlsRef.current.dollySpeed = Math.max(zoomSensitivity, 0);
5321
5424
  }, [zoomSensitivity]);
@@ -5325,7 +5428,7 @@ function ModelViewer({
5325
5428
  const pendingObjectId = useRef11(null);
5326
5429
  const rendererCaptureRef = useRef11(null);
5327
5430
  const createdBlobUrls = useRef11({});
5328
- useEffect14(() => {
5431
+ useEffect15(() => {
5329
5432
  return () => {
5330
5433
  for (const url of Object.values(createdBlobUrls.current)) {
5331
5434
  URL.revokeObjectURL(url);
@@ -5384,13 +5487,14 @@ function ModelViewer({
5384
5487
  const [measurePoints, setMeasurePoints] = useState10([]);
5385
5488
  const [showDimensions, setShowDimensions] = useState10(false);
5386
5489
  const [measureBox, setMeasureBox] = useState10(null);
5490
+ const [modelBounds, setModelBounds] = useState10(null);
5387
5491
  const measureModeRef = useRef11(false);
5388
5492
  measureModeRef.current = measureMode;
5389
5493
  const measurePointCounter = useRef11(0);
5390
5494
  const measureDownRef = useRef11(null);
5391
5495
  const [explodeFactor, setExplodeFactor] = useState10(0);
5392
5496
  const [explodeOpen, setExplodeOpen] = useState10(false);
5393
- useEffect14(() => {
5497
+ useEffect15(() => {
5394
5498
  setExplodeFactor(0);
5395
5499
  setExplodeOpen(false);
5396
5500
  }, [modelUrl]);
@@ -5411,11 +5515,11 @@ function ModelViewer({
5411
5515
  }, [cinematic, resolvedSceneConfig]);
5412
5516
  const [cinematicPlaying, setCinematicPlaying] = useState10(false);
5413
5517
  const cinematicAutoStartedRef = useRef11(false);
5414
- useEffect14(() => {
5518
+ useEffect15(() => {
5415
5519
  setCinematicPlaying(false);
5416
5520
  cinematicAutoStartedRef.current = false;
5417
5521
  }, [modelUrl]);
5418
- useEffect14(() => {
5522
+ useEffect15(() => {
5419
5523
  if (!cinematicConfig?.autoPlay || cinematicAutoStartedRef.current) return;
5420
5524
  if (modelLoaded && cameraPositioned && !isAutoFraming) {
5421
5525
  cinematicAutoStartedRef.current = true;
@@ -5443,7 +5547,7 @@ function ModelViewer({
5443
5547
  const [isHandheld, setIsHandheld] = useState10(true);
5444
5548
  const [resolvedMobileHandoffUrl, setResolvedMobileHandoffUrl] = useState10(null);
5445
5549
  const [isMobileHandoffOpen, setIsMobileHandoffOpen] = useState10(false);
5446
- useEffect14(() => {
5550
+ useEffect15(() => {
5447
5551
  setIsHandheld(isHandheldDevice());
5448
5552
  }, []);
5449
5553
  const resolveMobileHandoffUrl = useCallback4(() => {
@@ -5458,7 +5562,7 @@ function ModelViewer({
5458
5562
  return null;
5459
5563
  }
5460
5564
  }, [mobileHandoffUrl]);
5461
- useEffect14(() => {
5565
+ useEffect15(() => {
5462
5566
  if (!enableXR || isHandheld) {
5463
5567
  setResolvedMobileHandoffUrl(null);
5464
5568
  setIsMobileHandoffOpen(false);
@@ -5477,17 +5581,17 @@ function ModelViewer({
5477
5581
  DEFAULT_AR_OVERLAY_STATE
5478
5582
  );
5479
5583
  const [relocatePressed, setRelocatePressed] = useState10(false);
5480
- useEffect14(() => {
5584
+ useEffect15(() => {
5481
5585
  if (!xrStore) return;
5482
5586
  return xrStore.subscribe(
5483
5587
  (state) => setInARSession(state.session != null && state.mode === "immersive-ar")
5484
5588
  );
5485
5589
  }, [xrStore]);
5486
- useEffect14(() => {
5590
+ useEffect15(() => {
5487
5591
  setRelocatePressed(false);
5488
5592
  setArOverlayState(DEFAULT_AR_OVERLAY_STATE);
5489
5593
  }, [inARSession]);
5490
- useEffect14(() => {
5594
+ useEffect15(() => {
5491
5595
  if (!enableXR) return;
5492
5596
  const xr = navigator.xr;
5493
5597
  if (!xr?.isSessionSupported) return;
@@ -5522,7 +5626,7 @@ function ModelViewer({
5522
5626
  setRelocatePressed(true);
5523
5627
  xrPresenterRef.current?.startPlacementMode();
5524
5628
  }, []);
5525
- useEffect14(() => {
5629
+ useEffect15(() => {
5526
5630
  if (!relocatePressed) return;
5527
5631
  const onPointerUp = () => finishRelocation(true);
5528
5632
  const onPointerCancel = () => finishRelocation(false);
@@ -5585,7 +5689,7 @@ function ModelViewer({
5585
5689
  const [annotations, setAnnotations] = useState10(
5586
5690
  () => sceneConfig?.annotations ?? []
5587
5691
  );
5588
- useEffect14(() => {
5692
+ useEffect15(() => {
5589
5693
  if (annotations.length > 0) {
5590
5694
  const maxId = annotations.reduce((max, ann) => Math.max(max, ann.id), 0);
5591
5695
  annotationCounterRef.current = maxId;
@@ -5751,23 +5855,23 @@ function ModelViewer({
5751
5855
  );
5752
5856
  }
5753
5857
  }, [setActiveAnnotation]);
5754
- useEffect14(() => {
5858
+ useEffect15(() => {
5755
5859
  setCurrentAnnotationIndex(-1);
5756
5860
  setIsGuidedTourActive(false);
5757
5861
  }, [sortedAnnotations.length]);
5758
- useEffect14(() => {
5862
+ useEffect15(() => {
5759
5863
  return () => {
5760
5864
  if (annotationSmoothRestoreTimeoutRef.current) {
5761
5865
  clearTimeout(annotationSmoothRestoreTimeoutRef.current);
5762
5866
  }
5763
5867
  };
5764
5868
  }, []);
5765
- useEffect14(() => {
5869
+ useEffect15(() => {
5766
5870
  if (currentAnnotation && isGuidedTourActive) {
5767
5871
  setActiveAnnotation(currentAnnotation);
5768
5872
  }
5769
5873
  }, [currentAnnotation, setActiveAnnotation, isGuidedTourActive]);
5770
- useEffect14(() => {
5874
+ useEffect15(() => {
5771
5875
  if (sceneConfig?.annotations === void 0) {
5772
5876
  return;
5773
5877
  }
@@ -5796,7 +5900,7 @@ function ModelViewer({
5796
5900
  );
5797
5901
  return annotation ?? null;
5798
5902
  }, [activeAnnotation, annotations]);
5799
- useEffect14(() => {
5903
+ useEffect15(() => {
5800
5904
  if (activeAnnotation && !activeViewingAnnotation) {
5801
5905
  closeViewingAnnotation();
5802
5906
  }
@@ -5848,12 +5952,12 @@ function ModelViewer({
5848
5952
  );
5849
5953
  const isPanelOpen = showObjectBindingDataPanel && currentSelectedObject !== null && currentSelectedObjectKey !== null;
5850
5954
  const currentPanelObject = (currentSelectedObjectKey ? currentObjectBindings[currentSelectedObjectKey] : null) ?? currentSelectedObject;
5851
- useEffect14(() => {
5955
+ useEffect15(() => {
5852
5956
  if (!isControlledBindings) {
5853
5957
  setInternalObjectBindings(objectBindings);
5854
5958
  }
5855
5959
  }, [isControlledBindings, objectBindings]);
5856
- useEffect14(() => {
5960
+ useEffect15(() => {
5857
5961
  onHiddenObjectsChange?.(currentHiddenObjects);
5858
5962
  }, [currentHiddenObjects, onHiddenObjectsChange]);
5859
5963
  const applySelection = useCallback4(
@@ -6035,24 +6139,24 @@ function ModelViewer({
6035
6139
  bindingKeyIndex
6036
6140
  ]
6037
6141
  );
6038
- useEffect14(() => {
6142
+ useEffect15(() => {
6039
6143
  if (!colorPicker) return;
6040
6144
  window.addEventListener("pointerdown", closeColorPicker);
6041
6145
  return () => window.removeEventListener("pointerdown", closeColorPicker);
6042
6146
  }, [colorPicker, closeColorPicker]);
6043
- useEffect14(() => {
6147
+ useEffect15(() => {
6044
6148
  if (!materialPopup) return;
6045
6149
  const close = () => setMaterialPopup(null);
6046
6150
  window.addEventListener("pointerdown", close);
6047
6151
  return () => window.removeEventListener("pointerdown", close);
6048
6152
  }, [materialPopup]);
6049
- useEffect14(() => {
6153
+ useEffect15(() => {
6050
6154
  if (!downloadMenuOpen) return;
6051
6155
  const close = () => setDownloadMenuOpen(false);
6052
6156
  window.addEventListener("pointerdown", close);
6053
6157
  return () => window.removeEventListener("pointerdown", close);
6054
6158
  }, [downloadMenuOpen]);
6055
- useEffect14(() => {
6159
+ useEffect15(() => {
6056
6160
  if (!activeViewingAnnotation) return;
6057
6161
  const handleClickOutside = (event) => {
6058
6162
  if (viewAnnotationPopupRef.current && !viewAnnotationPopupRef.current.contains(event.target)) {
@@ -6070,7 +6174,7 @@ function ModelViewer({
6070
6174
  document.removeEventListener("touchstart", handleClickOutside);
6071
6175
  };
6072
6176
  }, [activeViewingAnnotation, closeViewingAnnotation]);
6073
- useEffect14(() => {
6177
+ useEffect15(() => {
6074
6178
  if (!pendingAnnotation) return;
6075
6179
  const handleClickOutside = (event) => {
6076
6180
  const target = event.target;
@@ -6217,7 +6321,7 @@ function ModelViewer({
6217
6321
  },
6218
6322
  [captureInitialCameraState, notifyViewerReady]
6219
6323
  );
6220
- useEffect14(() => {
6324
+ useEffect15(() => {
6221
6325
  fitAttemptedRef.current = false;
6222
6326
  fitInFlightRef.current = false;
6223
6327
  initialCameraStateRef.current = null;
@@ -6289,7 +6393,7 @@ function ModelViewer({
6289
6393
  notifyViewerReady,
6290
6394
  onAutoFit
6291
6395
  ]);
6292
- useEffect14(() => {
6396
+ useEffect15(() => {
6293
6397
  if (!refitOnResize) {
6294
6398
  return;
6295
6399
  }
@@ -6398,7 +6502,7 @@ function ModelViewer({
6398
6502
  },
6399
6503
  [applySelection, resolveBindingKey2, disableZoom]
6400
6504
  );
6401
- useEffect14(() => {
6505
+ useEffect15(() => {
6402
6506
  notifyViewerReady();
6403
6507
  }, [notifyViewerReady]);
6404
6508
  const handleCameraChangeInternal = useCallback4(
@@ -6412,6 +6516,7 @@ function ModelViewer({
6412
6516
  (scene) => {
6413
6517
  sceneRef.current = scene;
6414
6518
  setMeasureBox(computeMeasureBox(scene));
6519
+ setModelBounds(computeModelBounds(scene));
6415
6520
  setModelLoaded(true);
6416
6521
  setModelLoadFailed(false);
6417
6522
  setMismatchDismissed(false);
@@ -6429,7 +6534,7 @@ function ModelViewer({
6429
6534
  },
6430
6535
  [onLoadError]
6431
6536
  );
6432
- useEffect14(() => {
6537
+ useEffect15(() => {
6433
6538
  const scene = sceneRef.current;
6434
6539
  if (!modelLoaded || !scene) {
6435
6540
  setBindingsMismatch(false);
@@ -6490,7 +6595,7 @@ function ModelViewer({
6490
6595
  },
6491
6596
  []
6492
6597
  );
6493
- useEffect14(() => {
6598
+ useEffect15(() => {
6494
6599
  setMeasurePoints([]);
6495
6600
  }, [modelUrl, resolvedSceneConfig.model.rotation]);
6496
6601
  const handleDownloadMesh = useCallback4(() => {
@@ -6767,7 +6872,8 @@ function ModelViewer({
6767
6872
  shadowConfig: resolvedSceneConfig.shadows,
6768
6873
  shadowsEnabled: shadows,
6769
6874
  environmentRotation: resolvedSceneConfig.environment.rotation,
6770
- environmentConfig: resolvedSceneConfig.environment
6875
+ environmentConfig: resolvedSceneConfig.environment,
6876
+ modelBounds
6771
6877
  }
6772
6878
  ),
6773
6879
  /* @__PURE__ */ jsx19(
@@ -7327,7 +7433,7 @@ function ModelViewer({
7327
7433
  }
7328
7434
 
7329
7435
  // src/components/BindingBuilder/BindingBuilder.tsx
7330
- import { useCallback as useCallback8, useEffect as useEffect19, useMemo as useMemo16, useRef as useRef18, useState as useState24 } from "react";
7436
+ import { useCallback as useCallback8, useEffect as useEffect20, useMemo as useMemo16, useRef as useRef18, useState as useState24 } from "react";
7331
7437
  import {
7332
7438
  Redo2,
7333
7439
  Undo2,
@@ -7579,7 +7685,7 @@ function useViewerCamera() {
7579
7685
  }
7580
7686
 
7581
7687
  // src/hooks/useViewerAnimations.ts
7582
- import { useCallback as useCallback6, useEffect as useEffect15, useRef as useRef13, useState as useState12 } from "react";
7688
+ import { useCallback as useCallback6, useEffect as useEffect16, useRef as useRef13, useState as useState12 } from "react";
7583
7689
  function useViewerAnimations() {
7584
7690
  const controlsRef = useRef13(null);
7585
7691
  const [clips, setClips] = useState12([]);
@@ -7590,7 +7696,7 @@ function useViewerAnimations() {
7590
7696
  const [speed, setSpeedState] = useState12(1);
7591
7697
  const [time, setTime] = useState12(0);
7592
7698
  const [duration, setDuration] = useState12(0);
7593
- useEffect15(
7699
+ useEffect16(
7594
7700
  () => () => {
7595
7701
  unsubscribeRef.current?.();
7596
7702
  },
@@ -7852,7 +7958,7 @@ function inferMimeType(path) {
7852
7958
  }
7853
7959
 
7854
7960
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
7855
- import { useState as useState19, useEffect as useEffect17 } from "react";
7961
+ import { useState as useState19, useEffect as useEffect18 } from "react";
7856
7962
 
7857
7963
  // src/components/BindingBuilder/SceneSettings/GeneralSceneSettings.tsx
7858
7964
  import { useState as useState14 } from "react";
@@ -7880,9 +7986,9 @@ function getContrastTextColor(hex) {
7880
7986
  }
7881
7987
 
7882
7988
  // src/hooks/useClickOutside.ts
7883
- import { useEffect as useEffect16 } from "react";
7989
+ import { useEffect as useEffect17 } from "react";
7884
7990
  function useClickOutside(refs, onOutside, active) {
7885
- useEffect16(() => {
7991
+ useEffect17(() => {
7886
7992
  if (!active) return;
7887
7993
  const refList = Array.isArray(refs) ? refs : [refs];
7888
7994
  const handlePointerDown = (event) => {
@@ -11664,7 +11770,7 @@ function SceneSettingsTab({
11664
11770
  const { tier } = useBindingBuilderContext();
11665
11771
  const canUseStarterFeatures = hasTier(tier, "starter");
11666
11772
  const canUseGrowthFeatures = hasTier(tier, "growth");
11667
- useEffect17(() => {
11773
+ useEffect18(() => {
11668
11774
  if (controlledActiveSubTab !== void 0) {
11669
11775
  setInternalActiveSubTab(controlledActiveSubTab);
11670
11776
  }
@@ -13152,7 +13258,7 @@ function MaterialPresetGallery({
13152
13258
  }
13153
13259
 
13154
13260
  // src/components/BindingBuilder/components/ToolbarExportMenu.tsx
13155
- import { useEffect as useEffect18, useRef as useRef16, useState as useState22 } from "react";
13261
+ import { useEffect as useEffect19, useRef as useRef16, useState as useState22 } from "react";
13156
13262
  import { ChevronDown as ChevronDown5, Download as Download2, FileCode2, FileJson } from "lucide-react";
13157
13263
  import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
13158
13264
  function ToolbarExportMenu({
@@ -13163,7 +13269,7 @@ function ToolbarExportMenu({
13163
13269
  }) {
13164
13270
  const [open, setOpen] = useState22(false);
13165
13271
  const wrapperRef = useRef16(null);
13166
- useEffect18(() => {
13272
+ useEffect19(() => {
13167
13273
  if (!open) return;
13168
13274
  const close = (event) => {
13169
13275
  if (!wrapperRef.current?.contains(event.target)) {
@@ -13603,8 +13709,6 @@ function CameraPositionPanel({
13603
13709
  import { useMemo as useMemo15, useState as useState23 } from "react";
13604
13710
 
13605
13711
  // src/components/BindingBuilder/utils/bindingFactory.ts
13606
- import { GLTFLoader } from "three-stdlib";
13607
- import { WebGLRenderer as WebGLRenderer2 } from "three";
13608
13712
  function slugify(value) {
13609
13713
  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
13714
  }
@@ -13670,51 +13774,15 @@ function buildBindingsFromNodes(nodeNames) {
13670
13774
  nodeNames.map((nodeName) => [nodeName, createDefaultBinding(nodeName)])
13671
13775
  );
13672
13776
  }
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, {});
13777
+ function collectNodeNames(scene) {
13778
+ const names = /* @__PURE__ */ new Set();
13779
+ scene.traverse((child) => {
13780
+ const mesh = child;
13781
+ if (mesh.isMesh && mesh.geometry && child.name) {
13782
+ names.add(child.name);
13686
13783
  }
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
13784
  });
13785
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
13718
13786
  }
13719
13787
 
13720
13788
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
@@ -14260,6 +14328,8 @@ function BindingBuilder({ licenseKey }) {
14260
14328
  const uploadedModelRef = useRef18(null);
14261
14329
  const previewDragDepthRef = useRef18(0);
14262
14330
  const loadRequestRef = useRef18(0);
14331
+ const pendingLoadRef = useRef18(null);
14332
+ const loadedSceneRef = useRef18(null);
14263
14333
  const {
14264
14334
  cameraState,
14265
14335
  resetView,
@@ -14317,7 +14387,7 @@ function BindingBuilder({ licenseKey }) {
14317
14387
  },
14318
14388
  []
14319
14389
  );
14320
- useEffect19(() => {
14390
+ useEffect20(() => {
14321
14391
  if (!selectedBinding) {
14322
14392
  setMetricsDraft("{}");
14323
14393
  setMetadataDraft("{}");
@@ -14330,7 +14400,7 @@ function BindingBuilder({ licenseKey }) {
14330
14400
  setMetricsError(null);
14331
14401
  setMetadataError(null);
14332
14402
  }, [selectedBinding]);
14333
- useEffect19(() => {
14403
+ useEffect20(() => {
14334
14404
  const derived = createMaterialPanelEnabledState(selectedMaterial);
14335
14405
  if (materialPanelItemRef.current !== selectedItem) {
14336
14406
  materialPanelItemRef.current = selectedItem;
@@ -14349,12 +14419,12 @@ function BindingBuilder({ licenseKey }) {
14349
14419
  return changed ? next : prev;
14350
14420
  });
14351
14421
  }, [selectedItem, selectedMaterial]);
14352
- useEffect19(() => {
14422
+ useEffect20(() => {
14353
14423
  return () => {
14354
14424
  uploadedModelRef.current?.cleanup();
14355
14425
  };
14356
14426
  }, []);
14357
- useEffect19(() => {
14427
+ useEffect20(() => {
14358
14428
  const handleKeyDown = (event) => {
14359
14429
  if (event.key.toLowerCase() !== "z" || !(event.metaKey || event.ctrlKey)) {
14360
14430
  return;
@@ -14375,7 +14445,7 @@ function BindingBuilder({ licenseKey }) {
14375
14445
  window.addEventListener("keydown", handleKeyDown);
14376
14446
  return () => window.removeEventListener("keydown", handleKeyDown);
14377
14447
  }, [activeEditorTab, bindingsHistory, sceneConfigHistory]);
14378
- useEffect19(() => {
14448
+ useEffect20(() => {
14379
14449
  if (loading || statusTone === "default" || statusTone === "success") return;
14380
14450
  setStatusBannerVisible(true);
14381
14451
  const timeout = window.setTimeout(
@@ -14428,42 +14498,76 @@ function BindingBuilder({ licenseKey }) {
14428
14498
  });
14429
14499
  }
14430
14500
  };
14431
- const generateBindingsForModel = async (nextModelUrl, nextModelName) => {
14501
+ const applyBindingsFromScene = useCallback8(
14502
+ (scene, nextModelName) => {
14503
+ const nodeNames = collectNodeNames(scene);
14504
+ bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
14505
+ setSelectedItem(nodeNames[0] ?? null);
14506
+ setSceneConfig((prev) => ({ ...prev, annotations: [] }));
14507
+ setStatusMessage(
14508
+ `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14509
+ );
14510
+ setStatusTone("success");
14511
+ setLoading(false);
14512
+ },
14513
+ [bindingsHistory, setSceneConfig]
14514
+ );
14515
+ const generateBindingsForModel = (nextModelUrl, nextModelName) => {
14432
14516
  const requestId = ++loadRequestRef.current;
14433
14517
  setLoading(true);
14434
14518
  setStatusMessage(
14435
14519
  `Reading ${nextModelName} and generating starter bindings...`
14436
14520
  );
14437
14521
  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);
14522
+ setModelName(nextModelName);
14523
+ setActiveAnnotation(null);
14524
+ const loaded = loadedSceneRef.current;
14525
+ if (nextModelUrl === modelUrl && loaded?.url === nextModelUrl) {
14526
+ pendingLoadRef.current = null;
14527
+ applyBindingsFromScene(loaded.scene, nextModelName);
14528
+ return;
14529
+ }
14530
+ pendingLoadRef.current = {
14531
+ requestId,
14532
+ modelName: nextModelName,
14533
+ modelUrl: nextModelUrl
14534
+ };
14535
+ setModelUrl(nextModelUrl);
14536
+ bindingsHistory.reset({});
14537
+ setSelectedItem(null);
14538
+ };
14539
+ const handleViewerModelLoaded = useCallback8(
14540
+ (scene) => {
14541
+ const pending = pendingLoadRef.current;
14542
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14543
+ pendingLoadRef.current = null;
14544
+ loadedSceneRef.current = { url: pending.modelUrl, scene };
14545
+ applyBindingsFromScene(scene, pending.modelName);
14546
+ },
14547
+ [applyBindingsFromScene]
14548
+ );
14549
+ const handleViewerLoadError = useCallback8((error) => {
14550
+ const pending = pendingLoadRef.current;
14551
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14552
+ pendingLoadRef.current = null;
14553
+ console.error(error);
14554
+ setStatusMessage(
14555
+ `Could not read ${pending.modelName}. Make sure the file is a valid GLB/GLTF asset.`
14556
+ );
14557
+ setStatusTone("error");
14558
+ setLoading(false);
14559
+ }, []);
14560
+ const handleAnimationsReady = useCallback8(
14561
+ (controls) => {
14562
+ viewerAnimations.handleAnimationsReady(controls);
14563
+ const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
14447
14564
  setSceneConfig((prev) => ({
14448
14565
  ...prev,
14449
- animations: mergeAnimationConfig(prev, animations),
14450
- annotations: []
14566
+ animations: mergeAnimationConfig(prev, details)
14451
14567
  }));
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
- };
14568
+ },
14569
+ [viewerAnimations, setSceneConfig]
14570
+ );
14467
14571
  const handleLoadDemo = async () => {
14468
14572
  uploadedModelRef.current?.cleanup();
14469
14573
  uploadedModelRef.current = null;
@@ -14493,6 +14597,8 @@ function BindingBuilder({ licenseKey }) {
14493
14597
  const clearModel = () => {
14494
14598
  uploadedModelRef.current?.cleanup();
14495
14599
  uploadedModelRef.current = null;
14600
+ loadedSceneRef.current = null;
14601
+ pendingLoadRef.current = null;
14496
14602
  previewDragDepthRef.current = 0;
14497
14603
  setIsPreviewDragActive(false);
14498
14604
  setModelUrl(null);
@@ -15291,7 +15397,9 @@ function BindingBuilder({ licenseKey }) {
15291
15397
  onAnnotationsChange: handleAnnotationsChange,
15292
15398
  activeAnnotation: activeAnnotation?.annotation ?? null,
15293
15399
  onActiveAnnotationChange: handleActiveAnnotationChange,
15294
- onAnimationsReady: viewerAnimations.handleAnimationsReady,
15400
+ onAnimationsReady: handleAnimationsReady,
15401
+ onModelLoaded: handleViewerModelLoaded,
15402
+ onLoadError: handleViewerLoadError,
15295
15403
  licenseKey,
15296
15404
  sceneConfig,
15297
15405
  disableZoom: false,
@@ -15374,7 +15482,7 @@ import {
15374
15482
  useRef as useRef19,
15375
15483
  useState as useState25,
15376
15484
  useCallback as useCallback9,
15377
- useEffect as useEffect20,
15485
+ useEffect as useEffect21,
15378
15486
  useMemo as useMemo17,
15379
15487
  Component as Component3,
15380
15488
  Suspense as Suspense3
@@ -15492,14 +15600,14 @@ function InnerModel({
15492
15600
  }
15493
15601
  return map;
15494
15602
  }, [meshMap]);
15495
- useEffect20(
15603
+ useEffect21(
15496
15604
  () => () => {
15497
15605
  Object.values(perKeyMaterials).forEach((m) => m.dispose());
15498
15606
  },
15499
15607
  [perKeyMaterials]
15500
15608
  );
15501
15609
  const notifiedUrl = useRef19(null);
15502
- useEffect20(() => {
15610
+ useEffect21(() => {
15503
15611
  if (notifiedUrl.current === url) return;
15504
15612
  notifiedUrl.current = url;
15505
15613
  scene.updateWorldMatrix(true, true);
@@ -15507,22 +15615,22 @@ function InnerModel({
15507
15615
  onMeshesReady(meshMap);
15508
15616
  onModelLoaded(scene);
15509
15617
  }, [url, scene, meshMap, onMeshesReady, onModelLoaded]);
15510
- useEffect20(() => {
15618
+ useEffect21(() => {
15511
15619
  for (const [key, { ref }] of Object.entries(meshMap)) {
15512
15620
  nodeRefsOut.current[key] = ref;
15513
15621
  }
15514
15622
  }, [meshMap, nodeRefsOut]);
15515
- useEffect20(() => {
15623
+ useEffect21(() => {
15516
15624
  if (!highlightOnHover && hoveredKey !== null) {
15517
15625
  setHoveredKey(null);
15518
15626
  }
15519
15627
  }, [highlightOnHover, hoveredKey]);
15520
- useEffect20(() => {
15628
+ useEffect21(() => {
15521
15629
  return () => {
15522
15630
  document.body.style.cursor = "default";
15523
15631
  };
15524
15632
  }, []);
15525
- useEffect20(() => {
15633
+ useEffect21(() => {
15526
15634
  for (const [key, { ref }] of Object.entries(meshMap)) {
15527
15635
  ref.visible = !hiddenKeys.has(key);
15528
15636
  const mat = perKeyMaterials[key];
@@ -15599,7 +15707,7 @@ function CameraFitter({
15599
15707
  const invalidate = useThree11((state) => state.invalidate);
15600
15708
  const size = useThree11((state) => state.size);
15601
15709
  const resizeTimerRef = useRef19(null);
15602
- useEffect20(() => {
15710
+ useEffect21(() => {
15603
15711
  fitted.current = false;
15604
15712
  invalidate();
15605
15713
  }, [invalidate, scene]);
@@ -15619,7 +15727,7 @@ function CameraFitter({
15619
15727
  }).catch(() => {
15620
15728
  }).finally(onInitialFitComplete);
15621
15729
  });
15622
- useEffect20(() => {
15730
+ useEffect21(() => {
15623
15731
  if (!refitOnResize || !fitted.current || !scene || !controlsRef.current) {
15624
15732
  return;
15625
15733
  }
@@ -15647,7 +15755,7 @@ function CameraFitter({
15647
15755
  function ExposureController({ exposure }) {
15648
15756
  const gl = useThree11((state) => state.gl);
15649
15757
  const invalidate = useThree11((state) => state.invalidate);
15650
- useEffect20(() => {
15758
+ useEffect21(() => {
15651
15759
  gl.toneMappingExposure = exposure;
15652
15760
  invalidate();
15653
15761
  }, [gl, exposure, invalidate]);
@@ -16150,7 +16258,7 @@ function SimpleModelViewer({
16150
16258
  },
16151
16259
  [handleUploadedModel]
16152
16260
  );
16153
- useEffect20(() => {
16261
+ useEffect21(() => {
16154
16262
  return () => {
16155
16263
  revokeUploadedModelUrl();
16156
16264
  };
@@ -16161,10 +16269,10 @@ function SimpleModelViewer({
16161
16269
  },
16162
16270
  []
16163
16271
  );
16164
- useEffect20(() => {
16272
+ useEffect21(() => {
16165
16273
  setEnvSettings((prev) => ({ ...prev, backgroundColor }));
16166
16274
  }, [backgroundColor]);
16167
- useEffect20(() => {
16275
+ useEffect21(() => {
16168
16276
  resetModelState();
16169
16277
  }, [activeModelUrl, resetModelState]);
16170
16278
  const handleMeshesReady = useCallback9((meshes) => {
@@ -16726,7 +16834,7 @@ function useViewerActions(objectBindings, options = {}) {
16726
16834
  }
16727
16835
 
16728
16836
  // src/hooks/useShareableViewerState.ts
16729
- import { useCallback as useCallback15, useEffect as useEffect21, useRef as useRef20, useState as useState29 } from "react";
16837
+ import { useCallback as useCallback15, useEffect as useEffect22, useRef as useRef20, useState as useState29 } from "react";
16730
16838
  function serializeState(cameraState, objectBindings, selectedObjectBinding) {
16731
16839
  const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
16732
16840
  return {
@@ -16780,7 +16888,7 @@ function useShareableViewerState({
16780
16888
  };
16781
16889
  const [restoreAttempted, setRestoreAttempted] = useState29(false);
16782
16890
  const lastSyncedHashRef = useRef20(null);
16783
- useEffect21(() => {
16891
+ useEffect22(() => {
16784
16892
  if (!autoSync || !restoreAttempted) return;
16785
16893
  const tick = () => {
16786
16894
  const current = latestStateRef.current;
@@ -16857,7 +16965,7 @@ function useShareableViewerState({
16857
16965
  setCameraState,
16858
16966
  setSelectedObjectBinding
16859
16967
  ]);
16860
- useEffect21(() => {
16968
+ useEffect22(() => {
16861
16969
  const handleHashChange = () => {
16862
16970
  void restoreFromUrl();
16863
16971
  };