@liveroom-tech/react-immersive 4.3.0 → 5.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.
Files changed (46) hide show
  1. package/README.md +697 -83
  2. package/dist/ModelViewer-CbbJT85n.d.mts +448 -0
  3. package/dist/ModelViewer-DOexHkJC.d.ts +448 -0
  4. package/dist/binding-builder.css +1 -0
  5. package/dist/binding-builder.d.mts +6 -0
  6. package/dist/binding-builder.d.ts +6 -0
  7. package/dist/binding-builder.js +3694 -0
  8. package/dist/binding-builder.mjs +1 -0
  9. package/dist/chunk-7EW6JZ46.mjs +27 -0
  10. package/dist/chunk-BGYK4Y2Y.mjs +1 -0
  11. package/dist/chunk-MMO7KCYU.mjs +3 -0
  12. package/dist/chunk-NVY7N7CU.mjs +3 -0
  13. package/dist/chunk-SEESCGUS.mjs +1 -0
  14. package/dist/chunk-WFEYRUZC.mjs +1 -0
  15. package/dist/chunk-XTLSAYAQ.mjs +1 -0
  16. package/dist/hosted.css +1 -1
  17. package/dist/hosted.d.mts +33 -13
  18. package/dist/hosted.d.ts +33 -13
  19. package/dist/hosted.js +11 -8
  20. package/dist/hosted.mjs +1 -1
  21. package/dist/index.css +1 -1
  22. package/dist/index.d.mts +107 -83
  23. package/dist/index.d.ts +107 -83
  24. package/dist/index.js +11 -8
  25. package/dist/index.mjs +1 -1
  26. package/dist/modelInfo-CEmmvVCH.d.mts +17 -0
  27. package/dist/modelInfo-CEmmvVCH.d.ts +17 -0
  28. package/dist/{objectBinding-CBRYMYk6.d.ts → objectCatalog-5W8n5xNQ.d.mts} +55 -3
  29. package/dist/{objectBinding-CBRYMYk6.d.mts → objectCatalog-5W8n5xNQ.d.ts} +55 -3
  30. package/dist/publicProps-tJuWZgT3.d.mts +12 -0
  31. package/dist/publicProps-tJuWZgT3.d.ts +12 -0
  32. package/dist/simple-model-viewer.css +1 -0
  33. package/dist/simple-model-viewer.d.mts +38 -0
  34. package/dist/simple-model-viewer.d.ts +38 -0
  35. package/dist/simple-model-viewer.js +3666 -0
  36. package/dist/simple-model-viewer.mjs +1 -0
  37. package/dist/utils.d.mts +49 -3
  38. package/dist/utils.d.ts +49 -3
  39. package/dist/utils.js +1 -1
  40. package/dist/utils.mjs +1 -1
  41. package/package.json +28 -18
  42. package/dist/chunk-7X37KAPE.mjs +0 -1
  43. package/dist/chunk-GYB4BOVW.mjs +0 -26
  44. package/dist/chunk-K7DMBJBJ.mjs +0 -1
  45. package/dist/publicProps-BST1E8an.d.mts +0 -349
  46. package/dist/publicProps-Bv3_PCP0.d.ts +0 -349
package/README.md CHANGED
@@ -10,16 +10,15 @@ The library currently exports:
10
10
 
11
11
  ```ts
12
12
  import {
13
- BindingBuilder,
14
13
  material,
15
14
  ModelViewer,
16
15
  patchBindings,
17
16
  patchSceneConfig,
18
- SimpleModelViewer,
19
17
  useObjectBinding,
20
18
  useObjectBindingIds,
21
19
  useObjectBindings,
22
20
  useSceneConfig,
21
+ useViewer,
23
22
  useViewerActions,
24
23
  useViewerAnimations,
25
24
  useViewerCamera,
@@ -31,8 +30,13 @@ import {
31
30
  MATERIAL_BLENDING_MODES,
32
31
  MATERIAL_SIDES,
33
32
  } from "@liveroom-tech/react-immersive";
33
+ import { SimpleModelViewer } from "@liveroom-tech/react-immersive/simple-model-viewer";
34
+ import { BindingBuilder } from "@liveroom-tech/react-immersive/binding-builder";
34
35
  ```
35
36
 
37
+ Each component has its own entry point, so an app downloads only the ones it
38
+ imports.
39
+
36
40
  It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindingsController`, `ObjectActionEvent`, `ObjectTransformSpace`, `SceneConfig`, `SceneConfigPatch`, `SceneConfigController`, `AnimationControls`, `CinematicConfig` / `CinematicWaypoint`, and related shapes used throughout this document.
37
41
 
38
42
  `ModelViewer` provides:
@@ -41,7 +45,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
41
45
  - camera-driven 3D Tiles streaming through `tilesetUrl`, with configurable screen-space error and bounded tile cache
42
46
  - orbit/pan/zoom camera controls via `CameraControls`
43
47
  - 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
44
- - optional custom scene lighting through a `lights` prop
48
+ - scene lighting through `sceneConfig.lighting`, with the `sceneLight` helper and a runtime lights controller
45
49
  - optional custom camera configuration through a `camera` prop
46
50
  - optional custom background color through a `backgroundColor` prop
47
51
  - optional shadow rendering toggle through a `shadows` prop (on by default)
@@ -51,6 +55,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
51
55
  - optional per-object move and rotate gizmo through `moveModeEnabled`, with World/Local axis alignment through `objectTransformSpace`
52
56
  - optional custom left object data panel through `customObjectBindingDataPanel`
53
57
  - optional custom right scene objects panel through `customSceneObjectsPanel`
58
+ - your own UI over the 3D view through `overlay`, and react-three-fiber content inside the scene through `unsafe_sceneChildren`
54
59
  - optional hiding of either built-in panel through `showObjectBindingDataPanel` and `showSceneObjectsPanel`
55
60
  - optional model export button through `showDownloadButton` and `downloadFilename`, opt-in PNG capture through `preserveDrawingBuffer`, plus an independent `showResetButton` toggle for the rest of that action bar
56
61
  - an optional UV Checker toolbar button through `showUvCheckerButton`, for inspecting UV scale, stretching, seams, orientation, and missing UV0 coordinates directly in `ModelViewer`
@@ -73,6 +78,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
73
78
  - hover and selected-state highlighting
74
79
  - external selection state control through `selectedObject` and `onObjectSelect`
75
80
  - hover callbacks through `onObjectHover`
81
+ - double-click, right-click, and touch long-press callbacks through `onObjectDoubleClick` and `onObjectContextMenu`
76
82
  - model-ready callbacks through `onModelLoaded`
77
83
  - model-load error callbacks through `onLoadError`
78
84
  - camera change callbacks through `onCameraChange`
@@ -98,7 +104,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
98
104
 
99
105
  `SimpleModelViewer` provides:
100
106
 
101
- - a lightweight GLB/GLTF/OBJ/FBX/USDZ viewer that does not require `objectBindings` and does not require a `licenseKey`
107
+ - a lightweight GLB/GLTF/OBJ/FBX/USDZ viewer with no bindings or actions that does not require a `licenseKey`
102
108
  - optional local `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, or model ZIP bundle upload mode for ad-hoc inspection without relying on the `modelUrl` asset
103
109
  - built-in scene objects panel with search and visibility toggles
104
110
  - a built-in environment/scene settings panel (background color, environment preset, auto-rotate, exposure, ambient + directional light) toggleable via `showSceneSettingsPanel`, with each control's initial value seedable via props
@@ -165,24 +171,33 @@ has its own meaning, state, or behavior," object bindings are a strong fit.
165
171
  ## Installation
166
172
 
167
173
  ```bash
168
- npm install @liveroom-tech/react-immersive
174
+ npm install @liveroom-tech/react-immersive three @react-three/fiber
169
175
  ```
170
176
 
171
- Import the library stylesheet once from your application root (for example,
172
- Next.js `app/layout.tsx`):
177
+ Import the stylesheet for each component you use, once, from your
178
+ application root (for example, Next.js `app/layout.tsx`):
173
179
 
