@liveroom-tech/react-immersive 2.2.0 → 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/README.md CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  useViewerActions,
22
22
  useViewerAnimations,
23
23
  useViewerCamera,
24
+ useViewerConnection,
25
+ useViewerEffects,
24
26
  useViewerHover,
25
27
  useViewerModel,
26
28
  useViewerSelection,
@@ -183,6 +185,7 @@ Peer dependencies:
183
185
 
184
186
  ```tsx
185
187
  import {
188
+ defineObjectBindings,
186
189
  ModelViewer,
187
190
  useObjectBinding,
188
191
  useObjectBindings,
@@ -192,10 +195,9 @@ import {
192
195
  useViewerHover,
193
196
  useViewerModel,
194
197
  useViewerSelection,
195
- type ObjectBinding,
196
198
  } from "@liveroom-tech/react-immersive";
197
199
 
198
- const initialBindings: Record<string, ObjectBinding> = {
200
+ const initialBindings = defineObjectBindings({
199
201
  CarBody: {
200
202
  id: "car-body",
201
203
  modelObjectId: "CarBody",
@@ -219,19 +221,27 @@ const initialBindings: Record<string, ObjectBinding> = {
219
221
  { id: "change-color", label: "Change Color", type: "command" },
220
222
  { id: "change-material", label: "Change Material", type: "command" },
221
223
  { id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
224
+ {
225
+ id: "toggle-body",
226
+ label: "Toggle Body",
227
+ type: "command",
228
+ effects: [{ target: "CarBody", visibility: "toggle" }],
229
+ },
222
230
  ],
223
231
  },
224
- };
232
+ });
225
233
 
226
234
  export default function Example() {
235
+ const bindings = useObjectBindings(initialBindings);
227
236
  const {
228
237
  objectBindings,
229
238
  setObjectBindings,
230
239
  hiddenObjects,
231
240
  toggleObjectVisibility,
232
- } = useObjectBindings(initialBindings);
241
+ } = bindings;
233
242
  const { selectedObjectBinding, handleObjectSelect } = useViewerSelection();
234
243
  const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
244
+ const camera = useViewerCamera();
235
245
  const {
236
246
  cameraState,
237
247
  resetView,
@@ -240,7 +250,7 @@ export default function Example() {
240
250
  setCameraTarget,
241
251
  handleCameraChange,
242
252
  handleViewerReady,
243
- } = useViewerCamera();
253
+ } = camera;
244
254
  const {
245
255
  isLoading,
246
256
  isReady,
@@ -250,9 +260,12 @@ export default function Example() {
250
260
  handleModelLoaded,
251
261
  handleLoadError,
252
262
  } = useViewerModel();
253
- const { getActionsForObject, runAction } = useViewerActions(objectBindings, {
263
+ const animations = useViewerAnimations();
264
+ const { getActionsForObject, handleAction, runAction } = useViewerActions({
265
+ bindings,
266
+ camera,
267
+ animations,
254
268
  onAction: (event) => {},
255
- onObjectBindingsChange: setObjectBindings,
256
269
  });
257
270
  const focusedBinding = useObjectBinding(objectBindings, "car-body");
258
271
 
@@ -270,14 +283,15 @@ export default function Example() {
270
283
  onLoadError={handleLoadError}
271
284
  onObjectBindingsChange={setObjectBindings}
272
285
  onViewerReady={handleViewerReady}
273
- onAction={(event) => {}}
286
+ onAnimationsReady={animations.handleAnimationsReady}
287
+ onAction={handleAction}
274
288
  />
275
289
 
276
290
  <button onClick={() => toggleObjectVisibility("car-body")}>
277
291
  Toggle Visibility
278
292
  </button>
279
- <button onClick={() => runAction("car-body", "toggle-visibility")}>
280
- Run visibility action
293
+ <button onClick={() => runAction("car-body", "toggle-body")}>
294
+ Run declarative action
281
295
  </button>
282
296
  <button onClick={() => resetView()}>Reset view</button>
283
297
  <button onClick={() => focusObject("car-body")}>Focus Object</button>
@@ -337,10 +351,18 @@ const {
337
351
  objectBindings,
338
352
  setObjectBindings,
339
353
  updateObjectBindings,
354
+ selectObjectBindings,
355
+ selectObjectBindingIds,
340
356
  setMaterial,
341
357
  setBaseColor,
358
+ setTexture,
359
+ clearTexture,
360
+ copyMaterial,
342
361
  resetMaterial,
343
362
  getMaterial,
363
+ setTransform,
364
+ resetTransform,
365
+ updateMetadata,
344
366
  hiddenObjects,
345
367
  hiddenObjectIds,
346
368
  hideObject,
@@ -366,11 +388,40 @@ Object identifiers accepted by its methods can be:
366
388
  - `binding.id`
367
389
  - `binding.modelObjectId`
368
390
 
391
+ You can also target bindings by a shared `group`, `tag`, or object `type`. Groups
392
+ represent one named set, while tags let a binding participate in several sets:
393
+
394
+ ```tsx
395
+ updateObjectBindings({ group: "paint" }, { selectable: false });
396
+ setBaseColor({ tag: "exterior" }, "#2563eb");
397
+ hideObject({ type: "wheel" });
398
+
399
+ const exteriorBindings = selectObjectBindings({ tag: "exterior" });
400
+ const paintIds = selectObjectBindingIds({ group: "paint" });
401
+
402
+ setTexture({ group: "screens" }, "/images/dashboard.png");
403
+ clearTexture({ tag: "printable" }, "baseColor");
404
+ copyMaterial("wheel-front", { group: "wheels" });
405
+
406
+ setTransform("door-left", {
407
+ rotation: [0, Math.PI / 2, 0],
408
+ });
409
+ resetTransform("door-left");
410
+ updateMetadata({ group: "lights" }, { state: "on" });
411
+ ```
412
+
413
+ When a parent or store owns the state, use the pure `selectBindings` and
414
+ `selectBindingKeys` helpers for the same queries, and `patchBindings` to update
415
+ all matches.
416
+
369
417
  The hook returns:
370
418
 
371
419
  - `objectBindings` and `setObjectBindings`
372
420
  - `updateObjectBindings` for any binding field or atomic edit batch
373
- - `setMaterial`, `setBaseColor`, `resetMaterial`, and `getMaterial`
421
+ - `selectObjectBindings` and `selectObjectBindingIds` for group, tag, or type queries
422
+ - `setMaterial`, `setBaseColor`, `setTexture`, `clearTexture`, `copyMaterial`, `resetMaterial`, and `getMaterial`
423
+ - `setTransform` and `resetTransform` for persistent local-space model transforms
424
+ - `updateMetadata` for recursively merged application data
374
425
  - `hiddenObjects`: a derived map of hidden binding keys
375
426
  - `hiddenObjectIds`: the currently hidden binding IDs
376
427
  - `hideObject`, `showObject`, `toggleObjectVisibility`
@@ -385,6 +436,7 @@ const {
385
436
  setSceneConfig,
386
437
  updateSceneConfig,
387
438
  resetSceneConfig,
439
+ lights,
388
440
  } = useSceneConfig(initialSceneConfig);
389
441
 
390
442
  const enableUvChecker = () =>
@@ -393,6 +445,14 @@ const enableUvChecker = () =>
393
445
  lighting: { ambient: { intensity: 0.5 } },
394
446
  });
395
447
 
448
+ lights.setIntensity("bulb-light", 4);
449
+ lights.setColor("bulb-light", "#ffd27d");
450
+ lights.toggle("bulb-light");
451
+ lights.attach("bulb-light", {
452
+ objectId: "bulb-filament",
453
+ offset: [0, -0.2, 0],
454
+ });
455
+
396
456
  <ModelViewer
397
457
  modelUrl="/model.glb"
398
458
  licenseKey="your-license-key"
@@ -406,7 +466,72 @@ const enableUvChecker = () =>
406
466
  Nested objects merge recursively, while arrays and tuples are replaced. Every
407
467
  update uses a functional state updater, so consecutive calls compose. Use the
408
468
  pure `patchSceneConfig(sceneConfig, patch)` utility when a store or parent owns
409
- the scene state.
469
+ the scene state. The namespaced `lights` controller also provides `add`,
470
+ `update`, `remove`, `show`, `hide`, `toggle`, `setIntensity`, `setColor`,
471
+ `attach`, and `detach`. Attached lights follow a bound model object without an
472
+ R3F light component. The custom `lights` prop replaces scene-config lights, so
473
+ leave it unset when using this controller.
474
+
475
+ For high-frequency visual effects, use `useViewerEffects`. It mutates the live
476
+ viewer efficiently, requests demand-rendered frames automatically, and restores
477
+ its runtime changes on cleanup. Application code does not need Three.js node or
478
+ material access:
479
+
480
+ ```tsx
481
+ const {
482
+ addPointLight,
483
+ flicker,
484
+ handleViewerReady,
485
+ overrideEffect,
486
+ stopEffect,
487
+ } = useViewerEffects();
488
+
489
+ useEffect(() => {
490
+ flicker(
491
+ {
492
+ materials: [
493
+ {
494
+ targets: { group: "filaments" },
495
+ values: { emissiveIntensity: [0.02, 8] },
496
+ },
497
+ ],
498
+ lights: [
499
+ {
500
+ lights: ["light-left", "light-right"],
501
+ intensity: [0, 180],
502
+ },
503
+ ],
504
+ },
505
+ { id: "bulbs", synchronized: true, blackoutChance: 0.1 },
506
+ );
507
+
508
+ return () => stopEffect("bulbs");
509
+ }, [flicker, stopEffect]);
510
+
511
+ <ModelViewer
512
+ modelUrl="/bulbs.glb"
513
+ licenseKey="your-license-key"
514
+ objectBindings={objectBindings}
515
+ onViewerReady={(viewer) => {
516
+ handleViewerReady(viewer);
517
+ addPointLight({
518
+ id: "light-left",
519
+ target: "filament-left",
520
+ color: "#ff9a3c",
521
+ intensity: 0,
522
+ distance: 30,
523
+ });
524
+ }}
525
+ />;
526
+
527
+ <button onClick={() => overrideEffect("bulbs", 0, 800)}>Blackout</button>;
528
+ ```
529
+
530
+ `transition`, `timeline`, `pulse`, and `flicker` share material, transform, and
531
+ light channels. `setMaterial`, `setTransform`, and `getWorldPosition` cover
532
+ immediate runtime work. `addPointLight`, `updatePointLight`, and
533
+ `removePointLight` manage lights attached to model objects. `resetRuntime` stops
534
+ active effects, restores snapshotted values, and disposes runtime lights.
410
535
 
411
536
  If you want to keep hover state in sync with the viewer, the library also exports:
412
537
 
@@ -461,24 +586,29 @@ const {
461
586
  If you want to inspect or trigger binding actions programmatically, the library also exports:
462
587
 
463
588
  ```tsx
