@liveroom-tech/react-immersive 1.4.0 → 2.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
@@ -9,11 +9,13 @@ The library currently exports:
9
9
  ```ts
10
10
  import {
11
11
  BindingBuilder,
12
+ material,
12
13
  ModelViewer,
14
+ patchBindings,
13
15
  SimpleModelViewer,
14
16
  useObjectBinding,
15
17
  useObjectBindingIds,
16
- useObjectVisibility,
18
+ useObjectBindings,
17
19
  useViewerActions,
18
20
  useViewerAnimations,
19
21
  useViewerCamera,
@@ -25,13 +27,13 @@ import {
25
27
  } from "@liveroom-tech/react-immersive";
26
28
  ```
27
29
 
28
- It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectActionEvent`, `SceneConfig`, `AnimationControls`, `CinematicConfig` / `CinematicWaypoint`, and related shapes used throughout this document.
30
+ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindingsController`, `ObjectActionEvent`, `SceneConfig`, `AnimationControls`, `CinematicConfig` / `CinematicWaypoint`, and related shapes used throughout this document.
29
31
 
30
32
  `ModelViewer` provides:
31
33
 
32
34
  - GLB/GLTF model rendering through `@react-three/fiber` and `@react-three/drei`
33
35
  - orbit/pan/zoom camera controls via `CameraControls`
34
- - auto-fits the model on load, and re-fits when the canvas is resized (window resize, device rotation, or a panel opening/closing) — toggleable through `refitOnResize` to instead preserve the user's orbit/zoom
36
+ - auto-fits the model on load, and re-fits when the canvas is resized (window resize, device rotation, or a panel opening/closing), toggleable through `refitOnResize` to instead preserve the user's orbit/zoom
35
37
  - optional custom scene lighting through a `lights` prop
36
38
  - optional custom camera configuration through a `camera` prop
37
39
  - optional custom background color through a `backgroundColor` prop
@@ -44,7 +46,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectActio
44
46
  - an optional UV Checker toolbar button through `showUvCheckerButton`, for inspecting UV scale, stretching, seams, orientation, and missing UV0 coordinates directly in `ModelViewer`
45
47
  - click-to-measure distance tool and a bounding-box dimensions overlay through `showMeasureTools` and `measurementUnit`
46
48
  - an exploded-view slider that slides each bound part outward from the model center to reveal interior/assembly structure through `showExplodeControls`
47
- - a cinematic auto-camera that glides the camera along authored waypoints (or a zero-config showcase orbit), like a film, through `cinematic` — with a play/pause control that yields the moment you touch the camera
49
+ - a cinematic auto-camera that glides the camera along authored waypoints (or a zero-config showcase orbit), like a film, through `cinematic`, with a play/pause control that yields the moment you touch the camera
48
50
  - a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via `showAnnotationNavigation`
49
51
  - optional annotation detail popups on marker hover via `showAnnotationOnHover`
50
52
  - PBR, Matcap, and UV Checker renderer modes, with the checker exposing UV stretching, seams, rotation, and missing UV0 data directly on the model
@@ -77,10 +79,10 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectActio
77
79
  - a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
78
80
  - undo/redo for both the Object tab's bindings and the Scene tab's config, independently, with `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z` shortcuts
79
81
  - a Scene tab for the same `sceneConfig` shape `ModelViewer` accepts (lighting, environment, background, post-processing, animations, annotations, and cinematic camera paths)
80
- - a Cinematic sub-tab for authoring a camera path visually — frame the model, capture the current view as a waypoint, reorder/preview/delete waypoints, and toggle loop/auto-play; stored on `sceneConfig.cinematic` and replayed by `ModelViewer`'s `cinematic` prop
82
+ - a Cinematic sub-tab for authoring a camera path visually, frame the model, capture the current view as a waypoint, reorder/preview/delete waypoints, and toggle loop/auto-play; stored on `sceneConfig.cinematic` and replayed by `ModelViewer`'s `cinematic` prop
81
83
  - import a previously exported `objectBindings.json` or `sceneConfig.json` and merge it back onto the current model/config
82
84
  - live preview using `ModelViewer`
83
- - export as JSON or TypeScript — bundled into a `.zip` with a `materials/` folder automatically when any texture field was filled by file upload, otherwise a plain file
85
+ - export as JSON or TypeScript, bundled into a `.zip` with a `materials/` folder automatically when any texture field was filled by file upload, otherwise a plain file
84
86
  - license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, post-processing)
85
87
 
86
88
  `SimpleModelViewer` provides:
@@ -162,36 +164,36 @@ Peer dependencies:
162
164
  - `react >= 17`
163
165
  - `react-dom >= 17`
164
166
 
