@liveroom-tech/react-immersive 5.1.0 → 5.2.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 (44) hide show
  1. package/README.md +486 -57
  2. package/dist/binding-builder.css +1 -1
  3. package/dist/binding-builder.js +17 -13
  4. package/dist/binding-builder.mjs +1 -1
  5. package/dist/chunk-4NLJSD3S.mjs +1 -0
  6. package/dist/chunk-B7REIGDG.mjs +1 -0
  7. package/dist/{chunk-WN6GMMHF.mjs → chunk-HYFJ5DYJ.mjs} +1 -1
  8. package/dist/chunk-QC3NT76G.mjs +3 -0
  9. package/dist/chunk-T3M3BQ7H.mjs +32 -0
  10. package/dist/chunk-ZPK67UG7.mjs +1 -0
  11. package/dist/hosted.css +1 -1
  12. package/dist/hosted.d.mts +4 -3
  13. package/dist/hosted.d.ts +4 -3
  14. package/dist/hosted.js +16 -12
  15. package/dist/hosted.mjs +1 -1
  16. package/dist/index.css +1 -1
  17. package/dist/index.d.mts +39 -8
  18. package/dist/index.d.ts +39 -8
  19. package/dist/index.js +14 -10
  20. package/dist/index.mjs +1 -1
  21. package/dist/materialVariants-DnZe9eMC.d.mts +309 -0
  22. package/dist/materialVariants-DnZe9eMC.d.ts +309 -0
  23. package/dist/modelInfo-BYebMPdZ.d.mts +43 -0
  24. package/dist/modelInfo-XfUQO7C_.d.ts +43 -0
  25. package/dist/{ModelViewer-Bj-KjiDL.d.mts → modelViewerProps-CIZ2R_Fv.d.ts} +82 -10
  26. package/dist/{ModelViewer-BkQ73y6N.d.ts → modelViewerProps-DoRiTZMn.d.mts} +82 -10
  27. package/dist/{objectCatalog-BIeXURoq.d.ts → rendererBackend-QwMzISTn.d.mts} +32 -4
  28. package/dist/{objectCatalog-BIeXURoq.d.mts → rendererBackend-QwMzISTn.d.ts} +32 -4
  29. package/dist/simple-model-viewer.css +1 -1
  30. package/dist/simple-model-viewer.d.mts +32 -4
  31. package/dist/simple-model-viewer.d.ts +32 -4
  32. package/dist/simple-model-viewer.js +3 -3
  33. package/dist/simple-model-viewer.mjs +1 -1
  34. package/dist/utils.d.mts +5 -4
  35. package/dist/utils.d.ts +5 -4
  36. package/dist/utils.js +1 -1
  37. package/dist/utils.mjs +1 -1
  38. package/package.json +2 -1
  39. package/dist/chunk-32WCLPTZ.mjs +0 -1
  40. package/dist/chunk-CSGNL4XU.mjs +0 -3
  41. package/dist/chunk-F2ILTIPG.mjs +0 -28
  42. package/dist/chunk-V6GALFL5.mjs +0 -1
  43. package/dist/modelInfo-DnkmTGMd.d.mts +0 -23
  44. package/dist/modelInfo-DnkmTGMd.d.ts +0 -23
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # React Immersive — 3D model viewer for React
2
2
 
3
- `@liveroom-tech/react-immersive` is a React-based 3D model viewer for interactive GLB, GLTF, OBJ, FBX, and USDZ assets. It renders a model with React Three Fiber, lets users click named meshes, shows a built-in side panel for the selected object, and supports per-object actions such as color changes, texture uploads, and visibility toggles.
3
+ `@liveroom-tech/react-immersive` is a React-based 3D model viewer for interactive GLB, GLTF, OBJ, FBX, USDZ, STL, and PLY assets. It renders a model with React Three Fiber, lets users click named meshes, shows a built-in side panel for the selected object, and supports per-object actions such as color changes, texture uploads, and visibility toggles.
4
4
 
