@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.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,56 @@ 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
+ function countTriangles(geometries) {
2507
+ let total = 0;
2508
+ for (const geometry of geometries) {
2509
+ const count = geometry.index ? geometry.index.count : geometry.attributes.position.count;
2510
+ total += count / 3;
2511
+ }
2512
+ return total;
2513
+ }
2514
+ var MAX_BVH_TRIANGLES = 12e6;
2515
+ async function buildSceneBVH(root, signal) {
2516
+ const geometries = collectGeometries(root);
2517
+ if (geometries.length === 0) return;
2518
+ if (countTriangles(geometries) > MAX_BVH_TRIANGLES) return;
2519
+ enableAcceleratedRaycast();
2520
+ for (const geometry of geometries) {
2521
+ if (signal?.aborted) return;
2522
+ const tree = import_three_mesh_bvh.computeBoundsTree.call(geometry);
2523
+ await new Promise((resolve) => setTimeout(resolve, 0));
2524
+ if (signal?.aborted) return;
2525
+ geometry.boundsTree = tree;
2526
+ }
2527
+ }
2528
+ function disposeSceneBVH(root) {
2529
+ root.traverse((node) => {
2530
+ const geometry = node.geometry;
2531
+ if (!geometry?.boundsTree) return;
2532
+ import_three_mesh_bvh.disposeBoundsTree.call(geometry);
2533
+ geometry.boundsTree = void 0;
2534
+ });
2535
+ }
2536
+
2454
2537
  // src/components/Model.tsx
2455
2538
  var import_jsx_runtime8 = require("react/jsx-runtime");
2456
2539
  function firstMaterial(node) {
@@ -2508,17 +2591,17 @@ function Model({
2508
2591
  }, [scene]);
2509
2592
  const shellNodeNames = (0, import_react11.useMemo)(() => {
2510
2593
  const shell = /* @__PURE__ */ new Set();
2511
- const sceneBox = new import_three13.Box3();
2594
+ const sceneBox = new import_three14.Box3();
2512
2595
  const meshNodes = [];
2513
2596
  for (const [name, node] of Object.entries(nodeMap)) {
2514
2597
  if (!isMeshNode(node)) continue;
2515
- const box = new import_three13.Box3().setFromObject(node);
2598
+ const box = new import_three14.Box3().setFromObject(node);
2516
2599
  if (box.isEmpty()) continue;
2517
2600
  meshNodes.push({ name, box });
2518
2601
  sceneBox.union(box);
2519
2602
  }
2520
- const sceneSize = sceneBox.getSize(new import_three13.Vector3());
2521
- const size = new import_three13.Vector3();
2603
+ const sceneSize = sceneBox.getSize(new import_three14.Vector3());
2604
+ const size = new import_three14.Vector3();
2522
2605
  for (const { name, box } of meshNodes) {
2523
2606
  box.getSize(size);
2524
2607
  const isShell = size.x > sceneSize.x * 0.7 && size.z > sceneSize.z * 0.7 && size.y > sceneSize.y * 0.4;
@@ -2537,6 +2620,14 @@ function Model({
2537
2620
  (0, import_react11.useEffect)(() => {
2538
2621
  refreshSkinnedBounds(scene);
2539
2622
  }, [scene]);
2623
+ (0, import_react11.useEffect)(() => {
2624
+ const controller = new AbortController();
2625
+ void buildSceneBVH(scene, controller.signal);
2626
+ return () => {
2627
+ controller.abort();
2628
+ disposeSceneBVH(scene);
2629
+ };
2630
+ }, [scene]);
2540
2631
  const bindingKeyByNodeName = (0, import_react11.useMemo)(() => {
2541
2632
  const map = {};
2542
2633
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -2584,7 +2675,7 @@ function Model({
2584
2675
  const map = {};
2585
2676
  if (shadingMode !== "unlit") return map;
2586
2677
  for (const [nodeName, material] of Object.entries(perNodeMaterials)) {
2587
- const basic = new import_three13.MeshBasicMaterial();
2678
+ const basic = new import_three14.MeshBasicMaterial();
2588
2679
  basic.color.copy(material.color);
2589
2680
  basic.map = material.map;
2590
2681
  basic.alphaMap = material.alphaMap;
@@ -2600,7 +2691,7 @@ function Model({
2600
2691
  const map = {};
2601
2692
  if (shadingMode !== "normals") return map;
2602
2693
  for (const nodeName of Object.keys(perNodeMaterials)) {
2603
- map[nodeName] = new import_three13.MeshNormalMaterial();
2694
+ map[nodeName] = new import_three14.MeshNormalMaterial();
2604
2695
  }
2605
2696
  return map;
2606
2697
  }, [perNodeMaterials, shadingMode]);
@@ -2609,7 +2700,7 @@ function Model({
2609
2700
  if (shadingMode !== "lit" || rendererMode !== "matcap") return map;
2610
2701
  const matcapTexture = getMatcapTexture();
2611
2702
  for (const [nodeName, material] of Object.entries(perNodeMaterials)) {
2612
- const matcap = new import_three13.MeshMatcapMaterial();
2703
+ const matcap = new import_three14.MeshMatcapMaterial();
2613
2704
  matcap.matcap = matcapTexture;
2614
2705
  matcap.color.copy(material.color);
2615
2706
  matcap.alphaMap = material.alphaMap;
@@ -2776,16 +2867,16 @@ function Model({
2776
2867
  const glbNodeName = binding.modelObjectId ?? nodeName;
2777
2868
  const node = nodeMap[glbNodeName];
2778
2869
  if (!isMeshNode(node)) continue;
2779
- const geometry = new import_three13.WireframeGeometry(
2870
+ const geometry = new import_three14.WireframeGeometry(
2780
2871
  node.geometry
2781
2872
  );
2782
- const lineMaterial = new import_three13.LineBasicMaterial({
2873
+ const lineMaterial = new import_three14.LineBasicMaterial({
2783
2874
  color: wireframeColor,
2784
2875
  transparent: true,
2785
2876
  opacity: wireframeOpacity,
2786
2877
  depthTest: false
2787
2878
  });
2788
- const line = new import_three13.LineSegments(geometry, lineMaterial);
2879
+ const line = new import_three14.LineSegments(geometry, lineMaterial);
2789
2880
  line.renderOrder = 1;
2790
2881
  node.add(line);
2791
2882
  added.push({ node, line });
@@ -2837,7 +2928,7 @@ function Model({
2837
2928
  const applyClipConfigToAction = (action, clipConfig) => {
2838
2929
  const loopMode = getClipLoopMode(clipConfig);
2839
2930
  action.setLoop(
2840
- loopMode === "once" ? import_three13.LoopOnce : import_three13.LoopRepeat,
2931
+ loopMode === "once" ? import_three14.LoopOnce : import_three14.LoopRepeat,
2841
2932
  loopMode === "once" ? 1 : Infinity
2842
2933
  );
2843
2934
  action.clampWhenFinished = loopMode === "once";
@@ -2904,7 +2995,7 @@ function Model({
2904
2995
  invalidate();
2905
2996
  };
2906
2997
  (0, import_react11.useEffect)(() => {
2907
- const mixer = new import_three13.AnimationMixer(scene);
2998
+ const mixer = new import_three14.AnimationMixer(scene);
2908
2999
  mixerRef.current = mixer;
2909
3000
  const actions = {};
2910
3001
  for (const { clip, sourceName } of resolvedAnimationClips) {
@@ -2995,17 +3086,17 @@ function Model({
2995
3086
  (0, import_fiber7.useFrame)((_, delta) => {
2996
3087
  if (!groupRef.current) return;
2997
3088
  const rotation = groupRef.current.rotation;
2998
- rotation.x = import_three13.MathUtils.lerp(
3089
+ rotation.x = import_three14.MathUtils.lerp(
2999
3090
  rotation.x,
3000
3091
  targetRotation[0],
3001
3092
  1 - Math.exp(-5 * delta)
3002
3093
  );
3003
- rotation.y = import_three13.MathUtils.lerp(
3094
+ rotation.y = import_three14.MathUtils.lerp(
3004
3095
  rotation.y,
3005
3096
  targetRotation[1],
3006
3097
  1 - Math.exp(-5 * delta)
3007
3098
  );
3008
- rotation.z = import_three13.MathUtils.lerp(
3099
+ rotation.z = import_three14.MathUtils.lerp(
3009
3100
  rotation.z,
3010
3101
  targetRotation[2],
3011
3102
  1 - Math.exp(-5 * delta)
@@ -3016,16 +3107,16 @@ function Model({
3016
3107
  const explodePartsRef = (0, import_react11.useRef)([]);
3017
3108
  const explodeStrengthRef = (0, import_react11.useRef)(0);
3018
3109
  const currentExplodeRef = (0, import_react11.useRef)(0);
3019
- const explodeTmp = (0, import_react11.useMemo)(() => new import_three13.Vector3(), []);
3110
+ const explodeTmp = (0, import_react11.useMemo)(() => new import_three14.Vector3(), []);
3020
3111
  (0, import_react11.useEffect)(() => {
3021
3112
  for (const part of explodePartsRef.current) {
3022
3113
  part.node.position.copy(part.restLocalPos);
3023
3114
  }
3024
3115
  currentExplodeRef.current = 0;
3025
3116
  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;
3117
+ const sceneBox = new import_three14.Box3().setFromObject(scene);
3118
+ const modelCenter = sceneBox.getCenter(new import_three14.Vector3());
3119
+ const radius = sceneBox.getSize(new import_three14.Vector3()).length() * 0.5;
3029
3120
  explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
3030
3121
  const targets = [];
3031
3122
  const targetSet = /* @__PURE__ */ new Set();
@@ -3036,7 +3127,7 @@ function Model({
3036
3127
  targets.push(node);
3037
3128
  }
3038
3129
  const parts = [];
3039
- const partCenter = new import_three13.Vector3();
3130
+ const partCenter = new import_three14.Vector3();
3040
3131
  for (const node of targets) {
3041
3132
  if (!node.parent) continue;
3042
3133
  let ancestor = node.parent;
@@ -3049,7 +3140,7 @@ function Model({
3049
3140
  ancestor = ancestor.parent;
3050
3141
  }
3051
3142
  if (nested) continue;
3052
- const box = new import_three13.Box3().setFromObject(node);
3143
+ const box = new import_three14.Box3().setFromObject(node);
3053
3144
  if (box.isEmpty()) continue;
3054
3145
  box.getCenter(partCenter);
3055
3146
  const worldDir = partCenter.clone().sub(modelCenter);
@@ -3058,9 +3149,9 @@ function Model({
3058
3149
  parts.push({
3059
3150
  node,
3060
3151
  restLocalPos: node.position.clone(),
3061
- restWorldPos: node.getWorldPosition(new import_three13.Vector3()),
3152
+ restWorldPos: node.getWorldPosition(new import_three14.Vector3()),
3062
3153
  worldDir,
3063
- parentInverse: new import_three13.Matrix4().copy(node.parent.matrixWorld).invert()
3154
+ parentInverse: new import_three14.Matrix4().copy(node.parent.matrixWorld).invert()
3064
3155
  });
3065
3156
  }
3066
3157
  explodePartsRef.current = parts;
@@ -3078,7 +3169,7 @@ function Model({
3078
3169
  if (current === target) return;
3079
3170
  currentExplodeRef.current = target;
3080
3171
  } else {
3081
- currentExplodeRef.current = import_three13.MathUtils.lerp(
3172
+ currentExplodeRef.current = import_three14.MathUtils.lerp(
3082
3173
  current,
3083
3174
  target,
3084
3175
  1 - Math.exp(-6 * delta)
@@ -3143,7 +3234,7 @@ function Model({
3143
3234
  var import_react12 = require("react");
3144
3235
  var import_drei4 = require("@react-three/drei");
3145
3236
  var import_fiber8 = require("@react-three/fiber");
3146
- var import_three14 = require("three");
3237
+ var import_three15 = require("three");
3147
3238
 
3148
3239
  // src/styles/AnnotationsLayer.css
3149
3240
  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 +3247,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3156
3247
  const scene = (0, import_fiber8.useThree)((state) => state.scene);
3157
3248
  const anchorNodeRef = (0, import_react12.useRef)(null);
3158
3249
  const baseLocalPosition = (0, import_react12.useMemo)(
3159
- () => new import_three14.Vector3(
3250
+ () => new import_three15.Vector3(
3160
3251
  ...annotation.anchorLocalPosition ?? annotation.localPosition ?? annotation.worldPosition
3161
3252
  ),
3162
3253
  [
@@ -3165,9 +3256,9 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3165
3256
  annotation.worldPosition
3166
3257
  ]
3167
3258
  );
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(), []);
3259
+ const worldPosition = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3260
+ const biasedWorldPosition = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3261
+ const toCamera = (0, import_react12.useMemo)(() => new import_three15.Vector3(), []);
3171
3262
  (0, import_react12.useEffect)(() => {
3172
3263
  anchorNodeRef.current = null;
3173
3264
  }, [annotation.anchorNodeName]);
@@ -3190,7 +3281,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3190
3281
  if (worldPositionsRef) {
3191
3282
  let stored = worldPositionsRef.current.get(annotation.id);
3192
3283
  if (!stored) {
3193
- stored = new import_three14.Vector3();
3284
+ stored = new import_three15.Vector3();
3194
3285
  worldPositionsRef.current.set(annotation.id, stored);
3195
3286
  }
3196
3287
  stored.copy(worldPosition);
@@ -3488,17 +3579,17 @@ function AnnotationsLayer({
3488
3579
  );
3489
3580
  (0, import_fiber8.useFrame)((_, delta) => {
3490
3581
  if (!groupRef.current) return;
3491
- groupRef.current.rotation.x = import_three14.MathUtils.lerp(
3582
+ groupRef.current.rotation.x = import_three15.MathUtils.lerp(
3492
3583
  groupRef.current.rotation.x,
3493
3584
  targetRotation[0],
3494
3585
  1 - Math.exp(-5 * delta)
3495
3586
  );
3496
- groupRef.current.rotation.y = import_three14.MathUtils.lerp(
3587
+ groupRef.current.rotation.y = import_three15.MathUtils.lerp(
3497
3588
  groupRef.current.rotation.y,
3498
3589
  targetRotation[1],
3499
3590
  1 - Math.exp(-5 * delta)
3500
3591
  );
3501
- groupRef.current.rotation.z = import_three14.MathUtils.lerp(
3592
+ groupRef.current.rotation.z = import_three15.MathUtils.lerp(
3502
3593
  groupRef.current.rotation.z,
3503
3594
  targetRotation[2],
3504
3595
  1 - Math.exp(-5 * delta)
@@ -3536,7 +3627,7 @@ function AnnotationsLayer({
3536
3627
  // src/components/ModelViewer/MeasurementLayer.tsx
3537
3628
  var import_react13 = require("react");
3538
3629
  var import_drei5 = require("@react-three/drei");
3539
- var import_three15 = require("three");
3630
+ var import_three16 = require("three");
3540
3631
  var import_jsx_runtime10 = require("react/jsx-runtime");
3541
3632
  var LINE_COLOR = "#22d3ee";
3542
3633
  var BOX_COLOR = "#facc15";
@@ -3564,8 +3655,8 @@ function DimensionsBox({ box, unit }) {
3564
3655
  );
3565
3656
  const edges = (0, import_react13.useMemo)(() => {
3566
3657
  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)
3658
+ const geometry = new import_three16.EdgesGeometry(
3659
+ new import_three16.BoxGeometry(w2 || 1e-4, h2 || 1e-4, d2 || 1e-4)
3569
3660
  );
3570
3661
  return geometry;
3571
3662
  }, [box.size]);
@@ -3611,7 +3702,7 @@ function MeasurementLayer({
3611
3702
  points.slice(1).map((point, index) => {
3612
3703
  const a = points[index].position;
3613
3704
  const b = point.position;
3614
- const distance = new import_three15.Vector3(...a).distanceTo(new import_three15.Vector3(...b));
3705
+ const distance = new import_three16.Vector3(...a).distanceTo(new import_three16.Vector3(...b));
3615
3706
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("group", { children: [
3616
3707
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_drei5.Line, { points: [a, b], color: LINE_COLOR, lineWidth: 2 }),
3617
3708
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MeasureLabel, { position: midpoint(a, b), children: formatMeasurement(distance, unit) })
@@ -3625,7 +3716,7 @@ function MeasurementLayer({
3625
3716
  var import_react14 = require("react");
3626
3717
  var import_xr3 = require("@react-three/xr");
3627
3718
  var import_fiber9 = require("@react-three/fiber");
3628
- var import_three16 = require("three");
3719
+ var import_three17 = require("three");
3629
3720
  var import_jsx_runtime11 = require("react/jsx-runtime");
3630
3721
  var TARGET_SIZE = 1;
3631
3722
  var FORWARD_DISTANCE = 1;
@@ -3662,13 +3753,13 @@ var XRModelPresenter = (0, import_react14.forwardRef)(
3662
3753
  () => box ? computeNormalization(box, scaleMode) : null,
3663
3754
  [box, scaleMode]
3664
3755
  );
3665
- const placedPosition = (0, import_react14.useRef)(new import_three16.Vector3(0, 0, -FORWARD_DISTANCE));
3756
+ const placedPosition = (0, import_react14.useRef)(new import_three17.Vector3(0, 0, -FORWARD_DISTANCE));
3666
3757
  const userScale = (0, import_react14.useRef)(1);
3667
3758
  const rotationY = (0, import_react14.useRef)(0);
3668
3759
  const placementMode = (0, import_react14.useRef)(true);
3669
3760
  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());
3761
+ const hitMatrix = (0, import_react14.useMemo)(() => new import_three17.Matrix4(), []);
3762
+ const hitPosition = (0, import_react14.useRef)(new import_three17.Vector3());
3672
3763
  const hasHit = (0, import_react14.useRef)(false);
3673
3764
  const lastARState = (0, import_react14.useRef)(null);
3674
3765
  const gesturing = (0, import_react14.useRef)(false);
@@ -3861,7 +3952,7 @@ var XRModelPresenter_default = XRModelPresenter;
3861
3952
  // src/components/ModelViewer/CinematicCamera.tsx
3862
3953
  var import_react15 = require("react");
3863
3954
  var import_fiber10 = require("@react-three/fiber");
3864
- var import_three17 = require("three");
3955
+ var import_three18 = require("three");
3865
3956
  var ENTER_BLEND_SECONDS = 1.2;
3866
3957
  function CinematicCamera({
3867
3958
  controlsRef,
@@ -3883,14 +3974,14 @@ function CinematicCamera({
3883
3974
  const curves = (0, import_react15.useMemo)(() => {
3884
3975
  if (!hasPath) return null;
3885
3976
  return {
3886
- position: new import_three17.CatmullRomCurve3(
3887
- waypoints.map((w) => new import_three17.Vector3(...w.position)),
3977
+ position: new import_three18.CatmullRomCurve3(
3978
+ waypoints.map((w) => new import_three18.Vector3(...w.position)),
3888
3979
  loop,
3889
3980
  "catmullrom",
3890
3981
  0.5
3891
3982
  ),
3892
- target: new import_three17.CatmullRomCurve3(
3893
- waypoints.map((w) => new import_three17.Vector3(...w.target)),
3983
+ target: new import_three18.CatmullRomCurve3(
3984
+ waypoints.map((w) => new import_three18.Vector3(...w.target)),
3894
3985
  loop,
3895
3986
  "catmullrom",
3896
3987
  0.5
@@ -3899,12 +3990,12 @@ function CinematicCamera({
3899
3990
  }, [signature, hasPath, loop]);
3900
3991
  const elapsedRef = (0, import_react15.useRef)(0);
3901
3992
  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());
3993
+ const enterPos = (0, import_react15.useRef)(new import_three18.Vector3());
3994
+ const enterTarget = (0, import_react15.useRef)(new import_three18.Vector3());
3904
3995
  const startedRef = (0, import_react15.useRef)(false);
3905
3996
  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(), []);
3997
+ const tmpPos = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
3998
+ const tmpTarget = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
3908
3999
  (0, import_react15.useEffect)(() => {
3909
4000
  if (!playing) {
3910
4001
  startedRef.current = false;
@@ -3920,7 +4011,7 @@ function CinematicCamera({
3920
4011
  if (!hasPath) {
3921
4012
  const scene = sceneRef.current;
3922
4013
  if (scene) {
3923
- const sphere = new import_three17.Box3().setFromObject(scene).getBoundingSphere(new import_three17.Sphere());
4014
+ const sphere = new import_three18.Box3().setFromObject(scene).getBoundingSphere(new import_three18.Sphere());
3924
4015
  orbitRef.current = {
3925
4016
  center: sphere.center.clone(),
3926
4017
  radius: sphere.radius > 0 ? sphere.radius : 1
@@ -5049,7 +5140,7 @@ function findInfluencingBone(hit) {
5049
5140
  if (!skinIndex || !skinWeight || !position) return null;
5050
5141
  const localHit = mesh.worldToLocal(hit.point.clone());
5051
5142
  const vertices = [hit.face.a, hit.face.b, hit.face.c];
5052
- const vertex = new import_three18.Vector3();
5143
+ const vertex = new import_three19.Vector3();
5053
5144
  let nearest = vertices[0];
5054
5145
  let nearestDist = Infinity;
5055
5146
  for (const vi of vertices) {
@@ -5099,9 +5190,9 @@ function GuidedTourCameraFollow({
5099
5190
  const scene = (0, import_fiber11.useThree)((state) => state.scene);
5100
5191
  const nodeRef = (0, import_react22.useRef)(null);
5101
5192
  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(), []);
5193
+ const live = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5194
+ const prev = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5195
+ const goal = (0, import_react22.useMemo)(() => new import_three19.Vector3(), []);
5105
5196
  const nodeName = annotation?.anchorNodeName;
5106
5197
  const localPosition = annotation?.anchorLocalPosition;
5107
5198
  (0, import_react22.useEffect)(() => {
@@ -5140,8 +5231,8 @@ var DEFAULT_AR_OVERLAY_STATE = {
5140
5231
  isModelPlaced: false
5141
5232
  };
5142
5233
  function computeMeasureBox(scene) {
5143
- const box = new import_three18.Box3().setFromObject(scene);
5144
- const size = new import_three18.Vector3();
5234
+ const box = new import_three19.Box3().setFromObject(scene);
5235
+ const size = new import_three19.Vector3();
5145
5236
  box.getSize(size);
5146
5237
  return {
5147
5238
  min: [box.min.x, box.min.y, box.min.z],
@@ -5149,11 +5240,19 @@ function computeMeasureBox(scene) {
5149
5240
  size: [size.x, size.y, size.z]
5150
5241
  };
5151
5242
  }
5243
+ function computeModelBounds(scene) {
5244
+ const box = new import_three19.Box3().setFromObject(scene);
5245
+ if (box.isEmpty()) return null;
5246
+ const center = box.getCenter(new import_three19.Vector3());
5247
+ const radius = box.getSize(new import_three19.Vector3()).length() / 2;
5248
+ if (!Number.isFinite(radius) || radius <= 0) return null;
5249
+ return { center: [center.x, center.y, center.z], radius };
5250
+ }
5152
5251
  function fitClippingPlanesToScene(scene, controls) {
5153
5252
  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();
5253
+ if (!(camera instanceof import_three19.PerspectiveCamera)) return;
5254
+ const box = new import_three19.Box3().setFromObject(scene);
5255
+ const size = box.getSize(new import_three19.Vector3()).length();
5157
5256
  if (!Number.isFinite(size) || size <= 0) return;
5158
5257
  camera.near = size / 100;
5159
5258
  camera.far = size * 100;
@@ -5306,8 +5405,8 @@ function ModelViewer({
5306
5405
  if (!controls || initialCameraStateRef.current) {
5307
5406
  return;
5308
5407
  }
5309
- const position = controls.getPosition(new import_three18.Vector3());
5310
- const target = controls.getTarget(new import_three18.Vector3());
5408
+ const position = controls.getPosition(new import_three19.Vector3());
5409
+ const target = controls.getTarget(new import_three19.Vector3());
5311
5410
  initialCameraStateRef.current = {
5312
5411
  position: [position.x, position.y, position.z],
5313
5412
  target: [target.x, target.y, target.z]
@@ -5318,6 +5417,7 @@ function ModelViewer({
5318
5417
  const [measurePoints, setMeasurePoints] = (0, import_react22.useState)([]);
5319
5418
  const [showDimensions, setShowDimensions] = (0, import_react22.useState)(false);
5320
5419
  const [measureBox, setMeasureBox] = (0, import_react22.useState)(null);
5420
+ const [modelBounds, setModelBounds] = (0, import_react22.useState)(null);
5321
5421
  const measureModeRef = (0, import_react22.useRef)(false);
5322
5422
  measureModeRef.current = measureMode;
5323
5423
  const measurePointCounter = (0, import_react22.useRef)(0);
@@ -5561,7 +5661,7 @@ function ModelViewer({
5561
5661
  const controls = controlsRef.current;
5562
5662
  const scene = sceneRef.current;
5563
5663
  if (!controls) return;
5564
- const target = new import_three18.Vector3();
5664
+ const target = new import_three19.Vector3();
5565
5665
  const livePosition = annotationWorldPositionsRef.current.get(
5566
5666
  annotation.id
5567
5667
  );
@@ -5579,15 +5679,15 @@ function ModelViewer({
5579
5679
  controls.smoothTime = 0.6;
5580
5680
  const storedNormal = annotation.worldNormal;
5581
5681
  if (scene) {
5582
- const box = new import_three18.Box3().setFromObject(scene);
5583
- const modelCenter = new import_three18.Vector3();
5682
+ const box = new import_three19.Box3().setFromObject(scene);
5683
+ const modelCenter = new import_three19.Vector3();
5584
5684
  box.getCenter(modelCenter);
5585
- const modelSize = new import_three18.Vector3();
5685
+ const modelSize = new import_three19.Vector3();
5586
5686
  box.getSize(modelSize);
5587
5687
  const modelRadius = modelSize.length() * 0.2;
5588
5688
  let outwardDir;
5589
5689
  if (storedNormal) {
5590
- outwardDir = new import_three18.Vector3(
5690
+ outwardDir = new import_three19.Vector3(
5591
5691
  storedNormal[0],
5592
5692
  storedNormal[1],
5593
5693
  storedNormal[2]
@@ -5885,8 +5985,8 @@ function ModelViewer({
5885
5985
  };
5886
5986
  if (canvasContainer && cameraControls && object) {
5887
5987
  const canvasRect = canvasContainer.getBoundingClientRect();
5888
- const objectCenter = new import_three18.Vector3();
5889
- new import_three18.Box3().setFromObject(object).getCenter(objectCenter);
5988
+ const objectCenter = new import_three19.Vector3();
5989
+ new import_three19.Box3().setFromObject(object).getCenter(objectCenter);
5890
5990
  objectCenter.project(cameraControls);
5891
5991
  const projectedX = (objectCenter.x + 1) / 2 * canvasRect.width + (canvasRect.left - rect.left);
5892
5992
  const projectedY = (1 - objectCenter.y) / 2 * canvasRect.height + (canvasRect.top - rect.top);
@@ -6110,14 +6210,14 @@ function ModelViewer({
6110
6210
  }
6111
6211
  try {
6112
6212
  if (flyIn) {
6113
- const target = controls.getTarget(new import_three18.Vector3());
6114
- const position = controls.getPosition(new import_three18.Vector3());
6213
+ const target = controls.getTarget(new import_three19.Vector3());
6214
+ const position = controls.getPosition(new import_three19.Vector3());
6115
6215
  const direction = position.clone().sub(target);
6116
6216
  if (direction.lengthSq() < 1e-6) {
6117
6217
  direction.set(0, 0, 1);
6118
6218
  }
6119
6219
  direction.normalize();
6120
- const sphere = new import_three18.Box3().setFromObject(scene).getBoundingSphere(new import_three18.Sphere());
6220
+ const sphere = new import_three19.Box3().setFromObject(scene).getBoundingSphere(new import_three19.Sphere());
6121
6221
  const farDistance = Math.max(sphere.radius * 10, 20);
6122
6222
  const farPosition = target.clone().add(direction.multiplyScalar(farDistance));
6123
6223
  await controls.setLookAt(
@@ -6346,6 +6446,7 @@ function ModelViewer({
6346
6446
  (scene) => {
6347
6447
  sceneRef.current = scene;
6348
6448
  setMeasureBox(computeMeasureBox(scene));
6449
+ setModelBounds(computeModelBounds(scene));
6349
6450
  setModelLoaded(true);
6350
6451
  setModelLoadFailed(false);
6351
6452
  setMismatchDismissed(false);
@@ -6408,8 +6509,8 @@ function ModelViewer({
6408
6509
  const rect = canvas.getBoundingClientRect();
6409
6510
  const ndcX = (event.clientX - rect.left) / rect.width * 2 - 1;
6410
6511
  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);
6512
+ const raycaster = new import_three19.Raycaster();
6513
+ raycaster.setFromCamera(new import_three19.Vector2(ndcX, ndcY), cam);
6413
6514
  const hits = raycaster.intersectObject(scene, true);
6414
6515
  if (hits.length === 0) return;
6415
6516
  const { point } = hits[0];
@@ -6505,8 +6606,8 @@ function ModelViewer({
6505
6606
  const rect = canvas.getBoundingClientRect();
6506
6607
  const ndcX = (e.clientX - rect.left) / rect.width * 2 - 1;
6507
6608
  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);
6609
+ const raycaster = new import_three19.Raycaster();
6610
+ raycaster.setFromCamera(new import_three19.Vector2(ndcX, ndcY), cam);
6510
6611
  const hits = raycaster.intersectObject(scene, true);
6511
6612
  if (hits.length === 0) return;
6512
6613
  const hit = hits[0];
@@ -6701,7 +6802,8 @@ function ModelViewer({
6701
6802
  shadowConfig: resolvedSceneConfig.shadows,
6702
6803
  shadowsEnabled: shadows,
6703
6804
  environmentRotation: resolvedSceneConfig.environment.rotation,
6704
- environmentConfig: resolvedSceneConfig.environment
6805
+ environmentConfig: resolvedSceneConfig.environment,
6806
+ modelBounds
6705
6807
  }
6706
6808
  ),
6707
6809
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -7269,7 +7371,7 @@ styleInject('.bb-root {\n --bb-font-family:\n Inter,\n "Segoe UI",\n H
7269
7371
 
7270
7372
  // src/hooks/useViewerCamera.ts
7271
7373
  var import_react23 = require("react");
7272
- var import_three19 = require("three");
7374
+ var import_three20 = require("three");
7273
7375
 
7274
7376
  // src/utils/resolveBindingKey.ts
7275
7377
  function resolveBindingKey(objectBindings, objectId) {
@@ -7293,8 +7395,8 @@ function toTuple(vector) {
7293
7395
  return [vector.x, vector.y, vector.z];
7294
7396
  }
7295
7397
  function buildCameraState(camera, controls) {
7296
- const position = new import_three19.Vector3();
7297
- const target = new import_three19.Vector3();
7398
+ const position = new import_three20.Vector3();
7399
+ const target = new import_three20.Vector3();
7298
7400
  controls.getPosition(position);
7299
7401
  controls.getTarget(target);
7300
7402
  return {
@@ -13518,8 +13620,6 @@ function CameraPositionPanel({
13518
13620
  var import_react36 = require("react");
13519
13621
 
13520
13622
  // src/components/BindingBuilder/utils/bindingFactory.ts
13521
- var import_three_stdlib4 = require("three-stdlib");
13522
- var import_three20 = require("three");
13523
13623
  function slugify(value) {
13524
13624
  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
13625
  }
@@ -13585,51 +13685,15 @@ function buildBindingsFromNodes(nodeNames) {
13585
13685
  nodeNames.map((nodeName) => [nodeName, createDefaultBinding(nodeName)])
13586
13686
  );
13587
13687
  }
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, {});
13688
+ function collectNodeNames(scene) {
13689
+ const names = /* @__PURE__ */ new Set();
13690
+ scene.traverse((child) => {
13691
+ const mesh = child;
13692
+ if (mesh.isMesh && mesh.geometry && child.name) {
13693
+ names.add(child.name);
13601
13694
  }
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
13695
  });
13696
+ return Array.from(names).sort((a, b) => a.localeCompare(b));
13633
13697
  }
13634
13698
 
13635
13699
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
@@ -14175,6 +14239,8 @@ function BindingBuilder({ licenseKey }) {
14175
14239
  const uploadedModelRef = (0, import_react38.useRef)(null);
14176
14240
  const previewDragDepthRef = (0, import_react38.useRef)(0);
14177
14241
  const loadRequestRef = (0, import_react38.useRef)(0);
14242
+ const pendingLoadRef = (0, import_react38.useRef)(null);
14243
+ const loadedSceneRef = (0, import_react38.useRef)(null);
14178
14244
  const {
14179
14245
  cameraState,
14180
14246
  resetView,
@@ -14343,42 +14409,76 @@ function BindingBuilder({ licenseKey }) {
14343
14409
  });
14344
14410
  }
14345
14411
  };
14346
- const generateBindingsForModel = async (nextModelUrl, nextModelName) => {
14412
+ const applyBindingsFromScene = (0, import_react38.useCallback)(
14413
+ (scene, nextModelName) => {
14414
+ const nodeNames = collectNodeNames(scene);
14415
+ bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
14416
+ setSelectedItem(nodeNames[0] ?? null);
14417
+ setSceneConfig((prev) => ({ ...prev, annotations: [] }));
14418
+ setStatusMessage(
14419
+ `Generated ${nodeNames.length} bindings from ${nextModelName}.`
14420
+ );
14421
+ setStatusTone("success");
14422
+ setLoading(false);
14423
+ },
14424
+ [bindingsHistory, setSceneConfig]
14425
+ );
14426
+ const generateBindingsForModel = (nextModelUrl, nextModelName) => {
14347
14427
  const requestId = ++loadRequestRef.current;
14348
14428
  setLoading(true);
14349
14429
  setStatusMessage(
14350
14430
  `Reading ${nextModelName} and generating starter bindings...`
14351
14431
  );
14352
14432
  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);
14433
+ setModelName(nextModelName);
14434
+ setActiveAnnotation(null);
14435
+ const loaded = loadedSceneRef.current;
14436
+ if (nextModelUrl === modelUrl && loaded?.url === nextModelUrl) {
14437
+ pendingLoadRef.current = null;
14438
+ applyBindingsFromScene(loaded.scene, nextModelName);
14439
+ return;
14440
+ }
14441
+ pendingLoadRef.current = {
14442
+ requestId,
14443
+ modelName: nextModelName,
14444
+ modelUrl: nextModelUrl
14445
+ };
14446
+ setModelUrl(nextModelUrl);
14447
+ bindingsHistory.reset({});
14448
+ setSelectedItem(null);
14449
+ };
14450
+ const handleViewerModelLoaded = (0, import_react38.useCallback)(
14451
+ (scene) => {
14452
+ const pending = pendingLoadRef.current;
14453
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14454
+ pendingLoadRef.current = null;
14455
+ loadedSceneRef.current = { url: pending.modelUrl, scene };
14456
+ applyBindingsFromScene(scene, pending.modelName);
14457
+ },
14458
+ [applyBindingsFromScene]
14459
+ );
14460
+ const handleViewerLoadError = (0, import_react38.useCallback)((error) => {
14461
+ const pending = pendingLoadRef.current;
14462
+ if (!pending || pending.requestId !== loadRequestRef.current) return;
14463
+ pendingLoadRef.current = null;
14464
+ console.error(error);
14465
+ setStatusMessage(
14466
+ `Could not read ${pending.modelName}. Make sure the file is a valid GLB/GLTF asset.`
14467
+ );
14468
+ setStatusTone("error");
14469
+ setLoading(false);
14470
+ }, []);
14471
+ const handleAnimationsReady = (0, import_react38.useCallback)(
14472
+ (controls) => {
14473
+ viewerAnimations.handleAnimationsReady(controls);
14474
+ const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
14362
14475
  setSceneConfig((prev) => ({
14363
14476
  ...prev,
14364
- animations: mergeAnimationConfig(prev, animations),
14365
- annotations: []
14477
+ animations: mergeAnimationConfig(prev, details)
14366
14478
  }));
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
- };
14479
+ },
14480
+ [viewerAnimations, setSceneConfig]
14481
+ );
14382
14482
  const handleLoadDemo = async () => {
14383
14483
  uploadedModelRef.current?.cleanup();
14384
14484
  uploadedModelRef.current = null;
@@ -14408,6 +14508,8 @@ function BindingBuilder({ licenseKey }) {
14408
14508
  const clearModel = () => {
14409
14509
  uploadedModelRef.current?.cleanup();
14410
14510
  uploadedModelRef.current = null;
14511
+ loadedSceneRef.current = null;
14512
+ pendingLoadRef.current = null;
14411
14513
  previewDragDepthRef.current = 0;
14412
14514
  setIsPreviewDragActive(false);
14413
14515
  setModelUrl(null);
@@ -15206,7 +15308,9 @@ function BindingBuilder({ licenseKey }) {
15206
15308
  onAnnotationsChange: handleAnnotationsChange,
15207
15309
  activeAnnotation: activeAnnotation?.annotation ?? null,
15208
15310
  onActiveAnnotationChange: handleActiveAnnotationChange,
15209
- onAnimationsReady: viewerAnimations.handleAnimationsReady,
15311
+ onAnimationsReady: handleAnimationsReady,
15312
+ onModelLoaded: handleViewerModelLoaded,
15313
+ onLoadError: handleViewerLoadError,
15210
15314
  licenseKey,
15211
15315
  sceneConfig,
15212
15316
  disableZoom: false,
@@ -15290,7 +15394,7 @@ var import_drei6 = require("@react-three/drei");
15290
15394
  var import_fiber12 = require("@react-three/fiber");
15291
15395
  var import_fiber13 = require("@react-three/fiber");
15292
15396
  var import_three21 = require("three");
15293
- var import_three_stdlib5 = require("three-stdlib");
15397
+ var import_three_stdlib4 = require("three-stdlib");
15294
15398
 
15295
15399
  // src/styles/SimpleModelViewer.css
15296
15400
  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 +15463,7 @@ function InnerModel({
15359
15463
  const invalidate = (0, import_fiber12.useThree)((state) => state.invalidate);
15360
15464
  const [hoveredKey, setHoveredKey] = (0, import_react39.useState)(null);
15361
15465
  const scene = (0, import_react39.useMemo)(
15362
- () => import_three_stdlib5.SkeletonUtils.clone(rawScene),
15466
+ () => import_three_stdlib4.SkeletonUtils.clone(rawScene),
15363
15467
  [rawScene]
15364
15468
  );
15365
15469
  const meshMap = (0, import_react39.useMemo)(() => {