174
180
  ```tsx
175
- import "@liveroom-tech/react-immersive/styles.css";
181
+ import "@liveroom-tech/react-immersive/styles.css"; // ModelViewer
182
+ import "@liveroom-tech/react-immersive/simple-model-viewer.css"; // SimpleModelViewer
183
+ import "@liveroom-tech/react-immersive/binding-builder.css"; // BindingBuilder
176
184
  ```
177
185
 
178
186
  The styles ship as a static CSS file rather than a runtime-injected `<style>`
179
187
  tag, so a strict CSP does not need `style-src 'unsafe-inline'`. Tailwind is not
180
188
  required in the consuming app.
181
189
 
182
- Peer dependencies:
190
+ If a component's stylesheet is missing, it renders unstyled, and in
191
+ development it logs a console warning naming the import to add.
192
+
193
+ Peer dependencies, which your app provides so there's exactly one copy of each:
194
+
195
+ - `react` and `react-dom` 19
196
+ - `three` 0.184
197
+ - `@react-three/fiber` 9
183
198
 
184
- - `react >= 17`
185
- - `react-dom >= 17`
199
+ Everything else, including drei, post-processing, and WebXR support, installs
200
+ with the package.
186
201
 
187
202
  `ModelViewer` and `BindingBuilder` require a `licenseKey` at runtime (see [`licenseKey`](#licensekey)). Get one from the Developer Portal, see [Resources](#resources) below.
188
203
 
@@ -199,6 +214,74 @@ Peer dependencies:
199
214
 
200
215
  ## Quick Start
201
216
 
217
+ The shortest working viewer lets `ModelViewer` generate the bindings itself:
218
+
219
+ ```tsx
220
+ import { ModelViewer } from "@liveroom-tech/react-immersive";
221
+
222
+ export default function Example() {
223
+ return (
224
+ <div style={{ height: "100vh", overflow: "hidden" }}>
225
+ <ModelViewer
226
+ modelUrl="/model.glb"
227
+ licenseKey="your-license-key"
228
+ />
229
+ </div>
230
+ );
231
+ }
232
+ ```
233
+
234
+ Every mesh in the model can then be hovered and selected, and opens the side
235
+ panel with Change Color, Change Material, and Toggle Visibility (or, for meshes
236
+ whose names contain "light", Toggle Light, Change Light Color, and Set
237
+ Brightness). See
238
+ [Generating bindings automatically](#generating-bindings-automatically) for how
239
+ to keep and refine the generated bindings.
240
+
241
+ To control the viewer from your own UI, use `useViewer`. It connects every
242
+ viewer hook to `ModelViewer` at once:
243
+
244
+ ```tsx
245
+ import { ModelViewer, useViewer } from "@liveroom-tech/react-immersive";
246
+
247
+ export default function Configurator() {
248
+ const viewer = useViewer();
249
+
250
+ return (
251
+ <div style={{ height: "100vh", overflow: "hidden" }}>
252
+ <ModelViewer
253
+ modelUrl="/model.glb"
254
+ licenseKey="your-license-key"
255
+ {...viewer.props}
256
+ />
257
+ <button
258
+ onClick={() => viewer.bindings.setBaseColor({ group: "paint" }, "#b91c1c")}
259
+ >
260
+ Paint it red
261
+ </button>
262
+ <button onClick={() => viewer.camera.resetView()}>Reset view</button>
263
+ </div>
264
+ );
265
+ }
266
+ ```
267
+
268
+ `viewer.bindings`, `viewer.selection`, `viewer.hover`, `viewer.camera`,
269
+ `viewer.model`, `viewer.animations`, `viewer.actions`, `viewer.scene`,
270
+ `viewer.effects`, and `viewer.connection` are the results of the hooks they're
271
+ named after (`useObjectBindings`, `useViewerSelection`, and so on), so
272
+ everything below applies to them. Without a `bindings` option, `useViewer`
273
+ generates bindings from the model like `objectBindings="auto"`; pass
274
+ `useViewer({ bindings })` to start from your own. It also accepts
275
+ `sceneConfig`, `camera` (the `useViewerCamera` options), and `onAction`,
276
+ `onObjectSelect`, `onObjectHover`, and `onLoadError` callbacks. `sceneConfig`
277
+ is passed to `ModelViewer` only once you've supplied one or changed the scene
278
+ through `viewer.scene`, so the default scene behaves as if none were passed.
279
+ Props set on `ModelViewer` after the spread take precedence.
280
+
281
+ The full example below authors bindings by hand and wires up every hook
282
+ individually, which is what `useViewer` does for you. Wire them yourself when
283
+ state lives elsewhere, such as bindings owned by a store:
284
+
202
285
  ```tsx
203
286
  import {
204
287
  ModelViewer,
@@ -488,8 +571,7 @@ pure `patchSceneConfig(sceneConfig, patch)` utility when a store or parent owns
488
571
  the scene state. The namespaced `lights` controller also provides `add`,
489
572
  `update`, `remove`, `show`, `hide`, `toggle`, `setIntensity`, `setColor`,
490
573
  `attach`, and `detach`. Attached lights follow a bound model object without an
491
- R3F light component. The custom `lights` prop replaces scene-config lights, so
492
- leave it unset when using this controller.
574
+ R3F light component.
493
575
 
494
576
  For high-frequency visual effects, use `useViewerEffects`. It mutates the live
495
577
  viewer efficiently, requests demand-rendered frames automatically, and restores
@@ -579,7 +661,9 @@ const {
579
661
  error,
580
662
  bounds,
581
663
  objectCount,
664
+ objects,
582
665
  handleModelLoaded,
666
+ handleObjectCatalog,
583
667
  handleLoadError,
584
668
  } = useViewerModel();
585
669
 
@@ -588,6 +672,7 @@ const {
588
672
  licenseKey="your-license-key"
589
673
  objectBindings={objectBindings}
590
674
  onModelLoaded={handleModelLoaded}
675
+ onObjectCatalog={handleObjectCatalog}
591
676
  onLoadError={handleLoadError}
592
677
  />;
593
678
  ```
@@ -599,7 +684,10 @@ const {
599
684
  - `error`: the most recent load/render error, or `null`
600
685
  - `bounds`: the loaded scene bounds as `min`, `max`, `center`, and `size`
601
686
  - `objectCount`: the number of mesh objects in the loaded scene
687
+ - `objects`: every bindable object in the model as a `ViewerObjectInfo`, once
688
+ `handleObjectCatalog` is passed to `onObjectCatalog`
602
689
  - `handleModelLoaded`: a callback you can pass directly to `onModelLoaded`
690
+ - `handleObjectCatalog`: a callback you can pass directly to `onObjectCatalog`
603
691
  - `handleLoadError`: a callback you can pass directly to `onLoadError`
604
692
 
605
693
  If you want to inspect or trigger binding actions programmatically, the library also exports:
@@ -636,7 +724,8 @@ Actions can declare ordered `effects` for object-binding patches, scene patches,
636
724
  Basic usage:
637
725
 
638
726
  ```tsx
639
- import { BindingBuilder } from "@liveroom-tech/react-immersive";
727
+ import "@liveroom-tech/react-immersive/binding-builder.css";
728
+ import { BindingBuilder } from "@liveroom-tech/react-immersive/binding-builder";
640
729
 
641
730
  export default function App() {
642
731
  return <BindingBuilder licenseKey="your-license-key" />;
@@ -655,31 +744,11 @@ type BindingBuilderProps = {
655
744
  };
656
745
  ```
657
746
 
658
- Pass `tilesetUrl` to `ModelViewer` for a processed 3D Tiles asset. The original
659
- `modelUrl` remains the durable source/fallback URL, but is not downloaded by the
660
- viewer while a tileset is present:
661
-
662
- ```tsx
663
- <ModelViewer
664
- modelUrl="https://assets.example.com/source/building.glb"
665
- tilesetUrl="https://assets.example.com/tiles/building/tileset.json"
666
- tilesErrorTarget={8}
667
- tilesCacheSize={800}
668
- licenseKey="your-license-key"
669
- objectBindings={{}}
670
- />
671
- ```
672
-
673
- Tiles are a dynamic level-of-detail scene, so automatic mesh binding generation
674
- is intentionally disabled in streamed `BindingBuilder` projects. Scene settings
675
- remain editable. Per-part editing requires the conversion pipeline to provide a
676
- stable CAD/object catalog that can be mapped to tile feature metadata.
677
-
678
747
  `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.
679
748
 
680
749
  Current behavior, Object tab:
681
750
 
682
- - upload a `.glb`, standalone/data-URI `.gltf`, or `.zip` containing a `.gltf` plus its referenced `.bin` and texture files
751
+ - upload a `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, or `.usdz` file, or a `.zip` bundle (a `.gltf` with its `.bin` and textures, an `.obj` with its MTL and textures, or an `.fbx` with external textures)
683
752
  - traverse renderable mesh nodes and generate starter bindings automatically
684
753
  - edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
685
754
  - move and rotate the selected object from the **Move & Rotate** panel using a geometry-centered pivot, World/Local axis alignment, or numeric position and degree fields; restore its authored transform with one click
@@ -691,6 +760,8 @@ Current behavior, Object tab:
691
760
  Current behavior, Scene tab:
692
761
 
693
762
  - configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
763
+ - configure auto-rotation and its speed, exported as `sceneConfig.model.autoRotate` and `sceneConfig.model.autoRotateSpeed`
764
+ - position and rotate directional, point, and spot lights with transform gizmos; spot lights also expose angle and penumbra controls
694
765
  - undo/redo the scene config independently of the Object tab's bindings history
695
766
  - import a previously exported `sceneConfig.json`, merged by top-level section
696
767
  - export the current scene config as JSON or TypeScript
@@ -698,6 +769,13 @@ Current behavior, Scene tab:
698
769
  Preview:
699
770
 
700
771
  - the selected node previews live in an embedded `ModelViewer`
772
+
773
+ Autosave:
774
+
775
+ - the session (the model, its object bindings, and the scene settings, with an uploaded model file included) is saved to this browser's IndexedDB, in a database named `react-immersive:binding-builder`, shortly after each edit and when the tab is hidden or closed
776
+ - **Restore last session** in the preview toolbar reopens it; one session is kept per browser
777
+ - while there are edits on screen, leaving or reloading the page asks for confirmation
778
+ - if the browser refuses to store a session, usually for lack of space, the editor says so; export your work to keep it
701
779
  - enable **Transform gizmo**, select a mesh, then use the arrows and plane handles to move it or the rings to rotate it; all changes are saved to `binding.transform`
702
780
 
703
781
  ### `BindingBuilder` licensing & plan tiers
@@ -719,7 +797,8 @@ A locked feature renders in place (not hidden) with a message naming the require
719
797
  Basic usage:
720
798
 
721
799
  ```tsx
722
- import { SimpleModelViewer } from "@liveroom-tech/react-immersive";
800
+ import "@liveroom-tech/react-immersive/simple-model-viewer.css";
801
+ import { SimpleModelViewer } from "@liveroom-tech/react-immersive/simple-model-viewer";
723
802
 
724
803
  export default function App() {
725
804
  return (
@@ -791,7 +870,7 @@ const {
791
870
  - `setCameraTarget`: sets the camera target to a given `[x, y, z]`
792
871
  - `setCameraState`: restores a full camera state (`position`/`target`/`fov`/`zoom`) previously read from `cameraState`
793
872
  - `lookAt`: moves the position and target together
794
- - `orbitTo`: moves to azimuth and polar angles in radians
873
+ - `orbitTo`: orbits to azimuth and polar angles in degrees (azimuth 0 is the front, 90 the right; polar 0 is straight down, 90 level)
795
874
  - `dollyTo`: moves to a distance from the current target
796
875
  - `savePreset`: captures the current camera under a name
797
876
  - `goToPreset`: moves to a named camera preset
@@ -871,35 +950,96 @@ const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
871
950
  ))}
872
951
  ```
873
952
 
953
+ ## Presets and grouped props
954
+
955
+ `preset` sets `ModelViewer` up for a common kind of page:
956
+
957
+ | Preset | For | What it sets |
958
+ | --- | --- | --- |
959
+ | `minimal` | Just the model | Hides both panels, the download and reset buttons, and tour controls |
960
+ | `showcase` | A product on display | Everything `minimal` does, plus auto-rotation |
961
+ | `configurator` | Customers changing parts | Shows the selected object's panel and the reset button; hides the scene objects panel and download |
962
+ | `inspector` | Checking a model | Every panel, the view gizmo, the measure, UV checker, and exploded-view tools, and keyboard navigation |
963
+
964
+ `MODEL_VIEWER_PRESETS` lists exactly which props each preset sets.
965
+
966
+ Related props are also available as groups, which keeps a configured viewer
967
+ readable:
968
+
969
+ ```tsx
970
+ <ModelViewer
971
+ modelUrl="/car.glb"
972
+ licenseKey="your-license-key"
973
+ preset="configurator"
974
+ ui={{ download: "car.glb" }}
975
+ tools={{ measure: { unit: "cm" } }}
976
+ controls={{ autoRotate: 0.5, zoom: false, limits: { maxDistance: 20 } }}
977
+ perf={{ profile: "low" }}
978
+ xr={{ enabled: true, usdzUrl: "/car.usdz" }}
979
+ />
980
+ ```
981
+
982
+ | Group | Fields, and the flat prop each one sets |
983
+ | --- | --- |
984
+ | `ui` | `objectPanel` (`showObjectBindingDataPanel`), `sceneObjects` (`showSceneObjectsPanel`), `download` (`showDownloadButton`; a string also sets `downloadFilename`), `reset` (`showResetButton`), `loadingOverlay` (`showLoadingOverlay`), `viewGizmo` (`showViewGizmo`), `mouseController` (`showMouseController`; `{ position, opacity }` also sets `mouseControllerPosition` and `mouseControllerOpacity`), `annotationNavigation` (`showAnnotationNavigation`), `annotationOnHover` (`showAnnotationOnHover`), `highlightOnHover` |
985
+ | `tools` | `measure` (`showMeasureTools`; `{ unit }` also sets `measurementUnit`), `uvChecker` (`showUvCheckerButton`), `explode` (`showExplodeControls`), `texturePositioning` (`texturePositioning`) |
986
+ | `controls` | `enabled` (`enableCameraControls`), `keyboard` (`enableKeyboardNavigation`), `zoom` (the opposite of `disableZoom`), `zoomOnSelect` (`zoomOnSelected`), `autoRotate` (a number also sets `autoRotateSpeed`), `refitOnResize`, `limits` (`cameraLimits`), `moveSensitivity`, `zoomSensitivity` |
987
+ | `perf` | `renderMode`, `maxDpr`, `profile` (`performanceProfile`), `preserveDrawingBuffer` |
988
+ | `tiles` | `url` (`tilesetUrl`), `errorTarget` (`tilesErrorTarget`), `cacheSize` (`tilesCacheSize`) |
989
+ | `xr` | `enabled` (`enableXR`), `usdzUrl`, `scaleMode` (`arScaleMode`), `mobileHandoffUrl` |
990
+ | `decoders` | `draco` (`dracoDecoderPath`), `ktx2` (`ktx2TranscoderPath`), `meshopt` |
991
+
992
+ A preset only fills in defaults: a flat prop overrides it, and a grouped prop
993
+ overrides both. The flat props keep working exactly as before.
994
+
874
995
  ## Public API
875
996
 
876
997
  The source code currently defines `ModelViewer` with these props:
877
998
 
878
999
  ```ts
879
- type ObjectTransformSpace = "local" | "world";
880
-
881
1000
  type Props = {
1001
+ preset?: "minimal" | "showcase" | "configurator" | "inspector";
1002
+ ui?: ModelViewerUiOptions;
1003
+ tools?: ModelViewerToolsOptions;
1004
+ controls?: ModelViewerControlsOptions;
1005
+ perf?: ModelViewerPerfOptions;
1006
+ tiles?: ModelViewerTilesOptions;
1007
+ xr?: ModelViewerXrOptions;
1008
+ decoders?: ModelViewerDecoderOptions;
882
1009
  modelUrl: string;
1010
+ tilesetUrl?: string | null;
1011
+ tilesErrorTarget?: number;
1012
+ tilesCacheSize?: number;
1013
+ onObjectCatalog?: (objects: ViewerObjectInfo[]) => void;
883
1014
  modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz";
884
1015
  licenseKey: string;
885
- objectBindings: Record<string, ObjectBinding>;
1016
+ objectBindings?: Record<string, ObjectBinding> | "auto"; // default "auto"
886
1017
  selectedObject?: ObjectBinding | null;
1018
+ panelHoveredObject?: ObjectBinding | null;
887
1019
  onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void;
888
1020
  onObjectSelect?: (binding: ObjectBinding | null) => void;
889
1021
  onObjectHover?: (binding: ObjectBinding | null) => void;
890
- onModelLoaded?: (scene: Object3D) => void;
1022
+ onObjectDoubleClick?: (event: ObjectPointerEvent) => boolean | void;
1023
+ onObjectContextMenu?: (event: ObjectPointerEvent) => void;
1024
+ onModelLoaded?: (model: ViewerModelInfo) => void;
891
1025
  onLoadError?: (error: unknown) => void;
1026
+ onProgress?: (event: ProgressEvent) => void;
1027
+ cameraControllerType?: "orbit" | "pointerLock";
1028
+ onCameraControllerChange?: (
1029
+ controller: "orbit" | "pointerLock",
1030
+ ) => void;
892
1031
  onAction?: (event: ObjectActionEvent) => void;
893
1032
  onHiddenObjectsChange?: (next: Record<string, boolean>) => void;
894
- onCameraChange?: (camera: Camera, controls: CameraControls) => void;
1033
+ onCameraChange?: (state: ViewerCameraState) => void;
895
1034
  onViewerReady?: (viewer: ViewerReadyState) => void;
896
1035
  onTextureUpload?: (file: File, objectId: string) => Promise<string>;
1036
+ texturePositioning?: boolean | TexturePositioningControls;
897
1037
  onAnimationsReady?: (controls: AnimationControls) => void;
898
1038
  onAnnotationsChange?: (annotations: AnnotationMarker[]) => void;
899
1039
  activeAnnotation?: AnnotationMarker | null;
900
1040
  onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
901
- lights?: React.ReactNode;
902
- camera?: React.ComponentProps<typeof Canvas>["camera"];
1041
+ camera?: ViewerCameraConfig;
1042
+ cameraLimits?: CameraLimits;
903
1043
  backgroundColor?: string;
904
1044
  shadows?: boolean;
905
1045
  showObjectBindingDataPanel?: boolean;
@@ -929,9 +1069,10 @@ type Props = {
929
1069
  sceneConfig?: SceneConfig;
930
1070
  disableZoom?: boolean;
931
1071
  zoomOnSelected?: boolean;
1072
+ highlightOnHover?: boolean;
932
1073
  enableCameraControls?: boolean;
933
1074
  moveModeEnabled?: boolean;
934
- objectTransformSpace?: ObjectTransformSpace;
1075
+ objectTransformSpace?: "local" | "world";
935
1076
  enableKeyboardNavigation?: boolean;
936
1077
  onAutoFit?: () => Promise<boolean>;
937
1078
  refitOnResize?: boolean;
@@ -953,7 +1094,12 @@ type Props = {
953
1094
  mobileHandoffUrl?: string;
954
1095
  showAnnotationNavigation?: boolean;
955
1096
  showAnnotationOnHover?: boolean;
1097
+ onSceneConfigChange?: (config: SceneConfig) => void;
956
1098
  showViewGizmo?: boolean;
1099
+ overlay?: React.ReactNode;
1100
+ unsafe_sceneChildren?: React.ReactNode;
1101
+ autoRotate?: boolean;
1102
+ autoRotateSpeed?: number;
957
1103
  };
958
1104
  ```
959
1105
 
@@ -972,13 +1118,44 @@ modelUrl = "/model.glb";
972
1118
  Optional explicit format for signed or extensionless model URLs. Normal URLs
973
1119
  ending in `.glb`, `.gltf`, `.obj`, `.fbx`, or `.usdz` are detected automatically.
974
1120
 
1121
+ ### 3D Tiles streaming
1122
+
1123
+ Use `tilesetUrl` to stream a processed 3D Tiles model instead of downloading
1124
+ `modelUrl` as one monolithic asset. `modelUrl` remains required by the component
1125
+ API, but `tilesetUrl` is the render source while streaming is enabled.
1126
+
1127
+ ```tsx
1128
+ <ModelViewer
1129
+ modelUrl="/models/campus.glb"
1130
+ tilesetUrl="/models/campus/tileset.json"
1131
+ tilesErrorTarget={8}
1132
+ tilesCacheSize={800}
1133
+ onObjectCatalog={(objects) => console.log(objects)}
1134
+ {...props}
1135
+ />
1136
+ ```
1137
+
1138
+ - `tilesErrorTarget` controls the screen-space error target. Lower values load
1139
+ sharper tiles and use more bandwidth/GPU memory. Default: `8`.
1140
+ - `tilesCacheSize` is the maximum number of parsed tiles kept in the client
1141
+ cache. Default: `800`.
1142
+ - `onObjectCatalog` receives the bindable-object catalog from `objects.json`
1143
+ before all referenced tiles have loaded. See [`onObjectCatalog`](#onobjectcatalog).
1144
+
1145
+ Bindings, selection, hover, material overrides, transforms, exploded view, and
1146
+ scene renderer modes use the same public API in streamed and monolithic modes.
1147
+ The tileset and its tile payloads must be browser-accessible, and the optional
1148
+ object catalog must sit beside `tileset.json` as `objects.json`.
1149
+
975
1150
  ### `licenseKey`
976
1151
 
977
1152
  License key for the library. Required.
978
1153
 
979
1154
  ### `objectBindings`
980
1155
 
981
- A map keyed by the model node name. Each key should match a mesh name from the GLB/GLTF file.
1156
+ A map keyed by the model node name, or `"auto"`, the default (see
1157
+ [Generating bindings automatically](#generating-bindings-automatically)). Each
1158
+ key should match a mesh name from the GLB/GLTF file.
982
1159
 
983
1160
  `objectBindings` is the single source of truth for all visual state. Changes to the binding record drive rendering:
984
1161
 
@@ -1024,6 +1201,69 @@ const objectBindings = defineObjectBindings({
1024
1201
 
1025
1202
  `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.
1026
1203
 
1204
+ If a binding's `modelObjectId` (or its key) doesn't match any node in the
1205
+ loaded model, that binding can never be selected. `ModelViewer` then shows a
1206
+ dismissible banner with the number of mismatched bindings, and logs a console
1207
+ warning naming them. Log [`onObjectCatalog`](#onobjectcatalog) to see the
1208
+ model's node names.
1209
+
1210
+ #### Generating bindings automatically
1211
+
1212
+ `objectBindings` defaults to `"auto"`: leave it out and `ModelViewer` generates
1213
+ a binding for every mesh when the model loads, so you don't need to know the
1214
+ node names up front:
1215
+
1216
+ ```tsx
1217
+ <ModelViewer modelUrl="/model.glb" licenseKey="your-license-key" />
1218
+ ```
1219
+
1220
+ Each generated binding is keyed by the node name, with a readable `label`
1221
+ (`"Wheel_FL"` becomes `"Wheel FL"`), an inferred `type`, a unique `id`, and
1222
+ built-in actions: `change-color`, `change-material`, and `toggle-visibility`,
1223
+ or `toggle-light`, `change-light-color`, and `set-brightness` for meshes whose
1224
+ names contain "light". For a streamed model (`tilesetUrl`), the bindings come
1225
+ from the object catalog, so objects are bindable before their tiles load.
1226
+
1227
+ The generated bindings are passed to `onObjectBindingsChange` once they exist,
1228
+ and again after every edit. To keep them, and to refine them later, store them
1229
+ and pass them back:
1230
+
1231
+ ```tsx
1232
+ const [objectBindings, setObjectBindings] = useState<
1233
+ Record<string, ObjectBinding> | "auto"
1234
+ >("auto");
1235
+
1236
+ <ModelViewer
1237
+ modelUrl={modelUrl}
1238
+ licenseKey="your-license-key"
1239
+ objectBindings={objectBindings}
1240
+ onObjectBindingsChange={setObjectBindings}
1241
+ />;
1242
+ ```
1243
+
1244
+ After the first change the viewer is controlled by your state as usual. Set the
1245
+ state back to `"auto"` when you switch to a different model. While the prop is
1246
+ `"auto"`, bindings the same model already had are kept when it reloads, so
1247
+ runtime edits survive; a different `modelUrl` starts from fresh bindings.
1248
+
1249
+ The same defaults are available as functions from both entry points (they are
1250
+ also what `BindingBuilder` generates):
1251
+
1252
+ ```ts
1253
+ import {
1254
+ buildBindingsFromNodes,
1255
+ createDefaultBinding,
1256
+ inferType,
1257
+ } from "@liveroom-tech/react-immersive/utils";
1258
+
1259
+ const bindings = buildBindingsFromNodes(["CarBody", "Wheel_FL"]);
1260
+ const door = { ...createDefaultBinding("FrontDoor"), label: "Front door" };
1261
+ inferType("Wheel_FL"); // "wheel"
1262
+ ```
1263
+
1264
+ Every generated action is one `ModelViewer` performs itself, so every
1265
+ generated button works without extra setup.
1266
+
1027
1267
  ### `selectedObject`
1028
1268
 
1029
1269
  Optional currently selected object binding, or `null` when nothing is selected.
@@ -1033,6 +1273,13 @@ If omitted, `ModelViewer` manages its own selection state internally.
1033
1273
 
1034
1274
  When the selected object has a `cameraState` with both `position` and `target`, the viewer uses that saved camera view for focus/select behavior instead of falling back to `fitToBox`.
1035
1275
 
1276
+ ### `panelHoveredObject`
1277
+
1278
+ Optional externally controlled binding to highlight as hovered by a custom
1279
+ scene-objects panel. It takes precedence over the built-in panel and canvas
1280
+ hover while `highlightOnHover` is enabled. Pass `null` when no panel row is
1281
+ hovered.
1282
+
1036
1283
  ### `onObjectSelect`
1037
1284
 
1038
1285
  Optional callback called when the user clicks a mesh or closes the side panel.
@@ -1053,6 +1300,47 @@ Behavior:
1053
1300
  - pointer over a mesh calls `onObjectHover(binding)`
1054
1301
  - pointer out calls `onObjectHover(null)`
1055
1302
 
1303
+ ### `onObjectDoubleClick`
1304
+
1305
+ Optional callback called when the user double-clicks an object.
1306
+
1307
+ ```ts
1308
+ onObjectDoubleClick?: (event: ObjectPointerEvent) => boolean | void
1309
+
1310
+ type ObjectPointerEvent = {
1311
+ binding: ObjectBinding;
1312
+ clientX: number; // viewport coordinates
1313
+ clientY: number;
1314
+ pointerType: string; // "mouse" | "pen" | "touch"
1315
+ };
1316
+ ```
1317
+
1318
+ Return `true` to say you handled it: the viewer then skips its own double-click
1319
+ behavior, which moves the camera's orbit point to that spot (or starts texture
1320
+ positioning, when `texturePositioning` is on). Return nothing to keep it.
1321
+
1322
+ ### `onObjectContextMenu`
1323
+
1324
+ Optional callback called when the user right-clicks an object, or long-presses
1325
+ it on a touch screen. Use it to open your own menu:
1326
+
1327
+ ```tsx
1328
+ <ModelViewer
1329
+ {...props}
1330
+ onObjectContextMenu={({ binding, clientX, clientY }) =>
1331
+ openMenu({ for: binding.id, x: clientX, y: clientY })
1332
+ }
1333
+ />
1334
+ ```
1335
+
1336
+ It receives the same `ObjectPointerEvent` as `onObjectDoubleClick`, with the
1337
+ position where the gesture started, ready for a `position: fixed` menu. A
1338
+ right-click fires when the button is released without the pointer moving, so a
1339
+ right-drag still pans the camera. A long-press fires after half a second, and
1340
+ not if the finger moves or a second finger touches down (a pinch); lifting the
1341
+ finger afterwards doesn't select the object. Objects with `selectable: false`
1342
+ don't fire it.
1343
+
1056
1344
  ### `onHiddenObjectsChange`
1057
1345
 
1058
1346
  Optional callback fired with the derived hidden-object map whenever the current binding data changes hidden visibility state.
@@ -1071,16 +1359,56 @@ This fires for:
1071
1359
 
1072
1360
  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.
1073
1361
 
1074
- ### `onModelLoaded`
1362
+ ### `onObjectCatalog`
1075
1363
 
1076
- Optional callback fired after the GLB/GLTF scene has been loaded.
1364
+ Optional callback listing every bindable object in the model: each named mesh,
1365
+ under the identifier `objectBindings` is keyed by. It's the quickest way to find
1366
+ out what your meshes are called:
1077
1367
 
1078
- Current callback shape:
1368
+ ```tsx
1369
+ <ModelViewer
1370
+ modelUrl="/model.glb"
1371
+ licenseKey="your-license-key"
1372
+ onObjectCatalog={(objects) => console.table(objects)}
1373
+ />
1374
+ ```
1375
+
1376
+ It fires each time a model loads. For a streamed model (`tilesetUrl`) the list
1377
+ comes from `objects.json` beside `tileset.json`, before the tiles containing
1378
+ those objects have necessarily loaded. Each entry is a `ViewerObjectInfo`:
1079
1379
 
1080
1380
  ```ts
1081
- (scene: Object3D) => void
1381
+ type ViewerObjectInfo = {
1382
+ name: string; // the key to use in objectBindings
1383
+ triangles: number;
1384
+ minimum: [number, number, number];
1385
+ maximum: [number, number, number];
1386
+ };
1082
1387
  ```
1083
1388
 
1389
+ `minimum` and `maximum` are the object's bounds in the model's own units, in
1390
+ Z-up coordinates as in 3D Tiles: a point at glTF `(x, y, z)` is `(x, -z, y)`
1391
+ here. They're the same for loaded and streamed models, and don't change when the
1392
+ viewer moves or scales the model. `TileObjectCatalogEntry` is a deprecated
1393
+ alias of this type. To keep the list in state, use `useViewerModel` and pass
1394
+ its `handleObjectCatalog`.
1395
+
1396
+ ### `onModelLoaded`
1397
+
1398
+ Optional callback fired after the model has loaded, with its bounds and mesh
1399
+ count:
1400
+
1401
+ ```ts
1402
+ (model: ViewerModelInfo) => void
1403
+
1404
+ type ViewerModelInfo = {
1405
+ bounds: { min: Vec3; max: Vec3; center: Vec3; size: Vec3 }; // world space
1406
+ objectCount: number; // meshes in the loaded scene
1407
+ };
1408
+ ```
1409
+
1410
+ For the model's object names, use [`onObjectCatalog`](#onobjectcatalog).
1411
+
1084
1412
  ### `onLoadError`
1085
1413
 
1086
1414
  Optional callback fired if the model fails to load or render.
@@ -1091,6 +1419,22 @@ Current callback shape:
1091
1419
  (error: unknown) => void
1092
1420
  ```
1093
1421
 
1422
+ Whether or not you pass it, `ModelViewer` shows a message in place of the
1423
+ model that explains the likely cause: a missing file (404), refused access
1424
+ (401/403), a blocked cross-origin request (CORS), a web page served instead of
1425
+ a model, or a file that isn't the expected format. It includes the URL and the
1426
+ underlying error. Set `showLoadingOverlay={false}` to show your own instead.
1427
+
1428
+ ### `onProgress`
1429
+
1430
+ Called with the loader's `ProgressEvent` while a monolithic model is being
1431
+ downloaded. When `event.lengthComputable` is true, progress is
1432
+ `event.loaded / event.total`; the built-in loading overlay uses the same value.
1433
+
1434
+ ```ts
1435
+ onProgress?: (event: ProgressEvent) => void
1436
+ ```
1437
+
1094
1438
  ### `onAction`
1095
1439
 
1096
1440
  Optional callback fired when a built-in action button is clicked from the side panel.
@@ -1123,11 +1467,15 @@ Current callback shape:
1123
1467
 
1124
1468
  ```ts
1125
1469
  type ViewerReadyState = {
1126
- controls: CameraControls;
1127
- scene: Object3D | null;
1128
1470
  objectBindings: Record<string, ObjectBinding>;
1129
- nodeRefs: Record<string, Object3D>;
1471
+ homeCameraState: { position: Vec3; target: Vec3 } | null;
1130
1472
  captureImage: (options?: CaptureImageOptions) => Promise<string>;
1473
+ invalidate: () => void;
1474
+ raw: {
1475
+ controls: CameraControls; // camera-controls
1476
+ scene: Object3D | null; // three.js
1477
+ nodes: Record<string, Object3D>; // model nodes by binding key and name
1478
+ };
1131
1479
  };
1132
1480
 
1133
1481
  type CaptureImageOptions = {
@@ -1137,6 +1485,11 @@ type CaptureImageOptions = {
1137
1485
  };
1138
1486
  ```
1139
1487
 
1488
+ `raw` holds the live three.js scene, the camera-controls instance, and the
1489
+ model's nodes, for integrations the rest of the API doesn't cover. You don't
1490
+ need it for anything the hooks and props already do, and its shapes are those
1491
+ libraries' own, so they can change when those libraries do.
1492
+
1140
1493
  `captureImage` requires the opt-in `preserveDrawingBuffer` prop, forces a render, and resolves with a `data:image/png` URL. Pass `width`/`height` to render at a resolution other than the canvas's current size, and `transparent` to hide the configured background so the PNG carries an alpha channel instead. It rejects if called before the viewer is ready or when framebuffer preservation is disabled. The Download PNG menu item is also shown only when this prop is enabled.
1141
1494
 
1142
1495
  ```tsx
@@ -1205,6 +1558,41 @@ Example:
1205
1558
  />
1206
1559
  ```
1207
1560
 
1561
+ ### `texturePositioning`
1562
+
1563
+ Turns on texture positioning for objects with an uploaded texture. Off by
1564
+ default. When on, the **Change Material** panel gains Offset X/Y and Rotation
1565
+ controls, and double-clicking the object puts the viewer in Move/Zoom mode:
1566
+ drag to move the texture, scroll to scale it, and click the canvas or
1567
+ **Done positioning** to finish.
1568
+
1569
+ ```tsx
1570
+ <ModelViewer modelUrl="/sofa.glb" licenseKey="your-license-key" texturePositioning />
1571
+ ```
1572
+
1573
+ Changes are written to the binding's `style.material.texture` (`offset`,
1574
+ `rotation` in radians, and `scale`), so they reach `onObjectBindingsChange`
1575
+ and your saved bindings like any other edit.
1576
+
1577
+ To handle some of it yourself, pass an object instead of `true`. Any field you
1578
+ leave out keeps the viewer's own behavior:
1579
+
1580
+ ```ts
1581
+ type TexturePositioningControls = {
1582
+ offset?: [number, number]; // shown in the Offset fields instead of the binding's
1583
+ rotationDegrees?: number; // shown in the Rotation field, 0–360
1584
+ onOffsetChange?: (objectId: string, axisIndex: 0 | 1, value: number) => void;
1585
+ onRotationChange?: (objectId: string, degrees: number) => void;
1586
+ isPositioning?: boolean; // shows "Done positioning" instead of the hint
1587
+ onEndPositioning?: () => void; // after Done positioning ends the mode
1588
+ };
1589
+ ```
1590
+
1591
+ `onOffsetChange` and `onRotationChange` replace the viewer's writes from the
1592
+ panel fields; dragging and scrolling on the canvas still write to the binding.
1593
+
1594
+ ---
1595
+
1208
1596
  ### `onAnimationsReady`
1209
1597
 
1210
1598
  Optional callback fired after the GLB/GLTF model is loaded, providing animation controls. If the model contains no animations, it is still called with an empty `clips` array and no-op control functions.
@@ -1244,38 +1632,69 @@ When `sceneConfig.animations.autoplayClip` is set, `ModelViewer` will auto-play
1244
1632
  that clip on load and respect per-clip `loopMode`, `speed`, `displayName`, and
1245
1633
  soft-delete (`hidden`) settings.
1246
1634
 
1247
- ### `lights`
1248
-
1249
- Optional custom lighting to render inside the scene.
1635
+ ### Lighting
1250
1636
 
1251
- If omitted, `ModelViewer` uses the library's default light rig.
1252
-
1253
- Example:
1637
+ Lighting lives in `sceneConfig.lighting`: an ambient light plus a list of
1638
+ directional, point, spot, and hemisphere lights. Without a `sceneConfig`,
1639
+ `ModelViewer` uses its default rig (ambient, a hemisphere fill, and a
1640
+ shadow-casting key light). To use your own lights, patch the default scene
1641
+ config. `sceneLight` fills in every setting you leave out:
1254
1642
 
1255
1643
  ```tsx
1256
- function CustomLights() {
1257
- return (
1258
- <>
1259
- <ambientLight intensity={0.5} />
1260
- <directionalLight position={[4, 8, 4]} intensity={1.6} castShadow />
1261
- <pointLight position={[-3, 3, 2]} intensity={0.8} />
1262
- </>
1263
- );
1264
- }
1644
+ import {
1645
+ DEFAULT_SCENE_CONFIG,
1646
+ patchSceneConfig,
1647
+ sceneLight,
1648
+ } from "@liveroom-tech/react-immersive/utils";
1649
+
1650
+ const sceneConfig = patchSceneConfig(DEFAULT_SCENE_CONFIG, {
1651
+ lighting: {
1652
+ ambient: { intensity: 0.5 },
1653
+ lights: [
1654
+ sceneLight({ id: "key", type: "directional", position: [4, 8, 4], intensity: 1.6, castShadow: true }),
1655
+ sceneLight({ id: "fill", type: "point", position: [-3, 3, 2], intensity: 0.8 }),
1656
+ ],
1657
+ },
1658
+ });
1265
1659
 
1266
1660
  <ModelViewer
1267
1661
  modelUrl="/model.glb"
1268
1662
  licenseKey="your-license-key"
1269
- objectBindings={objectBindings}
1270
- lights={<CustomLights />}
1663
+ sceneConfig={sceneConfig}
1271
1664
  />;
1272
1665
  ```
1273
1666
 
1667
+ To change lights while the app runs, use the lights controller from
1668
+ `useViewer` (or `useSceneConfig`):
1669
+
1670
+ ```tsx
1671
+ const viewer = useViewer({ sceneConfig });
1672
+
1673
+ viewer.scene.lights.setIntensity("key", 2.2);
1674
+ viewer.scene.lights.toggle("fill");
1675
+ viewer.scene.lights.add({ id: "bulb", type: "point", color: "#ffd27d", intensity: 4 });
1676
+ viewer.scene.lights.attach("bulb", { objectId: "Lamp", offset: [0, 1.4, 0] });
1677
+ ```
1678
+
1679
+ An attached light follows the object, offset from the object's origin in its
1680
+ local space (the origin is often at its base, not its center). Lighting set up
1681
+ in `BindingBuilder`'s Scene tab exports as the same `sceneConfig`.
1682
+
1274
1683
  ### `camera`
1275
1684
 
1276
- Optional custom camera configuration passed through to the underlying React Three Fiber `Canvas`.
1685
+ Optional starting camera: where it is and its lens.
1686
+
1687
+ ```ts
1688
+ type ViewerCameraConfig = {
1689
+ position?: [number, number, number];
1690
+ fov?: number; // vertical field of view in degrees; default 50
1691
+ near?: number;
1692
+ far?: number;
1693
+ zoom?: number;
1694
+ };
1695
+ ```
1277
1696
 
1278
- `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.
1697
+ Without a `position`, the camera starts at `[0, 0, 5]`. Pass one for predictable framing.
1279
1698
 
1280
1699
  Example:
1281
1700
 
@@ -1295,6 +1714,50 @@ Example:
1295
1714
 
1296
1715
  Passing an explicit `camera` counts as choosing the initial view yourself, so the mount-time auto-fit (see [`onAutoFit`](#onautofit)) is skipped automatically and your `position`/`fov` stick. Provide `onAutoFit` as well only if you want to run your _own_ fit-to-scene logic instead.
1297
1716
 
1717
+ ### `cameraLimits`
1718
+
1719
+ Constrains how far the orbit camera can zoom, tilt, turn, and pan. Angles are
1720
+ in degrees.
1721
+
1722
+ ```tsx
1723
+ <ModelViewer
1724
+ modelUrl="/car.glb"
1725
+ licenseKey="your-license-key"
1726
+ cameraLimits={{
1727
+ minDistance: 2,
1728
+ maxDistance: 12,
1729
+ maxPolarAngle: 85, // stay above the floor
1730
+ minAzimuthAngle: -60, // turn at most 60° either side of the front
1731
+ maxAzimuthAngle: 60,
1732
+ }}
1733
+ />
1734
+ ```
1735
+
1736
+ ```ts
1737
+ type CameraLimits = {
1738
+ minDistance?: number; // default 0
1739
+ maxDistance?: number; // default Infinity
1740
+ minPolarAngle?: number; // degrees, default 0
1741
+ maxPolarAngle?: number; // degrees, default 180
1742
+ minAzimuthAngle?: number; // degrees, default -Infinity
1743
+ maxAzimuthAngle?: number; // degrees, default Infinity
1744
+ panBoundary?: [
1745
+ min: [number, number, number],
1746
+ max: [number, number, number],
1747
+ ];
1748
+ boundaryEnclosesCamera?: boolean; // default false
1749
+ boundaryFriction?: number; // default 0
1750
+ };
1751
+ ```
1752
+
1753
+ - **Polar** is the vertical angle: `0` looks straight down from above, `90` is
1754
+ level with the target, and `180` looks straight up from below.
1755
+ - **Azimuth** is the horizontal angle around the vertical axis: `0` views the
1756
+ target from the front (+Z), `90` from its right (+X), and `-90` from its left.
1757
+
1758
+ `panBoundary` is an axis-aligned world-space box. These limits apply to the
1759
+ orbit controller; pointer-lock mode is not orbiting around the same target.
1760
+
1298
1761
  ### `backgroundColor`
1299
1762
 
1300
1763
  Optional background color for the viewer canvas.
@@ -1337,14 +1800,25 @@ Example:
1337
1800
 
1338
1801
  ### `onCameraChange`
1339
1802
 
1340
- Optional callback fired whenever the camera controls update the camera.
1341
-
1342
- Current callback shape:
1803
+ Optional callback fired whenever the camera moves, with its plain state. It
1804
+ fires often while the user orbits, pans, or zooms.
1343
1805
 
1344
1806
  ```ts
1345
- (camera: Camera, controls: CameraControls) => void
1807
+ (state: ViewerCameraState) => void
1808
+
1809
+ type ViewerCameraState = {
1810
+ position: [number, number, number];
1811
+ target: [number, number, number];
1812
+ fov: number | null;
1813
+ zoom: number;
1814
+ azimuthAngle?: number; // radians, for setCameraState to restore
1815
+ polarAngle?: number; // radians
1816
+ };
1346
1817
  ```
1347
1818
 
1819
+ It's the same state `useViewerCamera` exposes as `cameraState`, and
1820
+ `setCameraState` restores it.
1821
+
1348
1822
  ### `showObjectBindingDataPanel`
1349
1823
 
1350
1824
  Optional boolean that controls whether the built-in left object details panel is rendered.
@@ -1373,6 +1847,57 @@ type CustomObjectBindingDataPanelProps = {
1373
1847
 
1374
1848
  When provided, `ModelViewer` passes its existing internal handlers into your custom panel. Your panel can call them, wrap them, or ignore them.
1375
1849
 
1850
+ ### `overlay`
1851
+
1852
+ Your own UI over the 3D view: badges, HUDs, buttons, custom panels. It sits
1853
+ above the model and below the viewer's toolbars and banners.
1854
+
1855
+ ```tsx
1856
+ <ModelViewer
1857
+ modelUrl="/sofa.glb"
1858
+ licenseKey="your-license-key"
1859
+ overlay={
1860
+ <div style={{ position: "absolute", top: 16, left: 16 }}>
1861
+ In stock <button onClick={addToCart}>Add to cart</button>
1862
+ </div>
1863
+ }
1864
+ />
1865
+ ```
1866
+
1867
+ The layer covers the view but lets pointer events through, so the model stays
1868
+ draggable around your elements. Its direct children receive pointer events, so
1869
+ position them yourself (for example with `position: "absolute"`) rather than
1870
+ wrapping everything in one full-size container.
1871
+
1872
+ ### `unsafe_sceneChildren`
1873
+
1874
+ react-three-fiber elements rendered inside the scene, for anything the viewer
1875
+ can't do itself, such as custom geometry, a helper, or an effect driven by
1876
+ `useFrame`:
1877
+
1878
+ ```tsx
1879
+ import { useFrame } from "@react-three/fiber";
1880
+
1881
+ function Marker() {
1882
+ const ref = useRef<Mesh>(null);
1883
+ useFrame((_, delta) => (ref.current!.rotation.y += delta));
1884
+ return (
1885
+ <mesh ref={ref} position={[0, 1.5, 0]}>
1886
+ <boxGeometry args={[0.3, 0.3, 0.3]} />
1887
+ <meshStandardMaterial color="hotpink" />
1888
+ </mesh>
1889
+ );
1890
+ }
1891
+
1892
+ <ModelViewer {...props} unsafe_sceneChildren={<Marker />} />;
1893
+ ```
1894
+
1895
+ They're rendered inside their own `Suspense` boundary, so a child that loads
1896
+ assets doesn't hold up the model. Because they work with three.js directly,
1897
+ they're outside the viewer's own API: the name says so, and they can break when
1898
+ three.js or react-three-fiber change. Prefer the props and hooks for anything
1899
+ they cover.
1900
+
1376
1901
  ### `customSceneObjectsPanel`
1377
1902
 
1378
1903
  Optional render prop for replacing the built-in right scene objects panel.
@@ -1442,7 +1967,7 @@ true;
1442
1967
 
1443
1968
  ### `showLoadingOverlay`
1444
1969
 
1445
- Optional boolean controlling whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling.
1970
+ Optional boolean controlling whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling, and the load-error message if the model fails to load (see [`onLoadError`](#onloaderror)).
1446
1971
 
1447
1972
  Default:
1448
1973
 
@@ -1542,6 +2067,17 @@ magenta and trigger an explanatory notice.
1542
2067
  />
1543
2068
  ```
1544
2069
 
2070
+ ### `onSceneConfigChange`
2071
+
2072
+ Called with an updated `SceneConfig` when viewer-owned scene editing changes
2073
+ the configuration. Currently this is used when a light-position or rotation
2074
+ gizmo moves a scene-config light. Update controlled `sceneConfig` state with
2075
+ the value returned by this callback.
2076
+
2077
+ ```ts
2078
+ onSceneConfigChange?: (config: SceneConfig) => void
2079
+ ```
2080
+
1545
2081
  ### `disableZoom`
1546
2082
 
1547
2083
  Optional boolean. When `true`, the viewer never zooms the camera to an object on selection (selection still highlights and fires callbacks).
@@ -1562,6 +2098,12 @@ Default:
1562
2098
  true;
1563
2099
  ```
1564
2100
 
2101
+ ### `highlightOnHover`
2102
+
2103
+ Controls hover highlighting for the canvas, built-in object list, and
2104
+ `panelHoveredObject`. Default: `true`. On very large models, viewport hover is
2105
+ disabled for performance, while panel-row highlighting remains available.
2106
+
1565
2107
  ### Moving and rotating objects
1566
2108
 
1567
2109
  Set `moveModeEnabled` to show a centered Drei `PivotControls` gizmo for the
@@ -1623,6 +2165,41 @@ is managed internally.
1623
2165
  Optional boolean controlling orbit, pan, and zoom input. Default: `true`.
1624
2166
  Camera controls are temporarily disabled whenever `moveModeEnabled` is on.
1625
2167
 
2168
+ ### `cameraControllerType` / `onCameraControllerChange`
2169
+
2170
+ Selects the active camera interaction mode:
2171
+
2172
+ - `"orbit"` (default) uses orbit, pan, and zoom controls.
2173
+ - `"pointerLock"` captures the pointer for first-person look-around. Leaving
2174
+ pointer lock returns the viewer to orbit mode.
2175
+
2176
+ The viewer's camera-controller menu can manage this internally. Pass
2177
+ `cameraControllerType` to control it from application state and update that
2178
+ state from `onCameraControllerChange`.
2179
+
2180
+ ```tsx
2181
+ const [controller, setController] = useState<"orbit" | "pointerLock">("orbit");
2182
+
2183
+ <ModelViewer
2184
+ {...props}
2185
+ cameraControllerType={controller}
2186
+ onCameraControllerChange={setController}
2187
+ />
2188
+ ```
2189
+
2190
+ Auto-rotation pauses while pointer-lock mode is active.
2191
+
2192
+ ### `autoRotate` / `autoRotateSpeed`
2193
+
2194
+ Enables automatic orbiting and controls its speed. Defaults are `false` and
2195
+ `1`. When these props aren't passed, `sceneConfig.model.autoRotate` and
2196
+ `sceneConfig.model.autoRotateSpeed` apply, so a scene exported from
2197
+ `BindingBuilder` can carry the same behavior. When they are passed, they win.
2198
+
2199
+ ```tsx
2200
+ <ModelViewer {...props} autoRotate autoRotateSpeed={0.75} />
2201
+ ```
2202
+
1626
2203
  ### `enableKeyboardNavigation`
1627
2204
 
1628
2205
  Optional boolean enabling camera shortcuts after the viewer canvas is clicked
@@ -2007,7 +2584,7 @@ type SimpleModelViewerProps = {
2007
2584
  ambientColor?: string;
2008
2585
  directionalIntensity?: number;
2009
2586
  directionalColor?: string;
2010
- onModelLoaded?: (scene: Object3D) => void;
2587
+ onModelLoaded?: (model: ViewerModelInfo) => void;
2011
2588
  onLoadError?: (error: unknown) => void;
2012
2589
  };
2013
2590
  ```
@@ -2134,7 +2711,8 @@ false;
2134
2711
 
2135
2712
  ### `onModelLoaded`
2136
2713
 
2137
- Optional callback fired after the GLB/GLTF scene has loaded.
2714
+ Optional callback fired after the model has loaded, with its bounds and mesh
2715
+ count (`(model: ViewerModelInfo) => void`, the same payload as `ModelViewer`'s).
2138
2716
 
2139
2717
  ### `onLoadError`
2140
2718
 
@@ -2175,7 +2753,7 @@ type ObjectBindingTransform = {
2175
2753
 
2176
2754
  type ObjectBinding = {
2177
2755
  id: string;
2178
- type: ObjectBindingType; // "body" | "light" | "wheel" | "glass" | "interior" | "floor" | "wall" | "door" | "furniture" | "decor" | "electronics" | "other" | ...
2756
+ type?: ObjectBindingType; // any string; suggestions: "body" | "light" | "wheel" | "wall" | "door" | "furniture" | ...; default "other"
2179
2757
  modelObjectId: string;
2180
2758
  label?: string;
2181
2759
  group?: string;
@@ -2208,11 +2786,27 @@ Notes:
2208
2786
  - `BindingBuilder` intentionally omits exact Sketchfab glossiness, cavity, and subsurface-scattering workflows; those require preprocessing or a different material pipeline
2209
2787
  - `cameraState` stores a saved camera view for the object; when both `position` and `target` are set, the viewer uses that view when the object is focused or selected instead of falling back to `fitToBox`
2210
2788
  - `metrics` and `metadata` are displayed in the side panel as nested key/value fields (objects expand into indented child labels)
2211
- - `ObjectBindingType` is a large union covering vehicles, rooms, furniture, characters, weapons, environment, and more
2789
+ - `type` groups and filters bindings (for example `hideObject({ type: "wheel" })`). Any string works: `ObjectBindingType` suggests values for vehicles, rooms, furniture, characters, weapons, environment, and more, but a domain it doesn't cover can use its own (`"bone"`, `"turbine-blade"`). Left out, it counts as `"other"`
2212
2790
 
2213
2791
  ## Supported Built-In Actions
2214
2792
 
2215
- Action handling is currently hardcoded in `ModelViewer`.
2793
+ `ModelViewer` handles six action ids itself: `change-color`,
2794
+ `change-material`, `toggle-visibility`, `toggle-light`, `change-light-color`,
2795
+ and `set-brightness`. Any other id does something only when you handle it in
2796
+ `onAction` (pass `useViewerActions().handleAction` there to run an action's
2797
+ declared `effects`). If a binding has such an action and `ModelViewer` has no
2798
+ `onAction`, its button does nothing, and the viewer logs a console warning
2799
+ naming each one. Every action also reaches `onAction`, after the viewer's own
2800
+ handling.
2801
+
2802
+ `ACTION_CATALOG` lists the ids `BindingBuilder` offers, and whether the viewer
2803
+ or your app handles each one (`handling: "viewer" | "host"`):
2804
+
2805
+ ```ts
2806
+ import { ACTION_CATALOG, findCatalogAction } from "@liveroom-tech/react-immersive/utils";
2807
+
2808
+ findCatalogAction("toggle-light")?.handling; // "viewer"
2809
+ ```
2216
2810
 
2217
2811
  ### Implemented behaviors
2218
2812
 
@@ -2222,6 +2816,26 @@ Action handling is currently hardcoded in `ModelViewer`.
2222
2816
  Opens the color picker inline beneath the action; the chosen hex is applied as a live preview while dragging and committed into `binding.style.material.baseColor` via `onObjectBindingsChange` when the picker closes
2223
2817
  - `change-material`
2224
2818
  Opens the texture upload panel inline beneath the action for JPG, PNG, and WebP files; if `onTextureUpload` is provided, the file is passed to it and the returned URL is stored in `binding.style.material.texture.path`; otherwise a session-scoped blob URL is used as the fallback
2819
+ - `toggle-light`
2820
+ Turns the object's light off, or back on
2821
+ - `change-light-color`
2822
+ Opens a color picker inline beneath the action that sets the light's color
2823
+ - `set-brightness`
2824
+ Opens a slider inline beneath the action that sets the light's brightness, from 0 to 200% of what it was when it opened
2825
+
2826
+ What "the object's light" is depends on the scene:
2827
+
2828
+ - If scene lights are attached to the object (in `BindingBuilder`'s Lighting
2829
+ tab, or with `useSceneConfig().lights.attach`), the actions control those
2830
+ lights, which light up the room around it. The changes go to
2831
+ `sceneConfig.lighting`: to `onSceneConfigChange` if you pass it (`useViewer`
2832
+ does, so `viewer.scene` stays current), otherwise `ModelViewer` keeps them
2833
+ itself, over whatever `sceneConfig` it's given.
2834
+ - Otherwise they control the object's own glow: its emissive color and strength,
2835
+ saved on the binding as `style.material.emissive` and
2836
+ `style.material.emissiveIntensity`, like any other edit. This works on any
2837
+ light fixture with no setup, but a glow doesn't light up the objects around
2838
+ it. Toggling off and back on restores the previous strength.
2225
2839
 
2226
2840
  ## How Rendering Works
2227
2841
 
@@ -2247,7 +2861,7 @@ When `shadows` is enabled (the default), the canvas renders soft shadow maps and
2247
2861
  - 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
2248
2862
  - a post-processing stack (`EffectComposer`) with SSAO ambient occlusion (normal pass enabled), bloom, vignette, and ACES filmic tone mapping
2249
2863
 
2250
- Pass `shadows={false}` to disable shadow rendering entirely, or pass a custom `lights` rig to replace the default lighting.
2864
+ Pass `shadows={false}` to disable shadow rendering entirely, or set your own lights in `sceneConfig.lighting` (see [Lighting](#lighting)) to replace the default rig.
2251
2865
 
2252
2866
  ## UI Behavior
2253
2867