5
5
  [Website](https://react-immersive.liveroom.dev) · [Documentation](https://react-immersive.liveroom.dev/docs) · [Live editor](https://react-immersive.liveroom.dev/editor) · [npm](https://www.npmjs.com/package/@liveroom-tech/react-immersive) · [GitHub](https://github.com/Liveroom-Technologies/react-immersive-docs)
6
6
 
@@ -18,6 +18,7 @@ import {
18
18
  useObjectBindingIds,
19
19
  useObjectBindings,
20
20
  useSceneConfig,
21
+ useShareableViewerState,
21
22
  useViewer,
22
23
  useViewerActions,
23
24
  useViewerAnimations,
@@ -25,6 +26,7 @@ import {
25
26
  useViewerConnection,
26
27
  useViewerEffects,
27
28
  useViewerHover,
29
+ useViewerIsolation,
28
30
  useViewerModel,
29
31
  useViewerSelection,
30
32
  MATERIAL_BLENDING_MODES,
@@ -41,7 +43,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
41
43
 
42
44
  `ModelViewer` provides:
43
45
 
44
- - GLB/GLTF, OBJ/MTL, FBX, and USDZ model rendering through `@react-three/fiber`
46
+ - GLB/GLTF, OBJ/MTL, FBX, USDZ, STL, and PLY model rendering through `@react-three/fiber`
45
47
  - camera-driven 3D Tiles streaming through `tilesetUrl`, with configurable screen-space error and bounded tile cache
46
48
  - orbit/pan/zoom camera controls via `CameraControls`
47
49
  - 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
@@ -51,6 +53,7 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
51
53
  - optional shadow rendering toggle through a `shadows` prop (on by default)
52
54
  - optional on-screen movement controller through `showMouseController` and its tuning props
53
55
  - optional canvas-focused keyboard camera navigation through `enableKeyboardNavigation`
56
+ - keyboard object navigation, screen-reader selection announcements, and reduced motion for visitors who ask for it (see [Accessibility](#accessibility))
54
57
  - optional top-right camera orientation control through `showViewGizmo`
55
58
  - optional per-object move and rotate gizmo through `moveModeEnabled`, with World/Local axis alignment through `objectTransformSpace`
56
59
  - optional custom left object data panel through `customObjectBindingDataPanel`
@@ -59,18 +62,21 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
59
62
  - optional hiding of either built-in panel through `showObjectBindingDataPanel` and `showSceneObjectsPanel`
60
63
  - 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
61
64
  - an optional UV Checker toolbar button through `showUvCheckerButton`, for inspecting UV scale, stretching, seams, orientation, and missing UV0 coordinates directly in `ModelViewer`
62
- - glTF material variants (`KHR_materials_variants`) with a built-in picker through `showMaterialVariants`, selected by `sceneConfig.model.materialVariant`
65
+ - glTF material variants (`KHR_materials_variants`) as swatch rows, one per option group, through `showMaterialVariants`, selected by `sceneConfig.model.activeVariants`
63
66
  - distance, angle, polygon area, and point-to-plane measurements with vertex/edge snapping and bounding dimensions through `showMeasureTools` and `measurementUnit`
64
67
  - an exploded-view slider that slides each bound part outward from the model center to reveal interior/assembly structure through `showExplodeControls`
65
68
  - isolation that ghosts or hides everything but the objects you focus on, through `isolation` / `onIsolationChange` and `showIsolateControls`
66
69
  - a section cut that opens the model along an axis, with a draggable plane, through `showSectionTools` and `sceneConfig.section`
67
70
  - 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
68
- - video export of a cinematic pass or turntable revolution, through the download menu or `captureVideo`, using browser-supported WebM/MP4 encoding
71
+ - video export of a cinematic pass or turntable revolution, through the download menu or `captureVideo`, as browser-supported WebM/MP4 or an animated GIF
72
+ - `CompareViewer`, which shows two configurations (or two models) with one camera, as a before/after slider or side by side
73
+ - opt-in WebGPU rendering through `perf={{ webgpu: true }}`, falling back to WebGL where the browser lacks it
74
+ - lazy loading: a viewer starts its 3D view when it's near the screen, and a page of viewers stays within the browser's limit on 3D views, with a `poster` image and an optional "View in 3D" button (`reveal`) until the view is there (see [Lazy loading and posters](#lazy-loading-and-posters))
69
75
  - a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via `showAnnotationNavigation`
70
76
  - optional annotation detail popups on marker hover via `showAnnotationOnHover`
71
77
  - PBR, Matcap, and UV Checker renderer modes, with the checker exposing UV stretching, seams, rotation, and missing UV0 data directly on the model
72
78
  - a `sceneConfig` prop accepting the same scene-wide config (lighting, environment, background, post-processing, animations, annotations) authored by `BindingBuilder`'s Scene tab
73
- - WebGL context-loss recovery with a reload button and automatic rebuild when the context returns
79
+ - WebGL context-loss recovery with a reload button and automatic rebuild when the context returns, keeping the camera and the visitor's changes
74
80
  - render-loop and perf tuning through `renderMode`, `maxDpr`, `performanceProfile`, and compressed-asset decoder options (`dracoDecoderPath`, `ktx2TranscoderPath`, `meshopt`)
75
81
  - WebXR "View in your space" (AR) and "Enter VR" through `enableXR`, with tap-to-place, pinch-to-resize, and twist-to-rotate AR placement gestures
76
82
  - mesh selection by object name
@@ -97,8 +103,8 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
97
103
 
98
104
  `BindingBuilder` provides:
99
105
 
100
- - a design-time UI for generating starter bindings from GLB, GLTF, OBJ, FBX, and USDZ assets, gated by `licenseKey` (see "`BindingBuilder` licensing & plan tiers" below)
101
- - demo model loading or custom `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, or model ZIP bundle upload
106
+ - a design-time UI for generating starter bindings from GLB, GLTF, OBJ, FBX, USDZ, STL, and PLY assets, gated by `licenseKey` (see "`BindingBuilder` licensing & plan tiers" below)
107
+ - demo model loading or custom `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or model ZIP bundle upload
102
108
  - editable binding fields for identity, basics, style, actions, metrics, metadata, and saved camera state
103
109
  - per-object move and rotate authoring with a geometry-centered pivot, World/Local axes, numeric fields, and restore-to-authored-transform support
104
110
  - a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
@@ -112,8 +118,8 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
112
118
 
113
119
  `SimpleModelViewer` provides:
114
120
 
115
- - a lightweight GLB/GLTF/OBJ/FBX/USDZ viewer with no bindings or actions that does not require a `licenseKey`
116
- - 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
121
+ - a lightweight GLB/GLTF/OBJ/FBX/USDZ/STL/PLY viewer with no bindings or actions that does not require a `licenseKey`
122
+ - optional local `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or model ZIP bundle upload mode for ad-hoc inspection without relying on the `modelUrl` asset
117
123
  - built-in scene objects panel with search and visibility toggles
118
124
  - 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
119
125
  - click-to-select and fit-to-object focus behavior
@@ -763,8 +769,9 @@ type BindingBuilderProps = {
763
769
 
764
770
  Current behavior, Object tab:
765
771
 
766
- - 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)
772
+ - upload a `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` 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)
767
773
  - traverse renderable mesh nodes and generate starter bindings automatically
774
+ - for an object without texture coordinates (STL, most PLY), the **Material** image map fields are turned off with a note, and the starter actions leave out **Change Material** (see [STL and PLY](#stl-and-ply))
768
775
  - edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
769
776
  - add decals to the selected object from the **Decals** panel: upload an image or add text to place it automatically at the centre of the camera-facing surface, then set its size, rotation, and opacity; **Move** lets you drag the decal across the object's surface. Release to save, or press Escape to cancel. Uploaded images are exported under `materials/` with the bindings
770
777
  - 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
@@ -778,7 +785,8 @@ Current behavior, Scene tab:
778
785
  - configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
779
786
  - configure auto-rotation and its speed, exported as `sceneConfig.model.autoRotate` and `sceneConfig.model.autoRotateSpeed`
780
787
  - preview animation clips, several at once and forwards or backwards, and set each clip's loop mode (loop, play once, or ping-pong), speed, and display name
781
- - pick which of the model's glTF material variants the scene starts with, exported as `sceneConfig.model.materialVariant`
788
+ - check changes with **Compare** above the preview: a before/after divider across the preview, with the project as it was opened on the left and as it is now on the right; orbiting either side moves both, and **Pin** on the "Before" label makes the current state the "before" for the next round of changes
789
+ - pick which of the model's glTF material variants the scene starts with, one per option group, exported as `sceneConfig.model.activeVariants`
782
790
  - isolate a node from the Scene Nodes list to see it on its own in the preview while you edit it (not exported)
783
791
  - set up a section cut (**General → Section Cut**: axis, position, and side to keep), exported as `sceneConfig.section`; the preview has the section toolbar and draggable plane too
784
792
  - build style rules (**Rules** sub-tab) that restyle objects from their metrics, status, and metadata, with suggestions from the model's bindings, a live match count, and a live preview; exported as `sceneConfig.styleRules`
@@ -794,7 +802,7 @@ Preview:
794
802
  Autosave:
795
803
 
796
804
  - 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
797
- - **Restore last session** in the preview toolbar reopens it; one session is kept per browser
805
+ - **Restore last session** in the preview's "⋯" menu reopens it; one session is kept per browser
798
806
  - while there are edits on screen, leaving or reloading the page asks for confirmation
799
807
  - if the browser refuses to store a session, usually for lack of space, the editor says so; export your work to keep it
800
808
  - 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`
@@ -813,7 +821,7 @@ A locked feature renders in place (not hidden) with a message naming the require
813
821
 
814
822
  ## `SimpleModelViewer`
815
823
 
816
- `SimpleModelViewer` is an exported lightweight viewer for cases where you want to load a GLB, GLTF, OBJ, FBX, or USDZ asset, inspect meshes, and toggle visibility without building `objectBindings`. It can either load a fixed `modelUrl` or, when `enableModelUpload` is turned on, let the user drag-and-drop or choose a local model at runtime.
824
+ `SimpleModelViewer` is an exported lightweight viewer for cases where you want to load a GLB, GLTF, OBJ, FBX, USDZ, STL, or PLY asset, inspect meshes, and toggle visibility without building `objectBindings`. It can either load a fixed `modelUrl` or, when `enableModelUpload` is turned on, let the user drag-and-drop or choose a local model at runtime.
817
825
 
818
826
  Basic usage:
819
827
 
@@ -900,6 +908,60 @@ const {
900
908
 
901
909
  Pass `initialPosition`, `initialTarget`, and optional named `presets` to `useViewerCamera` to configure the first view without writing an `onViewerReady` wrapper.
902
910
 
911
+ If you want a link that opens the viewer the way it looks now, the library also exports `useShareableViewerState`. It keeps the camera, the hidden objects, and the selection in the page URL's hash (`#view=…`), and puts them back when the link is opened:
912
+
913
+ ```tsx
914
+ const viewer = useViewer();
915
+ const share = useShareableViewerState({
916
+ cameraState: viewer.camera.cameraState,
917
+ setCameraState: viewer.camera.setCameraState,
918
+ objectBindings: viewer.bindings.objectBindings,
919
+ onObjectBindingsChange: viewer.bindings.setObjectBindings,
920
+ selectedObjectBinding: viewer.selection.selectedObjectBinding,
921
+ setSelectedObjectBinding: viewer.selection.setSelectedObjectBinding,
922
+ });
923
+
924
+ <ModelViewer
925
+ modelUrl="/model.glb"
926
+ licenseKey="your-license-key"
927
+ {...viewer.props}
928
+ onViewerReady={(ready) => {
929
+ viewer.props.onViewerReady(ready);
930
+ void share.restoreFromUrl();
931
+ }}
932
+ refitOnResize={false}
933
+ />;
934
+
935
+ <button onClick={() => navigator.clipboard.writeText(share.getShareUrl())}>
936
+ Copy link
937
+ </button>;
938
+ ```
939
+
940
+ `useShareableViewerState` takes:
941
+
942
+ - `cameraState` and `setCameraState`: from `useViewerCamera` (`viewer.camera`)
943
+ - `objectBindings` and `onObjectBindingsChange`: the bindings and their setter; hidden objects are restored by setting their `visible` to `false`
944
+ - `selectedObjectBinding` and `setSelectedObjectBinding`: from `useViewerSelection` (`viewer.selection`). `ModelViewer` also needs `selectedObject` (`viewer.props` passes it), or a restored selection won't show
945
+ - `paramKey`: the hash key the view is stored under. Default `"view"`
946
+ - `debounceMs`: how long after a change the hash is updated, in milliseconds. Default `400`
947
+ - `autoSync`: keep the hash up to date as the view changes. Default `true`. With `false`, write it with `syncUrl` or build links with `getShareUrl`
948
+
949
+ `useShareableViewerState` returns:
950
+
951
+ - `getShareUrl`: the current page URL with the current view in its hash, without changing the address bar
952
+ - `syncUrl`: writes the current view into the hash now, with `history.replaceState`, so no history entry is added
953
+ - `restoreFromUrl`: applies the view in the hash, if there is one, and resolves `true` once it has applied all of it. Called again, it does nothing until the hash holds a different view, so it's safe to call from `onViewerReady`, which runs often
954
+
955
+ Things to know:
956
+
957
+ - The link holds the view, not the model. Keep whatever identifies the model, such as a route or query parameter, in the URL too. Objects are stored by `binding.id`, so links keep working while the ids stay the same.
958
+ - The hash is only updated after `restoreFromUrl` has been called once, so an incoming link isn't overwritten before it's read. Call it even if you don't restore links; with nothing in the hash it does nothing.
959
+ - When the bindings come from the model, as with `useViewer` or `objectBindings="auto"`, they exist only once it has loaded, after the first `onViewerReady`. A link that hides or selects objects then restores its camera at once and waits for them: `onViewerReady` runs again when the bindings arrive, and that call hides and selects them. The hash isn't updated while it waits.
960
+ - The camera jumps to the shared view without a transition. `refitOnResize={false}` keeps a later resize from re-framing the model away from it.
961
+ - The hook listens for `hashchange`, so pasting a newer link into an open tab applies it.
962
+
963
+ See the [Shareable View example](https://react-immersive.liveroom.dev/examples/shareable-view) and the [`useShareableViewerState` docs](https://react-immersive.liveroom.dev/docs/hooks/use-shareable-viewer-state).
964
+
903
965
  If your GLB/GLTF model contains animations, the library also exports:
904
966
 
905
967
  ```tsx
@@ -1000,6 +1062,90 @@ stays open. `"ping-pong"` plays forwards then backwards, repeatedly. Playing a
1000
1062
  clip that's already playing in the same direction leaves it running; playing
1001
1063
  it the other way turns it around where it is.
1002
1064
 
1065
+ ## `CompareViewer`
1066
+
1067
+ Shows two configurations next to each other with one camera: a before and
1068
+ after of a renovation, two paint options, the current part against its
1069
+ replacement. Each side is a full [`ModelViewer`](#public-api),
1070
+ so a side can differ in anything a viewer takes: `objectBindings`,
1071
+ `sceneConfig`, material variants, even a different `modelUrl`.
1072
+
1073
+ ```tsx
1074
+ import "@liveroom-tech/react-immersive/styles.css";
1075
+ import { CompareViewer } from "@liveroom-tech/react-immersive";
1076
+
1077
+ <div style={{ height: 600 }}>
1078
+ <CompareViewer
1079
+ modelUrl="/kitchen.glb"
1080
+ licenseKey="your-license-key"
1081
+ before={{ label: "Current", objectBindings: currentBindings }}
1082
+ after={{ label: "Proposed", objectBindings: proposedBindings }}
1083
+ />
1084
+ </div>;
1085
+ ```
1086
+
1087
+ ### Layouts
1088
+
1089
+ - **`layout="slider"`** (default) lays the two views over each other. A divider
1090
+ splits them: the left of it shows `before`, the right shows `after`. Drag the
1091
+ divider, or focus it and use the arrow keys, Page Up/Down, Home, and End. It
1092
+ is a slider to screen readers, with each side's label and share.
1093
+ - **`layout="side-by-side"`** shows the two views next to each other, each half
1094
+ the width.
1095
+
1096
+ ```tsx
1097
+ <CompareViewer
1098
+ modelUrl="/car.glb"
1099
+ licenseKey="your-license-key"
1100
+ layout="side-by-side"
1101
+ before={{ sceneConfig: { ...config, model: { ...config.model, activeVariants: ["Paint: Red"] } } }}
1102
+ after={{ sceneConfig: { ...config, model: { ...config.model, activeVariants: ["Paint: Blue"] } } }}
1103
+ />
1104
+ ```
1105
+
1106
+ ### One camera
1107
+
1108
+ Orbiting, panning, or zooming either side moves both. The side you last
1109
+ grabbed leads and the other copies it every frame, so a fly-in, a zoom to a
1110
+ selected object, or a refit on one side shows on both. With two different
1111
+ models, the views line up on the same point in space, so the models should
1112
+ share a coordinate system.
1113
+
1114
+ ### Props
1115
+
1116
+ ```ts
1117
+ type CompareViewerProps = Partial<ModelViewerProps> & {
1118
+ licenseKey: string;
1119
+ before: CompareViewerSide; // the left side
1120
+ after: CompareViewerSide; // the right side
1121
+ layout?: "slider" | "side-by-side"; // default "slider"
1122
+ split?: number; // the divider, 0 (left edge) to 1; default 0.5
1123
+ onSplitChange?: (split: number) => void;
1124
+ };
1125
+
1126
+ type CompareViewerSide = Partial<ModelViewerProps> & {
1127
+ label?: string; // shown over the side; default "Before" / "After"
1128
+ };
1129
+ ```
1130
+
1131
+ Every other prop is shared by both sides, and a side's own props win.
1132
+ Callbacks run per side: give `before.onObjectSelect` and
1133
+ `after.onObjectSelect` to tell the sides apart. Both sides start from the
1134
+ `minimal` [preset](#presets-and-grouped-props),
1135
+ with no panels or buttons, so their interfaces don't overlap; pass `preset`
1136
+ or `ui` to change that. Pass `split` to control the divider yourself;
1137
+ without it, `CompareViewer` keeps the position.
1138
+
1139
+ > Each side has its own WebGL (or [WebGPU](#webgpu)) canvas. The two share one download and parse of the model file, but the GPU holds two copies of its geometry and textures.
1140
+
1141
+ `poster`, `loading` and `reveal` work as on
1142
+ [`ModelViewer`](#lazy-loading-and-posters). With `reveal="interaction"`, one
1143
+ **View in 3D** button starts both sides.
1144
+
1145
+ The divider and the default side labels use the
1146
+ [`labels`](https://react-immersive.liveroom.dev/docs/reference/labels) keys `compareBefore`, `compareAfter`,
1147
+ `compareDivider`, and `compareDividerValue`.
1148
+
1003
1149
  ## Presets and grouped props
1004
1150
 
1005
1151
  `preset` sets `ModelViewer` up for a common kind of page:
@@ -1031,10 +1177,10 @@ readable:
1031
1177
 
1032
1178
  | Group | Fields, and the flat prop each one sets |
1033
1179
  | --- | --- |
1034
- | `ui` | `objectPanel` (`showObjectBindingDataPanel`), `sceneObjects` (`showSceneObjectsPanel`), `download` (`showDownloadButton`; a string also sets `downloadFilename`), `reset` (`showResetButton`), `fullscreen` (`showFullscreenButton`), `loadingOverlay` (`showLoadingOverlay`), `viewGizmo` (`showViewGizmo`), `mouseController` (`showMouseController`; `{ position, opacity }` also sets `mouseControllerPosition` and `mouseControllerOpacity`), `annotationNavigation` (`showAnnotationNavigation`), `annotationOnHover` (`showAnnotationOnHover`), `highlightOnHover`, `materialVariants` (`showMaterialVariants`) |
1180
+ | `ui` | `objectPanel` (`showObjectBindingDataPanel`), `sceneObjects` (`showSceneObjectsPanel`), `download` (`showDownloadButton`; a string also sets `downloadFilename`), `reset` (`showResetButton`), `fullscreen` (`showFullscreenButton`), `loadingOverlay` (`showLoadingOverlay`), `viewGizmo` (`showViewGizmo`), `mouseController` (`showMouseController`; `{ position, opacity }` also sets `mouseControllerPosition` and `mouseControllerOpacity`), `annotationNavigation` (`showAnnotationNavigation`), `annotationOnHover` (`showAnnotationOnHover`), `highlightOnHover`, `materialVariants` (`showMaterialVariants`), `objectNavigator` (`showObjectNavigator`) |
1035
1181
  | `tools` | `measure` (`showMeasureTools`; `{ unit }` also sets `measurementUnit`), `uvChecker` (`showUvCheckerButton`), `explode` (`showExplodeControls`), `section` (`showSectionTools`), `isolate` (`showIsolateControls`), `texturePositioning` (`texturePositioning`) |
1036
- | `controls` | `enabled` (`enableCameraControls`), `keyboard` (`enableKeyboardNavigation`), `zoom` (the opposite of `disableZoom`), `zoomOnSelect` (`zoomOnSelected`), `autoRotate` (a number also sets `autoRotateSpeed`), `refitOnResize`, `limits` (`cameraLimits`), `moveSensitivity`, `zoomSensitivity` |
1037
- | `perf` | `renderMode`, `maxDpr`, `profile` (`performanceProfile`), `preserveDrawingBuffer` |
1182
+ | `controls` | `enabled` (`enableCameraControls`), `keyboard` (`enableKeyboardNavigation`), `zoom` (the opposite of `disableZoom`), `zoomOnSelect` (`zoomOnSelected`), `autoRotate` (a number also sets `autoRotateSpeed`), `refitOnResize`, `limits` (`cameraLimits`), `moveSensitivity`, `zoomSensitivity`, `reducedMotion` |
1183
+ | `perf` | `renderMode`, `maxDpr`, `profile` (`performanceProfile`), `preserveDrawingBuffer`, `webgpu` |
1038
1184
  | `tiles` | `url` (`tilesetUrl`), `errorTarget` (`tilesErrorTarget`), `cacheSize` (`tilesCacheSize`) |
1039
1185
  | `xr` | `enabled` (`enableXR`), `usdzUrl`, `scaleMode` (`arScaleMode`), `mobileHandoffUrl` |
1040
1186
  | `decoders` | `draco` (`dracoDecoderPath`), `ktx2` (`ktx2TranscoderPath`), `meshopt` |
@@ -1057,11 +1203,14 @@ type Props = {
1057
1203
  xr?: ModelViewerXrOptions;
1058
1204
  decoders?: ModelViewerDecoderOptions;
1059
1205
  modelUrl: string;
1206
+ poster?: string;
1207
+ loading?: "lazy" | "eager"; // default "lazy"
1208
+ reveal?: "auto" | "interaction"; // default "auto"
1060
1209
  tilesetUrl?: string | null;
1061
1210
  tilesErrorTarget?: number;
1062
1211
  tilesCacheSize?: number;
1063
1212
  onObjectCatalog?: (objects: ViewerObjectInfo[]) => void;
1064
- modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz";
1213
+ modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz" | "stl" | "ply";
1065
1214
  licenseKey: string;
1066
1215
  objectBindings?: Record<string, ObjectBinding> | "auto"; // default "auto"
1067
1216
  selectedObject?: ObjectBinding | null;
@@ -1127,11 +1276,14 @@ type Props = {
1127
1276
  moveModeEnabled?: boolean;
1128
1277
  objectTransformSpace?: "local" | "world";
1129
1278
  enableKeyboardNavigation?: boolean;
1279
+ showObjectNavigator?: boolean;
1280
+ reducedMotion?: "user" | "always" | "never";
1130
1281
  onAutoFit?: () => Promise<boolean>;
1131
1282
  refitOnResize?: boolean;
1132
1283
  renderMode?: "always" | "demand";
1133
1284
  maxDpr?: number;
1134
1285
  preserveDrawingBuffer?: boolean;
1286
+ webgpu?: boolean;
1135
1287
  performanceProfile?: "auto" | "high" | "low";
1136
1288
  dracoDecoderPath?: string | false;
1137
1289
  ktx2TranscoderPath?: string | false;
@@ -1163,7 +1315,7 @@ type Props = {
1163
1315
 
1164
1316
  ### `modelUrl`
1165
1317
 
1166
- URL or public path to a `.glb`, `.gltf`, `.obj`, `.fbx`, or `.usdz` model. Hosted GLTF, OBJ, and FBX assets must serve external buffers, MTL files, and textures at their declared relative paths. USDZ dependencies are contained in the USDZ package. Use `modelFormat` when a signed or extensionless URL cannot be auto-detected.
1318
+ URL or public path to a `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` model. Hosted GLTF, OBJ, and FBX assets must serve external buffers, MTL files, and textures at their declared relative paths. USDZ dependencies are contained in the USDZ package. Use `modelFormat` when a signed or extensionless URL cannot be auto-detected.
1167
1319
 
1168
1320
  Example:
1169
1321
 
@@ -1174,7 +1326,33 @@ modelUrl = "/model.glb";
1174
1326
  ### `modelFormat`
1175
1327
 
1176
1328
  Optional explicit format for signed or extensionless model URLs. Normal URLs
1177
- ending in `.glb`, `.gltf`, `.obj`, `.fbx`, or `.usdz` are detected automatically.
1329
+ ending in `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` are detected automatically.
1330
+
1331
+ ### STL and PLY
1332
+
1333
+ STL and PLY files hold geometry only: no object names, materials or
1334
+ animations.
1335
+
1336
+ - **One object per file**, named after the file: `bracket.stl` loads as one
1337
+ object keyed `bracket`, which its bindings use. A text STL with several named
1338
+ solids loads one object per solid. A `.zip` holding one STL or PLY file also
1339
+ works.
1340
+ - **Colour:** objects start a matte grey. A PLY file's vertex colours show as
1341
+ they are, and picking a colour (or texture) replaces them.
1342
+ - **No texture coordinates:** STL files have none and PLY files rarely do, so an
1343
+ image can't be mapped onto them. The viewer's **Change Material** says so
1344
+ instead of offering an upload, BindingBuilder turns off its image map fields
1345
+ for that object, and new bindings leave Change Material out. [Decals](#decals)
1346
+ work without texture coordinates. This applies to any object without them, in
1347
+ any format.
1348
+ - **Units and orientation:** STL files are usually in millimetres with Z up, so a
1349
+ part can load lying on its back. Turn it upright with
1350
+ [`sceneConfig.model.rotation`](#sceneconfig) (BindingBuilder: **Scene** →
1351
+ model rotation, −90° on X), and set [`measurementUnit`](#measurementunit) to
1352
+ `"mm"`. In AR, the default `"normalized"` [`arScaleMode`](#arscalemode) fits the
1353
+ model to a metre; `"real-world"` would read millimetres as metres.
1354
+ - **PLY point clouds** (points with no faces) can't be shown; the viewer
1355
+ explains why instead of loading them.
1178
1356
 
1179
1357
  ### 3D Tiles streaming
1180
1358
 
@@ -1463,6 +1641,7 @@ type ViewerModelInfo = {
1463
1641
  bounds: { min: Vec3; max: Vec3; center: Vec3; size: Vec3 }; // world space
1464
1642
  objectCount: number; // meshes in the loaded scene
1465
1643
  materialVariants: string[]; // glTF material variant names, if any
1644
+ materialVariantGroups: MaterialVariantGroup[]; // the same, as option groups
1466
1645
  };
1467
1646
  ```
1468
1647
 
@@ -1546,8 +1725,8 @@ elapsed time includes loading and background time; it is not an attention metric
1546
1725
 
1547
1726
  | Event `type` | Details and trigger |
1548
1727
  | --- | --- |
1549
- | `part-viewed` | `objectId`, `objectKey`, optional `label`, and `source` (`"canvas"` or `"panel"`). Selecting/focusing a different part; reselecting the current part is ignored. |
1550
- | `variant-chosen` | `variant` and `previousVariant` (names or `null` for the default). Choosing a different built-in material variant. |
1728
+ | `part-viewed` | `objectId`, `objectKey`, optional `label`, and `source` (`"canvas"`, `"panel"`, or `"keyboard"`). Selecting/focusing a different part; reselecting the current part is ignored. |
1729
+ | `variant-chosen` | `variant`, `previousVariant` (names, or `null` for the default), and `group` (the option group's name, or `null`). Choosing or clearing a built-in material variant; `previousVariant` is the earlier choice in the same group. |
1551
1730
  | `ar-started` | A confirmed immersive WebXR AR session begins. |
1552
1731
  | `ar-ended` | `durationMs` and `reason` (`"session-ended"` or `"interrupted"`). A confirmed AR session ends, or tracking stops on model change, unmount, or context loss. |
1553
1732
  | `ar-launch-requested` | `channel` (`"quick-look"` or `"mobile-handoff"`). Opening external AR or its QR handoff; this does not claim an AR session started. |
@@ -1585,6 +1764,7 @@ type ViewerReadyState = {
1585
1764
  captureImage: (options?: CaptureImageOptions) => Promise<string>;
1586
1765
  captureVideo: (options?: CaptureVideoOptions) => Promise<Blob>;
1587
1766
  invalidate: () => void;
1767
+ rendererBackend: "webgl" | "webgpu"; // which renderer draws; see `webgpu`
1588
1768
  raw: {
1589
1769
  controls: CameraControls; // camera-controls
1590
1770
  scene: Object3D | null; // three.js
@@ -1651,7 +1831,7 @@ const abort = new AbortController();
1651
1831
  // Run in an event handler. Upload the Blob or preview it with an object URL.
1652
1832
  const clip = await viewer.connection.captureVideo({
1653
1833
  mode: "turntable", // or "cinematic" to use the enabled camera path
1654
- format: "auto", // prefers WebM; falls back to MP4
1834
+ format: "auto", // prefers WebM; falls back to MP4. "gif" for an animated GIF
1655
1835
  duration: 12, // seconds; defaults to path timing or a 24-second orbit
1656
1836
  fps: 30, // 1–60; actual throughput depends on rendering/encoding
1657
1837
  videoBitsPerSecond: 8_000_000,
@@ -1661,7 +1841,7 @@ const clip = await viewer.connection.captureVideo({
1661
1841
  ```
1662
1842
 
1663
1843
  The download menu offers **Record turntable video** and, when cinematic is
1664
- enabled, **Record cinematic video**, plus Auto/WebM/MP4 format selection. A
1844
+ enabled, **Record cinematic video**, plus Auto/WebM/MP4/GIF format selection. A
1665
1845
  recording shows progress and a Cancel control. The filename and Blob MIME type
1666
1846
  match the encoded format; an explicitly requested unsupported format rejects
1667
1847
  instead of silently changing formats. With `loop: true`, a cinematic export
@@ -1671,11 +1851,32 @@ Recording runs in real time at the canvas's current pixel resolution, includes
1671
1851
  post-processing, and hides editor gizmos. It pauses cinematic playback, blocks
1672
1852
  viewer gestures, then restores the previous camera pose and controls on success,
1673
1853
  cancellation, or failure. Keep the tab visible until recording completes.
1674
- `preserveDrawingBuffer` is unnecessary for video. No audio, HTML overlays,
1675
- GIF encoding, or format transcoding is included. The browser must support
1676
- `canvas.captureStream` and `MediaRecorder`; exports reject before framing,
1854
+ `preserveDrawingBuffer` is unnecessary for video. No audio, HTML overlays, or
1855
+ format transcoding is included. The browser must support
1856
+ `canvas.captureStream`, and `MediaRecorder` for WebM/MP4; exports reject before framing,
1677
1857
  during XR, during another recording, or for durations outside (0, 300] seconds.
1678
1858
 
1859
+ **Animated GIF.** Pass `format: "gif"` (or pick **GIF** in the menu's format
1860
+ select) for a GIF that loops forever, for places that don't play video, such as
1861
+ email, chat, and some marketplaces:
1862
+
1863
+ ```ts
1864
+ const gif = await connection.captureVideo({
1865
+ mode: "turntable",
1866
+ format: "gif",
1867
+ gifWidth: 480, // pixels; the height follows the canvas. Default 480
1868
+ // fps defaults to 15, and a turntable to an 8-second turn
1869
+ });
1870
+ ```
1871
+
1872
+ A GIF holds 256 colours per frame and stores every frame whole, so it is much
1873
+ larger than a video of the same length and shows some banding on smooth
1874
+ gradients. Frames are taken from the same stream as a video and encoded as
1875
+ they're captured, so a long recording doesn't build up memory. Each frame lasts
1876
+ as long as it really showed, so a busy device drops frames rather than playing
1877
+ the GIF fast. `videoBitsPerSecond` doesn't apply to GIFs. The encoder
1878
+ ([gifenc](https://github.com/mattdesl/gifenc)) loads only when a GIF is made.
1879
+
1679
1880
  ### `onTextureUpload`
1680
1881
 
1681
1882
  Optional async callback for handling texture file uploads. When provided, the viewer calls it instead of creating a blob URL, letting consumers upload to their own storage (S3, Cloudinary, a CDN, etc.) and return a durable URL that survives page reloads.
@@ -2466,13 +2667,76 @@ Enables automatic orbiting and controls its speed. Defaults are `false` and
2466
2667
  <ModelViewer {...props} autoRotate autoRotateSpeed={0.75} />
2467
2668
  ```
2468
2669
 
2670
+ Auto-rotate doesn't start for visitors who ask their device to reduce motion.
2671
+ See [`reducedMotion`](#reducedmotion).
2672
+
2469
2673
  ### `enableKeyboardNavigation`
2470
2674
 
2471
2675
  Optional boolean enabling camera shortcuts after the viewer canvas is clicked
2472
2676
  or focused. Default: `false`. Use `W`/`S` to move forward/back, `A`/`D` to
2473
2677
  truck left/right, Space/`C` to move up/down, and the arrow keys to orbit.
2474
2678
  `Shift` + Up/Down moves forward/back. Keyboard navigation is inactive while
2475
- object editing or camera controls are disabled.
2679
+ object editing or camera controls are disabled. While the canvas has focus, a
2680
+ screen reader reads out what its keys do (the `cameraKeyboardHelp` label).
2681
+
2682
+ ### Accessibility
2683
+
2684
+ The viewer can be used without a mouse, and reads out what is happening:
2685
+
2686
+ - **Moving through objects with the keyboard.** Tabbing into the viewer reaches
2687
+ a small object list at the top of the canvas, shown only while it has focus.
2688
+ The arrow keys (or `Home` and `End`) move through the objects and highlight
2689
+ each one in the scene. Typing a letter jumps to an object whose name starts
2690
+ with it. `Enter` or Space selects the object, exactly as clicking it would,
2691
+ and `Escape` clears the selection. Screen readers hear each object's name and
2692
+ its position ("Wheel, 3 of 12"). The list follows the scene objects panel's
2693
+ order. It skips hidden objects, and objects an isolation has set aside.
2694
+ - **Announcing the selection.** Every change of selection is announced, whether
2695
+ it came from a click, the keyboard, the objects panel, or your app ("Wheel
2696
+ selected", "Selection cleared").
2697
+ - **The scene objects panel.** Each row is a button, so the panel can be used
2698
+ from the keyboard too. Focusing a row highlights its object, and the
2699
+ visibility buttons name the object they hide or show.
2700
+ - **Reduced motion.** See [`reducedMotion`](#reducedmotion) below.
2701
+
2702
+ All of this text can be translated with `labels`.
2703
+
2704
+ ### `showObjectNavigator`
2705
+
2706
+ Optional boolean for the keyboard object list described above. Default: `true`.
2707
+ It takes no space until a keyboard user reaches it, so it's on by default. Set it
2708
+ to `false` when your app provides its own way to choose objects from the
2709
+ keyboard. It is hidden while measuring, since clicks then place points instead
2710
+ of selecting. Keyboard selections reach `onEngagement` as `part-viewed` events
2711
+ with `source: "keyboard"`.
2712
+
2713
+ ```tsx
2714
+ <ModelViewer {...props} ui={{ objectNavigator: false }} />
2715
+ ```
2716
+
2717
+ ### `reducedMotion`
2718
+
2719
+ Cuts motion the viewer starts on its own, for visitors who find it hard to
2720
+ watch. Default: `"user"`, which follows the visitor's device setting
2721
+ (`prefers-reduced-motion`). `"always"` and `"never"` override it. Also
2722
+ `controls.reducedMotion`; the type is exported as `ReducedMotionSetting`.
2723
+
2724
+ When motion is reduced:
2725
+
2726
+ - Auto-rotate doesn't start.
2727
+ - A `cinematic` path with `autoPlay` waits for its play button.
2728
+ - Camera moves jump straight to where they're going instead of gliding: the
2729
+ first framing, focusing a selected object, resetting the view, saved views,
2730
+ guided-tour stops, and moves made through `useViewerCamera`.
2731
+ - Panels, menus and popups appear in place instead of sliding or fading in.
2732
+ Loading spinners keep turning, since they show that something is still
2733
+ happening.
2734
+
2735
+ Motion the visitor asks for still plays: the cinematic play button, video
2736
+ export, and dragging the camera. The viewer doesn't change animations your app
2737
+ starts, such as model clips, effects, or style rule pulses. To respect the
2738
+ setting there too, check `matchMedia("(prefers-reduced-motion: reduce)")` in
2739
+ your app. `SimpleModelViewer` takes the same prop.
2476
2740
 
2477
2741
  ### `showViewGizmo`
2478
2742
 
@@ -2495,6 +2759,8 @@ Current callback shape:
2495
2759
 
2496
2760
  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.
2497
2761
 
2762
+ While a selected object is framed (see `zoomOnSelected`), a resize frames that object again rather than the whole model, so the object panel opening doesn't undo the zoom. An object with an authored `cameraState` keeps that view. Once the selection is cleared, or something else moves the camera away from the object, resizes frame the whole model again.
2763
+
2498
2764
  Default:
2499
2765
 
2500
2766
  ```ts
@@ -2560,6 +2826,89 @@ Default:
2560
2826
  false;
2561
2827
  ```
2562
2828
 
2829
+ ### `webgpu`
2830
+
2831
+ Optional boolean. Draws with three.js's WebGPU renderer when the browser has
2832
+ WebGPU, and with WebGL otherwise. Default: `false`. Also `perf.webgpu`.
2833
+
2834
+ ```tsx
2835
+ <ModelViewer {...props} perf={{ webgpu: true }} />
2836
+ ```
2837
+
2838
+ The viewer asks the browser for a WebGPU adapter once per page before
2839
+ creating its canvas. Without one, it uses WebGL and says so once in the
2840
+ console. `onViewerReady` reports which renderer is drawing as
2841
+ `rendererBackend` (`"webgl"` or `"webgpu"`). The WebGPU renderer is loaded only
2842
+ by viewers that use it.
2843
+
2844
+ With WebGPU, the model, materials, bindings, hover highlight, picking,
2845
+ animations, decals, annotations, measurement, video and GIF export, and tone
2846
+ mapping and exposure work as they do with WebGL. Some features are built on
2847
+ WebGL and are left out:
2848
+
2849
+ - The post-processing effects (outline, bloom, depth of field, and the rest)
2850
+ don't draw. Tone mapping and exposure from `sceneConfig.postProcessing` still
2851
+ apply, through the renderer.
2852
+ - Contact shadows (`sceneConfig.groundShadows` in shadow-catcher mode) don't
2853
+ draw.
2854
+ - The section cut and the move gizmo (`moveModeEnabled`) aren't available, and
2855
+ the console says so if they're asked for.
2856
+ - AR and VR sessions in the browser (WebXR) aren't offered. Quick Look on iOS
2857
+ and the phone hand-off still work.
2858
+ - Measurement lines are one pixel wide.
2859
+
2860
+ `captureImage` and the Download PNG action work with WebGPU without
2861
+ `preserveDrawingBuffer`. A lost WebGPU device shows the same **Reload viewer**
2862
+ message as a lost WebGL context.
2863
+
2864
+ ### Lazy loading and posters
2865
+
2866
+ `loading`, `poster` and `reveal` decide when a viewer creates its 3D view, and
2867
+ what shows in its place until then. They matter most on pages with many
2868
+ viewers, such as a product grid: browsers allow a page about 16 WebGL contexts
2869
+ and take the oldest away past that.
2870
+
2871
+ ```tsx
2872
+ <ModelViewer {...props} poster="/chair.webp" />
2873
+ <ModelViewer {...props} poster="/chair.webp" reveal="interaction" />
2874
+ ```
2875
+
2876
+ `loading` is `"lazy"` by default. A lazy viewer:
2877
+
2878
+ - starts its 3D view when it comes within about 200px of the screen. One that's
2879
+ on the screen when the page loads starts at once.
2880
+ - shares the page's live 3D views with the other lazy viewers: at most 12, or 6
2881
+ on phones and tablets. Viewers on the screen come first. One scrolled away
2882
+ from keeps its view while there's room, so coming back is instant. When the
2883
+ room is needed, it's put away, leaving a picture of its last frame, and is
2884
+ built again when it comes back near the screen.
2885
+ - comes back as it was: the camera, picked variants, light and section-cut
2886
+ changes, isolation, the exploded view, the camera controller, the UV checker,
2887
+ the annotation filter, and, when the viewer keeps `objectBindings` itself,
2888
+ the changes made to them. The selection and open panels don't come back. `onModelLoaded` and
2889
+ `onViewerReady` run again, with a new viewer.
2890
+ - stops drawing while it's off the screen, and starts again when it's back.
2891
+ Browsers already stop drawing in hidden tabs.
2892
+
2893
+ `loading="eager"` starts the 3D view at once and keeps it. Eager viewers don't
2894
+ count towards the 12. `BindingBuilder`'s preview is eager.
2895
+
2896
+ `poster` is an image shown in the viewer's place until its 3D view is drawn,
2897
+ then fades out; the loading message shows over it. It's fitted inside the
2898
+ viewer. To fill the viewer instead, set the CSS variable
2899
+ `--ri-poster-fit: cover` on a parent. A render of the model framed as the
2900
+ viewer frames it works best: [`captureImage`](#onviewerready) can make one.
2901
+
2902
+ `reveal="interaction"` keeps the poster, with a **View in 3D** button, until the
2903
+ visitor presses it, so only the products someone picks use a 3D view. The
2904
+ button's text is the [`labels`](https://react-immersive.liveroom.dev/docs/reference/labels)
2905
+ key `viewIn3D`. `reveal` is `"auto"` by default: the view starts on its own.
2906
+
2907
+ The same three props work on [`SimpleModelViewer`](#simplemodelviewer-public-api)
2908
+ and [`CompareViewer`](#compareviewer).
2909
+
2910
+ ---
2911
+
2563
2912
  ### `performanceProfile`
2564
2913
 
2565
2914
  Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.
@@ -2584,9 +2933,11 @@ If the WebGL context is lost, rendering pauses and the viewer displays a
2584
2933
  the viewer automatically rebuilds its canvas and scene; the reload button starts
2585
2934
  the same rebuild without waiting for browser restoration.
2586
2935
 
2587
- Recovery uses the current props and cached model assets, cancels video recording,
2588
- and resets transient viewer state (camera, selection, measurements, and playback).
2589
- Keep configuration edits in controlled props to retain them across a rebuild.
2936
+ Recovery uses the current props and cached model assets, and cancels video
2937
+ recording. The rebuilt viewer keeps its camera and the visitor's changes, as a
2938
+ lazy viewer does when it comes back (see
2939
+ [Lazy loading and posters](#lazy-loading-and-posters)); the selection,
2940
+ measurements and playback start over.
2590
2941
  `onSceneLoaded`, `onModelLoaded`, `onAnimationsReady`, and `onViewerReady` run again
2591
2942
  for the rebuilt scene, so integrations receive fresh controls and object refs.
2592
2943
 
@@ -2713,43 +3064,79 @@ false;
2713
3064
 
2714
3065
  ### `showMaterialVariants`
2715
3066
 
2716
- Shows a row of buttons, centered at the bottom of the viewer, for switching
3067
+ Shows swatch buttons, centered at the bottom of the viewer, for switching
2717
3068
  between the model's glTF material variants (the `KHR_materials_variants`
2718
- extension): the colorways, finishes, or trims a product file ships with. It
2719
- appears only for a glTF/GLB with more than one variant. Default `true`.
3069
+ extension): the colorways, finishes, or trims a product file ships with. Each
3070
+ button shows the variant's colour when its material is a plain colour. It
3071
+ appears for a glTF/GLB with any variants. Default `true`.
3072
+
3073
+ Variants come in **option groups**, one row each. Choosing a variant replaces
3074
+ the other choice in its row and keeps the other rows, so a paint colour and a
3075
+ wheel style combine. Clicking the chosen variant again returns that row to the
3076
+ file's default materials. The viewer works out the groups from the file:
2720
3077
 
2721
- Picking a variant sets `sceneConfig.model.materialVariant`, which is also how
2722
- you choose one yourself:
3078
+ - A name like `"Paint: Red"` or `"Paint / Red"` puts the variant in a row
3079
+ labelled **Paint**, with a button labelled **Red**.
3080
+ - Otherwise, variants that restyle the same parts share a row, and variants
3081
+ that restyle different parts get separate rows. A shoe's `"midnight"`,
3082
+ `"beach"` and `"street"` colorways, which all restyle the same parts, are one
3083
+ row.
3084
+
3085
+ The variants shown are `sceneConfig.model.activeVariants`, at most one from
3086
+ each group, which is also how you choose them yourself:
2723
3087
 
2724
3088
  ```tsx
3089
+ import { chooseMaterialVariant, useViewer } from "@liveroom-tech/react-immersive";
3090
+
2725
3091
  const viewer = useViewer();
2726
3092
 
2727
- <ModelViewer modelUrl="/shoe.glb" licenseKey="your-license-key" {...viewer.props} />;
3093
+ <ModelViewer modelUrl="/car.glb" licenseKey="your-license-key" {...viewer.props} />;
3094
+
3095
+ // Once the model has loaded
3096
+ viewer.model.materialVariants; // ["Paint: Red", "Paint: Blue", "Wheels: Sport", "Wheels: Classic"]
3097
+ viewer.model.materialVariantGroups;
3098
+ // [
3099
+ // { name: "Paint", variants: [{ name: "Paint: Red", label: "Red", swatch: "#ff0000" }, ...] },
3100
+ // { name: "Wheels", variants: [{ name: "Wheels: Sport", label: "Sport" }, ...] },
3101
+ // ]
2728
3102
 
2729
- // The model's variant names, once it has loaded
2730
- viewer.model.materialVariants; // ["midnight", "beach", "street"]
3103
+ viewer.scene.updateSceneConfig({ model: { activeVariants: ["Paint: Red", "Wheels: Sport"] } });
3104
+ viewer.scene.updateSceneConfig({ model: { activeVariants: [] } }); // the defaults
2731
3105
 
2732
- viewer.scene.updateSceneConfig({ model: { materialVariant: "beach" } });
2733
- viewer.scene.updateSceneConfig({ model: { materialVariant: null } }); // the defaults
3106
+ // Your own swatches: swap a choice within its group, keeping the others.
3107
+ const active = viewer.scene.sceneConfig.model.activeVariants ?? [];
3108
+ viewer.scene.updateSceneConfig({
3109
+ model: {
3110
+ activeVariants: chooseMaterialVariant(active, "Paint: Blue", viewer.model.materialVariantGroups),
3111
+ },
3112
+ });
2734
3113
  ```
2735
3114
 
2736
3115
  - A variant sets each object's starting material. `objectBindings` material
2737
3116
  overrides (`style.material`) still apply on top of it, and clearing an
2738
3117
  override returns to the variant's material rather than the file's default.
2739
- - Objects the variant doesn't map keep their default material, bound or not.
3118
+ - Objects no active variant maps keep their default material, bound or not.
3119
+ If two active variants map the same object, the later one in the list wins.
2740
3120
  - A variant's materials and textures load the first time it's chosen; the
2741
3121
  current look stays until they're ready.
2742
3122
  - With `onSceneConfigChange`, a pick goes to your scene config (`useViewer`
2743
- wires this up). Without it, `ModelViewer` keeps the pick itself until
2744
- `sceneConfig.model.materialVariant` or `modelUrl` changes.
2745
- - A name the model doesn't have shows the defaults and logs a console warning
2746
- listing the model's variants.
3123
+ wires this up). Without it, `ModelViewer` keeps the picks itself until
3124
+ `sceneConfig.model.activeVariants` or `modelUrl` changes.
3125
+ - A name the model doesn't have is skipped with a console warning listing the
3126
+ model's variants.
2747
3127
  - An action can switch variants with a scene-config effect:
2748
- `{ sceneConfig: { model: { materialVariant: "beach" } } }`.
2749
- - `onModelLoaded` reports the names as `materialVariants`, and BindingBuilder's
2750
- Scene tab (**General → Material variant**) picks the one the exported scene
2751
- config starts with. OBJ, FBX, USDZ, and streamed 3D Tiles models have no
2752
- variants.
3128
+ `{ sceneConfig: { model: { activeVariants: ["Paint: Red"] } } }`.
3129
+ - Swatches use the colour of the material a variant puts on the most vertices.
3130
+ A textured material gets no swatch, just its label, so showing the buttons
3131
+ never loads textures.
3132
+ - Each pick reaches [`onEngagement`](#onengagement--debugengagement) as a
3133
+ `variant-chosen` event with its `group`.
3134
+ - `onModelLoaded` reports `materialVariants` and `materialVariantGroups`, and
3135
+ BindingBuilder's Scene tab (**General**, one select per group) picks the
3136
+ variants the exported scene config starts with. OBJ, FBX, USDZ, STL, PLY, and
3137
+ streamed 3D Tiles models have no variants.
3138
+ - Scene configs saved before option groups, with a single
3139
+ `model.materialVariant`, still show that variant.
2753
3140
 
2754
3141
  ### Decals
2755
3142
 
@@ -2899,7 +3286,8 @@ showIsolateControls?: boolean; // tools.isolate
2899
3286
 
2900
3287
  Shows a section-cut button over the canvas (`tools.section`). It cuts the model
2901
3288
  open along an axis, hiding everything on one side of a plane, to look inside
2902
- assemblies, equipment, and buildings. Default `false`.
3289
+ assemblies, equipment, and buildings. Default `false`. Not available with
3290
+ [`webgpu`](#webgpu).
2903
3291
 
2904
3292
  With the cut on, the toolbar picks the axis (X, Y, or Z), the position along
2905
3293
  it, and which side stays, and a plane appears across the model that you can
@@ -2951,7 +3339,7 @@ type CinematicConfig = {
2951
3339
  waypoints?: CinematicWaypoint[]; // camera keyframes to glide through
2952
3340
  duration?: number; // seconds for one full pass (auto-scales when omitted)
2953
3341
  loop?: boolean; // default true
2954
- autoPlay?: boolean; // start once the model has loaded and framed (default false)
3342
+ autoPlay?: boolean; // start once the model has loaded and framed (default false); waits for the play button under reduced motion
2955
3343
  };
2956
3344
 
2957
3345
  type CinematicWaypoint = {
@@ -3099,7 +3487,10 @@ See the full variable list in the [`ModelViewer` docs](https://react-immersive.l
3099
3487
  ```ts
3100
3488
  type SimpleModelViewerProps = {
3101
3489
  modelUrl: string;
3102
- modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz";
3490
+ poster?: string; // see ModelViewer's poster
3491
+ loading?: "lazy" | "eager"; // default "lazy"
3492
+ reveal?: "auto" | "interaction"; // default "auto"
3493
+ modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz" | "stl" | "ply";
3103
3494
  dracoDecoderPath?: string | false;
3104
3495
  ktx2TranscoderPath?: string | false;
3105
3496
  meshopt?: boolean;
@@ -3111,6 +3502,7 @@ type SimpleModelViewerProps = {
3111
3502
  highlightOnHover?: boolean;
3112
3503
  showLoadingOverlay?: boolean;
3113
3504
  refitOnResize?: boolean;
3505
+ reducedMotion?: "user" | "always" | "never"; // see ModelViewer's reducedMotion
3114
3506
  // Initial values for the scene settings (environment) panel
3115
3507
  backgroundEnabled?: boolean;
3116
3508
  autoRotate?: boolean;
@@ -3129,13 +3521,21 @@ type SimpleModelViewerProps = {
3129
3521
 
3130
3522
  ### `modelUrl`
3131
3523
 
3132
- URL or public path to the `.glb`, `.gltf`, `.obj`, `.fbx`, or `.usdz` model to inspect. Hosted assets must serve external dependencies at their declared relative paths; USDZ dependencies are packaged internally. Use `modelFormat` when a signed or extensionless URL cannot be auto-detected.
3524
+ URL or public path to the `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` model to inspect. Hosted assets must serve external dependencies at their declared relative paths; USDZ dependencies are packaged internally. Use `modelFormat` when a signed or extensionless URL cannot be auto-detected.
3133
3525
 
3134
3526
  ### `modelFormat`
3135
3527
 
3136
3528
  Optional explicit format for signed or extensionless model URLs. Normal URLs
3137
3529
  ending in a supported extension are detected automatically.
3138
3530
 
3531
+ ### `poster` / `loading` / `reveal`
3532
+
3533
+ Work as on [`ModelViewer`](#lazy-loading-and-posters), sharing the same
3534
+ page-wide count of live 3D views. When a `SimpleModelViewer` is put away, only
3535
+ its canvas goes: the panels and their settings stay, and the canvas comes back
3536
+ with its camera, hidden objects and selection.
3537
+ `onModelLoaded` runs again when it does.
3538
+
3139
3539
  ### `backgroundColor`
3140
3540
 
3141
3541
  Optional canvas background color.
@@ -3237,7 +3637,7 @@ true;
3237
3637
 
3238
3638
  ### `enableModelUpload`
3239
3639
 
3240
- Optional boolean that swaps the fixed `modelUrl` workflow for a built-in upload UI. It accepts `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, or a `.zip` bundle. ZIP bundles may contain a GLTF with its buffers/textures, an OBJ with its optional MTL/textures, or an FBX with external textures. A USDZ file is already a self-contained package and is uploaded directly.
3640
+ Optional boolean that swaps the fixed `modelUrl` workflow for a built-in upload UI. It accepts `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or a `.zip` bundle. ZIP bundles may contain a GLTF with its buffers/textures, an OBJ with its optional MTL/textures, or an FBX with external textures. A USDZ file is already a self-contained package and is uploaded directly.
3241
3641
 
3242
3642
  Default:
3243
3643
 
@@ -3424,8 +3824,8 @@ To use this library successfully, your model asset should follow these expectati
3424
3824
  - the mesh node names must match the keys in `objectBindings` (or be referenced via `modelObjectId`)
3425
3825
  - target nodes should have geometry and a supported Three.js mesh material; imported materials are normalized into the editable PBR pipeline
3426
3826
  - the file should be accessible from the browser at `modelUrl`; hosted GLTF, OBJ, and FBX assets must also serve external dependencies at their declared relative paths
3427
- - OBJ does not contain animations; supported FBX and USDZ animation clips are exposed through the same animation controls as glTF
3428
- - OBJ has no standard unit metadata, so set an appropriate measurement label/scene scale when real-world dimensions matter
3827
+ - OBJ, STL, and PLY do not contain animations; supported FBX and USDZ animation clips are exposed through the same animation controls as glTF
3828
+ - OBJ, STL, and PLY have no standard unit metadata, so set an appropriate measurement label/scene scale when real-world dimensions matter
3429
3829
 
3430
3830
  If a node name or material name does not match, that object will not render through the interactive binding flow.
3431
3831
 
@@ -3465,3 +3865,32 @@ Two related picking notes that apply to models of every size:
3465
3865
  ## License
3466
3866
 
3467
3867
  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.
3868
+
3869
+ ### Rich annotations
3870
+
3871
+ `sceneConfig.annotations` supports optional `cameraView` (position, target,
3872
+ orbit angles, fov and zoom), `images: [{ src, alt }]`, `links: [{ href, label }]`,
3873
+ `category`, and `minDistance` / `maxDistance` in scene units. BindingBuilder's
3874
+ Annotations tab authors these fields; new markers use automatic close-up tour
3875
+ framing. Explicitly capture a camera view for custom framing. Marker selection
3876
+ and guided tours restore saved views when provided. The viewer's category chips
3877
+ filter markers and tour steps.
3878
+ Use `annotationCategories` (`null` = all, `[]` = none) and
3879
+ `onAnnotationCategoriesChange` for controlled filtering, and
3880
+ `ui.annotationFilters: false` to hide the built-in chips. Distance limits affect
3881
+ markers only, so their content remains available through tours. Images stay at
3882
+ their HTTP(S) or relative URLs and are not bundled into exports automatically.
3883
+
3884
+ ## Translatable viewer text
3885
+
3886
+ Both viewers accept a partial `labels` dictionary. Omitted keys use English; changing labels updates text without reloading the model. Named placeholders such as `{count}` can be reordered in translations.
3887
+
3888
+ ```tsx
3889
+ <ModelViewer
3890
+ licenseKey="your-license-key"
3891
+ modelUrl="/car.glb"
3892
+ labels={{ guidedTour: "Visite guidée", tapToPlace: "Touchez pour placer", visibleEntries: "{count} éléments visibles" }}
3893
+ />
3894
+ ```
3895
+
3896
+ `ModelViewerLabels`, `ModelViewerLabelKey`, and `DEFAULT_MODEL_VIEWER_LABELS` are exported from the main package, `/simple-model-viewer`, and `/utils`. See the [full catalog](https://react-immersive.liveroom.dev/docs/reference/labels). Annotation content, metadata, and custom panels use your application's translations.