@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.js CHANGED
@@ -55,7 +55,7 @@ var import_react_dom = require("react-dom");
55
55
  var import_fiber11 = require("@react-three/fiber");
56
56
  var import_xr4 = require("@react-three/xr");
57
57
  var import_qrcode = require("qrcode.react");
58
- var import_three18 = require("three");
58
+ var import_three19 = require("three");
59
59
 
60
60
  // #style-inject:#style-inject
61
61
  function styleInject(css, { insertAt } = {}) {
@@ -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
@@ -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 = (0, import_react9.useRef)(null);
1648
1668
  const pointRef = (0, import_react9.useRef)(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 = (0, import_react9.useMemo)(
1683
+ () => light.type === "directional" && castShadow && modelBounds && !light.attachedToCamera ? fitShadowCamera(modelBounds, position) : null,
1684
+ [light.type, light.attachedToCamera, castShadow, modelBounds, position]
1685
+ );
1686
+ (0, import_react9.useEffect)(() => {
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__ */ (0, import_jsx_runtime7.jsx)(
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,28 +1767,29 @@ 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__ */ (0, import_jsx_runtime7.jsx)(
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
  ] });
@@ -1854,7 +1887,7 @@ function useConfiguredGLTF(url, config = {}) {
1854
1887
  }
1855
1888
 
1856
1889
  // src/components/Model.tsx
1857
- var import_three13 = require("three");
1890
+ var import_three14 = require("three");
1858
1891
  var import_three_stdlib3 = require("three-stdlib");
1859
1892
 
1860
1893
  // src/utils/animationClips.ts
@@ -2451,6 +2484,68 @@ function refreshSkinnedBounds(root) {
2451
2484
  });
2452
2485
  }
2453
2486
 
2487
+ // src/utils/bvh.ts
2488
+ var import_three13 = require("three");
2489
+ var import_three_mesh_bvh = require("three-mesh-bvh");
2490
+ var raycastPatched = false;
2491
+ function enableAcceleratedRaycast() {
2492
+ if (raycastPatched) return;
2493
+ import_three13.Mesh.prototype.raycast = import_three_mesh_bvh.acceleratedRaycast;
2494
+ raycastPatched = true;
2495
+ }
2496
+ function collectGeometries(root) {
2497
+ const seen = /* @__PURE__ */ new Set();
2498
+ root.traverse((node) => {
2499
+ const geometry = node.geometry;
2500
+ if (!geometry || !geometry.attributes?.position) return;
2501
+ if (geometry.boundsTree) return;
2502
+ seen.add(geometry);
2503
+ });
2504
+ return [...seen];
2505
+ }
2506
+ async function buildSceneBVH(root, signal) {
2507
+ const geometries = collectGeometries(root);
2508
+ if (geometries.length === 0) return;
2509
+ enableAcceleratedRaycast();
2510
+ let worker = null;
2511
+ if (typeof window !== "undefined" && typeof Worker !== "undefined") {
2512
+ try {
2513
+ const mod = await import("three-mesh-bvh/src/workers/ParallelMeshBVHWorker.js");
2514
+ worker = new mod.ParallelMeshBVHWorker();
2515
+ } catch {
2516
+ worker = null;
2517
+ }
2518
+ }
2519
+ try {
2520
+ for (const geometry of geometries) {
2521
+ if (signal?.aborted) return;
2522
+ let tree;
2523
+ if (worker) {
2524
+ try {
2525
+ tree = await worker.generate(geometry);
2526
+ } catch {
2527
+ }
2528
+ }
2529
+ if (tree === void 0) {
2530
+ tree = import_three_mesh_bvh.computeBoundsTree.call(geometry);
2531
+ await new Promise((resolve) => setTimeout(resolve, 0));
2532
+ }
2533
+ if (signal?.aborted) return;
2534
+ geometry.boundsTree = tree;
2535
+ }
2536
+ } finally {
2537
+ worker?.dispose?.();
2538
+ }
2539
+ }
2540
+ function disposeSceneBVH(root) {
2541
+ root.traverse((node) => {
2542
+ const geometry = node.geometry;
2543
+ if (!geometry?.boundsTree) return;
2544
+ import_three_mesh_bvh.disposeBoundsTree.call(geometry);
2545
+ geometry.boundsTree = void 0;
2546
+ });
2547
+ }
2548
+
2454
2549
  // src/components/Model.tsx
2455
2550
  var import_jsx_runtime8 = require("react/jsx-runtime");
2456
2551
  function firstMaterial(node) {
@@ -2508,17 +2603,17 @@ function Model({
2508
2603
  }, [scene]);
2509
2604
  const shellNodeNames = (0, import_react11.useMemo)(() => {
2510
2605
  const shell = /* @__PURE__ */ new Set();
2511
- const sceneBox = new import_three13.Box3();
2606
+ const sceneBox = new import_three14.Box3();
2512
2607
  const meshNodes = [];
2513
2608
  for (const [name, node] of Object.entries(nodeMap)) {
2514
2609
  if (!isMeshNode(node)) continue;
2515
- const box = new import_three13.Box3().setFromObject(node);
2610
+ const box = new import_three14.Box3().setFromObject(node);
2516
2611
  if (box.isEmpty()) continue;
2517
2612
  meshNodes.push({ name, box });
2518
2613
  sceneBox.union(box);
2519
2614
  }
2520
- const sceneSize = sceneBox.getSize(new import_three13.Vector3());
2521
- const size = new import_three13.Vector3();
2615
+ const sceneSize = sceneBox.getSize(new import_three14.Vector3());
2616
+ const size = new import_three14.Vector3();
2522
2617
  for (const { name, box } of meshNodes) {
2523
2618
  box.getSize(size);
2524
2619
  const isShell = size.x > sceneSize.x * 0.7 && size.z > sceneSize.z * 0.7 && size.y > sceneSize.y * 0.4;
@@ -2537,6 +2632,14 @@ function Model({
2537
2632
  (0, import_react11.useEffect)(() => {
2538
2633
  refreshSkinnedBounds(scene);
2539
2634
  }, [scene]);
2635
+ (0, import_react11.useEffect)(() => {
2636
+ const controller = new AbortController();
2637
+ void buildSceneBVH(scene, controller.signal);
2638
+ return () => {
2639
+ controller.abort();
2640
+ disposeSceneBVH(scene);
2641
+ };
2642
+ }, [scene]);
2540
2643
  const bindingKeyByNodeName = (0, import_react11.useMemo)(() => {
2541
2644
  const map = {};
2542
2645
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2584,7 +2687,7 @@ function Model({
2584
2687
  const map = {};
2585
2688
  if (shadingMode !== "unlit") return map;
2586
2689
  for (const [nodeName, material] of Object.entries(perNodeMaterials)) {
2587
- const basic = new import_three13.MeshBasicMaterial();
2690
+ const basic = new import_three14.MeshBasicMaterial();
2588
2691
  basic.color.copy(material.color);
2589
2692
  basic.map = material.map;
2590
2693
  basic.alphaMap = material.alphaMap;
@@ -2600,7 +2703,7 @@ function Model({
2600
2703
  const map = {};
2601
2704
  if (shadingMode !== "normals") return map;
2602
2705
  for (const nodeName of Object.keys(perNodeMaterials)) {
2603
- map[nodeName] = new import_three13.MeshNormalMaterial();
2706
+ map[nodeName] = new import_three14.MeshNormalMaterial();
2604
2707
  }
2605
2708
  return map;
2606
2709
  }, [perNodeMaterials, shadingMode]);
@@ -2609,7 +2712,7 @@ function Model({
2609
2712
  if (shadingMode !== "lit" || rendererMode !== "matcap") return map;
2610
2713
  const matcapTexture = getMatcapTexture();
2611
2714
  for (const [nodeName, material] of Object.entries(perNodeMaterials)) {
2612
- const matcap = new import_three13.MeshMatcapMaterial();
2715
+ const matcap = new import_three14.MeshMatcapMaterial();
2613
2716
  matcap.matcap = matcapTexture;
2614
2717
  matcap.color.copy(material.color);
2615
2718
  matcap.alphaMap = material.alphaMap;
@@ -2776,16 +2879,16 @@ function Model({
2776
2879
  const glbNodeName = binding.modelObjectId ?? nodeName;
2777
2880
  const node = nodeMap[glbNodeName];
2778
2881
  if (!isMeshNode(node)) continue;
2779
- const geometry = new import_three13.WireframeGeometry(
2882
+ const geometry = new import_three14.WireframeGeometry(
2780
2883
  node.geometry
2781
2884
  );
2782
- const lineMaterial = new import_three13.LineBasicMaterial({
2885
+ const lineMaterial = new import_three14.LineBasicMaterial({
2783
2886
  color: wireframeColor,
2784
2887
  transparent: true,
2785
2888
  opacity: wireframeOpacity,
2786
2889
  depthTest: false
2787
2890
  });
2788
- const line = new import_three13.LineSegments(geometry, lineMaterial);
2891
+ const line = new import_three14.LineSegments(geometry, lineMaterial);
2789
2892
  line.renderOrder = 1;
2790
2893
  node.add(line);
2791
2894
  added.push({ node, line });
@@ -2837,7 +2940,7 @@ function Model({
2837
2940
  const applyClipConfigToAction = (action, clipConfig) => {
2838
2941
  const loopMode = getClipLoopMode(clipConfig);
2839
2942
  action.setLoop(
2840
- loopMode === "once" ? import_three13.LoopOnce : import_three13.LoopRepeat,
2943
+ loopMode === "once" ? import_three14.LoopOnce : import_three14.LoopRepeat,
2841
2944
  loopMode === "once" ? 1 : Infinity
2842
2945
  );
2843
2946
  action.clampWhenFinished = loopMode === "once";
@@ -2904,7 +3007,7 @@ function Model({
2904
3007
  invalidate();
2905
3008
  };
2906
3009
  (0, import_react11.useEffect)(() => {
2907
- const mixer = new import_three13.AnimationMixer(scene);
3010
+ const mixer = new import_three14.AnimationMixer(scene);
2908
3011
  mixerRef.current = mixer;
2909
3012
  const actions = {};
2910
3013
  for (const { clip, sourceName } of resolvedAnimationClips) {
@@ -2995,17 +3098,17 @@ function Model({
2995
3098
  (0, import_fiber7.useFrame)((_, delta) => {
2996
3099
  if (!groupRef.current) return;
2997
3100
  const rotation = groupRef.current.rotation;
2998
- rotation.x = import_three13.MathUtils.lerp(
3101
+ rotation.x = import_three14.MathUtils.lerp(
2999
3102
  rotation.x,
3000
3103
  targetRotation[0],
3001
3104
  1 - Math.exp(-5 * delta)
3002
3105
  );
3003
- rotation.y = import_three13.MathUtils.lerp(
3106
+ rotation.y = import_three14.MathUtils.lerp(
3004
3107
  rotation.y,
3005
3108
  targetRotation[1],
3006
3109
  1 - Math.exp(-5 * delta)
3007
3110
  );
3008
- rotation.z = import_three13.MathUtils.lerp(
3111
+ rotation.z = import_three14.MathUtils.lerp(
3009
3112
  rotation.z,
3010
3113
  targetRotation[2],
3011
3114
  1 - Math.exp(-5 * delta)
@@ -3016,16 +3119,16 @@ function Model({
3016
3119
  const explodePartsRef = (0, import_react11.useRef)([]);
3017
3120
  const explodeStrengthRef = (0, import_react11.useRef)(0);
3018
3121
  const currentExplodeRef = (0, import_react11.useRef)(0);
3019
- const explodeTmp = (0, import_react11.useMemo)(() => new import_three13.Vector3(), []);
3122
+ const explodeTmp = (0, import_react11.useMemo)(() => new import_three14.Vector3(), []);
3020
3123
  (0, import_react11.useEffect)(() => {
3021
3124
  for (const part of explodePartsRef.current) {
3022
3125
  part.node.position.copy(part.restLocalPos);
3023
3126
  }
3024
3127
  currentExplodeRef.current = 0;
3025
3128
  scene.updateWorldMatrix(true, true);
3026
- const sceneBox = new import_three13.Box3().setFromObject(scene);
3027
- const modelCenter = sceneBox.getCenter(new import_three13.Vector3());
3028
- const radius = sceneBox.getSize(new import_three13.Vector3()).length() * 0.5;
3129
+ const sceneBox = new import_three14.Box3().setFromObject(scene);
3130
+ const modelCenter = sceneBox.getCenter(new import_three14.Vector3());
3131
+ const radius = sceneBox.getSize(new import_three14.Vector3()).length() * 0.5;
3029
3132
  explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
3030
3133
  const targets = [];
3031
3134
  const targetSet = /* @__PURE__ */ new Set();
@@ -3036,7 +3139,7 @@ function Model({
3036
3139
  targets.push(node);
3037
3140
  }
3038
3141
  const parts = [];
3039
- const partCenter = new import_three13.Vector3();
3142
+ const partCenter = new import_three14.Vector3();
3040
3143
  for (const node of targets) {
3041
3144
  if (!node.parent) continue;
3042
3145
  let ancestor = node.parent;
@@ -3049,7 +3152,7 @@ function Model({
3049
3152
  ancestor = ancestor.parent;
3050
3153
  }
3051
3154
  if (nested) continue;
3052
- const box = new import_three13.Box3().setFromObject(node);
3155
+ const box = new import_three14.Box3().setFromObject(node);
3053
3156
  if (box.isEmpty()) continue;
3054
3157
  box.getCenter(partCenter);
3055
3158
  const worldDir = partCenter.clone().sub(modelCenter);
@@ -3058,9 +3161,9 @@ function Model({
3058
3161
  parts.push({
3059
3162
  node,
3060
3163
  restLocalPos: node.position.clone(),
3061
- restWorldPos: node.getWorldPosition(new import_three13.Vector3()),
3164
+ restWorldPos: node.getWorldPosition(new import_three14.Vector3()),
3062
3165
  worldDir,
3063
- parentInverse: new import_three13.Matrix4().copy(node.parent.matrixWorld).invert()
3166
+ parentInverse: new import_three14.Matrix4().copy(node.parent.matrixWorld).invert()
3064
3167
  });
3065
3168
  }
3066
3169
  explodePartsRef.current = parts;
@@ -3078,7 +3181,7 @@ function Model({
3078
3181
  if (current === target) return;
3079
3182
  currentExplodeRef.current = target;
3080
3183
  } else {
3081
- currentExplodeRef.current = import_three13.MathUtils.lerp(
3184
+ currentExplodeRef.current = import_three14.MathUtils.lerp(
3082
3185
  current,
3083
3186
  target,
3084
3187
  1 - Math.exp(-6 * delta)
@@ -3143,7 +3246,7 @@ function Model({
3143
3246
  var import_react12 = require("react");
3144
3247
  var import_drei4 = require("@react-three/drei");
3145
3248
  var import_fiber8 = require("@react-three/fiber");
3146
- var import_three14 = require("three");
3249
+ var import_three15 = require("three");
3147
3250
 
3148
3251
  // src/styles/AnnotationsLayer.css
3149
3252
  styleInject(".ri-annotationMarker--hovered {\n transform: scale(1.08);\n box-shadow: 0 12px 28px rgba(15, 23, 42, 0.18);\n background: rgba(40, 40, 40, 0.92);\n}\n.ri-viewAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.ri-viewAnnotationTitle {\n margin: 0;\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n");
@@ -3156,7 +3259,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3156
3259
  const scene = (0, import_fiber8.useThree)((state) => state.scene);
3157
3260
  const anchorNodeRef = (0, import_react12.useRef)(null);
3158
3261
  const baseLocalPosition = (0, import_react12.useMemo)(
3159
- () => new import_three14.Vector3(
3262
+ () => new import_three15.Vector3(
3160
3263
  ...annotation.anchorLocalPosition ?? annotation.localPosition ?? annotation.worldPosition
3161
3264
  ),
3162
3265
  [
@@ -3165,9 +3268,9 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3165
3268
  annotation.worldPosition
3166
3269
  ]
3167
3270
  );
3168
- const worldPosition = (0, import_react12.useMemo)(() => new import_three14.Vector3(), []);
3169
- const biasedWorldPosition = (0, import_react12.useMemo)(() => new import_three14.Vector3(), []);
3170
- const toCamera = (0, import_react12.useMemo)(() => new import_three14.Vector3(), []);
3271
+ const worldPosition = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3272
+ const biasedWorldPosition = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3273
+ const toCamera = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3171
3274
  (0, import_react12.useEffect)(() => {
3172
3275
  anchorNodeRef.current = null;
3173
3276
  }, [annotation.anchorNodeName]);
@@ -3190,7 +3293,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3190
3293
  if (worldPositionsRef) {
3191
3294
  let stored = worldPositionsRef.current.get(annotation.id);
3192
3295
  if (!stored) {
3193
- stored = new import_three14.Vector3();
3296
+ stored = new import_three15.Vector3();
3194
3297
  worldPositionsRef.current.set(annotation.id, stored);
3195
3298
  }
3196
3299
  stored.copy(worldPosition);
@@ -3488,17 +3591,17 @@ function AnnotationsLayer({
3488
3591
  );
3489
3592
  (0, import_fiber8.useFrame)((_, delta) => {
3490
3593
  if (!groupRef.current) return;
3491
- groupRef.current.rotation.x = import_three14.MathUtils.lerp(
3594
+ groupRef.current.rotation.x = import_three15.MathUtils.lerp(
3492
3595
  groupRef.current.rotation.x,
3493
3596
  targetRotation[0],
3494
3597
  1 - Math.exp(-5 * delta)
3495
3598
  );
3496
- groupRef.current.rotation.y = import_three14.MathUtils.lerp(
3599
+ groupRef.current.rotation.y = import_three15.MathUtils.lerp(
3497
3600
  groupRef.current.rotation.y,
3498
3601
  targetRotation[1],
3499
3602
  1 - Math.exp(-5 * delta)
3500
3603
  );
3501
- groupRef.current.rotation.z = import_three14.MathUtils.lerp(
3604
+ groupRef.current.rotation.z = import_three15.MathUtils.lerp(
3502
3605
  groupRef.current.rotation.z,
3503
3606
  targetRotation[2],
3504
3607
  1 - Math.exp(-5 * delta)
@@ -3536,7 +3639,7 @@ function AnnotationsLayer({
3536
3639
  // src/components/ModelViewer/MeasurementLayer.tsx
3537
3640
  var import_react13 = require("react");
3538
3641
  var import_drei5 = require("@react-three/drei");
3539
- var import_three15 = require("three");
3642
+ var import_three16 = require("three");
3540
3643
  var import_jsx_runtime10 = require("react/jsx-runtime");
3541
3644
  var LINE_COLOR = "#22d3ee";
3542
3645
  var BOX_COLOR = "#facc15";
@@ -3564,8 +3667,8 @@ function DimensionsBox({ box, unit }) {
3564
3667
  );
3565
3668
  const edges = (0, import_react13.useMemo)(() => {
3566
3669
  const [w2, h2, d2] = box.size;
3567
- const geometry = new import_three15.EdgesGeometry(
3568
- new import_three15.BoxGeometry(w2 || 1e-4, h2 || 1e-4, d2 || 1e-4)
3670
+ const geometry = new import_three16.EdgesGeometry(
3671
+ new import_three16.BoxGeometry(w2 || 1e-4, h2 || 1e-4, d2 || 1e-4)
3569
3672
  );
3570
3673
  return geometry;
3571
3674
  }, [box.size]);
@@ -3611,7 +3714,7 @@ function MeasurementLayer({
3611
3714
  points.slice(1).map((point, index) => {
3612
3715
  const a = points[index].position;
3613
3716
  const b = point.position;
3614
- const distance = new import_three15.Vector3(...a).distanceTo(new import_three15.Vector3(...b));
3717
+ const distance = new import_three16.Vector3(...a).distanceTo(new import_three16.Vector3(...b));
3615
3718
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("group", { children: [
3616
3719
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_drei5.Line, { points: [a, b], color: LINE_COLOR, lineWidth: 2 }),
3617
3720
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MeasureLabel, { position: midpoint(a, b), children: formatMeasurement(distance, unit) })
@@ -3625,7 +3728,7 @@ function MeasurementLayer({
3625
3728
  var import_react14 = require("react");
3626
3729
  var import_xr3 = require("@react-three/xr");
3627
3730
  var import_fiber9 = require("@react-three/fiber");
3628
- var import_three16 = require("three");
3731
+ var import_three17 = require("three");
3629
3732
  var import_jsx_runtime11 = require("react/jsx-runtime");
3630
3733
  var TARGET_SIZE = 1;
3631
3734
  var FORWARD_DISTANCE = 1;
@@ -3662,13 +3765,13 @@ var XRModelPresenter = (0, import_react14.forwardRef)(
3662
3765
  () => box ? computeNormalization(box, scaleMode) : null,
3663
3766
  [box, scaleMode]
3664
3767
  );
3665
- const placedPosition = (0, import_react14.useRef)(new import_three16.Vector3(0, 0, -FORWARD_DISTANCE));
3768
+ const placedPosition = (0, import_react14.useRef)(new import_three17.Vector3(0, 0, -FORWARD_DISTANCE));
3666
3769
  const userScale = (0, import_react14.useRef)(1);
3667
3770
  const rotationY = (0, import_react14.useRef)(0);
3668
3771
  const placementMode = (0, import_react14.useRef)(true);
3669
3772
  const modelPlaced = (0, import_react14.useRef)(false);
3670
- const hitMatrix = (0, import_react14.useMemo)(() => new import_three16.Matrix4(), []);
3671
- const hitPosition = (0, import_react14.useRef)(new import_three16.Vector3());
3773
+ const hitMatrix = (0, import_react14.useMemo)(() => new import_three17.Matrix4(), []);
3774
+ const hitPosition = (0, import_react14.useRef)(new import_three17.Vector3());
3672
3775
  const hasHit = (0, import_react14.useRef)(false);
3673
3776
  const lastARState = (0, import_react14.useRef)(null);
3674
3777
  const gesturing = (0, import_react14.useRef)(false);
@@ -3861,7 +3964,7 @@ var XRModelPresenter_default = XRModelPresenter;
3861
3964
  // src/components/ModelViewer/CinematicCamera.tsx
3862
3965
  var import_react15 = require("react");
3863
3966
  var import_fiber10 = require("@react-three/fiber");
3864
- var import_three17 = require("three");
3967
+ var import_three18 = require("three");
3865
3968
  var ENTER_BLEND_SECONDS = 1.2;
3866
3969
  function CinematicCamera({
3867
3970
  controlsRef,
@@ -3883,14 +3986,14 @@ function CinematicCamera({
3883
3986
  const curves = (0, import_react15.useMemo)(() => {
3884
3987
  if (!hasPath) return null;
3885
3988
  return {
3886
- position: new import_three17.CatmullRomCurve3(
3887
- waypoints.map((w) => new import_three17.Vector3(...w.position)),
3989
+ position: new import_three18.CatmullRomCurve3(
3990
+ waypoints.map((w) => new import_three18.Vector3(...w.position)),
3888
3991
  loop,
3889
3992
  "catmullrom",
3890
3993
  0.5
3891
3994
  ),
3892
- target: new import_three17.CatmullRomCurve3(
3893
- waypoints.map((w) => new import_three17.Vector3(...w.target)),
3995
+ target: new import_three18.CatmullRomCurve3(
3996
+ waypoints.map((w) => new import_three18.Vector3(...w.target)),
3894
3997
  loop,
3895
3998
  "catmullrom",
3896
3999
  0.5
@@ -3899,12 +4002,12 @@ function CinematicCamera({
3899
4002
  }, [signature, hasPath, loop]);
3900
4003
  const elapsedRef = (0, import_react15.useRef)(0);
3901
4004
  const enterElapsedRef = (0, import_react15.useRef)(0);
3902
- const enterPos = (0, import_react15.useRef)(new import_three17.Vector3());
3903
- const enterTarget = (0, import_react15.useRef)(new import_three17.Vector3());
4005
+ const enterPos = (0, import_react15.useRef)(new import_three18.Vector3());
4006
+ const enterTarget = (0, import_react15.useRef)(new import_three18.Vector3());
3904
4007
  const startedRef = (0, import_react15.useRef)(false);
3905
4008
  const orbitRef = (0, import_react15.useRef)(null);
3906
- const tmpPos = (0, import_react15.useMemo)(() => new import_three17.Vector3(), []);
3907
- const tmpTarget = (0, import_react15.useMemo)(() => new import_three17.Vector3(), []);
4009
+ const tmpPos = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
4010
+ const tmpTarget = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
3908
4011
  (0, import_react15.useEffect)(() => {
3909
4012
  if (!playing) {
3910
4013
  startedRef.current = false;
@@ -3920,7 +4023,7 @@ function CinematicCamera({
3920
4023
  if (!hasPath) {
3921
4024
  const scene = sceneRef.current;
3922
4025
  if (scene) {
3923
- const sphere = new import_three17.Box3().setFromObject(scene).getBoundingSphere(new import_three17.Sphere());
4026
+ const sphere = new import_three18.Box3().setFromObject(scene).getBoundingSphere(new import_three18.Sphere());
3924
4027
  orbitRef.current = {
3925
4028
  center: sphere.center.clone(),
3926
4029
  radius: sphere.radius > 0 ? sphere.radius : 1
@@ -5049,7 +5152,7 @@ function findInfluencingBone(hit) {
5049
5152
  if (!skinIndex || !skinWeight || !position) return null;
5050
5153
  const localHit = mesh.worldToLocal(hit.point.clone());
5051
5154
  const vertices = [hit.face.a, hit.face.b, hit.face.c];
5052
- const vertex = new import_three18.Vector3();
5155
+ const vertex = new import_three19.Vector3();
5053
5156
  let nearest = vertices[0];
5054
5157
  let nearestDist = Infinity;
5055
5158
  for (const vi of vertices) {
@@ -5099,9 +5202,9 @@ function GuidedTourCameraFollow({
5099
5202
  const scene = (0, import_fiber11.useThree)((state) => state.scene);
5100
5203
  const nodeRef = (0, import_react22.useRef)(null);
5101
5204
  const hasPrev = (0, import_react22.useRef)(false);
5102
- const live = (0, import_react22.useMemo)(() => new import_three18.Vector3(), []);
5103
- const prev = (0, import_react22.useMemo)(() => new import_three18.Vector3(), []);
5104
- const goal = (0, import_react22.useMemo)(() => new import_three18.Vector3(), []);
5205
+ const live = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5206
+ const prev = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5207
+ const goal = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5105
5208
  const nodeName = annotation?.anchorNodeName;
5106
5209
  const localPosition = annotation?.anchorLocalPosition;
5107
5210
  (0, import_react22.useEffect)(() => {
@@ -5140,8 +5243,8 @@ var DEFAULT_AR_OVERLAY_STATE = {
5140
5243
  isModelPlaced: false
5141
5244
  };
5142
5245
  function computeMeasureBox(scene) {
5143
- const box = new import_three18.Box3().setFromObject(scene);
5144
- const size = new import_three18.Vector3();
5246
+ const box = new import_three19.Box3().setFromObject(scene);
5247
+ const size = new import_three19.Vector3();
5145
5248
  box.getSize(size);
5146
5249
  return {
5147
5250
  min: [box.min.x, box.min.y, box.min.z],
@@ -5149,11 +5252,19 @@ function computeMeasureBox(scene) {
5149
5252
  size: [size.x, size.y, size.z]
5150
5253
  };
5151
5254
  }
5255
+ function computeModelBounds(scene) {
5256
+ const box = new import_three19.Box3().setFromObject(scene);
5257
+ if (box.isEmpty()) return null;
5258
+ const center = box.getCenter(new import_three19.Vector3());
5259
+ const radius = box.getSize(new import_three19.Vector3()).length() / 2;
5260
+ if (!Number.isFinite(radius) || radius <= 0) return null;
5261
+ return { center: [center.x, center.y, center.z], radius };
5262
+ }
5152
5263
  function fitClippingPlanesToScene(scene, controls) {
5153
5264
  const camera = controls?.camera;
5154
- if (!(camera instanceof import_three18.PerspectiveCamera)) return;
5155
- const box = new import_three18.Box3().setFromObject(scene);
5156
- const size = box.getSize(new import_three18.Vector3()).length();
5265
+ if (!(camera instanceof import_three19.PerspectiveCamera)) return;
5266
+ const box = new import_three19.Box3().setFromObject(scene);
5267
+ const size = box.getSize(new import_three19.Vector3()).length();
5157
5268
  if (!Number.isFinite(size) || size <= 0) return;
5158
5269
  camera.near = size / 100;
5159
5270
  camera.far = size * 100;
@@ -5306,8 +5417,8 @@ function ModelViewer({
5306
5417
  if (!controls || initialCameraStateRef.current) {
5307
5418
  return;
5308
5419
  }
5309
- const position = controls.getPosition(new import_three18.Vector3());
5310
- const target = controls.getTarget(new import_three18.Vector3());
5420
+ const position = controls.getPosition(new import_three19.Vector3());
5421
+ const target = controls.getTarget(new import_three19.Vector3());
5311
5422
  initialCameraStateRef.current = {
5312
5423
  position: [position.x, position.y, position.z],
5313
5424
  target: [target.x, target.y, target.z]
@@ -5318,6 +5429,7 @@ function ModelViewer({
5318
5429
  const [measurePoints, setMeasurePoints] = (0, import_react22.useState)([]);
5319
5430
  const [showDimensions, setShowDimensions] = (0, import_react22.useState)(false);
5320
5431
  const [measureBox, setMeasureBox] = (0, import_react22.useState)(null);
5432
+ const [modelBounds, setModelBounds] = (0, import_react22.useState)(null);
5321
5433
  const measureModeRef = (0, import_react22.useRef)(false);
5322
5434
  measureModeRef.current = measureMode;
5323
5435
  const measurePointCounter = (0, import_react22.useRef)(0);
@@ -5561,7 +5673,7 @@ function ModelViewer({
5561
5673
  const controls = controlsRef.current;
5562
5674
  const scene = sceneRef.current;
5563
5675
  if (!controls) return;
5564
- const target = new import_three18.Vector3();
5676
+ const target = new import_three19.Vector3();
5565
5677
  const livePosition = annotationWorldPositionsRef.current.get(
5566
5678
  annotation.id
5567
5679
  );
@@ -5579,15 +5691,15 @@ function ModelViewer({
5579
5691
  controls.smoothTime = 0.6;
5580
5692
  const storedNormal = annotation.worldNormal;
5581
5693
  if (scene) {
5582
- const box = new import_three18.Box3().setFromObject(scene);
5583
- const modelCenter = new import_three18.Vector3();
5694
+ const box = new import_three19.Box3().setFromObject(scene);
5695
+ const modelCenter = new import_three19.Vector3();
5584
5696
  box.getCenter(modelCenter);
5585
- const modelSize = new import_three18.Vector3();
5697
+ const modelSize = new import_three19.Vector3();
5586
5698
  box.getSize(modelSize);
5587
5699
  const modelRadius = modelSize.length() * 0.2;
5588
5700
  let outwardDir;
5589
5701
  if (storedNormal) {
5590
- outwardDir = new import_three18.Vector3(
5702
+ outwardDir = new import_three19.Vector3(
5591
5703
  storedNormal[0],
5592
5704
  storedNormal[1],
5593
5705
  storedNormal[2]
@@ -5885,8 +5997,8 @@ function ModelViewer({
5885
5997
  };
5886
5998
  if (canvasContainer && cameraControls && object) {
5887
5999
  const canvasRect = canvasContainer.getBoundingClientRect();
5888
- const objectCenter = new import_three18.Vector3();
5889
- new import_three18.Box3().setFromObject(object).getCenter(objectCenter);
6000
+ const objectCenter = new import_three19.Vector3();
6001
+ new import_three19.Box3().setFromObject(object).getCenter(objectCenter);
5890
6002
  objectCenter.project(cameraControls);
5891
6003
  const projectedX = (objectCenter.x + 1) / 2 * canvasRect.width + (canvasRect.left - rect.left);
5892
6004
  const projectedY = (1 - objectCenter.y) / 2 * canvasRect.height + (canvasRect.top - rect.top);
@@ -6110,14 +6222,14 @@ function ModelViewer({
6110
6222
  }
6111
6223
  try {
6112
6224
  if (flyIn) {
6113
- const target = controls.getTarget(new import_three18.Vector3());
6114
- const position = controls.getPosition(new import_three18.Vector3());
6225
+ const target = controls.getTarget(new import_three19.Vector3());
6226
+ const position = controls.getPosition(new import_three19.Vector3());
6115
6227
  const direction = position.clone().sub(target);
6116
6228
  if (direction.lengthSq() < 1e-6) {
6117
6229
  direction.set(0, 0, 1);
6118
6230
  }
6119
6231
  direction.normalize();
6120
- const sphere = new import_three18.Box3().setFromObject(scene).getBoundingSphere(new import_three18.Sphere());
6232
+ const sphere = new import_three19.Box3().setFromObject(scene).getBoundingSphere(new import_three19.Sphere());
6121
6233
  const farDistance = Math.max(sphere.radius * 10, 20);
6122
6234
  const farPosition = target.clone().add(direction.multiplyScalar(farDistance));
6123
6235
  await controls.setLookAt(
@@ -6346,6 +6458,7 @@ function ModelViewer({
6346
6458
  (scene) => {
6347
6459
  sceneRef.current = scene;
6348
6460
  setMeasureBox(computeMeasureBox(scene));
6461
+ setModelBounds(computeModelBounds(scene));
6349
6462
  setModelLoaded(true);
6350
6463
  setModelLoadFailed(false);
6351
6464
  setMismatchDismissed(false);
@@ -6408,8 +6521,8 @@ function ModelViewer({
6408
6521
  const rect = canvas.getBoundingClientRect();
6409
6522
  const ndcX = (event.clientX - rect.left) / rect.width * 2 - 1;
6410
6523
  const ndcY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
6411
- const raycaster = new import_three18.Raycaster();
6412
- raycaster.setFromCamera(new import_three18.Vector2(ndcX, ndcY), cam);
6524
+ const raycaster = new import_three19.Raycaster();
6525
+ raycaster.setFromCamera(new import_three19.Vector2(ndcX, ndcY), cam);
6413
6526
  const hits = raycaster.intersectObject(scene, true);
6414
6527
  if (hits.length === 0) return;
6415
6528
  const { point } = hits[0];
@@ -6505,8 +6618,8 @@ function ModelViewer({
6505
6618
  const rect = canvas.getBoundingClientRect();
6506
6619
  const ndcX = (e.clientX - rect.left) / rect.width * 2 - 1;
6507
6620
  const ndcY = -((e.clientY - rect.top) / rect.height) * 2 + 1;
6508
- const raycaster = new import_three18.Raycaster();
6509
- raycaster.setFromCamera(new import_three18.Vector2(ndcX, ndcY), cam);
6621
+ const raycaster = new import_three19.Raycaster();
6622
+ raycaster.setFromCamera(new import_three19.Vector2(ndcX, ndcY), cam);
6510
6623
  const hits = raycaster.intersectObject(scene, true);
6511
6624
  if (hits.length === 0) return;
6512
6625
  const hit = hits[0];
@@ -6701,7 +6814,8 @@ function ModelViewer({
6701
6814
  shadowConfig: resolvedSceneConfig.shadows,
6702
6815
  shadowsEnabled: shadows,
6703
6816
  environmentRotation: resolvedSceneConfig.environment.rotation,
6704
- environmentConfig: resolvedSceneConfig.environment
6817
+ environmentConfig: resolvedSceneConfig.environment,
6818
+ modelBounds
6705
6819
  }
6706
6820
  ),
6707
6821
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -7269,7 +7383,7 @@ styleInject('.bb-root {\n --bb-font-family:\n Inter,\n "Segoe UI",\n H
7269
7383
 
7270
7384
  // src/hooks/useViewerCamera.ts
7271
7385
  var import_react23 = require("react");
7272
- var import_three19 = require("three");
7386
+ var import_three20 = require("three");
7273
7387
 
7274
7388
  // src/utils/resolveBindingKey.ts
7275
7389
  function resolveBindingKey(objectBindings, objectId) {
@@ -7293,8 +7407,8 @@ function toTuple(vector) {
7293
7407
  return [vector.x, vector.y, vector.z];
7294
7408
  }
7295
7409
  function buildCameraState(camera, controls) {
7296
- const position = new import_three19.Vector3();
7297
- const target = new import_three19.Vector3();
7410
+ const position = new import_three20.Vector3();
7411
+ const target = new import_three20.Vector3();
7298
7412
  controls.getPosition(position);
7299
7413
  controls.getTarget(target);
7300
7414
  return {
@@ -13518,8 +13632,6 @@ function CameraPositionPanel({
13518
13632
  var import_react36 = require("react");
13519
13633
 
13520
13634
  // src/components/BindingBuilder/utils/bindingFactory.ts
13521
- var import_three_stdlib4 = require("three-stdlib");
13522
- var import_three20 = require("three");
13523
13635
  function slugify(value) {
13524
13636
  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();
13525
13637
  }
@@ -13585,51 +13697,15 @@ function buildBindingsFromNodes(nodeNames) {
13585
13697
  nodeNames.map((nodeName) => [nodeName, createDefaultBinding(nodeName)])
13586
13698
  );
13587
13699
  }
13588
- function collectModelMetadata(modelUrl) {
13589
- const loader = new import_three_stdlib4.GLTFLoader();
13590
- let renderer = null;
13591
- if (typeof document !== "undefined") {
13592
- try {
13593
- renderer = new import_three20.WebGLRenderer({
13594
- canvas: document.createElement("canvas"),
13595
- antialias: false
13596
- });
13597
- configureGLTFLoader(loader, {}, renderer);
13598
- } catch {
13599
- renderer = null;
13600
- configureGLTFLoader(loader, {});
13700
+ function collectNodeNames(scene) {
13701
+ const names = /* @__PURE__ */ new Set();
13702
+ scene.traverse((child) => {
13703
+ const mesh = child;
13704
+ if (mesh.isMesh && mesh.geometry && child.name) {
13705
+ names.add(child.name);
13601
13706
  }
13602
- } else {
13603
- configureGLTFLoader(loader, {});
13604
- }
13605
- return new Promise((resolve, reject) => {
13606
- loader.load(
13607
- modelUrl,
13608
- (gltf) => {
13609
- const names = /* @__PURE__ */ new Set();
13610
- gltf.scene.traverse((child) => {
13611
- if (child?.isMesh && child.geometry && child.name) {
13612
- names.add(child.name);
13613
- }
13614
- });
13615
- resolve({
13616
- nodeNames: Array.from(names).sort((a, b) => a.localeCompare(b)),
13617
- animations: resolveAnimationClips(gltf.animations).map(
13618
- ({ sourceName, duration }) => ({
13619
- sourceName,
13620
- duration
13621
- })
13622
- )
13623
- });
13624
- renderer?.dispose();
13625
- },
13626
- void 0,
13627
- (error) => {
13628
- renderer?.dispose();
13629
- reject(error);
13630
- }
13631
- );
13632
13707
  });
13708
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
13633
13709
  }
13634
13710
 
13635
13711
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
@@ -14175,6 +14251,8 @@ function BindingBuilder({ licenseKey }) {
14175
14251
  const uploadedModelRef = (0, import_react38.useRef)(null);
14176
14252
  const previewDragDepthRef = (0, import_react38.useRef)(0);
14177
14253
  const loadRequestRef = (0, import_react38.useRef)(0);
14254
+ const pendingLoadRef = (0, import_react38.useRef)(null);
14255
+ const loadedSceneRef = (0, import_react38.useRef)(null);
14178
14256
  const {
14179
14257
  cameraState,
14180
14258
  resetView,
@@ -14343,42 +14421,76 @@ function BindingBuilder({ licenseKey }) {
14343
14421
  });
14344
14422
  }
14345
14423
  };
14346
- const generateBindingsForModel = async (nextModelUrl, nextModelName) => {
14424
+ const applyBindingsFromScene = (0, import_react38.useCallback)(
14425
+ (scene, nextModelName) => {
14426
+ const nodeNames = collectNodeNames(scene);
14427
+ bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
14428
+ setSelectedItem(nodeNames[0] ?? null);
14429
+ setSceneConfig((prev) => ({ ...prev, annotations: [] }));
14430
+ setStatusMessage(
14431
+ `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14432
+ );
14433
+ setStatusTone("success");
14434
+ setLoading(false);
14435
+ },
14436
+ [bindingsHistory, setSceneConfig]
14437
+ );
14438
+ const generateBindingsForModel = (nextModelUrl, nextModelName) => {
14347
14439
  const requestId = ++loadRequestRef.current;
14348
14440
  setLoading(true);
14349
14441
  setStatusMessage(
14350
14442
  `Reading ${nextModelName} and generating starter bindings...`
14351
14443
  );
14352
14444
  setStatusTone("default");
14353
- try {
14354
- const { nodeNames, animations } = await collectModelMetadata(nextModelUrl);
14355
- if (requestId !== loadRequestRef.current) return;
14356
- const nextBindings = buildBindingsFromNodes(nodeNames);
14357
- setModelUrl(nextModelUrl);
14358
- setModelName(nextModelName);
14359
- bindingsHistory.reset(nextBindings);
14360
- setSelectedItem(nodeNames[0] ?? null);
14361
- setActiveAnnotation(null);
14445
+ setModelName(nextModelName);
14446
+ setActiveAnnotation(null);
14447
+ const loaded = loadedSceneRef.current;
14448
+ if (nextModelUrl === modelUrl && loaded?.url === nextModelUrl) {
14449
+ pendingLoadRef.current = null;
14450
+ applyBindingsFromScene(loaded.scene, nextModelName);
14451
+ return;
14452
+ }
14453
+ pendingLoadRef.current = {
14454
+ requestId,
14455
+ modelName: nextModelName,
14456
+ modelUrl: nextModelUrl
14457
+ };
14458
+ setModelUrl(nextModelUrl);
14459
+ bindingsHistory.reset({});
14460
+ setSelectedItem(null);
14461
+ };
14462
+ const handleViewerModelLoaded = (0, import_react38.useCallback)(
14463
+ (scene) => {
14464
+ const pending = pendingLoadRef.current;
14465
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14466
+ pendingLoadRef.current = null;
14467
+ loadedSceneRef.current = { url: pending.modelUrl, scene };
14468
+ applyBindingsFromScene(scene, pending.modelName);
14469
+ },
14470
+ [applyBindingsFromScene]
14471
+ );
14472
+ const handleViewerLoadError = (0, import_react38.useCallback)((error) => {
14473
+ const pending = pendingLoadRef.current;
14474
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14475
+ pendingLoadRef.current = null;
14476
+ console.error(error);
14477
+ setStatusMessage(
14478
+ `Could not read ${pending.modelName}. Make sure the file is a valid GLB/GLTF asset.`
14479
+ );
14480
+ setStatusTone("error");
14481
+ setLoading(false);
14482
+ }, []);
14483
+ const handleAnimationsReady = (0, import_react38.useCallback)(
14484
+ (controls) => {
14485
+ viewerAnimations.handleAnimationsReady(controls);
14486
+ const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
14362
14487
  setSceneConfig((prev) => ({
14363
14488
  ...prev,
14364
- animations: mergeAnimationConfig(prev, animations),
14365
- annotations: []
14489
+ animations: mergeAnimationConfig(prev, details)
14366
14490
  }));
14367
- setStatusMessage(
14368
- `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14369
- );
14370
- setStatusTone("success");
14371
- } catch (error) {
14372
- if (requestId !== loadRequestRef.current) return;
14373
- console.error(error);
14374
- setStatusMessage(
14375
- `Could not read ${nextModelName}. Make sure the file is a valid GLB/GLTF asset.`
14376
- );
14377
- setStatusTone("error");
14378
- } finally {
14379
- if (requestId === loadRequestRef.current) setLoading(false);
14380
- }
14381
- };
14491
+ },
14492
+ [viewerAnimations, setSceneConfig]
14493
+ );
14382
14494
  const handleLoadDemo = async () => {
14383
14495
  uploadedModelRef.current?.cleanup();
14384
14496
  uploadedModelRef.current = null;
@@ -14408,6 +14520,8 @@ function BindingBuilder({ licenseKey }) {
14408
14520
  const clearModel = () => {
14409
14521
  uploadedModelRef.current?.cleanup();
14410
14522
  uploadedModelRef.current = null;
14523
+ loadedSceneRef.current = null;
14524
+ pendingLoadRef.current = null;
14411
14525
  previewDragDepthRef.current = 0;
14412
14526
  setIsPreviewDragActive(false);
14413
14527
  setModelUrl(null);
@@ -15206,7 +15320,9 @@ function BindingBuilder({ licenseKey }) {
15206
15320
  onAnnotationsChange: handleAnnotationsChange,
15207
15321
  activeAnnotation: activeAnnotation?.annotation ?? null,
15208
15322
  onActiveAnnotationChange: handleActiveAnnotationChange,
15209
- onAnimationsReady: viewerAnimations.handleAnimationsReady,
15323
+ onAnimationsReady: handleAnimationsReady,
15324
+ onModelLoaded: handleViewerModelLoaded,
15325
+ onLoadError: handleViewerLoadError,
15210
15326
  licenseKey,
15211
15327
  sceneConfig,
15212
15328
  disableZoom: false,
@@ -15290,7 +15406,7 @@ var import_drei6 = require("@react-three/drei");
15290
15406
  var import_fiber12 = require("@react-three/fiber");
15291
15407
  var import_fiber13 = require("@react-three/fiber");
15292
15408
  var import_three21 = require("three");
15293
- var import_three_stdlib5 = require("three-stdlib");
15409
+ var import_three_stdlib4 = require("three-stdlib");
15294
15410
 
15295
15411
  // src/styles/SimpleModelViewer.css
15296
15412
  styleInject('.smv-root {\n --smv-panel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --smv-panel-bg: #141824;\n --smv-panel-bg-strong: #1a1f2e;\n --smv-panel-bg-strong-hover: #252b3b;\n --smv-panel-border: rgba(255, 255, 255, 0.06);\n --smv-panel-border-strong: rgba(255, 255, 255, 0.08);\n --smv-panel-border-active: rgba(255, 255, 255, 0.18);\n --smv-panel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-text: #f1f5f9;\n --smv-panel-text-emphasis: #ffffff;\n --smv-panel-text-muted: #94a3b8;\n --smv-panel-text-subtle: #64748b;\n --smv-panel-surface: rgba(255, 255, 255, 0.05);\n --smv-panel-surface-hover: rgba(255, 255, 255, 0.1);\n --smv-panel-surface-soft: rgba(255, 255, 255, 0.04);\n --smv-panel-accent: #3b82f6;\n --smv-panel-accent-contrast: #ffffff;\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n display: flex;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.smv-canvasArea {\n flex: 1;\n position: relative;\n min-width: 0;\n}\n.smv-upload-input {\n display: none;\n}\n.smv-upload-toolbar {\n position: absolute;\n top: 12px;\n left: 50%;\n z-index: 120;\n display: flex;\n align-items: center;\n gap: 10px;\n max-width: calc(100% - 32px);\n padding: 5px 6px;\n border: 1px solid var(--smv-panel-border-strong);\n border-radius: 10px;\n background: rgba(20, 24, 36, 0.88);\n color: var(--smv-panel-text);\n box-shadow: 0 16px 30px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n transform: translateX(-50%);\n}\n.smv-upload-toolbar__name {\n max-width: 260px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--smv-panel-text-muted);\n font-size: 12px;\n font-weight: 500;\n}\n.smv-upload-toolbar__button {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n min-height: 34px;\n padding: 0 14px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: var(--smv-panel-text-emphasis);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n.smv-upload-toolbar__button--ghost {\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text-muted);\n}\n.smv-upload-empty-state {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 24px;\n}\n.smv-upload-empty-state__card {\n width: min(440px, 100%);\n padding: 28px;\n border: 1px dashed rgba(255, 255, 255, 0.2);\n border-radius: 8px;\n color: var(--smv-panel-text);\n text-align: center;\n box-shadow: 0 24px 40px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n}\n.smv-upload-empty-state__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n margin-bottom: 18px;\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: #bfdbfe;\n}\n.smv-upload-empty-state__title {\n margin: 0 0 10px;\n color: var(--smv-panel-text-emphasis);\n font-size: 24px;\n line-height: 1.15;\n}\n.smv-upload-empty-state__body {\n margin: 0 0 18px;\n color: var(--smv-panel-text-muted);\n font-size: 14px;\n font-weight: 700;\n line-height: 1.6;\n}\n.smv-upload-empty-state__body code {\n color: var(--smv-panel-text-emphasis);\n font-family: var(--smv-panel-font-family);\n}\n.smv-upload-empty-state__button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 0 18px;\n border: none;\n border-radius: 8px;\n background: #3b82f6;\n color: #eff6ff;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n}\n.smv-upload-drop-overlay {\n position: absolute;\n inset: 0;\n z-index: 130;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: rgba(8, 12, 20, 0.48);\n backdrop-filter: blur(4px);\n}\n.smv-upload-drop-overlay__card {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 16px 20px;\n border: 1px dashed rgba(191, 219, 254, 0.75);\n border-radius: 10px;\n background: rgba(30, 41, 59, 0.92);\n color: #dbeafe;\n font-size: 14px;\n font-weight: 700;\n box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);\n}\n.smv-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 110;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.smv-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.smv-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8fa;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.right-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n}\n.right-panel-wrapper--open {\n width: 300px;\n flex-shrink: 0;\n}\n.right-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.left-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n order: -1;\n}\n.left-panel-wrapper--open {\n width: 280px;\n flex-shrink: 0;\n}\n.left-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.side-panel__open-btn.side-panel__open-btn--left {\n left: 12px;\n right: auto;\n}\n.side-panel.side-panel--left {\n left: 0;\n right: auto;\n width: 280px;\n transform: translateX(-100%);\n border-left: none;\n border-right: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-left);\n}\n.side-panel.side-panel--open-left {\n transform: translateX(0);\n}\n.side-panel.side-panel--closed-left {\n transform: translateX(-100%);\n}\n.env-controls {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n flex: 1;\n}\n.env-section {\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.env-section__header {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 9px 14px;\n border: none;\n background: var(--smv-panel-surface-soft);\n color: var(--smv-panel-text-muted);\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.env-section__chevron {\n transition: transform 0.15s;\n}\n.env-section__chevron--collapsed {\n transform: rotate(-90deg);\n}\n.env-section__body {\n display: flex;\n flex-direction: column;\n}\n.env-row {\n display: flex;\n gap: 6px;\n align-items: center;\n min-height: 32px;\n border-bottom: 1px solid var(--smv-panel-surface);\n}\n.env-row__label {\n flex: 0 0 42%;\n padding: 0 0 0 14px;\n font-size: 11.5px;\n font-weight: 500;\n color: var(--smv-panel-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.env-row__value {\n flex: 1;\n display: flex;\n align-items: center;\n padding: 5px 10px 5px 8px;\n min-width: 0;\n}\n.env-row__value--slider {\n gap: 6px;\n}\n.env-checkbox-btn {\n display: flex;\n align-items: center;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n}\n.env-checkbox {\n width: 15px;\n height: 15px;\n border-radius: 3px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: var(--smv-panel-surface-soft);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.env-checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n.env-select {\n width: 100%;\n padding: 5px 8px;\n border-radius: 4px;\n border: none;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n font-size: 11.5px;\n outline: none;\n box-sizing: border-box;\n text-transform: capitalize;\n}\n.env-color-swatch {\n position: relative;\n width: 100%;\n height: 22px;\n border-radius: 4px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n cursor: pointer;\n}\n.env-color-swatch__hex {\n font-size: 10.5px;\n font-weight: 700;\n color: #0f172a;\n text-shadow: 0 1px 1px rgba(255, 255, 255, 0.35);\n pointer-events: none;\n text-transform: uppercase;\n}\n.env-color-swatch__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n border: none;\n padding: 0;\n opacity: 0;\n cursor: pointer;\n}\n.env-slider {\n flex: 1;\n accent-color: var(--smv-panel-accent);\n cursor: pointer;\n min-width: 0;\n}\n.env-slider__value {\n flex-shrink: 0;\n min-width: 32px;\n padding: 2px 5px;\n border-radius: 4px;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-accent);\n font-size: 10.5px;\n font-weight: 600;\n text-align: center;\n}\n.side-panel__open-btn {\n position: absolute;\n top: 12px;\n right: 12px;\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 7px 13px;\n border-radius: 7px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-bg-strong);\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: var(--smv-panel-text);\n z-index: 190;\n transition: background 0.15s;\n}\n.side-panel__open-btn:hover {\n background: var(--smv-panel-bg-strong-hover);\n}\n.side-panel {\n position: absolute;\n top: 0;\n right: 0;\n width: 300px;\n height: 100%;\n background: var(--smv-panel-bg);\n display: flex;\n flex-direction: column;\n font-family: var(--smv-panel-font-family);\n transform: translateX(100%);\n transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n z-index: 200;\n border-left: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-right);\n}\n.side-panel--open {\n transform: translateX(0);\n}\n.side-panel--closed {\n transform: translateX(100%);\n}\n.side-panel__header {\n padding: 24px 20px 16px 20px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 4px;\n}\n.side-panel__eyebrow {\n font-size: 15px;\n font-weight: 700;\n letter-spacing: 0.12em;\n color: var(--smv-panel-text-subtle);\n text-transform: uppercase;\n}\n.side-panel__title {\n font-size: 22px;\n font-weight: 700;\n color: var(--smv-panel-text);\n margin: 0;\n line-height: 1.2;\n}\n.side-panel__close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n border-radius: 8px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n cursor: pointer;\n color: var(--smv-panel-text-muted);\n transition: background 0.15s, color 0.15s;\n flex-shrink: 0;\n}\n.side-panel__close-btn:hover {\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n}\n.side-panel__search-wrap {\n padding: 14px 16px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__search {\n width: 100%;\n padding: 10px 14px;\n border-radius: 10px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text);\n font-size: 13px;\n outline: none;\n box-sizing: border-box;\n transition: border-color 0.15s, background 0.15s;\n}\n.side-panel__search::placeholder {\n color: var(--smv-panel-text-subtle);\n}\n.side-panel__search:focus {\n border-color: var(--smv-panel-border-active);\n background: var(--smv-panel-surface-hover);\n}\n.side-panel__list {\n display: flex;\n flex-direction: column;\n padding: 10px 0;\n overflow-y: auto;\n flex: 1;\n}\n.side-panel__list::-webkit-scrollbar {\n width: 4px;\n}\n.side-panel__list::-webkit-scrollbar-track {\n background: transparent;\n}\n.side-panel__list::-webkit-scrollbar-thumb {\n background: var(--smv-panel-surface-hover);\n border-radius: 4px;\n}\n.side-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 20px;\n cursor: pointer;\n transition: background 0.12s;\n border-bottom: 1px solid var(--smv-panel-surface-soft);\n}\n.side-panel__row:hover,\n.side-panel__row--hovered {\n background: var(--smv-panel-surface);\n}\n.side-panel__label {\n font-size: 14px;\n font-weight: 600;\n color: var(--smv-panel-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.side-panel__label--hovered {\n color: var(--smv-panel-text-emphasis);\n}\n.side-panel__vis-btn {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n margin-left: 10px;\n}\n.side-panel__checkbox {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.side-panel__checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n@media (max-width: 900px) {\n .smv-upload-toolbar {\n top: 56px;\n gap: 8px;\n max-width: calc(100% - 24px);\n padding: 8px 10px;\n }\n .smv-upload-toolbar__name {\n max-width: 140px;\n }\n .right-panel-wrapper--open,\n .left-panel-wrapper--open {\n width: 0;\n }\n .side-panel,\n .side-panel.side-panel--left {\n width: min(300px, 85vw);\n }\n}\n');
@@ -15359,7 +15475,7 @@ function InnerModel({
15359
15475
  const invalidate = (0, import_fiber12.useThree)((state) => state.invalidate);
15360
15476
  const [hoveredKey, setHoveredKey] = (0, import_react39.useState)(null);
15361
15477
  const scene = (0, import_react39.useMemo)(
15362
- () => import_three_stdlib5.SkeletonUtils.clone(rawScene),
15478
+ () => import_three_stdlib4.SkeletonUtils.clone(rawScene),
15363
15479
  [rawScene]
15364
15480
  );
15365
15481
  const meshMap = (0, import_react39.useMemo)(() => {