464
- const { getActionsForObject, runAction } = useViewerActions(objectBindings, {
589
+ const { getActionsForObject, handleAction, runAction } = useViewerActions({
590
+ bindings,
591
+ scene,
592
+ camera,
593
+ animations,
465
594
  onAction: (event) => {},
466
- onObjectBindingsChange: setObjectBindings,
467
595
  });
468
596
 
469
597
  const actions = getActionsForObject("car-body");
470
- runAction("car-body", "toggle-visibility");
598
+ await runAction("car-body", "power-on");
599
+
600
+ <ModelViewer onAction={handleAction} />;
471
601
  ```
472
602
 
473
603
  `useViewerActions` returns:
474
604
 
475
605
  - `getActionsForObject`: resolves the actions configured for a binding by key, `binding.id`, or `binding.modelObjectId`
476
- - `runAction`: dispatches an `ObjectActionEvent` and returns it, or `null` if the binding or action cannot be found
606
+ - `handleAction`: executes an action event and can be passed directly to `ModelViewer.onAction`
607
+ - `runAction`: finds and executes an action, then resolves to its event, or `null` if the binding or action cannot be found
477
608
 
478
- Current note:
609
+ Actions can declare ordered `effects` for object-binding patches, scene patches, visibility, camera controls, and animation controls. Pass the corresponding hook controllers into `useViewerActions`; omitted optional controllers cause only their effect category to be skipped.
479
610
 
480
- - visibility-style actions (`toggle-visibility`) update binding data through `onObjectBindingsChange`
481
- - color/material actions still rely on `ModelViewer`'s built-in UI, so `runAction` dispatches those events but does not open the built-in color picker or texture upload panel by itself
611
+ `runAction` executes declarative effects only. It does not invoke viewer-owned behavior such as opening a picker or running the built-in visibility toggle. For programmatic visibility, call the bindings controller directly or declare a custom action with a visibility effect.
482
612
 
483
613
  ## `BindingBuilder`
484
614
 
@@ -512,7 +642,7 @@ Current behavior, Object tab:
512
642
  - apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
513
643
  - undo/redo the current bindings (toolbar buttons or `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z`); rapid edits coalesce into a single undo step
514
644
  - import a previously exported `objectBindings.json`, merged onto the current model by `modelObjectId`
515
- - export the current bindings as JSON or TypeScript, bundled into a `.zip` (with a `materials/` folder) when any texture field was filled by uploading a file, otherwise a plain file
645
+ - export the current bindings as JSON or TypeScript, with TypeScript exports wrapped in `defineObjectBindings` to preserve literal IDs, bundled into a `.zip` (with a `materials/` folder) when any texture field was filled by uploading a file, otherwise a plain file
516
646
 
517
647
  Current behavior, Scene tab:
518
648
 
@@ -578,15 +708,25 @@ Use `ModelViewer` when you need binding-driven styling, actions, metadata, custo
578
708
  If you want camera state and imperative camera helpers, the library also exports:
579
709
 
580
710
  ```tsx
711
+ const camera = useViewerCamera({
712
+ initialPosition: [0, 2, 8],
713
+ initialTarget: [0, 1.2, 0],
714
+ });
715
+
581
716
  const {
582
717
  cameraState,
583
718
  resetView,
584
719
  focusObject,
585
720
  fitScene,
586
721
  setCameraTarget,
722
+ lookAt,
723
+ orbitTo,
724
+ dollyTo,
725
+ savePreset,
726
+ goToPreset,
587
727
  handleCameraChange,
588
728
  handleViewerReady,
589
- } = useViewerCamera();
729
+ } = camera;
590
730
 
591
731
  <ModelViewer
592
732
  modelUrl="/model.glb"
@@ -605,9 +745,16 @@ const {
605
745
  - `fitScene`: fits the camera to the whole loaded scene
606
746
  - `setCameraTarget`: sets the camera target to a given `[x, y, z]`
607
747
  - `setCameraState`: restores a full camera state (`position`/`target`/`fov`/`zoom`) previously read from `cameraState`
748
+ - `lookAt`: moves the position and target together
749
+ - `orbitTo`: moves to azimuth and polar angles in radians
750
+ - `dollyTo`: moves to a distance from the current target
751
+ - `savePreset`: captures the current camera under a name
752
+ - `goToPreset`: moves to a named camera preset
608
753
  - `handleCameraChange`: a callback you can pass directly to `onCameraChange`
609
754
  - `handleViewerReady`: a callback you can pass directly to `onViewerReady`
610
755
 
756
+ Pass `initialPosition`, `initialTarget`, and optional named `presets` to `useViewerCamera` to configure the first view without writing an `onViewerReady` wrapper.
757
+
611
758
  If your GLB/GLTF model contains animations, the library also exports:
612
759
 
613
760
  ```tsx
@@ -785,7 +932,9 @@ A map keyed by the model node name. Each key should match a mesh name from the G
785
932
  Example:
786
933
 
787
934
  ```ts
788
- const objectBindings = {
935
+ import { defineObjectBindings } from "@liveroom-tech/react-immersive";
936
+
937
+ const objectBindings = defineObjectBindings({
789
938
  Object_2: {
790
939
  id: "obj-2",
791
940
  modelObjectId: "Object_2",
@@ -811,9 +960,11 @@ const objectBindings = {
811
960
  metrics: {},
812
961
  metadata: {},
813
962
  },
814
- };
963
+ });
815
964
  ```
816
965
 
966
+ `defineObjectBindings` validates the complete record while preserving its exact keys and literal values. Use `ObjectBindingKey<typeof objectBindings>` for the binding-key union and `ObjectBindingActionId<typeof objectBindings, "Object_2">` for that binding's action-id union. It returns the input unchanged at runtime.
967
+
817
968
  ### `selectedObject`
818
969
 
819
970
  Optional currently selected object binding, or `null` when nothing is selected.
@@ -940,13 +1091,16 @@ onViewerReady={(viewer) => {
940
1091
 
941
1092
  Note: requesting a custom `width`/`height` briefly resizes the live canvas to render at that resolution before restoring it, which can cause a momentary flicker on screen, fine for an occasional snapshot, not for rapid/looped calls.
942
1093
 
943
- If you're also using `useViewerCamera`'s `handleViewerReady`, call both from your own `onViewerReady` to get camera helpers and `captureImage` together:
1094
+ If camera, effects, screenshots, or custom setup all need the ready viewer, combine them with `useViewerConnection`:
944
1095
 
945
1096
  ```tsx
946
- onViewerReady={(viewer) => {
947
- handleViewerReady(viewer);
948
- captureImageRef.current = viewer.captureImage;
949
- }}
1097
+ const camera = useViewerCamera();
1098
+ const effects = useViewerEffects();
1099
+ const connection = useViewerConnection(camera, effects);
1100
+
1101
+ <ModelViewer onViewerReady={connection.handleViewerReady} />;
1102
+
1103
+ const dataUrl = await connection.captureImage({ width: 1920, height: 1080 });
950
1104
  ```
951
1105
 
952
1106
  ### `onTextureUpload`
@@ -1813,14 +1967,23 @@ type ObjectBindingStyle = {
1813
1967
  material?: ObjectBindingMaterial;
1814
1968
  };
1815
1969
 
1970
+ type ObjectBindingTransform = {
1971
+ position?: [number, number, number];
1972
+ rotation?: [number, number, number]; // radians
1973
+ scale?: [number, number, number];
1974
+ };
1975
+
1816
1976
  type ObjectBinding = {
1817
1977
  id: string;
1818
1978
  type: ObjectBindingType; // "body" | "light" | "wheel" | "glass" | "interior" | "floor" | "wall" | "door" | "furniture" | "decor" | "electronics" | "other" | ...
1819
1979
  modelObjectId: string;
1820
1980
  label?: string;
1981
+ group?: string;
1982
+ tags?: string[];
1821
1983
  visible?: boolean;
1822
1984
  selectable?: boolean;
1823
1985
  hoverable?: boolean;
1986
+ transform?: ObjectBindingTransform;
1824
1987
  style?: ObjectBindingStyle;
1825
1988
  actions?: ObjectBindingAction[];
1826
1989
  metrics?: Record<string, number>;
@@ -1832,7 +1995,9 @@ type ObjectBinding = {
1832
1995
  Notes:
1833
1996
 
1834
1997
  - the `objectBindings` record is keyed by node name, not by `id`
1998
+ - `group` and `tags` can be assigned in `BindingBuilder` and used as targets by binding helpers and runtime material or transform effects
1835
1999
  - `visible` is live render state, not just an initial default
2000
+ - `transform` is persistent local-space render state; missing fields retain their model-authored values and removing it restores the authored transform
1836
2001
  - `style.material.baseColor` is read by the renderer and is the committed source of truth for color picks
1837
2002
  - `style.material.texture.path` is read by the renderer and is the committed source of truth for texture uploads (stored as durable URLs via `onTextureUpload` when provided, or session-scoped blob URLs otherwise)
1838
2003
  - if no `texture.path` is provided and no `style.material.baseColor` is set, the viewer falls back to the node's original GLB/GLTF material