165
- `ModelViewer` and `BindingBuilder` require a `licenseKey` at runtime (see [`licenseKey`](#licensekey)). Get one from the Developer Portal — see [Resources](#resources) below.
167
+ `ModelViewer` and `BindingBuilder` require a `licenseKey` at runtime (see [`licenseKey`](#licensekey)). Get one from the Developer Portal, see [Resources](#resources) below.
166
168
 
167
169
  ## Resources
168
170
 
169
- - [Developer Portal](https://react-immersive.liveroom.dev) — sign in, pick a plan, and generate a `licenseKey`
170
- - [Docs](https://react-immersive.liveroom.dev/docs) — full reference for every component, hook, and prop
171
- - [Examples & community repo](https://github.com/liveroom-technologies/react-immersive) — public docs source, starter examples, and community resources
172
- - [Report a bug / request a feature](https://github.com/liveroom-technologies/react-immersive-docs/issues) — for public bug reports, docs issues, and feature requests
173
- - [Discussions & Q&A](https://github.com/liveroom-technologies/react-immersive-docs/discussions) — ask questions, share ideas, and compare approaches
174
- - [Private support / security](mailto:developers@liveroom.xyz?subject=React%20Immersive%20Support) — for license, account, confidential customer, or security-sensitive issues
175
- - [Live editor](https://react-immersive.liveroom.dev/editor) — try `BindingBuilder` in the browser without installing anything
171
+ - [Developer Portal](https://react-immersive.liveroom.dev), sign in, pick a plan, and generate a `licenseKey`
172
+ - [Docs](https://react-immersive.liveroom.dev/docs), full reference for every component, hook, and prop
173
+ - [Examples & community repo](https://github.com/liveroom-technologies/react-immersive), public docs source, starter examples, and community resources
174
+ - [Report a bug / request a feature](https://github.com/liveroom-technologies/react-immersive-docs/issues), for public bug reports, docs issues, and feature requests
175
+ - [Discussions & Q&A](https://github.com/liveroom-technologies/react-immersive-docs/discussions), ask questions, share ideas, and compare approaches
176
+ - [Private support / security](mailto:developers@liveroom.xyz?subject=React%20Immersive%20Support), for license, account, confidential customer, or security-sensitive issues
177
+ - [Live editor](https://react-immersive.liveroom.dev/editor), try `BindingBuilder` in the browser without installing anything
176
178
  - [npm package](https://www.npmjs.com/package/@liveroom-tech/react-immersive)
177
179
 
178
180
  ## Quick Start
179
181
 
180
182
  ```tsx
181
- import { useState } from "react";
182
183
  import {
183
184
  ModelViewer,
184
185
  useObjectBinding,
185
- useObjectVisibility,
186
+ useObjectBindings,
186
187
  useViewerActions,
187
188
  useViewerAnimations,
188
189
  useViewerCamera,
189
190
  useViewerHover,
190
191
  useViewerModel,
191
192
  useViewerSelection,
193
+ type ObjectBinding,
192
194
  } from "@liveroom-tech/react-immersive";
193
195
 
194
- const initialBindings = {
196
+ const initialBindings: Record<string, ObjectBinding> = {
195
197
  CarBody: {
196
198
  id: "car-body",
197
199
  modelObjectId: "CarBody",
@@ -220,7 +222,12 @@ const initialBindings = {
220
222
  };
221
223
 
222
224
  export default function Example() {
223
- const [objectBindings, setObjectBindings] = useState(initialBindings);
225
+ const {
226
+ objectBindings,
227
+ setObjectBindings,
228
+ hiddenObjects,
229
+ toggleObjectVisibility,
230
+ } = useObjectBindings(initialBindings);
224
231
  const { selectedObjectBinding, handleObjectSelect } = useViewerSelection();
225
232
  const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
226
233
  const {
@@ -241,10 +248,6 @@ export default function Example() {
241
248
  handleModelLoaded,
242
249
  handleLoadError,
243
250
  } = useViewerModel();
244
- const { hiddenObjects, toggleObjectVisibility } = useObjectVisibility(
245
- objectBindings,
246
- setObjectBindings,
247
- );
248
251
  const { getActionsForObject, runAction } = useViewerActions(objectBindings, {
249
252
  onAction: (event) => {},
250
253
  onObjectBindingsChange: setObjectBindings,
@@ -325,12 +328,17 @@ const ids = useObjectBindingIds(objectBindings);
325
328
  // → ["CarBody", "WheelFL", "WheelFR", ...]
326
329
  ```
327
330
 
328
- If you want to control object visibility outside the viewer, the library also exports:
331
+ If you want one state owner for binding patches, materials, and visibility, use `useObjectBindings`:
329
332
 
330
333
  ```tsx
331
- const [objectBindings, setObjectBindings] = useState(initialBindings);
332
-
333
334
  const {
335
+ objectBindings,
336
+ setObjectBindings,
337
+ patch,
338
+ setMaterial,
339
+ setBaseColor,
340
+ resetMaterial,
341
+ getMaterial,
334
342
  hiddenObjects,
335
343
  hiddenObjectIds,
336
344
  hideObject,
@@ -338,7 +346,7 @@ const {
338
346
  toggleObjectVisibility,
339
347
  isObjectHidden,
340
348
  clearHiddenObjects,
341
- } = useObjectVisibility(objectBindings, setObjectBindings);
349
+ } = useObjectBindings(initialBindings);
342
350
 
343
351
  <ModelViewer
344
352
  modelUrl="/model.glb"
@@ -348,16 +356,19 @@ const {
348
356
  />;
349
357
  ```
350
358
 
351
- `useObjectVisibility` is a stateless utility hook. It does not own its own copy of `objectBindings` — the consumer owns the state and passes both the current value and its setter. The hook's methods (`hideObject`, `showObject`, etc.) call the setter directly, so there is no risk of the hook and the viewer drifting out of sync.
359
+ `useObjectBindings` owns the state used by the viewer. Every setter uses a functional update, so consecutive material, visibility, and general patch calls in the same tick compose instead of overwriting each other.
352
360
 
353
- It accepts either:
361
+ Object identifiers accepted by its methods can be:
354
362
 
355
363
  - the object binding map key
356
364
  - `binding.id`
357
365
  - `binding.modelObjectId`
358
366
 
359
- and returns:
367
+ The hook returns:
360
368
 
369
+ - `objectBindings` and `setObjectBindings`
370
+ - `patch` for any binding field or atomic edit batch
371
+ - `setMaterial`, `setBaseColor`, `resetMaterial`, and `getMaterial`
361
372
  - `hiddenObjects`: a derived map of hidden binding keys
362
373
  - `hiddenObjectIds`: the currently hidden binding IDs
363
374
  - `hideObject`, `showObject`, `toggleObjectVisibility`
@@ -460,7 +471,7 @@ type BindingBuilderProps = {
460
471
 
461
472
  `licenseKey` is validated the same way as `ModelViewer`'s (see [`licenseKey`](#licensekey) below). While the key is being checked, `BindingBuilder` renders a "Verifying license…" placeholder; if it's invalid, it renders an error message instead of the editor.
462
473
 
463
- Current behavior — Object tab:
474
+ Current behavior, Object tab:
464
475
 
465
476
  - load the bundled demo model or upload a `.glb`, standalone/data-URI `.gltf`, or `.zip` containing a `.gltf` plus its referenced `.bin` and texture files
466
477
  - traverse renderable mesh nodes and generate starter bindings automatically
@@ -468,9 +479,9 @@ Current behavior — Object tab:
468
479
  - apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
469
480
  - undo/redo the current bindings (toolbar buttons or `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z`); rapid edits coalesce into a single undo step
470
481
  - import a previously exported `objectBindings.json`, merged onto the current model by `modelObjectId`
471
- - 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
482
+ - 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
472
483
 
473
- Current behavior — Scene tab:
484
+ Current behavior, Scene tab:
474
485
 
475
486
  - configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
476
487
  - undo/redo the scene config independently of the Object tab's bindings history
@@ -511,7 +522,7 @@ export default function App() {
511
522
  }
512
523
  ```
513
524
 
514
- `SimpleModelViewer` does not take a `licenseKey` — licensing is enforced by `ModelViewer` and `BindingBuilder` only.
525
+ `SimpleModelViewer` does not take a `licenseKey`, licensing is enforced by `ModelViewer` and `BindingBuilder` only.
515
526
 
516
527
  Sizing note:
517
528
 
@@ -810,8 +821,8 @@ Optional callback fired when the viewer's built-in UI updates binding data.
810
821
  This fires for:
811
822
 
812
823
  - visibility toggles (`binding.visible`)
813
- - color picks (`binding.style.material.baseColor`) — committed when the color picker closes
814
- - texture uploads (`binding.style.material.texture.path`) — committed immediately (via `onTextureUpload` when provided, otherwise as a blob URL)
824
+ - color picks (`binding.style.material.baseColor`), committed when the color picker closes
825
+ - texture uploads (`binding.style.material.texture.path`), committed immediately (via `onTextureUpload` when provided, otherwise as a blob URL)
815
826
  - texture removal (`binding.style.material.texture` cleared)
816
827
 
817
828
  If you want the viewer and your app state to stay in sync, pass `objectBindings` from state and wire this callback back into that state setter.
@@ -894,7 +905,7 @@ onViewerReady={(viewer) => {
894
905
  }}
895
906
  ```
896
907
 
897
- 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.
908
+ 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.
898
909
 
899
910
  If you're also using `useViewerCamera`'s `handleViewerReady`, call both from your own `onViewerReady` to get camera helpers and `captureImage` together:
900
911
 
@@ -1011,7 +1022,7 @@ function CustomLights() {
1011
1022
 
1012
1023
  Optional custom camera configuration passed through to the underlying React Three Fiber `Canvas`.
1013
1024
 
1014
- `ModelViewer` only sets a default `fov: 50` — it does not set a default position, so an unset position falls back to React Three Fiber's own `Canvas` default (`[0, 0, 5]`). Pass an explicit `position` for predictable framing.
1025
+ `ModelViewer` only sets a default `fov: 50`, it does not set a default position, so an unset position falls back to React Three Fiber's own `Canvas` default (`[0, 0, 5]`). Pass an explicit `position` for predictable framing.
1015
1026
 
1016
1027
  Example:
1017
1028
 
@@ -1187,7 +1198,7 @@ true;
1187
1198
 
1188
1199
  ### `showMouseController`
1189
1200
 
1190
- Optional boolean that renders an on-screen joystick controller for moving the camera with a mouse or touch — useful on touch devices or kiosk layouts where drag-to-orbit is awkward.
1201
+ Optional boolean that renders an on-screen joystick controller for moving the camera with a mouse or touch, useful on touch devices or kiosk layouts where drag-to-orbit is awkward.
1191
1202
 
1192
1203
  Default:
1193
1204
 
@@ -1299,7 +1310,7 @@ true;
1299
1310
 
1300
1311
  ### `onAutoFit`
1301
1312
 
1302
- Optional async callback fired once the model has loaded and the scene is ready. When provided, the viewer calls it so consumers can run their own fit-to-scene behavior (e.g. an animated fit); if omitted, the viewer falls back to its internal `fitScene` — unless an explicit [`camera`](#camera) prop was given, in which case that position is treated as the chosen initial view and `fitScene` is skipped.
1313
+ Optional async callback fired once the model has loaded and the scene is ready. When provided, the viewer calls it so consumers can run their own fit-to-scene behavior (e.g. an animated fit); if omitted, the viewer falls back to its internal `fitScene`, unless an explicit [`camera`](#camera) prop was given, in which case that position is treated as the chosen initial view and `fitScene` is skipped.
1303
1314
 
1304
1315
  Current callback shape:
1305
1316
 
@@ -1309,7 +1320,7 @@ Current callback shape:
1309
1320
 
1310
1321
  ### `refitOnResize`
1311
1322
 
1312
- Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized — a browser window resize, a device rotation, or a side panel opening/closing (which changes how much width the canvas has). Set to `false` to preserve the user's current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn't re-centered. This only affects re-fits _after_ the initial mount-time auto-fit.
1323
+ Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized, a browser window resize, a device rotation, or a side panel opening/closing (which changes how much width the canvas has). Set to `false` to preserve the user's current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn't re-centered. This only affects re-fits _after_ the initial mount-time auto-fit.
1313
1324
 
1314
1325
  Default:
1315
1326
 
@@ -1372,9 +1383,9 @@ Optional control over how aggressively the viewer trades visual fidelity for a s
1372
1383
  performanceProfile?: "auto" | "high" | "low";
1373
1384
  ```
1374
1385
 
1375
- - `"auto"` (default) — applies a reduced profile on handheld/mobile browsers and other low-power touch devices: the postprocessing pipeline is skipped, soft shadows are disabled, and the device pixel ratio is capped. Desktop-class touch devices may still keep the higher-quality path when they advertise plenty of memory. This keeps mobile GPUs within their memory budget so the context isn't lost — which also keeps WebXR (`enableXR`) usable, since a lost context can't start a session.
1376
- - `"high"` — always render at full quality (postprocessing, soft shadows, full `maxDpr`), even on mobile. Use when you know the target devices can handle it.
1377
- - `"low"` — always apply the reduced profile, on any device.
1386
+ - `"auto"` (default), applies a reduced profile on handheld/mobile browsers and other low-power touch devices: the postprocessing pipeline is skipped, soft shadows are disabled, and the device pixel ratio is capped. Desktop-class touch devices may still keep the higher-quality path when they advertise plenty of memory. This keeps mobile GPUs within their memory budget so the context isn't lost, which also keeps WebXR (`enableXR`) usable, since a lost context can't start a session.
1387
+ - `"high"`, always render at full quality (postprocessing, soft shadows, full `maxDpr`), even on mobile. Use when you know the target devices can handle it.
1388
+ - `"low"`, always apply the reduced profile, on any device.
1378
1389
 
1379
1390
  Default:
1380
1391
 
@@ -1420,7 +1431,7 @@ false;
1420
1431
 
1421
1432
  ### `measurementUnit`
1422
1433
 
1423
- Optional unit suffix appended to measurement readouts. glTF models are authored in meters by spec, so values are not converted — this only changes the displayed label.
1434
+ Optional unit suffix appended to measurement readouts. glTF models are authored in meters by spec, so values are not converted, this only changes the displayed label.
1424
1435
 
1425
1436
  Default:
1426
1437
 
@@ -1430,7 +1441,7 @@ Default:
1430
1441
 
1431
1442
  ### `showExplodeControls`
1432
1443
 
1433
- Optional boolean that shows an exploded-view slider over the canvas. It slides each bound part outward from the model center to reveal interior/assembly structure, then back to reassemble. Best suited to static product/CAD models — the offset writes node positions each frame, so it conflicts with a playing skeletal animation.
1444
+ Optional boolean that shows an exploded-view slider over the canvas. It slides each bound part outward from the model center to reveal interior/assembly structure, then back to reassemble. Best suited to static product/CAD models, the offset writes node positions each frame, so it conflicts with a playing skeletal animation.
1434
1445
 
1435
1446
  Default:
1436
1447
 
@@ -1440,7 +1451,7 @@ false;
1440
1451
 
1441
1452
  ### `cinematic`
1442
1453
 
1443
- Optional cinematic auto-camera. The camera glides along a path on its own, like a film — ideal for showing off a gallery, apartment, or product with no user interaction. Renders a play/pause button over the canvas; grabbing the camera (or selecting a part) pauses it.
1454
+ Optional cinematic auto-camera. The camera glides along a path on its own, like a film, ideal for showing off a gallery, apartment, or product with no user interaction. Renders a play/pause button over the canvas; grabbing the camera (or selecting a part) pauses it.
1444
1455
 
1445
1456
  ```ts
1446
1457
  cinematic?: boolean | CinematicConfig;
@@ -1462,7 +1473,7 @@ type CinematicWaypoint = {
1462
1473
  - Two or more `waypoints` author a **walkthrough**: the camera follows a smooth Catmull-Rom spline through them at a constant on-screen speed, looping seamlessly.
1463
1474
  - Waypoints are easiest to capture visually in `BindingBuilder`'s **Cinematic** tab, which stores them on `sceneConfig.cinematic`.
1464
1475
 
1465
- A path can also come from `sceneConfig.cinematic`. The `cinematic` prop overrides it: a boolean toggles the feature (`true` keeps any authored waypoints, `false` forces it off), and an object overrides field-by-field — so you can keep the authored waypoints while overriding, say, `autoPlay` or `loop`.
1476
+ A path can also come from `sceneConfig.cinematic`. The `cinematic` prop overrides it: a boolean toggles the feature (`true` keeps any authored waypoints, `false` forces it off), and an object overrides field-by-field, so you can keep the authored waypoints while overriding, say, `autoPlay` or `loop`.
1466
1477
 
1467
1478
  ```tsx
1468
1479
  // Zero-config showcase orbit
@@ -1498,7 +1509,7 @@ arbitrary uploads and room scenes are framed safely. Set
1498
1509
  [`arScaleMode`](#arscalemode) to `"real-world"` for product AR, which preserves
1499
1510
  the GLB's authored metre scale.
1500
1511
 
1501
- **AR placement gestures** — inside an AR session the viewer now uses an explicit placement flow:
1512
+ **AR placement gestures**, inside an AR session the viewer now uses an explicit placement flow:
1502
1513
 
1503
1514
  - before placement, a bottom label says **"Move your device over a flat surface"** until a hit-testable surface is found
1504
1515
  - once a surface is found, a reticle appears on it and the label changes to **"Tap to place"**
@@ -1508,7 +1519,7 @@ the GLB's authored metre scale.
1508
1519
 
1509
1520
  VR sessions keep the initial framed placement (there are no real surfaces to hit-test against).
1510
1521
 
1511
- > Postprocessing is automatically disabled during an XR session (the effect composer isn't WebXR-aware). Note also that on low-power devices the default [`performanceProfile`](#performanceprofile) of `"auto"` already disables postprocessing and soft shadows — important for keeping the WebGL context alive so AR can start at all.
1522
+ > Postprocessing is automatically disabled during an XR session (the effect composer isn't WebXR-aware). Note also that on low-power devices the default [`performanceProfile`](#performanceprofile) of `"auto"` already disables postprocessing and soft shadows, important for keeping the WebGL context alive so AR can start at all.
1512
1523
 
1513
1524
  Default:
1514
1525
 
@@ -1619,7 +1630,7 @@ type SimpleModelViewerProps = {
1619
1630
  };
1620
1631
  ```
1621
1632
 
1622
- `SimpleModelViewer` does not take a `licenseKey` — licensing is enforced by `ModelViewer` and `BindingBuilder` only.
1633
+ `SimpleModelViewer` does not take a `licenseKey`, licensing is enforced by `ModelViewer` and `BindingBuilder` only.
1623
1634
 
1624
1635
  ### `modelUrl`
1625
1636
 
@@ -1657,7 +1668,7 @@ true;
1657
1668
 
1658
1669
  ### Scene settings defaults
1659
1670
 
1660
- The following optional props seed the initial values of the scene settings (environment) panel. Each maps to one control, and the panel stays interactive so the user can still adjust them at runtime. They are initial values (not controlled props): changing one after mount does not override a value the user has since changed in the panel — the exception is `backgroundColor`, which stays in sync live.
1671
+ The following optional props seed the initial values of the scene settings (environment) panel. Each maps to one control, and the panel stays interactive so the user can still adjust them at runtime. They are initial values (not controlled props): changing one after mount does not override a value the user has since changed in the panel, the exception is `backgroundColor`, which stays in sync live.
1661
1672
 
1662
1673
  | Prop | Type | Panel control | Default |
1663
1674
  | ---------------------- | ------------- | ----------------------- | ----------- |
@@ -1716,7 +1727,7 @@ true;
1716
1727
 
1717
1728
  ### `refitOnResize`
1718
1729
 
1719
- Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized — a browser window resize, a device rotation, or a side panel (scene settings / scene objects) opening or closing. Set to `false` to preserve the user's current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn't re-centered.
1730
+ Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized, a browser window resize, a device rotation, or a side panel (scene settings / scene objects) opening or closing. Set to `false` to preserve the user's current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn't re-centered.
1720
1731
 
1721
1732
  Default:
1722
1733
 
@@ -1821,7 +1832,7 @@ At a high level, the current viewer does the following:
1821
1832
  3. looks up meshes by the keys in `objectBindings` (resolving `modelObjectId` to find the actual model node)
1822
1833
  4. clones each referenced material so edits stay isolated per object (keyed on a structural signature of the binding-to-mesh mapping, not the `objectBindings` reference, so materials are not recreated on every binding update)
1823
1834
  5. applies runtime color, texture, and style overrides from `objectBindings` in a `useEffect` (not during render)
1824
- 6. manages a reference-counted texture cache so the same texture URL is decoded once and disposed when no longer referenced; each texture is decoded in the correct color space — color maps (base color/albedo, emissive, sheen color, specular color) as sRGB and data maps (normal, roughness, metalness, AO, displacement, etc.) as linear — and the cache is keyed by URL **and** color space, so the same image can be reused safely as both a color and a data map
1835
+ 6. manages a reference-counted texture cache so the same texture URL is decoded once and disposed when no longer referenced; each texture is decoded in the correct color space, color maps (base color/albedo, emissive, sheen color, specular color) as sRGB and data maps (normal, roughness, metalness, AO, displacement, etc.) as linear, and the cache is keyed by URL **and** color space, so the same image can be reused safely as both a color and a data map
1825
1836
  7. adds hover, selected, and panel-hover emissive highlighting
1826
1837
  8. creates an `AnimationMixer` for the loaded scene, ticked every frame via `useFrame`, and exposes playback controls through `onAnimationsReady`
1827
1838
  9. renders a side panel for the selected object and its configured actions
@@ -1833,7 +1844,7 @@ Only objects listed in `objectBindings` are rendered interactively by the curren
1833
1844
  When `shadows` is enabled (the default), the canvas renders soft shadow maps and the viewer applies:
1834
1845
 
1835
1846
  - a default light rig (`Lights`) with ambient + hemisphere fill and a directional key light that casts shadows, sized to the model
1836
- - per-mesh `castShadow`/`receiveShadow`, with one exception: the large enclosing "shell" mesh (walls/ceiling of an interior model) is detected by its bounding-box size and excluded from **casting** so a top-down light still reaches the interior — it continues to **receive** shadows, so furniture casts visible contact shadows onto the floor and walls
1847
+ - per-mesh `castShadow`/`receiveShadow`, with one exception: the large enclosing "shell" mesh (walls/ceiling of an interior model) is detected by its bounding-box size and excluded from **casting** so a top-down light still reaches the interior, it continues to **receive** shadows, so furniture casts visible contact shadows onto the floor and walls
1837
1848
  - a post-processing stack (`EffectComposer`) with SSAO ambient occlusion (normal pass enabled), bloom, vignette, and ACES filmic tone mapping
1838
1849
 
1839
1850
  Pass `shadows={false}` to disable shadow rendering entirely, or pass a custom `lights` rig to replace the default lighting.
@@ -1874,16 +1885,16 @@ instead of freezing:
1874
1885
  still works
1875
1886
  - a dismissible banner tells the user the model is very large, may render
1876
1887
  slowly on their device, and that selection is available through the panel
1877
- - the loading overlay calls out the extra work — uploading that much geometry
1888
+ - the loading overlay calls out the extra work, uploading that much geometry
1878
1889
  to the GPU is a single synchronous operation that can block the page for
1879
1890
  tens of seconds in any WebGL viewer
1880
1891
 
1881
1892
  Orbit, zoom, animations, annotations, and the panels keep working. There is no
1882
- viewer-side way to make models of this size feel fast — the per-frame cost is
1893
+ viewer-side way to make models of this size feel fast, the per-frame cost is
1883
1894
  inherent, and on mobile devices the decoded geometry alone can exceed the
1884
1895
  tab's memory budget. Decimate such assets at ingestion (to roughly 2–3M
1885
1896
  triangles) instead of shipping the raw export; note that very large
1886
- Draco-compressed files need Blender or native tooling to decimate — the WASM
1897
+ Draco-compressed files need Blender or native tooling to decimate, the WASM
1887
1898
  builds of `gltf-transform` and `gltfpack` cannot decode them within their 4GB
1888
1899
  address space.
1889
1900
 
@@ -1891,9 +1902,9 @@ Two related picking notes that apply to models of every size:
1891
1902
 
1892
1903
  - clicks resolve the front-most surface under the pointer; a mesh with
1893
1904
  `selectable: false` does not pass the click through to meshes behind it
1894
- - `SimpleModelViewer` renders on demand — an idle viewer draws no frames, so
1905
+ - `SimpleModelViewer` renders on demand, an idle viewer draws no frames, so
1895
1906
  it costs no GPU time or battery until something changes
1896
1907
 
1897
1908
  ## License
1898
1909
 
1899
- Proprietary — see [LICENSE](./LICENSE). Installing the package does not itself grant a right to use it in production; a valid `licenseKey` from the [Developer Portal](https://react-immersive.liveroom.dev) is required, and usage is bound to the terms of your license tier.
1910
+ Proprietary, see [LICENSE](./LICENSE). Installing the package does not itself grant a right to use it in production; a valid `licenseKey` from the [Developer Portal](https://react-immersive.liveroom.dev) is required, and usage is bound to the terms of your license tier.
package/dist/index.d.mts CHANGED
@@ -664,17 +664,55 @@ declare function useObjectBinding(objectBindings: Record<string, ObjectBinding>,
664
664
 
665
665
  declare function useObjectBindingIds(objectBindings: Record<string, ObjectBinding>): string[];
666
666
 
667
+ type DeepPartial<T> = T extends (infer _U)[] ? T : T extends object ? {
668
+ [K in keyof T]?: DeepPartial<T[K]>;
669
+ } : T;
670
+ type ObjectBindingPatch = DeepPartial<ObjectBinding>;
671
+ type BindingEdit = {
672
+ ids: string | string[];
673
+ patch: ObjectBindingPatch;
674
+ };
675
+ /**
676
+ * Applies field-level patches to object bindings and returns the updated
677
+ * record. Pure — nothing is mutated, and the original reference comes back
678
+ * untouched when no id matched, so React can skip the re-render.
679
+ *
680
+ * Ids resolve the same way everywhere else in the library: binding key first,
681
+ * then `binding.id`, then `binding.modelObjectId`.
682
+ */
683
+ declare function patchBindings(bindings: Record<string, ObjectBinding>, objectId: string | string[], patch: ObjectBindingPatch): Record<string, ObjectBinding>;
684
+ declare function patchBindings(bindings: Record<string, ObjectBinding>, edits: BindingEdit[]): Record<string, ObjectBinding>;
685
+ /** Shorthand for the common `{ style: { material: … } }` nesting. */
686
+ declare function material(value: ObjectBindingMaterial): ObjectBindingPatch;
687
+
667
688
  type HiddenObjectsState = Record<string, boolean>;
668
- type ObjectVisibilityProps = {
689
+ type ObjectBindingsController = {
690
+ objectBindings: Record<string, ObjectBinding>;
691
+ setObjectBindings: React.Dispatch<React.SetStateAction<Record<string, ObjectBinding>>>;
692
+ patch: {
693
+ (objectId: string | string[], patch: ObjectBindingPatch): void;
694
+ (edits: BindingEdit[]): void;
695
+ };
696
+ setMaterial: (objectId: string | string[], material: ObjectBindingMaterial) => void;
697
+ setBaseColor: (objectId: string | string[], color: string) => void;
698
+ resetMaterial: (objectId: string | string[]) => void;
699
+ getMaterial: (objectId: string) => ObjectBindingMaterial | null;
669
700
  hiddenObjects: HiddenObjectsState;
670
701
  hiddenObjectIds: string[];
671
- hideObject: (objectId: string) => void;
672
- showObject: (objectId: string) => void;
673
- toggleObjectVisibility: (objectId: string) => void;
702
+ hideObject: (objectId: string | string[]) => void;
703
+ showObject: (objectId: string | string[]) => void;
704
+ toggleObjectVisibility: (objectId: string | string[]) => void;
674
705
  isObjectHidden: (objectId: string) => boolean;
675
706
  clearHiddenObjects: () => void;
676
707
  };
677
- declare function useObjectVisibility(objectBindings: Record<string, ObjectBinding>, onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void): ObjectVisibilityProps;
708
+ /**
709
+ * Owns object-binding state and exposes field-level, material, and visibility
710
+ * operations over the same functional state updater.
711
+ *
712
+ * Consecutive calls in the same tick compose instead of overwriting each other,
713
+ * including calls across material and visibility concerns.
714
+ */
715
+ declare function useObjectBindings(initialBindings: Record<string, ObjectBinding> | (() => Record<string, ObjectBinding>)): ObjectBindingsController;
678
716
 
679
717
  type ViewerHoverProps = {
680
718
  hoveredObjectBinding: ObjectBinding | null;
@@ -728,4 +766,4 @@ type ShareableViewerStateProps = {
728
766
  };
729
767
  declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
730
768
 
731
- export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type CaptureImageOptions, type CinematicConfig, type CinematicWaypoint, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingTextureSlot, type ObjectVisibilityProps, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneCinematicConfig, type SceneCinematicWaypoint, type SceneConfig, type SceneModelRenderer, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, type XRScaleMode, useObjectBinding, useObjectBindingIds, useObjectVisibility, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
769
+ export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type BindingEdit, type CaptureImageOptions, type CinematicConfig, type CinematicWaypoint, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingPatch, type ObjectBindingTextureSlot, type ObjectBindingsController, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneCinematicConfig, type SceneCinematicWaypoint, type SceneConfig, type SceneModelRenderer, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, type XRScaleMode, material, patchBindings, useObjectBinding, useObjectBindingIds, useObjectBindings, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
package/dist/index.d.ts CHANGED
@@ -664,17 +664,55 @@ declare function useObjectBinding(objectBindings: Record<string, ObjectBinding>,
664
664
 
665
665
  declare function useObjectBindingIds(objectBindings: Record<string, ObjectBinding>): string[];
666
666
 
667
+ type DeepPartial<T> = T extends (infer _U)[] ? T : T extends object ? {
668
+ [K in keyof T]?: DeepPartial<T[K]>;
669
+ } : T;
670
+ type ObjectBindingPatch = DeepPartial<ObjectBinding>;
671
+ type BindingEdit = {
672
+ ids: string | string[];
673
+ patch: ObjectBindingPatch;
674
+ };
675
+ /**
676
+ * Applies field-level patches to object bindings and returns the updated
677
+ * record. Pure — nothing is mutated, and the original reference comes back
678
+ * untouched when no id matched, so React can skip the re-render.
679
+ *
680
+ * Ids resolve the same way everywhere else in the library: binding key first,
681
+ * then `binding.id`, then `binding.modelObjectId`.
682
+ */
683
+ declare function patchBindings(bindings: Record<string, ObjectBinding>, objectId: string | string[], patch: ObjectBindingPatch): Record<string, ObjectBinding>;
684
+ declare function patchBindings(bindings: Record<string, ObjectBinding>, edits: BindingEdit[]): Record<string, ObjectBinding>;
685
+ /** Shorthand for the common `{ style: { material: … } }` nesting. */
686
+ declare function material(value: ObjectBindingMaterial): ObjectBindingPatch;
687
+
667
688
  type HiddenObjectsState = Record<string, boolean>;
668
- type ObjectVisibilityProps = {
689
+ type ObjectBindingsController = {
690
+ objectBindings: Record<string, ObjectBinding>;
691
+ setObjectBindings: React.Dispatch<React.SetStateAction<Record<string, ObjectBinding>>>;
692
+ patch: {
693
+ (objectId: string | string[], patch: ObjectBindingPatch): void;
694
+ (edits: BindingEdit[]): void;
695
+ };
696
+ setMaterial: (objectId: string | string[], material: ObjectBindingMaterial) => void;
697
+ setBaseColor: (objectId: string | string[], color: string) => void;
698
+ resetMaterial: (objectId: string | string[]) => void;
699
+ getMaterial: (objectId: string) => ObjectBindingMaterial | null;
669
700
  hiddenObjects: HiddenObjectsState;
670
701
  hiddenObjectIds: string[];
671
- hideObject: (objectId: string) => void;
672
- showObject: (objectId: string) => void;
673
- toggleObjectVisibility: (objectId: string) => void;
702
+ hideObject: (objectId: string | string[]) => void;
703
+ showObject: (objectId: string | string[]) => void;
704
+ toggleObjectVisibility: (objectId: string | string[]) => void;
674
705
  isObjectHidden: (objectId: string) => boolean;
675
706
  clearHiddenObjects: () => void;
676
707
  };
677
- declare function useObjectVisibility(objectBindings: Record<string, ObjectBinding>, onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void): ObjectVisibilityProps;
708
+ /**
709
+ * Owns object-binding state and exposes field-level, material, and visibility
710
+ * operations over the same functional state updater.
711
+ *
712
+ * Consecutive calls in the same tick compose instead of overwriting each other,
713
+ * including calls across material and visibility concerns.
714
+ */
715
+ declare function useObjectBindings(initialBindings: Record<string, ObjectBinding> | (() => Record<string, ObjectBinding>)): ObjectBindingsController;
678
716
 
679
717
  type ViewerHoverProps = {
680
718
  hoveredObjectBinding: ObjectBinding | null;
@@ -728,4 +766,4 @@ type ShareableViewerStateProps = {
728
766
  };
729
767
  declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
730
768
 
731
- export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type CaptureImageOptions, type CinematicConfig, type CinematicWaypoint, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingTextureSlot, type ObjectVisibilityProps, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneCinematicConfig, type SceneCinematicWaypoint, type SceneConfig, type SceneModelRenderer, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, type XRScaleMode, useObjectBinding, useObjectBindingIds, useObjectVisibility, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
769
+ export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type BindingEdit, type CaptureImageOptions, type CinematicConfig, type CinematicWaypoint, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingPatch, type ObjectBindingTextureSlot, type ObjectBindingsController, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneCinematicConfig, type SceneCinematicWaypoint, type SceneConfig, type SceneModelRenderer, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, type XRScaleMode, material, patchBindings, useObjectBinding, useObjectBindingIds, useObjectBindings, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };