@liveroom-tech/react-immersive 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -174,7 +174,7 @@ import {
174
174
  Raycaster,
175
175
  Sphere as Sphere3,
176
176
  Vector2 as Vector26,
177
- Vector3 as Vector38
177
+ Vector3 as Vector39
178
178
  } from "three";
179
179
 
180
180
  // #style-inject:#style-inject
@@ -2053,6 +2053,28 @@ function Controls({ controlsRef, onCameraChange }) {
2053
2053
  // src/components/Lights.tsx
2054
2054
  import { useRef as useRef4, useMemo as useMemo5, useEffect as useEffect6 } from "react";
2055
2055
  import { useFrame as useFrame3, useThree as useThree5 } from "@react-three/fiber";
2056
+ import {
2057
+ Vector3 as Vector32
2058
+ } from "three";
2059
+
2060
+ // src/utils/resolveBindingKey.ts
2061
+ function resolveBindingKey(objectBindings, objectId) {
2062
+ if (objectBindings[objectId]) {
2063
+ return objectId;
2064
+ }
2065
+ const idMatch = Object.entries(objectBindings).find(
2066
+ ([, binding]) => binding.id === objectId
2067
+ )?.[0];
2068
+ if (idMatch) {
2069
+ return idMatch;
2070
+ }
2071
+ const modelObjectMatch = Object.entries(objectBindings).find(
2072
+ ([, binding]) => binding.modelObjectId === objectId
2073
+ )?.[0];
2074
+ return modelObjectMatch ?? null;
2075
+ }
2076
+
2077
+ // src/components/Lights.tsx
2056
2078
  import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
2057
2079
  function rotatePositionY(pos, deg) {
2058
2080
  if (!deg) return pos;
@@ -2132,21 +2154,42 @@ function LightInstance({
2132
2154
  shadows,
2133
2155
  shadowsEnabled,
2134
2156
  environmentRotation,
2135
- modelBounds
2157
+ modelBounds,
2158
+ objectBindings,
2159
+ nodeRefs
2136
2160
  }) {
2137
2161
  const dirRef = useRef4(null);
2138
2162
  const pointRef = useRef4(null);
2139
2163
  const spotRef = useRef4(null);
2140
2164
  const camera = useThree5((s) => s.camera);
2165
+ const rootScene = useThree5((s) => s.scene);
2166
+ const attachedPosition = useMemo5(() => new Vector32(), []);
2141
2167
  const position = useMemo5(
2142
- () => light.attachedToCamera ? light.position : rotatePositionY(light.position, environmentRotation),
2143
- [light.position, light.attachedToCamera, environmentRotation]
2168
+ () => light.attachedToCamera || light.attachment ? light.position : rotatePositionY(light.position, environmentRotation),
2169
+ [
2170
+ light.position,
2171
+ light.attachedToCamera,
2172
+ light.attachment,
2173
+ environmentRotation
2174
+ ]
2144
2175
  );
2145
2176
  useFrame3(() => {
2146
- if (!light.attachedToCamera) return;
2147
- if (dirRef.current) dirRef.current.position.copy(camera.position);
2148
- if (pointRef.current) pointRef.current.position.copy(camera.position);
2149
- if (spotRef.current) spotRef.current.position.copy(camera.position);
2177
+ const instance = dirRef.current ?? pointRef.current ?? spotRef.current;
2178
+ if (!instance) return;
2179
+ if (light.attachedToCamera) {
2180
+ instance.position.copy(camera.position);
2181
+ return;
2182
+ }
2183
+ const attachment = light.attachment;
2184
+ if (!attachment) return;
2185
+ const bindingKey = objectBindings ? resolveBindingKey(objectBindings, attachment.objectId) : null;
2186
+ const binding = bindingKey ? objectBindings?.[bindingKey] : void 0;
2187
+ const modelObjectId = binding?.modelObjectId ?? attachment.objectId;
2188
+ const target = (bindingKey ? nodeRefs?.current[bindingKey] : void 0) ?? rootScene.getObjectByName(modelObjectId);
2189
+ if (!target) return;
2190
+ attachedPosition.fromArray(attachment.offset);
2191
+ target.localToWorld(attachedPosition);
2192
+ instance.position.copy(attachedPosition);
2150
2193
  });
2151
2194
  const castShadow = shadowsEnabled && shadows.enabled && light.castShadow;
2152
2195
  const fit = useMemo5(
@@ -2192,6 +2235,8 @@ function LightInstance({
2192
2235
  color: light.color,
2193
2236
  intensity: light.intensity,
2194
2237
  position,
2238
+ distance: light.distance,
2239
+ decay: light.decay,
2195
2240
  castShadow,
2196
2241
  "shadow-mapSize-width": shadows.mapSize,
2197
2242
  "shadow-mapSize-height": shadows.mapSize,
@@ -2217,7 +2262,9 @@ function Lights({
2217
2262
  shadowsEnabled = true,
2218
2263
  environmentRotation = 0,
2219
2264
  environmentConfig,
2220
- modelBounds
2265
+ modelBounds,
2266
+ objectBindings,
2267
+ nodeRefs
2221
2268
  }) {
2222
2269
  const lighting = lightingConfig ?? DEFAULT_LIGHTING;
2223
2270
  const shadows = shadowConfig ?? DEFAULT_SHADOWS;
@@ -2238,7 +2285,9 @@ function Lights({
2238
2285
  shadows,
2239
2286
  shadowsEnabled,
2240
2287
  environmentRotation,
2241
- modelBounds
2288
+ modelBounds,
2289
+ objectBindings,
2290
+ nodeRefs
2242
2291
  },
2243
2292
  light.id
2244
2293
  )),
@@ -2259,7 +2308,9 @@ function Lights({
2259
2308
  shadows,
2260
2309
  shadowsEnabled,
2261
2310
  environmentRotation: 0,
2262
- modelBounds
2311
+ modelBounds,
2312
+ objectBindings,
2313
+ nodeRefs
2263
2314
  }
2264
2315
  )
2265
2316
  ] });
@@ -2368,7 +2419,7 @@ import {
2368
2419
  LoopOnce,
2369
2420
  LoopRepeat,
2370
2421
  Box3 as Box32,
2371
- Vector3 as Vector32,
2422
+ Vector3 as Vector33,
2372
2423
  Matrix4,
2373
2424
  MathUtils as MathUtils2
2374
2425
  } from "three";
@@ -2984,6 +3035,24 @@ function refreshSkinnedBounds(root) {
2984
3035
 
2985
3036
  // src/components/Model.tsx
2986
3037
  import { jsx as jsx8 } from "react/jsx-runtime";
3038
+ function applyBindingTransform(node, transform, original) {
3039
+ if (!original) return;
3040
+ if (transform?.position) node.position.fromArray(transform.position);
3041
+ else node.position.copy(original.position);
3042
+ if (transform?.rotation) {
3043
+ node.rotation.set(
3044
+ transform.rotation[0],
3045
+ transform.rotation[1],
3046
+ transform.rotation[2],
3047
+ original.rotation.order
3048
+ );
3049
+ } else {
3050
+ node.rotation.copy(original.rotation);
3051
+ }
3052
+ if (transform?.scale) node.scale.fromArray(transform.scale);
3053
+ else node.scale.copy(original.scale);
3054
+ node.updateMatrix();
3055
+ }
2987
3056
  function firstMaterial(node) {
2988
3057
  const mat = node.material;
2989
3058
  if (!mat) return void 0;
@@ -3037,6 +3106,17 @@ function Model({
3037
3106
  });
3038
3107
  return map;
3039
3108
  }, [scene]);
3109
+ const originalNodeTransforms = useMemo7(() => {
3110
+ const transforms = /* @__PURE__ */ new WeakMap();
3111
+ scene.traverse((node) => {
3112
+ transforms.set(node, {
3113
+ position: node.position.clone(),
3114
+ rotation: node.rotation.clone(),
3115
+ scale: node.scale.clone()
3116
+ });
3117
+ });
3118
+ return transforms;
3119
+ }, [scene]);
3040
3120
  const shellNodeNames = useMemo7(() => {
3041
3121
  const shell = /* @__PURE__ */ new Set();
3042
3122
  const sceneBox = new Box32();
@@ -3048,8 +3128,8 @@ function Model({
3048
3128
  meshNodes.push({ name, box });
3049
3129
  sceneBox.union(box);
3050
3130
  }
3051
- const sceneSize = sceneBox.getSize(new Vector32());
3052
- const size = new Vector32();
3131
+ const sceneSize = sceneBox.getSize(new Vector33());
3132
+ const size = new Vector33();
3053
3133
  for (const { name, box } of meshNodes) {
3054
3134
  box.getSize(size);
3055
3135
  const isShell = size.x > sceneSize.x * 0.7 && size.z > sceneSize.z * 0.7 && size.y > sceneSize.y * 0.4;
@@ -3089,6 +3169,43 @@ function Model({
3089
3169
  ).sort().join(""),
3090
3170
  [objectBindings]
3091
3171
  );
3172
+ const bindingTransformSignature = useMemo7(
3173
+ () => Object.entries(objectBindings).map(
3174
+ ([nodeName, binding]) => JSON.stringify([
3175
+ nodeName,
3176
+ binding.modelObjectId ?? nodeName,
3177
+ binding.transform ?? null
3178
+ ])
3179
+ ).sort().join(""),
3180
+ [objectBindings]
3181
+ );
3182
+ const transformedNodesRef = useRef5(/* @__PURE__ */ new Set());
3183
+ useEffect7(() => {
3184
+ for (const node of transformedNodesRef.current) {
3185
+ applyBindingTransform(node, void 0, originalNodeTransforms.get(node));
3186
+ }
3187
+ const nextTransformedNodes = /* @__PURE__ */ new Set();
3188
+ for (const [nodeName, binding] of Object.entries(objectBindings)) {
3189
+ if (!binding.transform) continue;
3190
+ const node = nodeMap[binding.modelObjectId ?? nodeName];
3191
+ if (!node) continue;
3192
+ applyBindingTransform(
3193
+ node,
3194
+ binding.transform,
3195
+ originalNodeTransforms.get(node)
3196
+ );
3197
+ nextTransformedNodes.add(node);
3198
+ }
3199
+ transformedNodesRef.current = nextTransformedNodes;
3200
+ scene.updateMatrixWorld(true);
3201
+ invalidate();
3202
+ }, [
3203
+ scene,
3204
+ nodeMap,
3205
+ originalNodeTransforms,
3206
+ bindingTransformSignature,
3207
+ invalidate
3208
+ ]);
3092
3209
  const perNodeMaterials = useMemo7(() => {
3093
3210
  const map = {};
3094
3211
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -3592,27 +3709,30 @@ function Model({
3592
3709
  const explodePartsRef = useRef5([]);
3593
3710
  const explodeStrengthRef = useRef5(0);
3594
3711
  const currentExplodeRef = useRef5(0);
3595
- const explodeTmp = useMemo7(() => new Vector32(), []);
3712
+ const explodeTmp = useMemo7(() => new Vector33(), []);
3596
3713
  useEffect7(() => {
3714
+ const bindingByNode = /* @__PURE__ */ new Map();
3715
+ for (const [nodeName, binding] of Object.entries(objectBindings)) {
3716
+ const node = nodeMap[binding.modelObjectId ?? nodeName];
3717
+ if (node) bindingByNode.set(node, binding);
3718
+ }
3597
3719
  for (const part of explodePartsRef.current) {
3598
- part.node.position.copy(part.restLocalPos);
3720
+ applyBindingTransform(
3721
+ part.node,
3722
+ bindingByNode.get(part.node)?.transform,
3723
+ originalNodeTransforms.get(part.node)
3724
+ );
3599
3725
  }
3600
3726
  currentExplodeRef.current = 0;
3601
3727
  scene.updateWorldMatrix(true, true);
3602
3728
  const sceneBox = new Box32().setFromObject(scene);
3603
- const modelCenter = sceneBox.getCenter(new Vector32());
3604
- const radius = sceneBox.getSize(new Vector32()).length() * 0.5;
3729
+ const modelCenter = sceneBox.getCenter(new Vector33());
3730
+ const radius = sceneBox.getSize(new Vector33()).length() * 0.5;
3605
3731
  explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
3606
- const targets = [];
3607
- const targetSet = /* @__PURE__ */ new Set();
3608
- for (const [nodeName, binding] of Object.entries(objectBindings)) {
3609
- const node = nodeMap[binding.modelObjectId ?? nodeName];
3610
- if (!node || targetSet.has(node)) continue;
3611
- targetSet.add(node);
3612
- targets.push(node);
3613
- }
3732
+ const targets = [...bindingByNode.keys()];
3733
+ const targetSet = new Set(targets);
3614
3734
  const parts = [];
3615
- const partCenter = new Vector32();
3735
+ const partCenter = new Vector33();
3616
3736
  for (const node of targets) {
3617
3737
  if (!node.parent) continue;
3618
3738
  let ancestor = node.parent;
@@ -3634,14 +3754,21 @@ function Model({
3634
3754
  parts.push({
3635
3755
  node,
3636
3756
  restLocalPos: node.position.clone(),
3637
- restWorldPos: node.getWorldPosition(new Vector32()),
3757
+ restWorldPos: node.getWorldPosition(new Vector33()),
3638
3758
  worldDir,
3639
3759
  parentInverse: new Matrix4().copy(node.parent.matrixWorld).invert()
3640
3760
  });
3641
3761
  }
3642
3762
  explodePartsRef.current = parts;
3643
3763
  invalidate();
3644
- }, [scene, nodeMap, nodeBindingSignature, invalidate]);
3764
+ }, [
3765
+ scene,
3766
+ nodeMap,
3767
+ nodeBindingSignature,
3768
+ bindingTransformSignature,
3769
+ originalNodeTransforms,
3770
+ invalidate
3771
+ ]);
3645
3772
  useEffect7(() => {
3646
3773
  invalidate();
3647
3774
  }, [explodeFactor, invalidate]);
@@ -3733,7 +3860,7 @@ import {
3733
3860
  } from "react";
3734
3861
  import { Html } from "@react-three/drei";
3735
3862
  import { useFrame as useFrame5, useThree as useThree8 } from "@react-three/fiber";
3736
- import { MathUtils as MathUtils3, Vector3 as Vector33 } from "three";
3863
+ import { MathUtils as MathUtils3, Vector3 as Vector34 } from "three";
3737
3864
 
3738
3865
  // src/styles/AnnotationsLayer.css
3739
3866
  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");
@@ -3746,7 +3873,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3746
3873
  const scene = useThree8((state) => state.scene);
3747
3874
  const anchorNodeRef = useRef6(null);
3748
3875
  const baseLocalPosition = useMemo8(
3749
- () => new Vector33(
3876
+ () => new Vector34(
3750
3877
  ...annotation.anchorLocalPosition ?? annotation.localPosition ?? annotation.worldPosition
3751
3878
  ),
3752
3879
  [
@@ -3755,9 +3882,9 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3755
3882
  annotation.worldPosition
3756
3883
  ]
3757
3884
  );
3758
- const worldPosition = useMemo8(() => new Vector33(), []);
3759
- const biasedWorldPosition = useMemo8(() => new Vector33(), []);
3760
- const toCamera = useMemo8(() => new Vector33(), []);
3885
+ const worldPosition = useMemo8(() => new Vector34(), []);
3886
+ const biasedWorldPosition = useMemo8(() => new Vector34(), []);
3887
+ const toCamera = useMemo8(() => new Vector34(), []);
3761
3888
  useEffect8(() => {
3762
3889
  anchorNodeRef.current = null;
3763
3890
  }, [annotation.anchorNodeName]);
@@ -3780,7 +3907,7 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3780
3907
  if (worldPositionsRef) {
3781
3908
  let stored = worldPositionsRef.current.get(annotation.id);
3782
3909
  if (!stored) {
3783
- stored = new Vector33();
3910
+ stored = new Vector34();
3784
3911
  worldPositionsRef.current.set(annotation.id, stored);
3785
3912
  }
3786
3913
  stored.copy(worldPosition);
@@ -4178,7 +4305,7 @@ function AnnotationsLayer({
4178
4305
  // src/components/ModelViewer/MeasurementLayer.tsx
4179
4306
  import { useEffect as useEffect9, useMemo as useMemo9 } from "react";
4180
4307
  import { Html as Html2, Line } from "@react-three/drei";
4181
- import { BoxGeometry, EdgesGeometry, Vector3 as Vector34 } from "three";
4308
+ import { BoxGeometry, EdgesGeometry, Vector3 as Vector35 } from "three";
4182
4309
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
4183
4310
  var LINE_COLOR = "#22d3ee";
4184
4311
  var BOX_COLOR = "#facc15";
@@ -4253,7 +4380,7 @@ function MeasurementLayer({
4253
4380
  points.slice(1).map((point, index) => {
4254
4381
  const a = points[index].position;
4255
4382
  const b = point.position;
4256
- const distance = new Vector34(...a).distanceTo(new Vector34(...b));
4383
+ const distance = new Vector35(...a).distanceTo(new Vector35(...b));
4257
4384
  return /* @__PURE__ */ jsxs6("group", { children: [
4258
4385
  /* @__PURE__ */ jsx10(Line, { points: [a, b], color: LINE_COLOR, lineWidth: 2 }),
4259
4386
  /* @__PURE__ */ jsx10(MeasureLabel, { position: midpoint(a, b), children: formatMeasurement(distance, unit) })
@@ -4274,7 +4401,7 @@ import {
4274
4401
  } from "react";
4275
4402
  import { useXR as useXR2, useXRHitTest, useXRInputSourceEvent } from "@react-three/xr";
4276
4403
  import { useFrame as useFrame6 } from "@react-three/fiber";
4277
- import { Matrix4 as Matrix42, Vector3 as Vector35 } from "three";
4404
+ import { Matrix4 as Matrix42, Vector3 as Vector36 } from "three";
4278
4405
  import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
4279
4406
  var TARGET_SIZE = 1;
4280
4407
  var FORWARD_DISTANCE = 1;
@@ -4311,13 +4438,13 @@ var XRModelPresenter = forwardRef2(
4311
4438
  () => box ? computeNormalization(box, scaleMode) : null,
4312
4439
  [box, scaleMode]
4313
4440
  );
4314
- const placedPosition = useRef7(new Vector35(0, 0, -FORWARD_DISTANCE));
4441
+ const placedPosition = useRef7(new Vector36(0, 0, -FORWARD_DISTANCE));
4315
4442
  const userScale = useRef7(1);
4316
4443
  const rotationY = useRef7(0);
4317
4444
  const placementMode = useRef7(true);
4318
4445
  const modelPlaced = useRef7(false);
4319
4446
  const hitMatrix = useMemo10(() => new Matrix42(), []);
4320
- const hitPosition = useRef7(new Vector35());
4447
+ const hitPosition = useRef7(new Vector36());
4321
4448
  const hasHit = useRef7(false);
4322
4449
  const lastARState = useRef7(null);
4323
4450
  const gesturing = useRef7(false);
@@ -4515,7 +4642,7 @@ import {
4515
4642
  CatmullRomCurve3,
4516
4643
  Sphere,
4517
4644
  Spherical,
4518
- Vector3 as Vector36
4645
+ Vector3 as Vector37
4519
4646
  } from "three";
4520
4647
  var ENTER_BLEND_SECONDS = 1.2;
4521
4648
  function CinematicCamera({
@@ -4596,13 +4723,13 @@ function CinematicCamera({
4596
4723
  if (!hasPath) return null;
4597
4724
  return {
4598
4725
  position: new CatmullRomCurve3(
4599
- waypoints.map((w) => new Vector36(...w.position)),
4726
+ waypoints.map((w) => new Vector37(...w.position)),
4600
4727
  loop,
4601
4728
  "catmullrom",
4602
4729
  0.5
4603
4730
  ),
4604
4731
  target: new CatmullRomCurve3(
4605
- waypoints.map((w) => new Vector36(...w.target)),
4732
+ waypoints.map((w) => new Vector37(...w.target)),
4606
4733
  loop,
4607
4734
  "catmullrom",
4608
4735
  0.5
@@ -4618,8 +4745,8 @@ function CinematicCamera({
4618
4745
  const points = waypoints.map((waypoint) => ({
4619
4746
  azimuth: waypoint.azimuthAngle,
4620
4747
  polar: waypoint.polarAngle,
4621
- distance: new Vector36(...waypoint.position).distanceTo(
4622
- new Vector36(...waypoint.target)
4748
+ distance: new Vector37(...waypoint.position).distanceTo(
4749
+ new Vector37(...waypoint.target)
4623
4750
  )
4624
4751
  }));
4625
4752
  for (let index = 1; index < points.length; index += 1) {
@@ -4631,14 +4758,14 @@ function CinematicCamera({
4631
4758
  }, [signature, hasPath]);
4632
4759
  const elapsedRef = useRef8(0);
4633
4760
  const enterElapsedRef = useRef8(0);
4634
- const enterPos = useRef8(new Vector36());
4635
- const enterTarget = useRef8(new Vector36());
4761
+ const enterPos = useRef8(new Vector37());
4762
+ const enterTarget = useRef8(new Vector37());
4636
4763
  const startedRef = useRef8(false);
4637
4764
  const pausedElapsedRef = useRef8(0);
4638
4765
  const hasFinishedRef = useRef8(false);
4639
4766
  const orbitRef = useRef8(null);
4640
- const tmpPos = useMemo11(() => new Vector36(), []);
4641
- const tmpTarget = useMemo11(() => new Vector36(), []);
4767
+ const tmpPos = useMemo11(() => new Vector37(), []);
4768
+ const tmpTarget = useMemo11(() => new Vector37(), []);
4642
4769
  const tmpSpherical = useMemo11(() => new Spherical(), []);
4643
4770
  useEffect11(() => {
4644
4771
  elapsedRef.current = 0;
@@ -5877,7 +6004,7 @@ import { useRef as useRef11, useEffect as useEffect15, useMemo as useMemo13, use
5877
6004
  import { useThree as useThree10, useFrame as useFrame8 } from "@react-three/fiber";
5878
6005
  import { Sphere as Sphere2, Line as Line2 } from "@react-three/drei";
5879
6006
  import {
5880
- Vector3 as Vector37,
6007
+ Vector3 as Vector38,
5881
6008
  Vector2 as Vector25,
5882
6009
  AdditiveBlending as AdditiveBlending2,
5883
6010
  Plane,
@@ -5905,7 +6032,7 @@ var RING_SCREEN_SIZE = SUN_SCREEN_SIZE * 3;
5905
6032
  var RING_COLOR = "#4d7dff";
5906
6033
  var LINE_COLOR2 = "#ffcc00";
5907
6034
  var LINE_GLOW_RADIUS = 0.02;
5908
- var UP_AXIS = new Vector37(0, 1, 0);
6035
+ var UP_AXIS = new Vector38(0, 1, 0);
5909
6036
  var AXIS_ARROW_LENGTH = 1.4;
5910
6037
  var AXIS_CONE_LENGTH = 0.35;
5911
6038
  var AXIS_CONE_RADIUS = 0.13;
@@ -5917,17 +6044,17 @@ var AXIS_COLORS = {
5917
6044
  z: "#4444ff"
5918
6045
  };
5919
6046
  function axisVector(axis) {
5920
- return new Vector37(axis === "x" ? 1 : 0, axis === "y" ? 1 : 0, axis === "z" ? 1 : 0);
6047
+ return new Vector38(axis === "x" ? 1 : 0, axis === "y" ? 1 : 0, axis === "z" ? 1 : 0);
5921
6048
  }
5922
6049
  function computeAxisDragPlaneNormal(axis, eye) {
5923
6050
  const axisVec = axisVector(axis);
5924
6051
  if (eye.lengthSq() < 1e-8) {
5925
6052
  eye = axisVec.clone();
5926
6053
  }
5927
- let normal = new Vector37().crossVectors(axisVec, new Vector37().crossVectors(eye, axisVec));
6054
+ let normal = new Vector38().crossVectors(axisVec, new Vector38().crossVectors(eye, axisVec));
5928
6055
  if (normal.lengthSq() < 1e-8) {
5929
- const helper = Math.abs(axisVec.y) < 0.9 ? UP_AXIS : new Vector37(1, 0, 0);
5930
- normal = new Vector37().crossVectors(axisVec, helper);
6056
+ const helper = Math.abs(axisVec.y) < 0.9 ? UP_AXIS : new Vector38(1, 0, 0);
6057
+ normal = new Vector38().crossVectors(axisVec, helper);
5931
6058
  }
5932
6059
  return normal.normalize();
5933
6060
  }
@@ -6000,17 +6127,17 @@ function LightHelpers({
6000
6127
  const shaftZRef = useRef11(null);
6001
6128
  const axisDragRef = useRef11({ dragging: false, axis: null });
6002
6129
  const axisDragPlaneRef = useRef11(new Plane());
6003
- const dragStartPosRef = useRef11(new Vector37());
6130
+ const dragStartPosRef = useRef11(new Vector38());
6004
6131
  const dragLineRef = useRef11(null);
6005
6132
  const dragLineGeomRef = useRef11(null);
6006
- const currentPosRef = useRef11(new Vector37());
6133
+ const currentPosRef = useRef11(new Vector38());
6007
6134
  const isDraggingRef = useRef11(false);
6008
6135
  const dragPlaneRef = useRef11(new Plane());
6009
6136
  const onDraggingChangeRef = useRef11(onDraggingChange);
6010
6137
  useEffect15(() => {
6011
6138
  onDraggingChangeRef.current = onDraggingChange;
6012
6139
  }, [onDraggingChange]);
6013
- const lightPos = useMemo13(() => new Vector37(position[0], position[1], position[2]), [position]);
6140
+ const lightPos = useMemo13(() => new Vector38(position[0], position[1], position[2]), [position]);
6014
6141
  const linePositions = useMemo13(() => new Float32Array(6), []);
6015
6142
  const dragLinePositions = useMemo13(() => new Float32Array(6), []);
6016
6143
  const ringXPositions = useMemo13(() => makeRingPositions("x"), []);
@@ -6169,12 +6296,12 @@ function LightHelpers({
6169
6296
  const axis = axisDragRef.current.axis;
6170
6297
  const currentPos = currentPosRef.current;
6171
6298
  const axisVec = axisVector(axis);
6172
- const eye = new Vector37().subVectors(camera.position, currentPos);
6299
+ const eye = new Vector38().subVectors(camera.position, currentPos);
6173
6300
  const planeNormal = computeAxisDragPlaneNormal(axis, eye);
6174
6301
  axisDragPlaneRef.current.setFromNormalAndCoplanarPoint(planeNormal, currentPos);
6175
6302
  const mouse2 = getMouseNdc(event);
6176
6303
  raycaster.setFromCamera(mouse2, camera);
6177
- const intersection2 = new Vector37();
6304
+ const intersection2 = new Vector38();
6178
6305
  const hitPoint2 = raycaster.ray.intersectPlane(axisDragPlaneRef.current, intersection2);
6179
6306
  if (!hitPoint2) return;
6180
6307
  const toPoint = hitPoint2.clone().sub(currentPos);
@@ -6193,7 +6320,7 @@ function LightHelpers({
6193
6320
  if (!isDraggingRef.current) return;
6194
6321
  const mouse = getMouseNdc(event);
6195
6322
  raycaster.setFromCamera(mouse, camera);
6196
- const intersection = new Vector37();
6323
+ const intersection = new Vector38();
6197
6324
  const hitPoint = raycaster.ray.intersectPlane(dragPlaneRef.current, intersection);
6198
6325
  if (!hitPoint) return;
6199
6326
  const newPos = [
@@ -6240,7 +6367,7 @@ function LightHelpers({
6240
6367
  setActiveTab?.("scene");
6241
6368
  setActiveSubTab?.("Lighting");
6242
6369
  }
6243
- const cameraDir = new Vector37();
6370
+ const cameraDir = new Vector38();
6244
6371
  camera.getWorldDirection(cameraDir);
6245
6372
  dragPlaneRef.current.setFromNormalAndCoplanarPoint(cameraDir, currentPosRef.current);
6246
6373
  isDraggingRef.current = true;
@@ -6373,17 +6500,17 @@ function LightHelpers({
6373
6500
  ] }),
6374
6501
  gizmoVisible && /* @__PURE__ */ jsx19("group", { ref: axisGroupRef, children: axisArrows.map((axis) => {
6375
6502
  const axisColor = AXIS_COLORS[axis];
6376
- const end = new Vector37(
6503
+ const end = new Vector38(
6377
6504
  axis === "x" ? AXIS_ARROW_LENGTH : 0,
6378
6505
  axis === "y" ? AXIS_ARROW_LENGTH : 0,
6379
6506
  axis === "z" ? AXIS_ARROW_LENGTH : 0
6380
6507
  );
6381
- const conePos = new Vector37(
6508
+ const conePos = new Vector38(
6382
6509
  axis === "x" ? AXIS_ARROW_LENGTH - AXIS_CONE_LENGTH / 2 : 0,
6383
6510
  axis === "y" ? AXIS_ARROW_LENGTH - AXIS_CONE_LENGTH / 2 : 0,
6384
6511
  axis === "z" ? AXIS_ARROW_LENGTH - AXIS_CONE_LENGTH / 2 : 0
6385
6512
  );
6386
- const shaftPos = new Vector37(
6513
+ const shaftPos = new Vector38(
6387
6514
  axis === "x" ? AXIS_ARROW_LENGTH / 2 : 0,
6388
6515
  axis === "y" ? AXIS_ARROW_LENGTH / 2 : 0,
6389
6516
  axis === "z" ? AXIS_ARROW_LENGTH / 2 : 0
@@ -6395,7 +6522,7 @@ function LightHelpers({
6395
6522
  /* @__PURE__ */ jsx19(
6396
6523
  Line2,
6397
6524
  {
6398
- points: [new Vector37(0, 0, 0), end],
6525
+ points: [new Vector38(0, 0, 0), end],
6399
6526
  color: axisColor,
6400
6527
  transparent: true,
6401
6528
  opacity: 0.9,
@@ -6484,7 +6611,7 @@ function findInfluencingBone(hit) {
6484
6611
  if (!skinIndex || !skinWeight || !position) return null;
6485
6612
  const localHit = mesh.worldToLocal(hit.point.clone());
6486
6613
  const vertices = [hit.face.a, hit.face.b, hit.face.c];
6487
- const vertex = new Vector38();
6614
+ const vertex = new Vector39();
6488
6615
  let nearest = vertices[0];
6489
6616
  let nearestDist = Infinity;
6490
6617
  for (const vi of vertices) {
@@ -6534,9 +6661,9 @@ function GuidedTourCameraFollow({
6534
6661
  const scene = useThree11((state) => state.scene);
6535
6662
  const nodeRef = useRef12(null);
6536
6663
  const hasPrev = useRef12(false);
6537
- const live = useMemo14(() => new Vector38(), []);
6538
- const prev = useMemo14(() => new Vector38(), []);
6539
- const goal = useMemo14(() => new Vector38(), []);
6664
+ const live = useMemo14(() => new Vector39(), []);
6665
+ const prev = useMemo14(() => new Vector39(), []);
6666
+ const goal = useMemo14(() => new Vector39(), []);
6540
6667
  const nodeName = annotation?.anchorNodeName;
6541
6668
  const localPosition = annotation?.anchorLocalPosition;
6542
6669
  useEffect16(() => {
@@ -6576,7 +6703,7 @@ var DEFAULT_AR_OVERLAY_STATE = {
6576
6703
  };
6577
6704
  function computeMeasureBox(scene) {
6578
6705
  const box = new Box34().setFromObject(scene);
6579
- const size = new Vector38();
6706
+ const size = new Vector39();
6580
6707
  box.getSize(size);
6581
6708
  return {
6582
6709
  min: [box.min.x, box.min.y, box.min.z],
@@ -6597,8 +6724,8 @@ function computeUvMeshStats(scene) {
6597
6724
  function computeModelBounds(scene) {
6598
6725
  const box = new Box34().setFromObject(scene);
6599
6726
  if (box.isEmpty()) return null;
6600
- const center = box.getCenter(new Vector38());
6601
- const radius = box.getSize(new Vector38()).length() / 2;
6727
+ const center = box.getCenter(new Vector39());
6728
+ const radius = box.getSize(new Vector39()).length() / 2;
6602
6729
  if (!Number.isFinite(radius) || radius <= 0) return null;
6603
6730
  return { center: [center.x, center.y, center.z], radius };
6604
6731
  }
@@ -6606,7 +6733,7 @@ function fitClippingPlanesToScene(scene, controls) {
6606
6733
  const camera = controls?.camera;
6607
6734
  if (!(camera instanceof PerspectiveCamera3)) return;
6608
6735
  const box = new Box34().setFromObject(scene);
6609
- const size = box.getSize(new Vector38()).length();
6736
+ const size = box.getSize(new Vector39()).length();
6610
6737
  if (!Number.isFinite(size) || size <= 0) return;
6611
6738
  camera.near = size / 100;
6612
6739
  camera.far = size * 100;
@@ -6791,8 +6918,8 @@ function ModelViewer({
6791
6918
  if (!controls || initialCameraStateRef.current) {
6792
6919
  return;
6793
6920
  }
6794
- const position = controls.getPosition(new Vector38());
6795
- const target = controls.getTarget(new Vector38());
6921
+ const position = controls.getPosition(new Vector39());
6922
+ const target = controls.getTarget(new Vector39());
6796
6923
  initialCameraStateRef.current = {
6797
6924
  position: [position.x, position.y, position.z],
6798
6925
  target: [target.x, target.y, target.z]
@@ -7050,7 +7177,7 @@ function ModelViewer({
7050
7177
  const controls = controlsRef.current;
7051
7178
  const scene = sceneRef.current;
7052
7179
  if (!controls) return;
7053
- const target = new Vector38();
7180
+ const target = new Vector39();
7054
7181
  const livePosition = annotationWorldPositionsRef.current.get(
7055
7182
  annotation.id
7056
7183
  );
@@ -7069,14 +7196,14 @@ function ModelViewer({
7069
7196
  const storedNormal = annotation.worldNormal;
7070
7197
  if (scene) {
7071
7198
  const box = new Box34().setFromObject(scene);
7072
- const modelCenter = new Vector38();
7199
+ const modelCenter = new Vector39();
7073
7200
  box.getCenter(modelCenter);
7074
- const modelSize = new Vector38();
7201
+ const modelSize = new Vector39();
7075
7202
  box.getSize(modelSize);
7076
7203
  const modelRadius = modelSize.length() * 0.2;
7077
7204
  let outwardDir;
7078
7205
  if (storedNormal) {
7079
- outwardDir = new Vector38(
7206
+ outwardDir = new Vector39(
7080
7207
  storedNormal[0],
7081
7208
  storedNormal[1],
7082
7209
  storedNormal[2]
@@ -7227,6 +7354,16 @@ function ModelViewer({
7227
7354
  const isControlledBindings = onObjectBindingsChange !== void 0;
7228
7355
  const currentObjectBindings = isControlledBindings ? objectBindings : internalObjectBindings;
7229
7356
  currentObjectBindingsRef.current = currentObjectBindings;
7357
+ const currentBindingTransformSignature = useMemo14(
7358
+ () => Object.entries(currentObjectBindings).map(
7359
+ ([bindingKey, binding]) => JSON.stringify([
7360
+ bindingKey,
7361
+ binding.modelObjectId,
7362
+ binding.transform ?? null
7363
+ ])
7364
+ ).sort().join(""),
7365
+ [currentObjectBindings]
7366
+ );
7230
7367
  const currentHiddenObjects = useMemo14(
7231
7368
  () => Object.fromEntries(
7232
7369
  Object.entries(currentObjectBindings).filter(([, binding]) => binding.visible === false).map(([bindingKey]) => [bindingKey, true])
@@ -7366,7 +7503,7 @@ function ModelViewer({
7366
7503
  const fallbackY = Math.max(minOffset, (rect.height - height) / 2);
7367
7504
  const objectKey = resolveObjectKey(objectId, binding);
7368
7505
  const object = objectKey ? nodeRefsMap.current[objectKey] : null;
7369
- const clamp2 = (value, max) => {
7506
+ const clamp3 = (value, max) => {
7370
7507
  if (max <= minOffset) {
7371
7508
  return minOffset;
7372
7509
  }
@@ -7374,23 +7511,23 @@ function ModelViewer({
7374
7511
  };
7375
7512
  if (canvasContainer && cameraControls && object) {
7376
7513
  const canvasRect = canvasContainer.getBoundingClientRect();
7377
- const objectCenter = new Vector38();
7514
+ const objectCenter = new Vector39();
7378
7515
  new Box34().setFromObject(object).getCenter(objectCenter);
7379
7516
  objectCenter.project(cameraControls);
7380
7517
  const projectedX = (objectCenter.x + 1) / 2 * canvasRect.width + (canvasRect.left - rect.left);
7381
7518
  const projectedY = (1 - objectCenter.y) / 2 * canvasRect.height + (canvasRect.top - rect.top);
7382
7519
  return {
7383
7520
  objectId,
7384
- x: clamp2(projectedX + 24, maxX),
7385
- y: clamp2(projectedY - height / 2, maxY)
7521
+ x: clamp3(projectedX + 24, maxX),
7522
+ y: clamp3(projectedY - height / 2, maxY)
7386
7523
  };
7387
7524
  }
7388
7525
  const rawX = typeof fallbackCoords?.screenX === "number" ? fallbackCoords.screenX - rect.left : fallbackX;
7389
7526
  const rawY = typeof fallbackCoords?.screenY === "number" ? fallbackCoords.screenY - rect.top : fallbackY;
7390
7527
  return {
7391
7528
  objectId,
7392
- x: clamp2(rawX, maxX),
7393
- y: clamp2(rawY, maxY)
7529
+ x: clamp3(rawX, maxX),
7530
+ y: clamp3(rawY, maxY)
7394
7531
  };
7395
7532
  },
7396
7533
  [resolveObjectKey]
@@ -7399,7 +7536,6 @@ function ModelViewer({
7399
7536
  (event) => {
7400
7537
  const { objectId, action, binding } = event;
7401
7538
  setCurrentAction(event);
7402
- onAction?.(event);
7403
7539
  switch (action.id) {
7404
7540
  case "toggle-visibility": {
7405
7541
  let targetKey = objectId;
@@ -7447,6 +7583,7 @@ function ModelViewer({
7447
7583
  default:
7448
7584
  break;
7449
7585
  }
7586
+ onAction?.(event);
7450
7587
  },
7451
7588
  [
7452
7589
  applyObjectBindings,
@@ -7587,7 +7724,8 @@ function ModelViewer({
7587
7724
  objectBindings: currentObjectBindingsRef.current,
7588
7725
  nodeRefs: nodeRefsMap.current,
7589
7726
  homeCameraState: initialCameraStateRef.current,
7590
- captureImage: (options) => captureImage(rendererCaptureRef.current, options)
7727
+ captureImage: (options) => captureImage(rendererCaptureRef.current, options),
7728
+ invalidate: () => rendererCaptureRef.current?.invalidate()
7591
7729
  });
7592
7730
  }, []);
7593
7731
  const fitScene = useCallback4(
@@ -7599,8 +7737,8 @@ function ModelViewer({
7599
7737
  }
7600
7738
  try {
7601
7739
  if (flyIn) {
7602
- const target = controls.getTarget(new Vector38());
7603
- const position = controls.getPosition(new Vector38());
7740
+ const target = controls.getTarget(new Vector39());
7741
+ const position = controls.getPosition(new Vector39());
7604
7742
  const direction = position.clone().sub(target);
7605
7743
  if (direction.lengthSq() < 1e-6) {
7606
7744
  direction.set(0, 0, 1);
@@ -7918,6 +8056,13 @@ function ModelViewer({
7918
8056
  );
7919
8057
  setBindingsMismatch(!hasAnyMatch);
7920
8058
  }, [modelLoaded, currentObjectBindings]);
8059
+ useEffect16(() => {
8060
+ const scene = sceneRef.current;
8061
+ if (!modelLoaded || !scene) return;
8062
+ scene.updateMatrixWorld(true);
8063
+ setMeasureBox(computeMeasureBox(scene));
8064
+ setModelBounds(computeModelBounds(scene));
8065
+ }, [modelLoaded, currentBindingTransformSignature]);
7921
8066
  const clearMeasurements = useCallback4(() => setMeasurePoints([]), []);
7922
8067
  const handleMeasurePointerDown = useCallback4(
7923
8068
  (event) => {
@@ -7961,7 +8106,11 @@ function ModelViewer({
7961
8106
  );
7962
8107
  useEffect16(() => {
7963
8108
  setMeasurePoints([]);
7964
- }, [modelUrl, resolvedSceneConfig.model.rotation]);
8109
+ }, [
8110
+ modelUrl,
8111
+ resolvedSceneConfig.model.rotation,
8112
+ currentBindingTransformSignature
8113
+ ]);
7965
8114
  const handleDownloadMesh = useCallback4(() => {
7966
8115
  const matRefs = materialRefsOut.current;
7967
8116
  const scene = sceneRef.current;
@@ -8304,11 +8453,13 @@ function ModelViewer({
8304
8453
  shadowsEnabled: shadows,
8305
8454
  environmentRotation: resolvedSceneConfig.environment.rotation,
8306
8455
  environmentConfig: resolvedSceneConfig.environment,
8307
- modelBounds
8456
+ modelBounds,
8457
+ objectBindings: currentObjectBindings,
8458
+ nodeRefs: nodeRefsMap
8308
8459
  }
8309
8460
  ),
8310
8461
  showLightHelpers && resolvedSceneConfig.lighting.lights.map((light) => {
8311
- if (!light.visible || light.type === "none" || light.type === "hemisphere") {
8462
+ if (!light.visible || light.type === "none" || light.type === "hemisphere" || light.attachment) {
8312
8463
  return null;
8313
8464
  }
8314
8465
  return /* @__PURE__ */ jsx20(
@@ -8950,7 +9101,7 @@ function ModelViewer({
8950
9101
  }
8951
9102
 
8952
9103
  // src/components/BindingBuilder/BindingBuilder.tsx
8953
- import { useCallback as useCallback9, useEffect as useEffect22, useMemo as useMemo17, useRef as useRef19, useState as useState26 } from "react";
9104
+ import { useCallback as useCallback9, useEffect as useEffect23, useMemo as useMemo17, useRef as useRef19, useState as useState27 } from "react";
8954
9105
  import {
8955
9106
  Redo2,
8956
9107
  Undo2,
@@ -8965,32 +9116,13 @@ styleInject('.bb-root {\n --bb-font-family:\n Inter,\n "Segoe UI",\n H
8965
9116
 
8966
9117
  // src/hooks/useViewerCamera.ts
8967
9118
  import { useCallback as useCallback5, useRef as useRef13, useState as useState11 } from "react";
8968
- import { Vector3 as Vector39 } from "three";
8969
-
8970
- // src/utils/resolveBindingKey.ts
8971
- function resolveBindingKey(objectBindings, objectId) {
8972
- if (objectBindings[objectId]) {
8973
- return objectId;
8974
- }
8975
- const idMatch = Object.entries(objectBindings).find(
8976
- ([, binding]) => binding.id === objectId
8977
- )?.[0];
8978
- if (idMatch) {
8979
- return idMatch;
8980
- }
8981
- const modelObjectMatch = Object.entries(objectBindings).find(
8982
- ([, binding]) => binding.modelObjectId === objectId
8983
- )?.[0];
8984
- return modelObjectMatch ?? null;
8985
- }
8986
-
8987
- // src/hooks/useViewerCamera.ts
9119
+ import { Vector3 as Vector310 } from "three";
8988
9120
  function toTuple(vector) {
8989
9121
  return [vector.x, vector.y, vector.z];
8990
9122
  }
8991
9123
  function buildCameraState(camera, controls) {
8992
- const position = new Vector39();
8993
- const target = new Vector39();
9124
+ const position = new Vector310();
9125
+ const target = new Vector310();
8994
9126
  controls.getPosition(position);
8995
9127
  controls.getTarget(target);
8996
9128
  return {
@@ -9017,11 +9149,16 @@ async function runSmoothCameraTransition2(controls, action, smoothTime = 0.6) {
9017
9149
  controls.smoothTime = originalSmoothTime;
9018
9150
  }
9019
9151
  }
9020
- function useViewerCamera() {
9152
+ function useViewerCamera(options = {}) {
9153
+ const optionsRef = useRef13(options);
9154
+ optionsRef.current = options;
9021
9155
  const viewerRef = useRef13(null);
9022
9156
  const [cameraState, setCameraStateValue] = useState11(
9023
9157
  null
9024
9158
  );
9159
+ const [cameraPresets, setCameraPresets] = useState11(() => ({ ...options.presets }));
9160
+ const cameraPresetsRef = useRef13(cameraPresets);
9161
+ cameraPresetsRef.current = cameraPresets;
9025
9162
  const updateCameraState = useCallback5((nextState) => {
9026
9163
  setCameraStateValue(
9027
9164
  (previousState) => isSameCameraState(previousState, nextState) ? previousState : nextState
@@ -9035,16 +9172,56 @@ function useViewerCamera() {
9035
9172
  );
9036
9173
  const homeCameraStateRef = useRef13(null);
9037
9174
  const homeSceneRef = useRef13(null);
9175
+ const initializedControlsRef = useRef13(null);
9038
9176
  const handleViewerReady = useCallback5((viewer) => {
9039
9177
  viewerRef.current = viewer;
9040
- updateCameraState(buildCameraState(viewer.controls.camera, viewer.controls));
9178
+ const currentState = buildCameraState(
9179
+ viewer.controls.camera,
9180
+ viewer.controls
9181
+ );
9182
+ updateCameraState(currentState);
9041
9183
  if (viewer.scene !== homeSceneRef.current) {
9042
9184
  homeSceneRef.current = viewer.scene;
9043
9185
  homeCameraStateRef.current = null;
9186
+ initializedControlsRef.current = null;
9044
9187
  }
9045
9188
  if (viewer.homeCameraState && !homeCameraStateRef.current) {
9046
9189
  homeCameraStateRef.current = viewer.homeCameraState;
9047
9190
  }
9191
+ const { initialPosition, initialTarget, initialTransition = false } = optionsRef.current;
9192
+ if ((initialPosition || initialTarget) && viewer.scene && // A target-only setup should preserve ModelViewer's fitted or authored
9193
+ // camera position. Wait until that initial framing has settled instead
9194
+ // of cancelling auto-fit while the camera is still at its fallback.
9195
+ (initialPosition || viewer.homeCameraState) && initializedControlsRef.current !== viewer.controls) {
9196
+ initializedControlsRef.current = viewer.controls;
9197
+ if (!viewer.homeCameraState) {
9198
+ markInitialAutoFitOverride(viewer.controls);
9199
+ }
9200
+ const position = initialPosition ?? currentState.position;
9201
+ const target = initialTarget ?? currentState.target;
9202
+ void viewer.controls.setLookAt(
9203
+ position[0],
9204
+ position[1],
9205
+ position[2],
9206
+ target[0],
9207
+ target[1],
9208
+ target[2],
9209
+ initialTransition
9210
+ ).then(() => {
9211
+ if (viewerRef.current?.controls !== viewer.controls) {
9212
+ return;
9213
+ }
9214
+ const nextState = buildCameraState(
9215
+ viewer.controls.camera,
9216
+ viewer.controls
9217
+ );
9218
+ homeCameraStateRef.current = {
9219
+ position: nextState.position,
9220
+ target: nextState.target
9221
+ };
9222
+ updateCameraState(nextState);
9223
+ }).catch(() => void 0);
9224
+ }
9048
9225
  }, [updateCameraState]);
9049
9226
  const resetView = useCallback5(async () => {
9050
9227
  const viewer = viewerRef.current;
@@ -9140,27 +9317,33 @@ function useViewerCamera() {
9140
9317
  return false;
9141
9318
  }
9142
9319
  try {
9320
+ if (!viewerRef.current?.homeCameraState) {
9321
+ markInitialAutoFitOverride(controls);
9322
+ }
9143
9323
  await controls.setTarget(
9144
9324
  target[0],
9145
9325
  target[1],
9146
9326
  target[2],
9147
9327
  enableTransition
9148
9328
  );
9329
+ updateCameraState(buildCameraState(controls.camera, controls));
9149
9330
  return true;
9150
9331
  } catch {
9151
9332
  return false;
9152
9333
  }
9153
9334
  },
9154
- []
9335
+ [updateCameraState]
9155
9336
  );
9156
- const setCameraState = useCallback5(
9337
+ const applyCameraPreset = useCallback5(
9157
9338
  async (state, enableTransition = true) => {
9158
9339
  const controls = viewerRef.current?.controls;
9159
9340
  if (!controls) {
9160
9341
  return false;
9161
9342
  }
9162
9343
  try {
9163
- markInitialAutoFitOverride(controls);
9344
+ if (!viewerRef.current?.homeCameraState) {
9345
+ markInitialAutoFitOverride(controls);
9346
+ }
9164
9347
  const transitions = [
9165
9348
  controls.setLookAt(
9166
9349
  state.position[0],
@@ -9184,11 +9367,11 @@ function useViewerCamera() {
9184
9367
  await Promise.all(transitions);
9185
9368
  const camera = controls.camera;
9186
9369
  let projectionChanged = false;
9187
- if (state.fov !== null && "fov" in camera && typeof camera.fov === "number") {
9370
+ if (state.fov != null && "fov" in camera && typeof camera.fov === "number") {
9188
9371
  camera.fov = state.fov;
9189
9372
  projectionChanged = true;
9190
9373
  }
9191
- if ("zoom" in camera && typeof camera.zoom === "number") {
9374
+ if (state.zoom != null && "zoom" in camera && typeof camera.zoom === "number") {
9192
9375
  camera.zoom = state.zoom;
9193
9376
  projectionChanged = true;
9194
9377
  }
@@ -9203,13 +9386,94 @@ function useViewerCamera() {
9203
9386
  },
9204
9387
  [updateCameraState]
9205
9388
  );
9389
+ const setCameraState = useCallback5(
9390
+ (state, enableTransition = true) => applyCameraPreset(state, enableTransition),
9391
+ [applyCameraPreset]
9392
+ );
9393
+ const lookAt = useCallback5(
9394
+ (position, target, enableTransition = true) => applyCameraPreset({ position, target }, enableTransition),
9395
+ [applyCameraPreset]
9396
+ );
9397
+ const orbitTo = useCallback5(
9398
+ async (azimuthAngle, polarAngle, enableTransition = true) => {
9399
+ const controls = viewerRef.current?.controls;
9400
+ if (!controls) {
9401
+ return false;
9402
+ }
9403
+ try {
9404
+ if (!viewerRef.current?.homeCameraState) {
9405
+ markInitialAutoFitOverride(controls);
9406
+ }
9407
+ await controls.rotateTo(
9408
+ azimuthAngle,
9409
+ polarAngle,
9410
+ enableTransition
9411
+ );
9412
+ updateCameraState(buildCameraState(controls.camera, controls));
9413
+ return true;
9414
+ } catch {
9415
+ return false;
9416
+ }
9417
+ },
9418
+ [updateCameraState]
9419
+ );
9420
+ const dollyTo = useCallback5(
9421
+ async (distance, enableTransition = true) => {
9422
+ const controls = viewerRef.current?.controls;
9423
+ if (!controls) {
9424
+ return false;
9425
+ }
9426
+ try {
9427
+ if (!viewerRef.current?.homeCameraState) {
9428
+ markInitialAutoFitOverride(controls);
9429
+ }
9430
+ await controls.dollyTo(distance, enableTransition);
9431
+ updateCameraState(buildCameraState(controls.camera, controls));
9432
+ return true;
9433
+ } catch {
9434
+ return false;
9435
+ }
9436
+ },
9437
+ [updateCameraState]
9438
+ );
9439
+ const savePreset = useCallback5((name) => {
9440
+ const controls = viewerRef.current?.controls;
9441
+ const trimmedName = name.trim();
9442
+ if (!controls || !trimmedName) {
9443
+ return null;
9444
+ }
9445
+ const nextState = buildCameraState(controls.camera, controls);
9446
+ cameraPresetsRef.current = {
9447
+ ...cameraPresetsRef.current,
9448
+ [trimmedName]: nextState
9449
+ };
9450
+ setCameraPresets((current) => ({
9451
+ ...current,
9452
+ [trimmedName]: nextState
9453
+ }));
9454
+ updateCameraState(nextState);
9455
+ return nextState;
9456
+ }, [updateCameraState]);
9457
+ const goToPreset = useCallback5(
9458
+ (name, enableTransition = true) => {
9459
+ const preset = cameraPresetsRef.current[name];
9460
+ return preset ? applyCameraPreset(preset, enableTransition) : Promise.resolve(false);
9461
+ },
9462
+ [applyCameraPreset]
9463
+ );
9206
9464
  return {
9207
9465
  cameraState,
9466
+ cameraPresets,
9208
9467
  resetView,
9209
9468
  focusObject,
9210
9469
  fitScene,
9211
9470
  setCameraTarget,
9212
9471
  setCameraState,
9472
+ lookAt,
9473
+ orbitTo,
9474
+ dollyTo,
9475
+ savePreset,
9476
+ goToPreset,
9213
9477
  handleCameraChange,
9214
9478
  handleViewerReady
9215
9479
  };
@@ -15078,6 +15342,9 @@ function IdentityPanel({
15078
15342
  ] }) });
15079
15343
  }
15080
15344
 
15345
+ // src/components/BindingBuilder/components/BasicsPanel.tsx
15346
+ import { useEffect as useEffect21, useState as useState24 } from "react";
15347
+
15081
15348
  // src/components/BindingBuilder/utils/constants.ts
15082
15349
  var typeSuggestions = [
15083
15350
  "other",
@@ -15117,6 +15384,31 @@ function BasicsPanel({
15117
15384
  onToggle,
15118
15385
  updateBinding
15119
15386
  }) {
15387
+ const [groupDraft, setGroupDraft] = useState24(selectedBinding.group ?? "");
15388
+ const [tagsDraft, setTagsDraft] = useState24(
15389
+ selectedBinding.tags?.join(", ") ?? ""
15390
+ );
15391
+ useEffect21(() => {
15392
+ setGroupDraft(selectedBinding.group ?? "");
15393
+ setTagsDraft(selectedBinding.tags?.join(", ") ?? "");
15394
+ }, [selectedItem, selectedBinding.group, selectedBinding.tags]);
15395
+ const commitGroup = () => {
15396
+ updateBinding(selectedItem, (binding) => ({
15397
+ ...binding,
15398
+ group: groupDraft.trim() || void 0
15399
+ }));
15400
+ };
15401
+ const commitTags = () => {
15402
+ const tags = [
15403
+ ...new Set(
15404
+ tagsDraft.split(",").map((tag) => tag.trim()).filter(Boolean)
15405
+ )
15406
+ ];
15407
+ updateBinding(selectedItem, (binding) => ({
15408
+ ...binding,
15409
+ tags: tags.length > 0 ? tags : void 0
15410
+ }));
15411
+ };
15120
15412
  return /* @__PURE__ */ jsxs36(BBSection, { title: "Basics", open, onToggle, children: [
15121
15413
  /* @__PURE__ */ jsxs36("div", { className: "bb-grid", children: [
15122
15414
  /* @__PURE__ */ jsxs36("div", { className: "bb-field", children: [
@@ -15163,6 +15455,38 @@ function BasicsPanel({
15163
15455
  children: statusOptions.map((option) => /* @__PURE__ */ jsx41("option", { value: option, children: option }, option))
15164
15456
  }
15165
15457
  )
15458
+ ] }),
15459
+ /* @__PURE__ */ jsxs36("div", { className: "bb-field", children: [
15460
+ /* @__PURE__ */ jsx41("label", { className: "bb-label", children: "Group" }),
15461
+ /* @__PURE__ */ jsx41(
15462
+ "input",
15463
+ {
15464
+ className: "bb-input",
15465
+ placeholder: "paint",
15466
+ value: groupDraft,
15467
+ onChange: (event) => setGroupDraft(event.target.value),
15468
+ onBlur: commitGroup,
15469
+ onKeyDown: (event) => {
15470
+ if (event.key === "Enter") event.currentTarget.blur();
15471
+ }
15472
+ }
15473
+ )
15474
+ ] }),
15475
+ /* @__PURE__ */ jsxs36("div", { className: "bb-field", children: [
15476
+ /* @__PURE__ */ jsx41("label", { className: "bb-label", children: "Tags" }),
15477
+ /* @__PURE__ */ jsx41(
15478
+ "input",
15479
+ {
15480
+ className: "bb-input",
15481
+ placeholder: "exterior, configurable",
15482
+ value: tagsDraft,
15483
+ onChange: (event) => setTagsDraft(event.target.value),
15484
+ onBlur: commitTags,
15485
+ onKeyDown: (event) => {
15486
+ if (event.key === "Enter") event.currentTarget.blur();
15487
+ }
15488
+ }
15489
+ )
15166
15490
  ] })
15167
15491
  ] }),
15168
15492
  /* @__PURE__ */ jsxs36("div", { className: "bb-checkboxRow", style: { marginTop: 12 }, children: [
@@ -15213,7 +15537,7 @@ function BasicsPanel({
15213
15537
  }
15214
15538
 
15215
15539
  // src/components/BindingBuilder/components/ActionsPanel.tsx
15216
- import { useEffect as useEffect21, useState as useState24 } from "react";
15540
+ import { useEffect as useEffect22, useState as useState25 } from "react";
15217
15541
 
15218
15542
  // src/components/BindingBuilder/utils/actionCatalog.ts
15219
15543
  var ACTION_CATALOG = [
@@ -15239,19 +15563,19 @@ var ACTION_CATALOG = [
15239
15563
  id: "toggle-light",
15240
15564
  label: "Toggle Light",
15241
15565
  handling: "host",
15242
- description: "Seeded on light bindings. Handle it in your onAction callback."
15566
+ description: "Seeded on light bindings. Add declarative effects for useViewerActions, or handle it in onAction."
15243
15567
  },
15244
15568
  {
15245
15569
  id: "change-light-color",
15246
15570
  label: "Change Light Color",
15247
15571
  handling: "host",
15248
- description: "Seeded on light bindings. Handle it in your onAction callback."
15572
+ description: "Seeded on light bindings. Add declarative effects for useViewerActions, or handle it in onAction."
15249
15573
  },
15250
15574
  {
15251
15575
  id: "set-brightness",
15252
15576
  label: "Set Brightness",
15253
15577
  handling: "host",
15254
- description: "Seeded on light bindings. Handle it in your onAction callback."
15578
+ description: "Seeded on light bindings. Add declarative effects for useViewerActions, or handle it in onAction."
15255
15579
  }
15256
15580
  ];
15257
15581
  var CUSTOM_ACTION_ID = "__custom__";
@@ -15312,8 +15636,8 @@ function ActionsPanel({
15312
15636
  onToggle,
15313
15637
  updateBinding
15314
15638
  }) {
15315
- const [customRows, setCustomRows] = useState24(/* @__PURE__ */ new Set());
15316
- useEffect21(() => {
15639
+ const [customRows, setCustomRows] = useState25(/* @__PURE__ */ new Set());
15640
+ useEffect22(() => {
15317
15641
  setCustomRows(/* @__PURE__ */ new Set());
15318
15642
  }, [selectedItem]);
15319
15643
  const actions = selectedBinding.actions ?? [];
@@ -15429,8 +15753,8 @@ function ActionsPanel({
15429
15753
  }
15430
15754
  )
15431
15755
  ] }),
15432
- catalogEntry ? /* @__PURE__ */ jsx42("span", { className: "bb-note", children: catalogEntry.description }) : trimmedId.length === 0 ? /* @__PURE__ */ jsx42("span", { className: "bb-note", style: { color: "#fca5a5" }, children: "An action ID is required \u2014 the button renders but can never be identified in onAction." }) : /* @__PURE__ */ jsxs37("span", { className: "bb-note", style: { color: "#fcd34d" }, children: [
15433
- "Unrecognised ID \u2014 the viewer has no built-in behaviour for it, so your app must handle it in onAction.",
15756
+ catalogEntry ? /* @__PURE__ */ jsx42("span", { className: "bb-note", children: catalogEntry.description }) : trimmedId.length === 0 ? /* @__PURE__ */ jsx42("span", { className: "bb-note", style: { color: "#fca5a5" }, children: "An action ID is required, the button renders but can never be identified in onAction." }) : /* @__PURE__ */ jsxs37("span", { className: "bb-note", style: { color: "#fcd34d" }, children: [
15757
+ "Unrecognised ID, the viewer has no built-in behavior for it. Add declarative effects and execute them with useViewerActions, or handle it in onAction.",
15434
15758
  suggestion ? /* @__PURE__ */ jsxs37(Fragment13, { children: [
15435
15759
  " Did you mean ",
15436
15760
  /* @__PURE__ */ jsx42(
@@ -15453,7 +15777,13 @@ function ActionsPanel({
15453
15777
  "?"
15454
15778
  ] }) : null
15455
15779
  ] }),
15456
- isDuplicate ? /* @__PURE__ */ jsx42("span", { className: "bb-note", style: { color: "#fcd34d" }, children: "Another action on this binding uses the same ID \u2014 they will render as two identical buttons." }) : null
15780
+ action.effects?.length ? /* @__PURE__ */ jsxs37("span", { className: "bb-note", children: [
15781
+ action.effects.length,
15782
+ " declarative effect",
15783
+ action.effects.length === 1 ? "" : "s",
15784
+ ", execute with useViewerActions."
15785
+ ] }) : null,
15786
+ isDuplicate ? /* @__PURE__ */ jsx42("span", { className: "bb-note", style: { color: "#fcd34d" }, children: "Another action on this binding uses the same ID, they will render as two identical buttons." }) : null
15457
15787
  ] }),
15458
15788
  /* @__PURE__ */ jsx42("div", { className: "bb-actionFooter", children: /* @__PURE__ */ jsx42(
15459
15789
  "button",
@@ -15602,7 +15932,7 @@ function CameraPositionPanel({
15602
15932
  }
15603
15933
 
15604
15934
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
15605
- import { useMemo as useMemo16, useState as useState25 } from "react";
15935
+ import { useMemo as useMemo16, useState as useState26 } from "react";
15606
15936
 
15607
15937
  // src/components/BindingBuilder/utils/bindingFactory.ts
15608
15938
  function slugify2(value) {
@@ -15691,7 +16021,7 @@ function SceneNodesPanel({
15691
16021
  isOpen = true,
15692
16022
  onToggle
15693
16023
  }) {
15694
- const [search, setSearch] = useState25("");
16024
+ const [search, setSearch] = useState26("");
15695
16025
  const nodeKeys = useMemo16(() => Object.keys(bindings), [bindings]);
15696
16026
  const filteredNodeKeys = useMemo16(() => {
15697
16027
  const query = search.trim().toLowerCase();
@@ -15922,9 +16252,9 @@ function toBindingsJson(bindings) {
15922
16252
  }
15923
16253
  function toBindingsTypeScript(bindings) {
15924
16254
  return [
15925
- 'import type { ObjectBinding } from "@liveroom-tech/react-immersive";',
16255
+ 'import { defineObjectBindings } from "@liveroom-tech/react-immersive";',
15926
16256
  "",
15927
- `export const objectBindings: Record<string, ObjectBinding> = ${JSON.stringify(bindings, null, 2)};`,
16257
+ `export const objectBindings = defineObjectBindings(${JSON.stringify(bindings, null, 2)});`,
15928
16258
  ""
15929
16259
  ].join("\n");
15930
16260
  }
@@ -16147,27 +16477,27 @@ function BindingBuilder({ licenseKey }) {
16147
16477
  const modelInputRef = useRef19(null);
16148
16478
  const bindingsInputRef = useRef19(null);
16149
16479
  const sceneConfigInputRef = useRef19(null);
16150
- const [modelUrl, setModelUrl] = useState26(null);
16151
- const [modelName, setModelName] = useState26("");
16152
- const [selectedItem, setSelectedItem] = useState26(null);
16480
+ const [modelUrl, setModelUrl] = useState27(null);
16481
+ const [modelName, setModelName] = useState27("");
16482
+ const [selectedItem, setSelectedItem] = useState27(null);
16153
16483
  const bindingsHistory = useUndoableState({});
16154
16484
  const bindings = bindingsHistory.value;
16155
16485
  const setBindings = bindingsHistory.setValue;
16156
- const [loading, setLoading] = useState26(false);
16157
- const [isExporting, setIsExporting] = useState26(false);
16158
- const [statusMessage, setStatusMessage] = useState26(
16486
+ const [loading, setLoading] = useState27(false);
16487
+ const [isExporting, setIsExporting] = useState27(false);
16488
+ const [statusMessage, setStatusMessage] = useState27(
16159
16489
  "Load the demo model or upload a GLB/GLTF model to generate bindings."
16160
16490
  );
16161
- const [statusTone, setStatusTone] = useState26("default");
16162
- const [statusBannerVisible, setStatusBannerVisible] = useState26(false);
16163
- const [metricsDraft, setMetricsDraft] = useState26("{}");
16164
- const [metadataDraft, setMetadataDraft] = useState26("{}");
16165
- const [metricsError, setMetricsError] = useState26(null);
16166
- const [metadataError, setMetadataError] = useState26(null);
16167
- const [isPreviewDragActive, setIsPreviewDragActive] = useState26(false);
16168
- const [isEditorCollapsed, setIsEditorCollapsed] = useState26(false);
16169
- const [isNodesCollapsed, setIsNodesCollapsed] = useState26(false);
16170
- const [openObjectSections, setOpenObjectSections] = useState26({
16491
+ const [statusTone, setStatusTone] = useState27("default");
16492
+ const [statusBannerVisible, setStatusBannerVisible] = useState27(false);
16493
+ const [metricsDraft, setMetricsDraft] = useState27("{}");
16494
+ const [metadataDraft, setMetadataDraft] = useState27("{}");
16495
+ const [metricsError, setMetricsError] = useState27(null);
16496
+ const [metadataError, setMetadataError] = useState27(null);
16497
+ const [isPreviewDragActive, setIsPreviewDragActive] = useState27(false);
16498
+ const [isEditorCollapsed, setIsEditorCollapsed] = useState27(false);
16499
+ const [isNodesCollapsed, setIsNodesCollapsed] = useState27(false);
16500
+ const [openObjectSections, setOpenObjectSections] = useState27({
16171
16501
  identity: true,
16172
16502
  basics: true,
16173
16503
  material: false,
@@ -16176,7 +16506,7 @@ function BindingBuilder({ licenseKey }) {
16176
16506
  metrics: false,
16177
16507
  metadata: false
16178
16508
  });
16179
- const [openMaterialPanels, setOpenMaterialPanels] = useState26({
16509
+ const [openMaterialPanels, setOpenMaterialPanels] = useState27({
16180
16510
  pbr: true,
16181
16511
  roughness: false,
16182
16512
  emission: false,
@@ -16192,24 +16522,24 @@ function BindingBuilder({ licenseKey }) {
16192
16522
  });
16193
16523
  const toggleObjectSection = (key) => setOpenObjectSections((prev) => ({ ...prev, [key]: !prev[key] }));
16194
16524
  const toggleMaterialPanel = (key) => setOpenMaterialPanels((prev) => ({ ...prev, [key]: !prev[key] }));
16195
- const [activeEditorTab, setActiveEditorTab] = useState26(
16525
+ const [activeEditorTab, setActiveEditorTab] = useState27(
16196
16526
  "object"
16197
16527
  );
16198
- const [activeSubTab, setActiveSubTab] = useState26("General");
16199
- const [pbrWorkflow, setPbrWorkflow] = useState26("metalness");
16200
- const [materialPanelEnabled, setMaterialPanelEnabled] = useState26(createMaterialPanelEnabledState());
16528
+ const [activeSubTab, setActiveSubTab] = useState27("General");
16529
+ const [pbrWorkflow, setPbrWorkflow] = useState27("metalness");
16530
+ const [materialPanelEnabled, setMaterialPanelEnabled] = useState27(createMaterialPanelEnabledState());
16201
16531
  const materialPanelItemRef = useRef19(null);
16202
- const [zoomOnSelected, setZoomOnSelected] = useState26(false);
16203
- const [highlightOnHover, setHighlightOnHover] = useState26(true);
16204
- const [resetDialogOpen, setResetDialogOpen] = useState26(false);
16205
- const [resetTarget, setResetTarget] = useState26(
16532
+ const [zoomOnSelected, setZoomOnSelected] = useState27(false);
16533
+ const [highlightOnHover, setHighlightOnHover] = useState27(true);
16534
+ const [resetDialogOpen, setResetDialogOpen] = useState27(false);
16535
+ const [resetTarget, setResetTarget] = useState27(
16206
16536
  null
16207
16537
  );
16208
16538
  const sceneConfigHistory = useUndoableState(DEFAULT_SCENE_CONFIG);
16209
16539
  const sceneConfig = sceneConfigHistory.value;
16210
16540
  const setSceneConfig = sceneConfigHistory.setValue;
16211
- const [activeAnnotation, setActiveAnnotation] = useState26(null);
16212
- const [panelHoveredBinding, setPanelHoveredBinding] = useState26(null);
16541
+ const [activeAnnotation, setActiveAnnotation] = useState27(null);
16542
+ const [panelHoveredBinding, setPanelHoveredBinding] = useState27(null);
16213
16543
  const license = useLicense(licenseKey);
16214
16544
  const tier = normalizeTier(license.tier);
16215
16545
  const bindingBuilderContextValue = useMemo17(
@@ -16287,7 +16617,7 @@ function BindingBuilder({ licenseKey }) {
16287
16617
  },
16288
16618
  []
16289
16619
  );
16290
- useEffect22(() => {
16620
+ useEffect23(() => {
16291
16621
  if (!selectedBinding) {
16292
16622
  setMetricsDraft("{}");
16293
16623
  setMetadataDraft("{}");
@@ -16300,7 +16630,7 @@ function BindingBuilder({ licenseKey }) {
16300
16630
  setMetricsError(null);
16301
16631
  setMetadataError(null);
16302
16632
  }, [selectedBinding]);
16303
- useEffect22(() => {
16633
+ useEffect23(() => {
16304
16634
  const derived = createMaterialPanelEnabledState(selectedMaterial);
16305
16635
  if (materialPanelItemRef.current !== selectedItem) {
16306
16636
  materialPanelItemRef.current = selectedItem;
@@ -16319,12 +16649,12 @@ function BindingBuilder({ licenseKey }) {
16319
16649
  return changed ? next : prev;
16320
16650
  });
16321
16651
  }, [selectedItem, selectedMaterial]);
16322
- useEffect22(() => {
16652
+ useEffect23(() => {
16323
16653
  return () => {
16324
16654
  uploadedModelRef.current?.cleanup();
16325
16655
  };
16326
16656
  }, []);
16327
- useEffect22(() => {
16657
+ useEffect23(() => {
16328
16658
  const handleKeyDown = (event) => {
16329
16659
  if (event.key.toLowerCase() !== "z" || !(event.metaKey || event.ctrlKey)) {
16330
16660
  return;
@@ -16345,7 +16675,7 @@ function BindingBuilder({ licenseKey }) {
16345
16675
  window.addEventListener("keydown", handleKeyDown);
16346
16676
  return () => window.removeEventListener("keydown", handleKeyDown);
16347
16677
  }, [activeEditorTab, bindingsHistory, sceneConfigHistory]);
16348
- useEffect22(() => {
16678
+ useEffect23(() => {
16349
16679
  if (loading || statusTone === "default" || statusTone === "success") return;
16350
16680
  setStatusBannerVisible(true);
16351
16681
  const timeout = window.setTimeout(
@@ -17375,7 +17705,7 @@ function BindingBuilder({ licenseKey }) {
17375
17705
  /* @__PURE__ */ jsx46("strong", { children: resetTarget === "bindings" ? "Object Bindings" : "Scene Config" }),
17376
17706
  "?"
17377
17707
  ] }),
17378
- /* @__PURE__ */ jsx46("p", { className: "bb-reset-details", children: resetTarget === "bindings" ? "This will remove all your custom changes to object properties, materials, actions, camera positions, metrics, and metadata." : "This will restore all scene settings (camera, lighting, environment, background, animations, annotations, etc.) to their default values." }),
17708
+ /* @__PURE__ */ jsx46("p", { className: "bb-reset-details", children: resetTarget === "bindings" ? "This will remove all your custom changes to object properties, groups, tags, transforms, materials, actions, camera positions, metrics, and metadata." : "This will restore all scene settings (camera, lighting, environment, background, animations, annotations, etc.) to their default values." }),
17379
17709
  /* @__PURE__ */ jsx46("p", { className: "bb-reset-warning", children: "This action cannot be undone!" })
17380
17710
  ] }),
17381
17711
  /* @__PURE__ */ jsxs41("div", { className: "bb-reset-footer", children: [
@@ -17389,9 +17719,9 @@ function BindingBuilder({ licenseKey }) {
17389
17719
  // src/components/SimpleModelViewer/SimpleModelViewer.tsx
17390
17720
  import {
17391
17721
  useRef as useRef20,
17392
- useState as useState27,
17722
+ useState as useState28,
17393
17723
  useCallback as useCallback10,
17394
- useEffect as useEffect23,
17724
+ useEffect as useEffect24,
17395
17725
  useMemo as useMemo18,
17396
17726
  Component as Component3,
17397
17727
  Suspense as Suspense3
@@ -17402,7 +17732,7 @@ import { Canvas as Canvas3 } from "@react-three/fiber";
17402
17732
  import {
17403
17733
  Box3 as Box35,
17404
17734
  PerspectiveCamera as PerspectiveCamera4,
17405
- Vector3 as Vector310
17735
+ Vector3 as Vector311
17406
17736
  } from "three";
17407
17737
  import { SkeletonUtils as SkeletonUtils2 } from "three-stdlib";
17408
17738
 
@@ -17441,7 +17771,7 @@ function applyClippingPlanes(scene, controls) {
17441
17771
  if (!controls) return;
17442
17772
  const camera = controls.camera;
17443
17773
  if (!(camera instanceof PerspectiveCamera4)) return;
17444
- const size = new Box35().setFromObject(scene).getSize(new Vector310()).length();
17774
+ const size = new Box35().setFromObject(scene).getSize(new Vector311()).length();
17445
17775
  if (!Number.isFinite(size) || size <= 0) return;
17446
17776
  camera.near = size / 100;
17447
17777
  camera.far = size * 100;
@@ -17480,7 +17810,7 @@ function InnerModel({
17480
17810
  meshopt: true
17481
17811
  });
17482
17812
  const invalidate = useThree12((state) => state.invalidate);
17483
- const [hoveredKey, setHoveredKey] = useState27(null);
17813
+ const [hoveredKey, setHoveredKey] = useState28(null);
17484
17814
  const scene = useMemo18(
17485
17815
  () => SkeletonUtils2.clone(rawScene),
17486
17816
  [rawScene]
@@ -17510,14 +17840,14 @@ function InnerModel({
17510
17840
  }
17511
17841
  return map;
17512
17842
  }, [meshMap]);
17513
- useEffect23(
17843
+ useEffect24(
17514
17844
  () => () => {
17515
17845
  Object.values(perKeyMaterials).forEach((m) => m.dispose());
17516
17846
  },
17517
17847
  [perKeyMaterials]
17518
17848
  );
17519
17849
  const notifiedUrl = useRef20(null);
17520
- useEffect23(() => {
17850
+ useEffect24(() => {
17521
17851
  if (notifiedUrl.current === url) return;
17522
17852
  notifiedUrl.current = url;
17523
17853
  scene.updateWorldMatrix(true, true);
@@ -17525,7 +17855,7 @@ function InnerModel({
17525
17855
  onMeshesReady(meshMap);
17526
17856
  onModelLoaded(scene);
17527
17857
  }, [url, scene, meshMap, onMeshesReady, onModelLoaded]);
17528
- useEffect23(() => {
17858
+ useEffect24(() => {
17529
17859
  const controller = new AbortController();
17530
17860
  void buildSceneBVH(scene, controller.signal);
17531
17861
  return () => {
@@ -17537,22 +17867,22 @@ function InnerModel({
17537
17867
  () => !exceedsBvhBudget(scene),
17538
17868
  [scene]
17539
17869
  );
17540
- useEffect23(() => {
17870
+ useEffect24(() => {
17541
17871
  for (const [key, { ref }] of Object.entries(meshMap)) {
17542
17872
  nodeRefsOut.current[key] = ref;
17543
17873
  }
17544
17874
  }, [meshMap, nodeRefsOut]);
17545
- useEffect23(() => {
17875
+ useEffect24(() => {
17546
17876
  if (!highlightOnHover && hoveredKey !== null) {
17547
17877
  setHoveredKey(null);
17548
17878
  }
17549
17879
  }, [highlightOnHover, hoveredKey]);
17550
- useEffect23(() => {
17880
+ useEffect24(() => {
17551
17881
  return () => {
17552
17882
  document.body.style.cursor = "default";
17553
17883
  };
17554
17884
  }, []);
17555
- useEffect23(() => {
17885
+ useEffect24(() => {
17556
17886
  for (const [key, { ref }] of Object.entries(meshMap)) {
17557
17887
  ref.visible = !hiddenKeys.has(key);
17558
17888
  const mat = perKeyMaterials[key];
@@ -17626,7 +17956,7 @@ function CameraFitter({
17626
17956
  const invalidate = useThree12((state) => state.invalidate);
17627
17957
  const size = useThree12((state) => state.size);
17628
17958
  const resizeTimerRef = useRef20(null);
17629
- useEffect23(() => {
17959
+ useEffect24(() => {
17630
17960
  fitted.current = false;
17631
17961
  invalidate();
17632
17962
  }, [invalidate, scene]);
@@ -17646,7 +17976,7 @@ function CameraFitter({
17646
17976
  }).catch(() => {
17647
17977
  }).finally(onInitialFitComplete);
17648
17978
  });
17649
- useEffect23(() => {
17979
+ useEffect24(() => {
17650
17980
  if (!refitOnResize || !fitted.current || !scene || !controlsRef.current) {
17651
17981
  return;
17652
17982
  }
@@ -17674,7 +18004,7 @@ function CameraFitter({
17674
18004
  function ExposureController({ exposure }) {
17675
18005
  const gl = useThree12((state) => state.gl);
17676
18006
  const invalidate = useThree12((state) => state.invalidate);
17677
- useEffect23(() => {
18007
+ useEffect24(() => {
17678
18008
  gl.toneMappingExposure = exposure;
17679
18009
  invalidate();
17680
18010
  }, [gl, exposure, invalidate]);
@@ -17782,7 +18112,7 @@ function EnvironmentSection({
17782
18112
  title,
17783
18113
  children
17784
18114
  }) {
17785
- const [collapsed, setCollapsed] = useState27(false);
18115
+ const [collapsed, setCollapsed] = useState28(false);
17786
18116
  return /* @__PURE__ */ jsxs42("div", { className: "env-section", children: [
17787
18117
  /* @__PURE__ */ jsxs42(
17788
18118
  "button",
@@ -17947,8 +18277,8 @@ function SceneObjectsPanel2({
17947
18277
  onHover,
17948
18278
  onToggleVisibility
17949
18279
  }) {
17950
- const [search, setSearch] = useState27("");
17951
- const [rowHovered, setRowHovered] = useState27(null);
18280
+ const [search, setSearch] = useState28("");
18281
+ const [rowHovered, setRowHovered] = useState28(null);
17952
18282
  const entries = Object.values(meshKeys).filter(
17953
18283
  (m) => m.label.toLowerCase().includes(search.toLowerCase())
17954
18284
  );
@@ -18077,30 +18407,30 @@ function SimpleModelViewer({
18077
18407
  const fileInputRef = useRef20(null);
18078
18408
  const uploadedModelRef = useRef20(null);
18079
18409
  const dragDepthRef = useRef20(0);
18080
- const [loadedScene, setLoadedScene] = useState27(null);
18081
- const [largeModelTriangles, setLargeModelTriangles] = useState27(
18410
+ const [loadedScene, setLoadedScene] = useState28(null);
18411
+ const [largeModelTriangles, setLargeModelTriangles] = useState28(
18082
18412
  null
18083
18413
  );
18084
- const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState27(false);
18085
- const [meshKeys, setMeshKeys] = useState27({});
18086
- const [hiddenKeys, setHiddenKeys] = useState27(/* @__PURE__ */ new Set());
18087
- const [selectedKey, setSelectedKey] = useState27(null);
18088
- const [panelHoveredKey, setPanelHoveredKey] = useState27(null);
18089
- const [uploadedModelUrl, setUploadedModelUrl] = useState27(null);
18090
- const [uploadedModelName, setUploadedModelName] = useState27(
18414
+ const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState28(false);
18415
+ const [meshKeys, setMeshKeys] = useState28({});
18416
+ const [hiddenKeys, setHiddenKeys] = useState28(/* @__PURE__ */ new Set());
18417
+ const [selectedKey, setSelectedKey] = useState28(null);
18418
+ const [panelHoveredKey, setPanelHoveredKey] = useState28(null);
18419
+ const [uploadedModelUrl, setUploadedModelUrl] = useState28(null);
18420
+ const [uploadedModelName, setUploadedModelName] = useState28(
18091
18421
  null
18092
18422
  );
18093
- const [modelLoadFailed, setModelLoadFailed] = useState27(false);
18094
- const [isResolvingUpload, setIsResolvingUpload] = useState27(false);
18095
- const [initialFitComplete, setInitialFitComplete] = useState27(false);
18096
- const [isDragActive, setIsDragActive] = useState27(false);
18097
- const [isPanelOpen, setIsPanelOpen] = useState27(
18423
+ const [modelLoadFailed, setModelLoadFailed] = useState28(false);
18424
+ const [isResolvingUpload, setIsResolvingUpload] = useState28(false);
18425
+ const [initialFitComplete, setInitialFitComplete] = useState28(false);
18426
+ const [isDragActive, setIsDragActive] = useState28(false);
18427
+ const [isPanelOpen, setIsPanelOpen] = useState28(
18098
18428
  () => typeof window === "undefined" ? true : window.innerWidth > 900
18099
18429
  );
18100
- const [isEnvPanelOpen, setIsEnvPanelOpen] = useState27(
18430
+ const [isEnvPanelOpen, setIsEnvPanelOpen] = useState28(
18101
18431
  () => typeof window === "undefined" ? true : window.innerWidth > 900
18102
18432
  );
18103
- const [envSettings, setEnvSettings] = useState27({
18433
+ const [envSettings, setEnvSettings] = useState28({
18104
18434
  backgroundEnabled,
18105
18435
  backgroundColor,
18106
18436
  autoRotate,
@@ -18183,7 +18513,7 @@ function SimpleModelViewer({
18183
18513
  },
18184
18514
  [handleUploadedModel]
18185
18515
  );
18186
- useEffect23(() => {
18516
+ useEffect24(() => {
18187
18517
  return () => {
18188
18518
  revokeUploadedModelUrl();
18189
18519
  };
@@ -18194,10 +18524,10 @@ function SimpleModelViewer({
18194
18524
  },
18195
18525
  []
18196
18526
  );
18197
- useEffect23(() => {
18527
+ useEffect24(() => {
18198
18528
  setEnvSettings((prev) => ({ ...prev, backgroundColor }));
18199
18529
  }, [backgroundColor]);
18200
- useEffect23(() => {
18530
+ useEffect24(() => {
18201
18531
  resetModelState();
18202
18532
  }, [activeModelUrl, resetModelState]);
18203
18533
  const handleMeshesReady = useCallback10((meshes) => {
@@ -18513,9 +18843,9 @@ function SimpleModelViewer({
18513
18843
  }
18514
18844
 
18515
18845
  // src/hooks/useViewerSelection.ts
18516
- import { useCallback as useCallback11, useState as useState28 } from "react";
18846
+ import { useCallback as useCallback11, useState as useState29 } from "react";
18517
18847
  function useViewerSelection(initialSelectedObjectBinding = null) {
18518
- const [selectedObjectBinding, setSelectedObjectBinding] = useState28(
18848
+ const [selectedObjectBinding, setSelectedObjectBinding] = useState29(
18519
18849
  initialSelectedObjectBinding
18520
18850
  );
18521
18851
  const handleObjectSelect = useCallback11(
@@ -18562,7 +18892,7 @@ function useObjectBindingIds(objectBindings) {
18562
18892
  }
18563
18893
 
18564
18894
  // src/hooks/useObjectBindings.ts
18565
- import { useCallback as useCallback12, useMemo as useMemo21, useState as useState29 } from "react";
18895
+ import { useCallback as useCallback12, useMemo as useMemo21, useState as useState30 } from "react";
18566
18896
 
18567
18897
  // src/utils/deepMerge.ts
18568
18898
  function isPlainObject2(value) {
@@ -18580,6 +18910,27 @@ function deepMerge(base, patch) {
18580
18910
  return target;
18581
18911
  }
18582
18912
 
18913
+ // src/utils/selectBindings.ts
18914
+ function isSelector(target) {
18915
+ return typeof target === "object" && !Array.isArray(target);
18916
+ }
18917
+ function matchesSelector(binding, selector) {
18918
+ if ("group" in selector) return binding.group === selector.group;
18919
+ if ("tag" in selector) return binding.tags?.includes(selector.tag) ?? false;
18920
+ return binding.type === selector.type;
18921
+ }
18922
+ function selectBindingKeys(bindings, target) {
18923
+ if (isSelector(target)) {
18924
+ return Object.entries(bindings).filter(([, binding]) => matchesSelector(binding, target)).map(([bindingKey]) => bindingKey);
18925
+ }
18926
+ const ids = typeof target === "string" ? [target] : target;
18927
+ const keys = ids.map((id) => resolveBindingKey(bindings, id)).filter((key) => key !== null);
18928
+ return [...new Set(keys)];
18929
+ }
18930
+ function selectBindings(bindings, target) {
18931
+ return selectBindingKeys(bindings, target).map((key) => bindings[key]).filter((binding) => binding !== void 0);
18932
+ }
18933
+
18583
18934
  // src/utils/patchBindings.ts
18584
18935
  function isBindingEditArray(value) {
18585
18936
  return Array.isArray(value) && value.length > 0 && typeof value[0] !== "string";
@@ -18588,10 +18939,7 @@ function patchBindings(bindings, objectIdOrEdits, patch) {
18588
18939
  const edits = isBindingEditArray(objectIdOrEdits) ? objectIdOrEdits : [{ ids: objectIdOrEdits, patch: patch ?? {} }];
18589
18940
  let next = bindings;
18590
18941
  for (const edit of edits) {
18591
- const ids = Array.isArray(edit.ids) ? edit.ids : [edit.ids];
18592
- for (const id of ids) {
18593
- const bindingKey = resolveBindingKey(next, id);
18594
- if (!bindingKey) continue;
18942
+ for (const bindingKey of selectBindingKeys(next, edit.ids)) {
18595
18943
  const binding = next[bindingKey];
18596
18944
  if (!binding) continue;
18597
18945
  const patchedBinding = deepMerge(binding, edit.patch);
@@ -18606,13 +18954,43 @@ function material(value) {
18606
18954
  }
18607
18955
 
18608
18956
  // src/hooks/useObjectBindings.ts
18957
+ var TEXTURE_PROPERTY_BY_CHANNEL = {
18958
+ baseColor: "texture",
18959
+ metalness: "metalnessMap",
18960
+ roughness: "roughnessMap",
18961
+ emissive: "emissiveMap",
18962
+ alpha: "alphaMap",
18963
+ normal: "normalMap",
18964
+ bump: "bumpMap",
18965
+ ao: "aoMap",
18966
+ displacement: "displacementMap",
18967
+ clearcoat: "clearcoatMap",
18968
+ clearcoatRoughness: "clearcoatRoughnessMap",
18969
+ clearcoatNormal: "clearcoatNormalMap",
18970
+ anisotropy: "anisotropyMap",
18971
+ transmission: "transmissionMap",
18972
+ thickness: "thicknessMap",
18973
+ sheenColor: "sheenColorMap",
18974
+ sheenRoughness: "sheenRoughnessMap",
18975
+ specularIntensity: "specularIntensityMap",
18976
+ specularColor: "specularColorMap"
18977
+ };
18978
+ function cloneMaterial(value) {
18979
+ if (!value) return void 0;
18980
+ return Object.fromEntries(
18981
+ Object.entries(value).map(([key, field]) => [
18982
+ key,
18983
+ field && typeof field === "object" ? { ...field } : field
18984
+ ])
18985
+ );
18986
+ }
18609
18987
  function getHiddenObjects(objectBindings) {
18610
18988
  return Object.fromEntries(
18611
18989
  Object.entries(objectBindings).filter(([, binding]) => binding.visible === false).map(([bindingKey]) => [bindingKey, true])
18612
18990
  );
18613
18991
  }
18614
18992
  function useObjectBindings(initialBindings) {
18615
- const [objectBindings, setObjectBindings] = useState29(initialBindings);
18993
+ const [objectBindings, setObjectBindings] = useState30(initialBindings);
18616
18994
  const updateObjectBindings = useCallback12(
18617
18995
  (objectIdOrEdits, patchValue) => {
18618
18996
  setObjectBindings(
@@ -18626,17 +19004,86 @@ function useObjectBindings(initialBindings) {
18626
19004
  []
18627
19005
  );
18628
19006
  const setMaterial = useCallback12(
18629
- (objectId, value) => updateObjectBindings(objectId, material(value)),
19007
+ (target, value) => updateObjectBindings(target, material(value)),
18630
19008
  [updateObjectBindings]
18631
19009
  );
18632
19010
  const setBaseColor = useCallback12(
18633
- (objectId, color) => setMaterial(objectId, { baseColor: color }),
19011
+ (target, color) => setMaterial(target, { baseColor: color }),
18634
19012
  [setMaterial]
18635
19013
  );
19014
+ const setTexture = useCallback12(
19015
+ (target, texture, channel = "baseColor") => {
19016
+ const property = TEXTURE_PROPERTY_BY_CHANNEL[channel];
19017
+ const slot = typeof texture === "string" ? { path: texture } : texture;
19018
+ updateObjectBindings(target, {
19019
+ style: { material: { [property]: slot } }
19020
+ });
19021
+ },
19022
+ [updateObjectBindings]
19023
+ );
19024
+ const clearTexture = useCallback12(
19025
+ (target, channel = "baseColor") => {
19026
+ const property = TEXTURE_PROPERTY_BY_CHANNEL[channel];
19027
+ updateObjectBindings(target, {
19028
+ style: { material: { [property]: void 0 } }
19029
+ });
19030
+ },
19031
+ [updateObjectBindings]
19032
+ );
19033
+ const copyMaterial = useCallback12(
19034
+ (source, target) => {
19035
+ setObjectBindings((current) => {
19036
+ const sourceKey = resolveBindingKey(current, source);
19037
+ if (!sourceKey) return current;
19038
+ const sourceMaterial = cloneMaterial(
19039
+ current[sourceKey]?.style?.material
19040
+ );
19041
+ const targetKeys = selectBindingKeys(current, target);
19042
+ if (targetKeys.length === 0) return current;
19043
+ let next = current;
19044
+ for (const targetKey of targetKeys) {
19045
+ const binding = current[targetKey];
19046
+ if (!binding) continue;
19047
+ if (next === current) next = { ...current };
19048
+ next[targetKey] = {
19049
+ ...binding,
19050
+ style: {
19051
+ ...binding.style,
19052
+ material: cloneMaterial(sourceMaterial)
19053
+ }
19054
+ };
19055
+ }
19056
+ return next;
19057
+ });
19058
+ },
19059
+ []
19060
+ );
18636
19061
  const resetMaterial = useCallback12(
18637
- (objectId) => updateObjectBindings(objectId, { style: { material: void 0 } }),
19062
+ (target) => updateObjectBindings(target, { style: { material: void 0 } }),
19063
+ [updateObjectBindings]
19064
+ );
19065
+ const setTransform = useCallback12(
19066
+ (target, transform) => updateObjectBindings(target, { transform }),
19067
+ [updateObjectBindings]
19068
+ );
19069
+ const resetTransform = useCallback12(
19070
+ (target) => updateObjectBindings(target, { transform: void 0 }),
18638
19071
  [updateObjectBindings]
18639
19072
  );
19073
+ const updateMetadata = useCallback12(
19074
+ (target, metadata) => updateObjectBindings(target, { metadata }),
19075
+ [updateObjectBindings]
19076
+ );
19077
+ const selectObjectBindings = useCallback12(
19078
+ (target) => selectBindings(objectBindings, target),
19079
+ [objectBindings]
19080
+ );
19081
+ const selectObjectBindingIds = useCallback12(
19082
+ (target) => selectBindingKeys(objectBindings, target).map(
19083
+ (bindingKey) => objectBindings[bindingKey]?.id ?? bindingKey
19084
+ ),
19085
+ [objectBindings]
19086
+ );
18640
19087
  const getMaterial = useCallback12(
18641
19088
  (objectId) => {
18642
19089
  const bindingKey = resolveBindingKey(objectBindings, objectId);
@@ -18645,27 +19092,28 @@ function useObjectBindings(initialBindings) {
18645
19092
  [objectBindings]
18646
19093
  );
18647
19094
  const hideObject = useCallback12(
18648
- (objectId) => updateObjectBindings(objectId, { visible: false }),
19095
+ (target) => updateObjectBindings(target, { visible: false }),
18649
19096
  [updateObjectBindings]
18650
19097
  );
18651
19098
  const showObject = useCallback12(
18652
- (objectId) => updateObjectBindings(objectId, { visible: true }),
19099
+ (target) => updateObjectBindings(target, { visible: true }),
18653
19100
  [updateObjectBindings]
18654
19101
  );
18655
19102
  const toggleObjectVisibility = useCallback12(
18656
- (objectId) => {
19103
+ (target) => {
18657
19104
  setObjectBindings((current) => {
18658
- const objectIds = Array.isArray(objectId) ? objectId : [objectId];
18659
- const edits = objectIds.reduce((acc, id) => {
18660
- const bindingKey = resolveBindingKey(current, id);
18661
- const binding = bindingKey ? current[bindingKey] : null;
18662
- if (!bindingKey || !binding) return acc;
18663
- acc.push({
18664
- ids: bindingKey,
18665
- patch: { visible: binding.visible === false }
18666
- });
18667
- return acc;
18668
- }, []);
19105
+ const edits = selectBindingKeys(current, target).reduce(
19106
+ (acc, bindingKey) => {
19107
+ const binding = current[bindingKey];
19108
+ if (!binding) return acc;
19109
+ acc.push({
19110
+ ids: bindingKey,
19111
+ patch: { visible: binding.visible === false }
19112
+ });
19113
+ return acc;
19114
+ },
19115
+ []
19116
+ );
18669
19117
  return patchBindings(current, edits);
18670
19118
  });
18671
19119
  },
@@ -18699,8 +19147,16 @@ function useObjectBindings(initialBindings) {
18699
19147
  updateObjectBindings,
18700
19148
  setMaterial,
18701
19149
  setBaseColor,
19150
+ setTexture,
19151
+ clearTexture,
19152
+ copyMaterial,
18702
19153
  resetMaterial,
18703
19154
  getMaterial,
19155
+ setTransform,
19156
+ resetTransform,
19157
+ updateMetadata,
19158
+ selectObjectBindings,
19159
+ selectObjectBindingIds,
18704
19160
  hiddenObjects,
18705
19161
  hiddenObjectIds,
18706
19162
  hideObject,
@@ -18711,8 +19167,13 @@ function useObjectBindings(initialBindings) {
18711
19167
  };
18712
19168
  }
18713
19169
 
19170
+ // src/utils/defineObjectBindings.ts
19171
+ function defineObjectBindings(bindings) {
19172
+ return bindings;
19173
+ }
19174
+
18714
19175
  // src/hooks/useSceneConfig.ts
18715
- import { useCallback as useCallback13, useRef as useRef21, useState as useState30 } from "react";
19176
+ import { useCallback as useCallback13, useMemo as useMemo22, useRef as useRef21, useState as useState31 } from "react";
18716
19177
 
18717
19178
  // src/utils/patchSceneConfig.ts
18718
19179
  function patchSceneConfig(sceneConfig, patch) {
@@ -18720,9 +19181,21 @@ function patchSceneConfig(sceneConfig, patch) {
18720
19181
  }
18721
19182
 
18722
19183
  // src/hooks/useSceneConfig.ts
19184
+ var DEFAULT_LIGHT = {
19185
+ type: "point",
19186
+ color: "#ffffff",
19187
+ intensity: 1,
19188
+ position: [0, 0, 0],
19189
+ distance: 0,
19190
+ decay: 2,
19191
+ attachedToCamera: false,
19192
+ castShadow: false,
19193
+ shadowBias: -5e-4,
19194
+ visible: true
19195
+ };
18723
19196
  function useSceneConfig(initialSceneConfig) {
18724
19197
  const initialSceneConfigRef = useRef21(null);
18725
- const [sceneConfig, setSceneConfig] = useState30(() => {
19198
+ const [sceneConfig, setSceneConfig] = useState31(() => {
18726
19199
  const initial = typeof initialSceneConfig === "function" ? initialSceneConfig() : initialSceneConfig;
18727
19200
  initialSceneConfigRef.current = initial;
18728
19201
  return initial;
@@ -18737,18 +19210,124 @@ function useSceneConfig(initialSceneConfig) {
18737
19210
  const initial = initialSceneConfigRef.current;
18738
19211
  if (initial) setSceneConfig(initial);
18739
19212
  }, []);
19213
+ const updateLight = useCallback13(
19214
+ (id, patch) => {
19215
+ updateSceneConfig((current) => ({
19216
+ lighting: {
19217
+ lights: current.lighting.lights.map(
19218
+ (light) => light.id === id ? { ...light, ...patch } : light
19219
+ )
19220
+ }
19221
+ }));
19222
+ },
19223
+ [updateSceneConfig]
19224
+ );
19225
+ const addLight = useCallback13(
19226
+ (light) => {
19227
+ updateSceneConfig((current) => {
19228
+ if (current.lighting.lights.some((item) => item.id === light.id)) {
19229
+ return {};
19230
+ }
19231
+ return {
19232
+ lighting: {
19233
+ lights: [
19234
+ ...current.lighting.lights,
19235
+ { ...DEFAULT_LIGHT, ...light }
19236
+ ]
19237
+ }
19238
+ };
19239
+ });
19240
+ },
19241
+ [updateSceneConfig]
19242
+ );
19243
+ const removeLight = useCallback13(
19244
+ (id) => {
19245
+ updateSceneConfig((current) => ({
19246
+ lighting: {
19247
+ lights: current.lighting.lights.filter((light) => light.id !== id)
19248
+ }
19249
+ }));
19250
+ },
19251
+ [updateSceneConfig]
19252
+ );
19253
+ const setLightIntensity = useCallback13(
19254
+ (id, intensity) => updateLight(id, { intensity }),
19255
+ [updateLight]
19256
+ );
19257
+ const setLightColor = useCallback13(
19258
+ (id, color) => updateLight(id, { color }),
19259
+ [updateLight]
19260
+ );
19261
+ const showLight = useCallback13(
19262
+ (id) => updateLight(id, { visible: true }),
19263
+ [updateLight]
19264
+ );
19265
+ const hideLight = useCallback13(
19266
+ (id) => updateLight(id, { visible: false }),
19267
+ [updateLight]
19268
+ );
19269
+ const toggleLight = useCallback13(
19270
+ (id) => {
19271
+ updateSceneConfig((current) => ({
19272
+ lighting: {
19273
+ lights: current.lighting.lights.map(
19274
+ (light) => light.id === id ? { ...light, visible: !light.visible } : light
19275
+ )
19276
+ }
19277
+ }));
19278
+ },
19279
+ [updateSceneConfig]
19280
+ );
19281
+ const attachLight = useCallback13(
19282
+ (id, attachment) => updateLight(id, { attachment, attachedToCamera: false }),
19283
+ [updateLight]
19284
+ );
19285
+ const detachLight = useCallback13(
19286
+ (id) => updateLight(id, { attachment: void 0 }),
19287
+ [updateLight]
19288
+ );
19289
+ const lights = useMemo22(
19290
+ () => ({
19291
+ items: sceneConfig.lighting.lights,
19292
+ get: (id) => sceneConfig.lighting.lights.find((light) => light.id === id),
19293
+ add: addLight,
19294
+ update: updateLight,
19295
+ remove: removeLight,
19296
+ setIntensity: setLightIntensity,
19297
+ setColor: setLightColor,
19298
+ show: showLight,
19299
+ hide: hideLight,
19300
+ toggle: toggleLight,
19301
+ attach: attachLight,
19302
+ detach: detachLight
19303
+ }),
19304
+ [
19305
+ sceneConfig.lighting.lights,
19306
+ addLight,
19307
+ updateLight,
19308
+ removeLight,
19309
+ setLightIntensity,
19310
+ setLightColor,
19311
+ showLight,
19312
+ hideLight,
19313
+ toggleLight,
19314
+ attachLight,
19315
+ detachLight
19316
+ ]
19317
+ );
18740
19318
  return {
18741
19319
  sceneConfig,
18742
19320
  setSceneConfig,
18743
19321
  updateSceneConfig,
18744
- resetSceneConfig
19322
+ resetSceneConfig,
19323
+ lights
18745
19324
  };
18746
19325
  }
18747
19326
 
18748
19327
  // src/hooks/useViewerHover.ts
18749
- import { useCallback as useCallback14, useState as useState31 } from "react";
19328
+ import { useCallback as useCallback14, useState as useState32 } from "react";
18750
19329
  function useViewerHover() {
18751
- const [hoveredObjectBinding, setHoveredObjectBinding] = useState31(null);
19330
+ const [hoveredObjectBinding, setHoveredObjectBinding] = useState32(null);
18752
19331
  const handleHoveredObject = useCallback14(
18753
19332
  (binding) => {
18754
19333
  setHoveredObjectBinding(binding);
@@ -18762,23 +19341,23 @@ function useViewerHover() {
18762
19341
  }
18763
19342
 
18764
19343
  // src/hooks/useViewerModel.ts
18765
- import { useCallback as useCallback15, useState as useState32 } from "react";
18766
- import { Box3 as Box36, Vector3 as Vector311 } from "three";
19344
+ import { useCallback as useCallback15, useState as useState33 } from "react";
19345
+ import { Box3 as Box36, Vector3 as Vector312 } from "three";
18767
19346
  function toTuple2(vector) {
18768
19347
  return [vector.x, vector.y, vector.z];
18769
19348
  }
18770
19349
  function useViewerModel() {
18771
- const [isLoading, setIsLoading] = useState32(true);
18772
- const [isReady, setIsReady] = useState32(false);
18773
- const [error, setError] = useState32(null);
18774
- const [bounds, setBounds] = useState32(null);
18775
- const [objectCount, setObjectCount] = useState32(0);
19350
+ const [isLoading, setIsLoading] = useState33(true);
19351
+ const [isReady, setIsReady] = useState33(false);
19352
+ const [error, setError] = useState33(null);
19353
+ const [bounds, setBounds] = useState33(null);
19354
+ const [objectCount, setObjectCount] = useState33(0);
18776
19355
  const handleModelLoaded = useCallback15((scene) => {
18777
19356
  const box = new Box36().setFromObject(scene);
18778
19357
  const min = box.min.clone();
18779
19358
  const max = box.max.clone();
18780
- const center = new Vector311();
18781
- const size = new Vector311();
19359
+ const center = new Vector312();
19360
+ const size = new Vector312();
18782
19361
  let nextObjectCount = 0;
18783
19362
  scene.traverse((child) => {
18784
19363
  if ("isMesh" in child && child.isMesh) {
@@ -18826,61 +19405,777 @@ function useViewerModel() {
18826
19405
 
18827
19406
  // src/hooks/useViewerActions.ts
18828
19407
  import { useCallback as useCallback16 } from "react";
18829
- function useViewerActions(objectBindings, options = {}) {
18830
- const { onAction, onObjectBindingsChange } = options;
19408
+ function useViewerActions({
19409
+ bindings,
19410
+ scene,
19411
+ camera,
19412
+ animations,
19413
+ onAction
19414
+ }) {
19415
+ const {
19416
+ objectBindings,
19417
+ updateObjectBindings,
19418
+ showObject,
19419
+ hideObject,
19420
+ toggleObjectVisibility
19421
+ } = bindings;
19422
+ const updateSceneConfig = scene?.updateSceneConfig;
19423
+ const focusObject = camera?.focusObject;
19424
+ const fitScene = camera?.fitScene;
19425
+ const resetView = camera?.resetView;
19426
+ const setCameraTarget = camera?.setCameraTarget;
19427
+ const setCameraState = camera?.setCameraState;
19428
+ const playAnimation = animations?.play;
19429
+ const pauseAnimation = animations?.pause;
19430
+ const stopAnimation = animations?.stop;
19431
+ const setAnimationSpeed = animations?.setSpeed;
19432
+ const seekAnimation = animations?.seek;
18831
19433
  const getActionsForObject = useCallback16(
18832
19434
  (objectId) => {
18833
19435
  const bindingKey = resolveBindingKey(objectBindings, objectId);
18834
- if (!bindingKey) {
18835
- return [];
18836
- }
18837
- return objectBindings[bindingKey]?.actions ?? [];
19436
+ return bindingKey ? objectBindings[bindingKey]?.actions ?? [] : [];
18838
19437
  },
18839
19438
  [objectBindings]
18840
19439
  );
19440
+ const handleAction = useCallback16(
19441
+ async (event) => {
19442
+ for (const effect of event.action.effects ?? []) {
19443
+ if ("objectBindings" in effect) {
19444
+ updateObjectBindings(effect.target, effect.objectBindings);
19445
+ continue;
19446
+ }
19447
+ if ("sceneConfig" in effect) {
19448
+ updateSceneConfig?.(effect.sceneConfig);
19449
+ continue;
19450
+ }
19451
+ if ("visibility" in effect) {
19452
+ if (effect.visibility === "show") showObject(effect.target);
19453
+ else if (effect.visibility === "hide") hideObject(effect.target);
19454
+ else toggleObjectVisibility(effect.target);
19455
+ continue;
19456
+ }
19457
+ if ("camera" in effect) {
19458
+ const action2 = effect.camera;
19459
+ if (action2.type === "focus-object") {
19460
+ await focusObject?.(action2.objectId ?? event.objectId);
19461
+ } else if (action2.type === "fit-scene") {
19462
+ await fitScene?.();
19463
+ } else if (action2.type === "reset") {
19464
+ await resetView?.();
19465
+ } else if (action2.type === "set-target") {
19466
+ await setCameraTarget?.(action2.target, action2.transition ?? true);
19467
+ } else {
19468
+ await setCameraState?.(action2.state, action2.transition ?? true);
19469
+ }
19470
+ continue;
19471
+ }
19472
+ const action = effect.animation;
19473
+ if (action.type === "play") playAnimation?.(action.clip);
19474
+ else if (action.type === "pause") pauseAnimation?.();
19475
+ else if (action.type === "stop") stopAnimation?.();
19476
+ else if (action.type === "set-speed") {
19477
+ setAnimationSpeed?.(action.speed);
19478
+ } else {
19479
+ seekAnimation?.(action.time);
19480
+ }
19481
+ }
19482
+ await onAction?.(event);
19483
+ },
19484
+ [
19485
+ fitScene,
19486
+ focusObject,
19487
+ hideObject,
19488
+ onAction,
19489
+ pauseAnimation,
19490
+ playAnimation,
19491
+ resetView,
19492
+ seekAnimation,
19493
+ setAnimationSpeed,
19494
+ setCameraState,
19495
+ setCameraTarget,
19496
+ showObject,
19497
+ stopAnimation,
19498
+ toggleObjectVisibility,
19499
+ updateObjectBindings,
19500
+ updateSceneConfig
19501
+ ]
19502
+ );
18841
19503
  const runAction = useCallback16(
18842
- (objectId, actionId) => {
19504
+ async (objectId, actionId) => {
18843
19505
  const bindingKey = resolveBindingKey(objectBindings, objectId);
18844
- if (!bindingKey) {
18845
- return null;
18846
- }
19506
+ if (!bindingKey) return null;
18847
19507
  const binding = objectBindings[bindingKey];
18848
- if (!binding) {
18849
- return null;
18850
- }
18851
- const action = (binding.actions ?? []).find(
18852
- (entry) => entry.id === actionId
18853
- );
18854
- if (!action) {
18855
- return null;
18856
- }
19508
+ const action = binding?.actions?.find((entry) => entry.id === actionId);
19509
+ if (!binding || !action) return null;
18857
19510
  const event = {
18858
19511
  objectId: bindingKey,
18859
19512
  action,
18860
19513
  binding
18861
19514
  };
18862
- if (action.id === "toggle-visibility" && onObjectBindingsChange) {
18863
- onObjectBindingsChange({
18864
- ...objectBindings,
18865
- [bindingKey]: {
18866
- ...binding,
18867
- visible: binding.visible === false
18868
- }
18869
- });
18870
- }
18871
- onAction?.(event);
19515
+ await handleAction(event);
18872
19516
  return event;
18873
19517
  },
18874
- [objectBindings, onAction, onObjectBindingsChange]
19518
+ [handleAction, objectBindings]
18875
19519
  );
18876
19520
  return {
18877
19521
  getActionsForObject,
19522
+ handleAction,
18878
19523
  runAction
18879
19524
  };
18880
19525
  }
18881
19526
 
19527
+ // src/hooks/useViewerConnection.ts
19528
+ import { useCallback as useCallback17, useRef as useRef22 } from "react";
19529
+ function getReadyHandler(consumer) {
19530
+ if (typeof consumer === "function") {
19531
+ return consumer;
19532
+ }
19533
+ return consumer?.handleViewerReady ?? null;
19534
+ }
19535
+ function useViewerConnection(...consumers) {
19536
+ const viewerRef = useRef22(null);
19537
+ const consumersRef = useRef22(consumers);
19538
+ consumersRef.current = consumers;
19539
+ const handleViewerReady = useCallback17((viewer) => {
19540
+ viewerRef.current = viewer;
19541
+ const invoked = /* @__PURE__ */ new Set();
19542
+ for (const consumer of consumersRef.current) {
19543
+ const handler = getReadyHandler(consumer);
19544
+ if (handler && !invoked.has(handler)) {
19545
+ invoked.add(handler);
19546
+ handler(viewer);
19547
+ }
19548
+ }
19549
+ }, []);
19550
+ const getViewer = useCallback17(() => viewerRef.current, []);
19551
+ const captureImage2 = useCallback17((options) => {
19552
+ const viewer = viewerRef.current;
19553
+ if (!viewer) {
19554
+ return Promise.reject(new Error("ModelViewer is not ready yet."));
19555
+ }
19556
+ return viewer.captureImage(options);
19557
+ }, []);
19558
+ const invalidate = useCallback17(() => {
19559
+ const viewer = viewerRef.current;
19560
+ if (!viewer) {
19561
+ return false;
19562
+ }
19563
+ viewer.invalidate();
19564
+ return true;
19565
+ }, []);
19566
+ return {
19567
+ handleViewerReady,
19568
+ getViewer,
19569
+ captureImage: captureImage2,
19570
+ invalidate
19571
+ };
19572
+ }
19573
+
19574
+ // src/hooks/useViewerEffects.ts
19575
+ import { useCallback as useCallback18, useEffect as useEffect25, useRef as useRef23 } from "react";
19576
+ import {
19577
+ Material as Material3,
19578
+ PointLight,
19579
+ Vector3 as Vector313
19580
+ } from "three";
19581
+ var NUMERIC_MATERIAL_KEYS = [
19582
+ "emissiveIntensity",
19583
+ "opacity",
19584
+ "metalness",
19585
+ "roughness",
19586
+ "transmission",
19587
+ "clearcoat",
19588
+ "clearcoatRoughness",
19589
+ "ior",
19590
+ "thickness"
19591
+ ];
19592
+ var nextEffectId = 1;
19593
+ function stringList(target) {
19594
+ return typeof target === "string" ? [target] : target;
19595
+ }
19596
+ function clamp2(value, min = 0, max = 1) {
19597
+ return Math.min(max, Math.max(min, value));
19598
+ }
19599
+ function randomBetween(min, max) {
19600
+ return min + Math.random() * (max - min);
19601
+ }
19602
+ function interpolate([from, to], level) {
19603
+ return from + (to - from) * level;
19604
+ }
19605
+ function interpolateVector([from, to], level) {
19606
+ return [
19607
+ from[0] + (to[0] - from[0]) * level,
19608
+ from[1] + (to[1] - from[1]) * level,
19609
+ from[2] + (to[2] - from[2]) * level
19610
+ ];
19611
+ }
19612
+ function ease(value, easing) {
19613
+ const t = clamp2(value);
19614
+ switch (easing) {
19615
+ case "ease-in":
19616
+ return t * t;
19617
+ case "ease-out":
19618
+ return 1 - (1 - t) * (1 - t);
19619
+ case "ease-in-out":
19620
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
19621
+ default:
19622
+ return t;
19623
+ }
19624
+ }
19625
+ function materialList(node) {
19626
+ const value = node.material;
19627
+ if (!value) return [];
19628
+ return (Array.isArray(value) ? value : [value]).filter(
19629
+ (entry) => entry instanceof Material3
19630
+ );
19631
+ }
19632
+ function readMaterial(material2) {
19633
+ const snapshot = {
19634
+ opacity: material2.opacity,
19635
+ transparent: material2.transparent
19636
+ };
19637
+ if (material2.color) snapshot.baseColor = `#${material2.color.getHexString()}`;
19638
+ if (material2.emissive) snapshot.emissive = `#${material2.emissive.getHexString()}`;
19639
+ for (const key of NUMERIC_MATERIAL_KEYS) {
19640
+ const value = material2[key];
19641
+ if (typeof value === "number") snapshot[key] = value;
19642
+ }
19643
+ return snapshot;
19644
+ }
19645
+ function applyMaterial(material2, patch) {
19646
+ if (patch.baseColor !== void 0 && material2.color) {
19647
+ material2.color.set(patch.baseColor);
19648
+ }
19649
+ if (patch.emissive !== void 0 && material2.emissive) {
19650
+ material2.emissive.set(patch.emissive);
19651
+ }
19652
+ for (const key of NUMERIC_MATERIAL_KEYS) {
19653
+ const value = patch[key];
19654
+ if (value !== void 0 && typeof material2[key] === "number") {
19655
+ material2[key] = value;
19656
+ }
19657
+ }
19658
+ if (patch.transparent !== void 0) material2.transparent = patch.transparent;
19659
+ material2.needsUpdate = true;
19660
+ }
19661
+ function readTransform(node) {
19662
+ return {
19663
+ position: [node.position.x, node.position.y, node.position.z],
19664
+ rotation: [node.rotation.x, node.rotation.y, node.rotation.z],
19665
+ scale: [node.scale.x, node.scale.y, node.scale.z],
19666
+ visible: node.visible
19667
+ };
19668
+ }
19669
+ function applyTransform(node, patch) {
19670
+ if (patch.position) node.position.set(...patch.position);
19671
+ if (patch.rotation) node.rotation.set(...patch.rotation);
19672
+ if (patch.scale !== void 0) {
19673
+ const scale = typeof patch.scale === "number" ? [patch.scale, patch.scale, patch.scale] : patch.scale;
19674
+ node.scale.set(...scale);
19675
+ }
19676
+ if (patch.visible !== void 0) node.visible = patch.visible;
19677
+ node.updateMatrix();
19678
+ node.updateMatrixWorld(true);
19679
+ }
19680
+ function useViewerEffects(options = {}) {
19681
+ const optionsRef = useRef23(options);
19682
+ optionsRef.current = options;
19683
+ const viewerRef = useRef23(null);
19684
+ const sceneRef = useRef23(null);
19685
+ const frameRef = useRef23(null);
19686
+ const effectsRef = useRef23(/* @__PURE__ */ new Map());
19687
+ const lightsRef = useRef23(/* @__PURE__ */ new Map());
19688
+ const materialSnapshotsRef = useRef23(
19689
+ /* @__PURE__ */ new Map()
19690
+ );
19691
+ const transformSnapshotsRef = useRef23(/* @__PURE__ */ new Map());
19692
+ const resolveObject = useCallback18((target) => {
19693
+ const viewer = viewerRef.current;
19694
+ if (!viewer) return null;
19695
+ const bindingKey = resolveBindingKey(viewer.objectBindings, target);
19696
+ return viewer.nodeRefs[target] ?? (bindingKey ? viewer.nodeRefs[bindingKey] : null) ?? viewer.scene?.getObjectByName(target) ?? null;
19697
+ }, []);
19698
+ const resolveTargets = useCallback18(
19699
+ (target) => {
19700
+ const viewer = viewerRef.current;
19701
+ if (!viewer) {
19702
+ return typeof target === "string" ? [target] : Array.isArray(target) ? target : [];
19703
+ }
19704
+ if (typeof target === "object" && !Array.isArray(target)) {
19705
+ return selectBindingKeys(viewer.objectBindings, target);
19706
+ }
19707
+ return stringList(target).map(
19708
+ (id) => resolveBindingKey(viewer.objectBindings, id) ?? id
19709
+ );
19710
+ },
19711
+ []
19712
+ );
19713
+ const invalidate = useCallback18(() => {
19714
+ viewerRef.current?.invalidate();
19715
+ }, []);
19716
+ const setMaterialInternal = useCallback18(
19717
+ (target, patch) => {
19718
+ let changed = false;
19719
+ for (const id of resolveTargets(target)) {
19720
+ const node = resolveObject(id);
19721
+ if (!node) continue;
19722
+ for (const entry of materialList(node)) {
19723
+ if (!materialSnapshotsRef.current.has(entry)) {
19724
+ materialSnapshotsRef.current.set(entry, readMaterial(entry));
19725
+ }
19726
+ applyMaterial(entry, patch);
19727
+ changed = true;
19728
+ }
19729
+ }
19730
+ return changed;
19731
+ },
19732
+ [resolveObject, resolveTargets]
19733
+ );
19734
+ const setMaterial = useCallback18(
19735
+ (target, patch) => {
19736
+ const changed = setMaterialInternal(target, patch);
19737
+ if (changed) invalidate();
19738
+ return changed;
19739
+ },
19740
+ [invalidate, setMaterialInternal]
19741
+ );
19742
+ const setTransformInternal = useCallback18(
19743
+ (target, patch) => {
19744
+ let changed = false;
19745
+ for (const id of resolveTargets(target)) {
19746
+ const node = resolveObject(id);
19747
+ if (!node) continue;
19748
+ if (!transformSnapshotsRef.current.has(node)) {
19749
+ transformSnapshotsRef.current.set(node, readTransform(node));
19750
+ }
19751
+ applyTransform(node, patch);
19752
+ changed = true;
19753
+ }
19754
+ return changed;
19755
+ },
19756
+ [resolveObject, resolveTargets]
19757
+ );
19758
+ const setTransform = useCallback18(
19759
+ (target, patch) => {
19760
+ const changed = setTransformInternal(target, patch);
19761
+ if (changed) invalidate();
19762
+ return changed;
19763
+ },
19764
+ [invalidate, setTransformInternal]
19765
+ );
19766
+ const getWorldPosition = useCallback18(
19767
+ (target) => {
19768
+ const node = resolveObject(target);
19769
+ if (!node) return null;
19770
+ node.updateWorldMatrix(true, false);
19771
+ const position = node.getWorldPosition(new Vector313());
19772
+ return [position.x, position.y, position.z];
19773
+ },
19774
+ [resolveObject]
19775
+ );
19776
+ const removePointLight = useCallback18(
19777
+ (id) => {
19778
+ const runtime = lightsRef.current.get(id);
19779
+ if (!runtime) return false;
19780
+ runtime.light.removeFromParent();
19781
+ runtime.light.dispose();
19782
+ lightsRef.current.delete(id);
19783
+ invalidate();
19784
+ return true;
19785
+ },
19786
+ [invalidate]
19787
+ );
19788
+ const addPointLight = useCallback18(
19789
+ (lightOptions) => {
19790
+ const target = resolveObject(lightOptions.target);
19791
+ if (!target) return false;
19792
+ const existing = lightsRef.current.get(lightOptions.id);
19793
+ const light = existing?.light ?? new PointLight();
19794
+ if (existing && existing.target !== lightOptions.target) {
19795
+ light.removeFromParent();
19796
+ }
19797
+ light.name = `react-immersive-runtime-light:${lightOptions.id}`;
19798
+ light.color.set(lightOptions.color ?? "#ffffff");
19799
+ light.intensity = lightOptions.intensity ?? 1;
19800
+ light.distance = lightOptions.distance ?? 0;
19801
+ light.decay = lightOptions.decay ?? 2;
19802
+ light.castShadow = lightOptions.castShadow ?? false;
19803
+ light.position.set(...lightOptions.offset ?? [0, 0, 0]);
19804
+ if (light.parent !== target) target.add(light);
19805
+ lightsRef.current.set(lightOptions.id, {
19806
+ light,
19807
+ target: lightOptions.target
19808
+ });
19809
+ invalidate();
19810
+ return true;
19811
+ },
19812
+ [invalidate, resolveObject]
19813
+ );
19814
+ const updatePointLight = useCallback18(
19815
+ (id, patch) => {
19816
+ const runtime = lightsRef.current.get(id);
19817
+ if (!runtime) return false;
19818
+ if (patch.target && patch.target !== runtime.target) {
19819
+ const target = resolveObject(patch.target);
19820
+ if (!target) return false;
19821
+ runtime.light.removeFromParent();
19822
+ target.add(runtime.light);
19823
+ runtime.target = patch.target;
19824
+ }
19825
+ if (patch.color !== void 0) runtime.light.color.set(patch.color);
19826
+ if (patch.intensity !== void 0) {
19827
+ runtime.light.intensity = patch.intensity;
19828
+ }
19829
+ if (patch.distance !== void 0) runtime.light.distance = patch.distance;
19830
+ if (patch.decay !== void 0) runtime.light.decay = patch.decay;
19831
+ if (patch.offset !== void 0) {
19832
+ runtime.light.position.set(...patch.offset);
19833
+ }
19834
+ if (patch.castShadow !== void 0) {
19835
+ runtime.light.castShadow = patch.castShadow;
19836
+ }
19837
+ invalidate();
19838
+ return true;
19839
+ },
19840
+ [invalidate, resolveObject]
19841
+ );
19842
+ const applyChannels = useCallback18(
19843
+ (channels, levels) => {
19844
+ const levelFor = (index) => levels[index % levels.length] ?? 0;
19845
+ for (const channel of channels.materials ?? []) {
19846
+ resolveTargets(channel.targets).forEach((target, index) => {
19847
+ const level = levelFor(index);
19848
+ const patch = {};
19849
+ for (const key of NUMERIC_MATERIAL_KEYS) {
19850
+ const range = channel.values[key];
19851
+ if (range) patch[key] = interpolate(range, level);
19852
+ }
19853
+ setMaterialInternal(target, patch);
19854
+ });
19855
+ }
19856
+ for (const channel of channels.transforms ?? []) {
19857
+ resolveTargets(channel.targets).forEach((target, index) => {
19858
+ const level = levelFor(index);
19859
+ const patch = {};
19860
+ if (channel.values.position) {
19861
+ patch.position = interpolateVector(channel.values.position, level);
19862
+ }
19863
+ if (channel.values.rotation) {
19864
+ patch.rotation = interpolateVector(channel.values.rotation, level);
19865
+ }
19866
+ if (channel.values.scale) {
19867
+ patch.scale = interpolateVector(channel.values.scale, level);
19868
+ }
19869
+ setTransformInternal(target, patch);
19870
+ });
19871
+ }
19872
+ for (const channel of channels.lights ?? []) {
19873
+ stringList(channel.lights).forEach((id, index) => {
19874
+ const runtime = lightsRef.current.get(id);
19875
+ if (runtime) {
19876
+ runtime.light.intensity = interpolate(
19877
+ channel.intensity,
19878
+ levelFor(index)
19879
+ );
19880
+ }
19881
+ });
19882
+ }
19883
+ },
19884
+ [resolveTargets, setMaterialInternal, setTransformInternal]
19885
+ );
19886
+ const scheduleFrameRef = useRef23(() => void 0);
19887
+ const runFrameRef = useRef23(() => void 0);
19888
+ runFrameRef.current = (now) => {
19889
+ frameRef.current = null;
19890
+ let rendered = false;
19891
+ for (const [id, effect] of effectsRef.current) {
19892
+ if (effect.startedAt === null) effect.startedAt = now;
19893
+ const previous = effect.lastFrameAt ?? now;
19894
+ effect.lastFrameAt = now;
19895
+ const override = effect.override && effect.override.until > now ? effect.override.level : null;
19896
+ if (effect.override && override === null) effect.override = null;
19897
+ const keepRunning = effect.update(
19898
+ now,
19899
+ now - effect.startedAt,
19900
+ Math.min(100, now - previous),
19901
+ override
19902
+ );
19903
+ rendered = true;
19904
+ if (!keepRunning) {
19905
+ effectsRef.current.delete(id);
19906
+ effect.onComplete?.();
19907
+ }
19908
+ }
19909
+ if (rendered) invalidate();
19910
+ if (effectsRef.current.size > 0) scheduleFrameRef.current();
19911
+ };
19912
+ scheduleFrameRef.current = () => {
19913
+ if (frameRef.current !== null || typeof requestAnimationFrame === "undefined") {
19914
+ return;
19915
+ }
19916
+ frameRef.current = requestAnimationFrame((now) => runFrameRef.current(now));
19917
+ };
19918
+ const registerEffect = useCallback18((effect) => {
19919
+ effectsRef.current.set(effect.id, effect);
19920
+ scheduleFrameRef.current();
19921
+ return effect.id;
19922
+ }, []);
19923
+ const transition = useCallback18(
19924
+ (channels, transitionOptions = {}) => {
19925
+ const id = transitionOptions.id ?? `transition-${nextEffectId++}`;
19926
+ const duration = Math.max(1, transitionOptions.duration ?? 400);
19927
+ const easing = transitionOptions.easing ?? "ease-in-out";
19928
+ return registerEffect({
19929
+ id,
19930
+ startedAt: null,
19931
+ lastFrameAt: null,
19932
+ override: null,
19933
+ onComplete: transitionOptions.onComplete,
19934
+ update: (_now, elapsed, _delta, override) => {
19935
+ const progress = override ?? ease(elapsed / duration, easing);
19936
+ applyChannels(channels, [progress]);
19937
+ return elapsed < duration;
19938
+ }
19939
+ });
19940
+ },
19941
+ [applyChannels, registerEffect]
19942
+ );
19943
+ const pulse = useCallback18(
19944
+ (channels, pulseOptions = {}) => {
19945
+ const id = pulseOptions.id ?? `pulse-${nextEffectId++}`;
19946
+ const duration = Math.max(1, pulseOptions.duration ?? 1e3);
19947
+ const minLevel = pulseOptions.minLevel ?? 0;
19948
+ const maxLevel = pulseOptions.maxLevel ?? 1;
19949
+ const repeat = pulseOptions.repeat ?? Infinity;
19950
+ const synchronized = pulseOptions.synchronized ?? true;
19951
+ const unitCount = Math.max(
19952
+ 1,
19953
+ ...[...channels.materials ?? [], ...channels.transforms ?? []].map(
19954
+ (channel) => resolveTargets(channel.targets).length
19955
+ ),
19956
+ ...(channels.lights ?? []).map(
19957
+ (channel) => stringList(channel.lights).length
19958
+ )
19959
+ );
19960
+ return registerEffect({
19961
+ id,
19962
+ startedAt: null,
19963
+ lastFrameAt: null,
19964
+ override: null,
19965
+ onComplete: pulseOptions.onComplete,
19966
+ update: (_now, elapsed, _delta, override) => {
19967
+ const cycles = elapsed / duration;
19968
+ const levels = Array.from(
19969
+ { length: synchronized ? 1 : unitCount },
19970
+ (_, index) => {
19971
+ if (override !== null) return override;
19972
+ const phase = cycles + (synchronized ? 0 : index / unitCount);
19973
+ const wave = 0.5 - Math.cos(phase % 1 * Math.PI * 2) / 2;
19974
+ return minLevel + (maxLevel - minLevel) * wave;
19975
+ }
19976
+ );
19977
+ applyChannels(channels, levels);
19978
+ return repeat === Infinity || cycles < repeat;
19979
+ }
19980
+ });
19981
+ },
19982
+ [applyChannels, registerEffect, resolveTargets]
19983
+ );
19984
+ const timeline = useCallback18(
19985
+ (steps, timelineOptions = {}) => {
19986
+ const id = timelineOptions.id ?? `timeline-${nextEffectId++}`;
19987
+ const repeat = timelineOptions.repeat ?? 1;
19988
+ const normalizedSteps = steps.map((step) => ({
19989
+ ...step,
19990
+ duration: Math.max(1, step.duration),
19991
+ hold: Math.max(0, step.hold ?? 0)
19992
+ }));
19993
+ const cycleDuration = Math.max(
19994
+ 1,
19995
+ normalizedSteps.reduce(
19996
+ (total, step) => total + step.duration + step.hold,
19997
+ 0
19998
+ )
19999
+ );
20000
+ return registerEffect({
20001
+ id,
20002
+ startedAt: null,
20003
+ lastFrameAt: null,
20004
+ override: null,
20005
+ onComplete: timelineOptions.onComplete,
20006
+ update: (_now, elapsed, _delta, override) => {
20007
+ if (normalizedSteps.length === 0 || repeat <= 0) return false;
20008
+ const cycles = elapsed / cycleDuration;
20009
+ if (repeat !== Infinity && cycles >= repeat) {
20010
+ for (const step of normalizedSteps) {
20011
+ applyChannels(step.channels, [1]);
20012
+ }
20013
+ return false;
20014
+ }
20015
+ const cycleTime = elapsed % cycleDuration;
20016
+ let cursor = 0;
20017
+ for (const step of normalizedSteps) {
20018
+ const transitionEnd = cursor + step.duration;
20019
+ const stepEnd = transitionEnd + step.hold;
20020
+ if (cycleTime >= transitionEnd) {
20021
+ applyChannels(step.channels, [override ?? 1]);
20022
+ cursor = stepEnd;
20023
+ continue;
20024
+ }
20025
+ if (cycleTime >= cursor) {
20026
+ const progress = override ?? ease(
20027
+ (cycleTime - cursor) / step.duration,
20028
+ step.easing ?? "ease-in-out"
20029
+ );
20030
+ applyChannels(step.channels, [progress]);
20031
+ break;
20032
+ }
20033
+ }
20034
+ return true;
20035
+ }
20036
+ });
20037
+ },
20038
+ [applyChannels, registerEffect]
20039
+ );
20040
+ const flicker = useCallback18(
20041
+ (channels, flickerOptions = {}) => {
20042
+ const id = flickerOptions.id ?? `flicker-${nextEffectId++}`;
20043
+ const synchronized = flickerOptions.synchronized ?? true;
20044
+ const minLevel = flickerOptions.minLevel ?? 0.15;
20045
+ const maxLevel = flickerOptions.maxLevel ?? 1;
20046
+ const interval = flickerOptions.interval ?? [45, 160];
20047
+ const rawIntervalRange = typeof interval === "number" ? [interval, interval] : interval;
20048
+ const intervalRange = [
20049
+ Math.max(1, Math.min(...rawIntervalRange)),
20050
+ Math.max(1, Math.max(...rawIntervalRange))
20051
+ ];
20052
+ const response = clamp2(flickerOptions.response ?? 0.7, 0.01, 1);
20053
+ const blackoutChance = clamp2(flickerOptions.blackoutChance ?? 0.08);
20054
+ const flashChance = clamp2(flickerOptions.flashChance ?? 0.12);
20055
+ const flashLevel = flickerOptions.flashLevel ?? 1.15;
20056
+ const unitCount = Math.max(
20057
+ 1,
20058
+ ...[...channels.materials ?? [], ...channels.transforms ?? []].map(
20059
+ (channel) => resolveTargets(channel.targets).length
20060
+ ),
20061
+ ...(channels.lights ?? []).map(
20062
+ (channel) => stringList(channel.lights).length
20063
+ )
20064
+ );
20065
+ const count = synchronized ? 1 : unitCount;
20066
+ const levels = Array.from({ length: count }, () => minLevel);
20067
+ const targets = Array.from({ length: count }, () => minLevel);
20068
+ const nextChanges = Array.from({ length: count }, () => 0);
20069
+ return registerEffect({
20070
+ id,
20071
+ startedAt: null,
20072
+ lastFrameAt: null,
20073
+ override: null,
20074
+ onComplete: flickerOptions.onComplete,
20075
+ update: (now, elapsed, delta, override) => {
20076
+ for (let index = 0; index < count; index += 1) {
20077
+ if (override !== null) {
20078
+ targets[index] = override;
20079
+ } else if (now >= nextChanges[index]) {
20080
+ const roll = Math.random();
20081
+ targets[index] = roll < blackoutChance ? randomBetween(0, Math.min(0.04, minLevel)) : roll < blackoutChance + flashChance ? flashLevel : randomBetween(minLevel, maxLevel);
20082
+ nextChanges[index] = now + randomBetween(...intervalRange);
20083
+ }
20084
+ const frameResponse = 1 - Math.pow(1 - response, Math.max(1, delta) / 16.67);
20085
+ levels[index] += (targets[index] - levels[index]) * frameResponse;
20086
+ }
20087
+ applyChannels(channels, levels);
20088
+ return flickerOptions.duration === void 0 || elapsed < flickerOptions.duration;
20089
+ }
20090
+ });
20091
+ },
20092
+ [applyChannels, registerEffect, resolveTargets]
20093
+ );
20094
+ const overrideEffect = useCallback18(
20095
+ (id, level, duration) => {
20096
+ const effect = effectsRef.current.get(id);
20097
+ if (!effect) return false;
20098
+ const now = typeof performance === "undefined" ? Date.now() : performance.now();
20099
+ effect.override = { level, until: now + Math.max(0, duration) };
20100
+ scheduleFrameRef.current();
20101
+ return true;
20102
+ },
20103
+ []
20104
+ );
20105
+ const stopEffect = useCallback18((id) => {
20106
+ return effectsRef.current.delete(id);
20107
+ }, []);
20108
+ const stopAllEffects = useCallback18(() => {
20109
+ effectsRef.current.clear();
20110
+ if (frameRef.current !== null && typeof cancelAnimationFrame !== "undefined") {
20111
+ cancelAnimationFrame(frameRef.current);
20112
+ frameRef.current = null;
20113
+ }
20114
+ }, []);
20115
+ const restoreRuntimeValues = useCallback18(() => {
20116
+ for (const [entry, snapshot] of materialSnapshotsRef.current) {
20117
+ applyMaterial(entry, snapshot);
20118
+ }
20119
+ materialSnapshotsRef.current.clear();
20120
+ for (const [node, snapshot] of transformSnapshotsRef.current) {
20121
+ node.position.set(...snapshot.position);
20122
+ node.rotation.set(...snapshot.rotation);
20123
+ node.scale.set(...snapshot.scale);
20124
+ node.visible = snapshot.visible;
20125
+ node.updateMatrix();
20126
+ node.updateMatrixWorld(true);
20127
+ }
20128
+ transformSnapshotsRef.current.clear();
20129
+ for (const runtime of lightsRef.current.values()) {
20130
+ runtime.light.removeFromParent();
20131
+ runtime.light.dispose();
20132
+ }
20133
+ lightsRef.current.clear();
20134
+ invalidate();
20135
+ }, [invalidate]);
20136
+ const resetRuntime = useCallback18(() => {
20137
+ stopAllEffects();
20138
+ restoreRuntimeValues();
20139
+ }, [restoreRuntimeValues, stopAllEffects]);
20140
+ const handleViewerReady = useCallback18(
20141
+ (viewer) => {
20142
+ if (sceneRef.current && sceneRef.current !== viewer.scene) {
20143
+ restoreRuntimeValues();
20144
+ }
20145
+ viewerRef.current = viewer;
20146
+ sceneRef.current = viewer.scene;
20147
+ optionsRef.current.onViewerReady?.(viewer);
20148
+ },
20149
+ [restoreRuntimeValues]
20150
+ );
20151
+ useEffect25(() => {
20152
+ return () => {
20153
+ resetRuntime();
20154
+ viewerRef.current = null;
20155
+ sceneRef.current = null;
20156
+ };
20157
+ }, [resetRuntime]);
20158
+ return {
20159
+ handleViewerReady,
20160
+ setMaterial,
20161
+ setTransform,
20162
+ getWorldPosition,
20163
+ addPointLight,
20164
+ updatePointLight,
20165
+ removePointLight,
20166
+ transition,
20167
+ timeline,
20168
+ pulse,
20169
+ flicker,
20170
+ overrideEffect,
20171
+ stopEffect,
20172
+ stopAllEffects,
20173
+ resetRuntime
20174
+ };
20175
+ }
20176
+
18882
20177
  // src/hooks/useShareableViewerState.ts
18883
- import { useCallback as useCallback17, useEffect as useEffect24, useRef as useRef22, useState as useState33 } from "react";
20178
+ import { useCallback as useCallback19, useEffect as useEffect26, useRef as useRef24, useState as useState34 } from "react";
18884
20179
  function serializeState(cameraState, objectBindings, selectedObjectBinding) {
18885
20180
  const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
18886
20181
  return {
@@ -18900,8 +20195,8 @@ function useShareableViewerState({
18900
20195
  debounceMs = 400,
18901
20196
  autoSync = true
18902
20197
  }) {
18903
- const lastWrittenRawRef = useRef22(null);
18904
- const buildHash = useCallback17(() => {
20198
+ const lastWrittenRawRef = useRef24(null);
20199
+ const buildHash = useCallback19(() => {
18905
20200
  const state = serializeState(
18906
20201
  cameraState,
18907
20202
  objectBindings,
@@ -18909,12 +20204,12 @@ function useShareableViewerState({
18909
20204
  );
18910
20205
  return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
18911
20206
  }, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
18912
- const getShareUrl = useCallback17(() => {
20207
+ const getShareUrl = useCallback19(() => {
18913
20208
  const url = new URL(window.location.href);
18914
20209
  url.hash = buildHash();
18915
20210
  return url.toString();
18916
20211
  }, [buildHash]);
18917
- const syncUrl = useCallback17(() => {
20212
+ const syncUrl = useCallback19(() => {
18918
20213
  const url = new URL(window.location.href);
18919
20214
  url.hash = buildHash();
18920
20215
  window.history.replaceState(null, "", url.toString());
@@ -18922,7 +20217,7 @@ function useShareableViewerState({
18922
20217
  paramKey
18923
20218
  );
18924
20219
  }, [buildHash, paramKey]);
18925
- const latestStateRef = useRef22({
20220
+ const latestStateRef = useRef24({
18926
20221
  cameraState,
18927
20222
  objectBindings,
18928
20223
  selectedObjectBinding
@@ -18932,9 +20227,9 @@ function useShareableViewerState({
18932
20227
  objectBindings,
18933
20228
  selectedObjectBinding
18934
20229
  };
18935
- const [restoreAttempted, setRestoreAttempted] = useState33(false);
18936
- const lastSyncedHashRef = useRef22(null);
18937
- useEffect24(() => {
20230
+ const [restoreAttempted, setRestoreAttempted] = useState34(false);
20231
+ const lastSyncedHashRef = useRef24(null);
20232
+ useEffect26(() => {
18938
20233
  if (!autoSync || !restoreAttempted) return;
18939
20234
  const tick = () => {
18940
20235
  const current = latestStateRef.current;
@@ -18956,9 +20251,9 @@ function useShareableViewerState({
18956
20251
  const id = window.setInterval(tick, debounceMs);
18957
20252
  return () => window.clearInterval(id);
18958
20253
  }, [autoSync, restoreAttempted, debounceMs, paramKey]);
18959
- const lastRestoredRawRef = useRef22(null);
18960
- const restoringRawRef = useRef22(null);
18961
- const restoreFromUrl = useCallback17(async () => {
20254
+ const lastRestoredRawRef = useRef24(null);
20255
+ const restoringRawRef = useRef24(null);
20256
+ const restoreFromUrl = useCallback19(async () => {
18962
20257
  setRestoreAttempted(true);
18963
20258
  const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
18964
20259
  const raw = new URLSearchParams(rawHash).get(paramKey);
@@ -19011,7 +20306,7 @@ function useShareableViewerState({
19011
20306
  setCameraState,
19012
20307
  setSelectedObjectBinding
19013
20308
  ]);
19014
- useEffect24(() => {
20309
+ useEffect26(() => {
19015
20310
  const handleHashChange = () => {
19016
20311
  void restoreFromUrl();
19017
20312
  };
@@ -19024,13 +20319,17 @@ function useShareableViewerState({
19024
20319
  }
19025
20320
  export {
19026
20321
  BindingBuilder,
20322
+ DEFAULT_SCENE_CONFIG,
19027
20323
  MATERIAL_BLENDING_MODES,
19028
20324
  MATERIAL_SIDES,
19029
20325
  ModelViewer,
19030
20326
  SimpleModelViewer,
20327
+ defineObjectBindings,
19031
20328
  material,
19032
20329
  patchBindings,
19033
20330
  patchSceneConfig,
20331
+ selectBindingKeys,
20332
+ selectBindings,
19034
20333
  useObjectBinding,
19035
20334
  useObjectBindingIds,
19036
20335
  useObjectBindings,
@@ -19039,6 +20338,8 @@ export {
19039
20338
  useViewerActions,
19040
20339
  useViewerAnimations,
19041
20340
  useViewerCamera,
20341
+ useViewerConnection,
20342
+ useViewerEffects,
19042
20343
  useViewerHover,
19043
20344
  useViewerModel,
19044
20345
  useViewerSelection