@liveroom-tech/react-immersive 5.0.2 → 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.
- package/README.md +1030 -59
- package/dist/binding-builder.css +1 -1
- package/dist/binding-builder.js +17 -12
- package/dist/binding-builder.mjs +1 -1
- package/dist/chunk-4NLJSD3S.mjs +1 -0
- package/dist/chunk-B7REIGDG.mjs +1 -0
- package/dist/chunk-HYFJ5DYJ.mjs +1 -0
- package/dist/chunk-QC3NT76G.mjs +3 -0
- package/dist/chunk-T3M3BQ7H.mjs +32 -0
- package/dist/chunk-ZPK67UG7.mjs +1 -0
- package/dist/hosted.css +1 -1
- package/dist/hosted.d.mts +4 -3
- package/dist/hosted.d.ts +4 -3
- package/dist/hosted.js +16 -11
- package/dist/hosted.mjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.mts +85 -10
- package/dist/index.d.ts +85 -10
- package/dist/index.js +15 -10
- package/dist/index.mjs +1 -1
- package/dist/materialVariants-DnZe9eMC.d.mts +309 -0
- package/dist/materialVariants-DnZe9eMC.d.ts +309 -0
- package/dist/modelInfo-BYebMPdZ.d.mts +43 -0
- package/dist/modelInfo-XfUQO7C_.d.ts +43 -0
- package/dist/{ModelViewer-CbbJT85n.d.mts → modelViewerProps-CIZ2R_Fv.d.ts} +164 -43
- package/dist/{ModelViewer-DOexHkJC.d.ts → modelViewerProps-DoRiTZMn.d.mts} +164 -43
- package/dist/{objectCatalog-5W8n5xNQ.d.ts → rendererBackend-QwMzISTn.d.mts} +304 -5
- package/dist/{objectCatalog-5W8n5xNQ.d.mts → rendererBackend-QwMzISTn.d.ts} +304 -5
- package/dist/simple-model-viewer.css +1 -1
- package/dist/simple-model-viewer.d.mts +32 -4
- package/dist/simple-model-viewer.d.ts +32 -4
- package/dist/simple-model-viewer.js +3 -3
- package/dist/simple-model-viewer.mjs +1 -1
- package/dist/utils.d.mts +5 -4
- package/dist/utils.d.ts +5 -4
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +2 -1
- package/dist/chunk-3GX7DOGS.mjs +0 -3
- package/dist/chunk-AH5NAZWX.mjs +0 -27
- package/dist/chunk-BGYK4Y2Y.mjs +0 -1
- package/dist/chunk-WFEYRUZC.mjs +0 -1
- package/dist/chunk-XTLSAYAQ.mjs +0 -1
- package/dist/modelInfo-CEmmvVCH.d.mts +0 -17
- package/dist/modelInfo-CEmmvVCH.d.ts +0 -17
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
|
|
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
|
|
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,13 +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
|
-
-
|
|
65
|
+
- glTF material variants (`KHR_materials_variants`) as swatch rows, one per option group, through `showMaterialVariants`, selected by `sceneConfig.model.activeVariants`
|
|
66
|
+
- distance, angle, polygon area, and point-to-plane measurements with vertex/edge snapping and bounding dimensions through `showMeasureTools` and `measurementUnit`
|
|
63
67
|
- an exploded-view slider that slides each bound part outward from the model center to reveal interior/assembly structure through `showExplodeControls`
|
|
68
|
+
- isolation that ghosts or hides everything but the objects you focus on, through `isolation` / `onIsolationChange` and `showIsolateControls`
|
|
69
|
+
- a section cut that opens the model along an axis, with a draggable plane, through `showSectionTools` and `sceneConfig.section`
|
|
64
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
|
|
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))
|
|
65
75
|
- a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via `showAnnotationNavigation`
|
|
66
76
|
- optional annotation detail popups on marker hover via `showAnnotationOnHover`
|
|
67
77
|
- PBR, Matcap, and UV Checker renderer modes, with the checker exposing UV stretching, seams, rotation, and missing UV0 data directly on the model
|
|
68
78
|
- a `sceneConfig` prop accepting the same scene-wide config (lighting, environment, background, post-processing, animations, annotations) authored by `BindingBuilder`'s Scene tab
|
|
79
|
+
- WebGL context-loss recovery with a reload button and automatic rebuild when the context returns, keeping the camera and the visitor's changes
|
|
69
80
|
- render-loop and perf tuning through `renderMode`, `maxDpr`, `performanceProfile`, and compressed-asset decoder options (`dracoDecoderPath`, `ktx2TranscoderPath`, `meshopt`)
|
|
70
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
|
|
71
82
|
- mesh selection by object name
|
|
@@ -75,6 +86,8 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
|
|
|
75
86
|
- built-in texture upload that commits a URL into `objectBindings.style.material.texture.path` (blob URL by default, or a durable URL when `onTextureUpload` is provided)
|
|
76
87
|
- per-object `MeshPhysicalMaterial` overrides for metalness, roughness, emissive, normal/bump, AO, displacement, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, sidedness, and limited blending modes
|
|
77
88
|
- per-object visibility toggling through `objectBindings.visible`
|
|
89
|
+
- decals: images and text printed on an object's surface through `objectBindings.decals`, placed from the surface point pointer events report
|
|
90
|
+
- data-driven looks through `sceneConfig.styleRules`: colour scales from metrics, material overrides by status or metadata, and pulsing alarms
|
|
78
91
|
- hover and selected-state highlighting
|
|
79
92
|
- external selection state control through `selectedObject` and `onObjectSelect`
|
|
80
93
|
- hover callbacks through `onObjectHover`
|
|
@@ -84,13 +97,14 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
|
|
|
84
97
|
- camera change callbacks through `onCameraChange`
|
|
85
98
|
- viewer-ready callbacks through `onViewerReady`
|
|
86
99
|
- action event callbacks through `onAction`
|
|
100
|
+
- opt-in engagement analytics through `onEngagement`, with `debugEngagement` console logging
|
|
87
101
|
- animation playback for GLB/GLTF, FBX, and USDZ models with supported embedded animations through `onAnimationsReady`
|
|
88
102
|
- annotation marker callbacks through `onAnnotationsChange` and controlled/uncontrolled `activeAnnotation` / `onActiveAnnotationChange`
|
|
89
103
|
|
|
90
104
|
`BindingBuilder` provides:
|
|
91
105
|
|
|
92
|
-
- a design-time UI for generating starter bindings from GLB, GLTF, OBJ, FBX, and
|
|
93
|
-
- 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
|
|
94
108
|
- editable binding fields for identity, basics, style, actions, metrics, metadata, and saved camera state
|
|
95
109
|
- per-object move and rotate authoring with a geometry-centered pivot, World/Local axes, numeric fields, and restore-to-authored-transform support
|
|
96
110
|
- a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
|
|
@@ -100,12 +114,12 @@ It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindi
|
|
|
100
114
|
- import a previously exported `objectBindings.json` or `sceneConfig.json` and merge it back onto the current model/config
|
|
101
115
|
- live preview using `ModelViewer`
|
|
102
116
|
- export as JSON or TypeScript, bundled into a `.zip` with a `materials/` folder automatically when any texture field was filled by file upload, otherwise a plain file
|
|
103
|
-
- license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, post-processing)
|
|
117
|
+
- license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, style rules, post-processing)
|
|
104
118
|
|
|
105
119
|
`SimpleModelViewer` provides:
|
|
106
120
|
|
|
107
|
-
- a lightweight GLB/GLTF/OBJ/FBX/USDZ viewer with no bindings or actions that does not require a `licenseKey`
|
|
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
|
|
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
|
|
109
123
|
- built-in scene objects panel with search and visibility toggles
|
|
110
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
|
|
111
125
|
- click-to-select and fit-to-object focus behavior
|
|
@@ -267,7 +281,7 @@ export default function Configurator() {
|
|
|
267
281
|
|
|
268
282
|
`viewer.bindings`, `viewer.selection`, `viewer.hover`, `viewer.camera`,
|
|
269
283
|
`viewer.model`, `viewer.animations`, `viewer.actions`, `viewer.scene`,
|
|
270
|
-
`viewer.effects`, and `viewer.connection` are the results of the hooks they're
|
|
284
|
+
`viewer.isolation`, `viewer.effects`, and `viewer.connection` are the results of the hooks they're
|
|
271
285
|
named after (`useObjectBindings`, `useViewerSelection`, and so on), so
|
|
272
286
|
everything below applies to them. Without a `bindings` option, `useViewer`
|
|
273
287
|
generates bindings from the model like `objectBindings="auto"`; pass
|
|
@@ -462,6 +476,9 @@ const {
|
|
|
462
476
|
setTransform,
|
|
463
477
|
resetTransform,
|
|
464
478
|
updateMetadata,
|
|
479
|
+
addDecal,
|
|
480
|
+
updateDecal,
|
|
481
|
+
removeDecal,
|
|
465
482
|
hiddenObjects,
|
|
466
483
|
hiddenObjectIds,
|
|
467
484
|
hideObject,
|
|
@@ -524,6 +541,7 @@ The hook returns:
|
|
|
524
541
|
- `setMaterial`, `setBaseColor`, `setTexture`, `clearTexture`, `copyMaterial`, `resetMaterial`, and `getMaterial`
|
|
525
542
|
- `setTransform` and `resetTransform` for persistent local-space model transforms
|
|
526
543
|
- `updateMetadata` for recursively merged application data
|
|
544
|
+
- `addDecal`, `updateDecal`, and `removeDecal` for images and text printed on objects (see [Decals](#decals))
|
|
527
545
|
- `hiddenObjects`: a derived map of hidden binding keys
|
|
528
546
|
- `hiddenObjectIds`: the currently hidden binding IDs
|
|
529
547
|
- `hideObject`, `showObject`, `toggleObjectVisibility`
|
|
@@ -661,6 +679,7 @@ const {
|
|
|
661
679
|
error,
|
|
662
680
|
bounds,
|
|
663
681
|
objectCount,
|
|
682
|
+
materialVariants,
|
|
664
683
|
objects,
|
|
665
684
|
handleModelLoaded,
|
|
666
685
|
handleObjectCatalog,
|
|
@@ -684,6 +703,8 @@ const {
|
|
|
684
703
|
- `error`: the most recent load/render error, or `null`
|
|
685
704
|
- `bounds`: the loaded scene bounds as `min`, `max`, `center`, and `size`
|
|
686
705
|
- `objectCount`: the number of mesh objects in the loaded scene
|
|
706
|
+
- `materialVariants`: the names of the model's glTF material variants, or an
|
|
707
|
+
empty array (see [`showMaterialVariants`](#showmaterialvariants))
|
|
687
708
|
- `objects`: every bindable object in the model as a `ViewerObjectInfo`, once
|
|
688
709
|
`handleObjectCatalog` is passed to `onObjectCatalog`
|
|
689
710
|
- `handleModelLoaded`: a callback you can pass directly to `onModelLoaded`
|
|
@@ -748,9 +769,11 @@ type BindingBuilderProps = {
|
|
|
748
769
|
|
|
749
770
|
Current behavior, Object tab:
|
|
750
771
|
|
|
751
|
-
- upload a `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, or `.
|
|
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)
|
|
752
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))
|
|
753
775
|
- edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
|
|
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
|
|
754
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
|
|
755
778
|
- apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
|
|
756
779
|
- undo/redo the current bindings (toolbar buttons or `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z`); rapid edits coalesce into a single undo step
|
|
@@ -761,6 +784,12 @@ Current behavior, Scene tab:
|
|
|
761
784
|
|
|
762
785
|
- configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
|
|
763
786
|
- configure auto-rotation and its speed, exported as `sceneConfig.model.autoRotate` and `sceneConfig.model.autoRotateSpeed`
|
|
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
|
|
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`
|
|
790
|
+
- isolate a node from the Scene Nodes list to see it on its own in the preview while you edit it (not exported)
|
|
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
|
|
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`
|
|
764
793
|
- position and rotate directional, point, and spot lights with transform gizmos; spot lights also expose angle and penumbra controls
|
|
765
794
|
- undo/redo the scene config independently of the Object tab's bindings history
|
|
766
795
|
- import a previously exported `sceneConfig.json`, merged by top-level section
|
|
@@ -773,7 +802,7 @@ Preview:
|
|
|
773
802
|
Autosave:
|
|
774
803
|
|
|
775
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
|
|
776
|
-
- **Restore last session** in the preview
|
|
805
|
+
- **Restore last session** in the preview's "⋯" menu reopens it; one session is kept per browser
|
|
777
806
|
- while there are edits on screen, leaving or reloading the page asks for confirmation
|
|
778
807
|
- if the browser refuses to store a session, usually for lack of space, the editor says so; export your work to keep it
|
|
779
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`
|
|
@@ -785,14 +814,14 @@ Some editor features are gated by the license's plan tier (the server-returned `
|
|
|
785
814
|
| Tier | Unlocks |
|
|
786
815
|
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
787
816
|
| `free` | All 3 components are available. `SimpleModelViewer` is fully available, while `ModelViewer` and `BindingBuilder` are limited to local development with base color, metalness/roughness sliders, and basic scene settings only |
|
|
788
|
-
| `starter` | + Texture maps, the specular workflow, reflectivity, environment lighting, environment backgrounds, wireframe preview, animation configuration, annotations
|
|
817
|
+
| `starter` | + Texture maps, the specular workflow, reflectivity, environment lighting, environment backgrounds, wireframe preview, animation configuration, annotations, style rules, decals |
|
|
789
818
|
| `growth` | + Post-processing effects |
|
|
790
819
|
|
|
791
820
|
A locked feature renders in place (not hidden) with a message naming the required plan, so it stays discoverable while editing.
|
|
792
821
|
|
|
793
822
|
## `SimpleModelViewer`
|
|
794
823
|
|
|
795
|
-
`SimpleModelViewer` is an exported lightweight viewer for cases where you want to load a GLB, GLTF, OBJ, FBX, or
|
|
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.
|
|
796
825
|
|
|
797
826
|
Basic usage:
|
|
798
827
|
|
|
@@ -879,6 +908,60 @@ const {
|
|
|
879
908
|
|
|
880
909
|
Pass `initialPosition`, `initialTarget`, and optional named `presets` to `useViewerCamera` to configure the first view without writing an `onViewerReady` wrapper.
|
|
881
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
|
+
|
|
882
965
|
If your GLB/GLTF model contains animations, the library also exports:
|
|
883
966
|
|
|
884
967
|
```tsx
|
|
@@ -906,16 +989,17 @@ const {
|
|
|
906
989
|
|
|
907
990
|
- `clips`: an array of animation clip names found in the GLB/GLTF file
|
|
908
991
|
- `clipDetails`: clip metadata including `sourceName` and `duration` in seconds
|
|
909
|
-
- `currentClip`: the
|
|
910
|
-
- `isPlaying`: `true` while
|
|
911
|
-
- `speed`: the
|
|
912
|
-
- `time`: the live playback position of
|
|
913
|
-
- `duration`: the length of
|
|
914
|
-
- `
|
|
915
|
-
- `
|
|
916
|
-
- `
|
|
917
|
-
- `
|
|
918
|
-
- `
|
|
992
|
+
- `currentClip`: the clip played most recently, while it's active, or `null`
|
|
993
|
+
- `isPlaying`: `true` while any clip is playing (not paused, finished, or stopped)
|
|
994
|
+
- `speed`: the playback speed of `currentClip` (default `1`)
|
|
995
|
+
- `time`: the live playback position of `currentClip`, in seconds (updates while playing)
|
|
996
|
+
- `duration`: the length of `currentClip`, in seconds
|
|
997
|
+
- `active`: every active clip (playing, paused, or finished) with its `status`, `time`, `duration`, `speed`, and `reverse`, most recently played last
|
|
998
|
+
- `play(clip, options?)`: starts a clip by name, replacing the clips already playing unless `options.alongside` is set; `options.fade` blends over that many seconds, and `options.reverse` plays it backwards. A paused clip resumes instead of restarting
|
|
999
|
+
- `pause(clip?)`: pauses one clip, or every active clip, at its current time
|
|
1000
|
+
- `stop(clip?)`: stops one clip, or every active clip, and resets the pose
|
|
1001
|
+
- `setSpeed(speed, clip?)`: changes the playback speed (e.g. `0.5` for half speed, `2` for double) of one clip, or of every active clip and those played later without their own speed
|
|
1002
|
+
- `seek(time, clip?)`: scrubs one clip, or every active clip, to an absolute time in seconds (works while playing or paused), clamped to the clip length
|
|
919
1003
|
- `handleAnimationsReady`: a callback you can pass directly to `onAnimationsReady`
|
|
920
1004
|
|
|
921
1005
|
Example with playback controls:
|
|
@@ -930,7 +1014,7 @@ const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
|
|
|
930
1014
|
</button>
|
|
931
1015
|
|
|
932
1016
|
// Stop
|
|
933
|
-
<button onClick={stop}>Stop</button>
|
|
1017
|
+
<button onClick={() => stop()}>Stop</button>
|
|
934
1018
|
|
|
935
1019
|
// Speed slider
|
|
936
1020
|
<input
|
|
@@ -950,16 +1034,128 @@ const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
|
|
|
950
1034
|
))}
|
|
951
1035
|
```
|
|
952
1036
|
|
|
1037
|
+
Several clips can play at once. `play` replaces the clips already playing
|
|
1038
|
+
unless you pass `alongside: true`, and the other controls act on one clip when
|
|
1039
|
+
you name it, or on every active clip when you don't:
|
|
1040
|
+
|
|
1041
|
+
```tsx
|
|
1042
|
+
const animations = useViewerAnimations();
|
|
1043
|
+
|
|
1044
|
+
// Open both doors together, then close the left one.
|
|
1045
|
+
animations.play("DoorLeftOpen", { alongside: true });
|
|
1046
|
+
animations.play("DoorRightOpen", { alongside: true });
|
|
1047
|
+
animations.play("DoorLeftOpen", { alongside: true, reverse: true });
|
|
1048
|
+
|
|
1049
|
+
// Blend from walking to running over half a second.
|
|
1050
|
+
animations.play("Run", { fade: 0.5 });
|
|
1051
|
+
|
|
1052
|
+
animations.pause("Wheels"); // one clip
|
|
1053
|
+
animations.stop(); // every clip
|
|
1054
|
+
|
|
1055
|
+
// What's active, and how far along each clip is
|
|
1056
|
+
animations.active.map(({ clip, status, time }) => `${clip}: ${status} at ${time}s`);
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
A clip whose `loopMode` is `"once"` holds its last pose when it finishes
|
|
1060
|
+
(`status: "finished"`) until it's stopped or played again, so an opened door
|
|
1061
|
+
stays open. `"ping-pong"` plays forwards then backwards, repeatedly. Playing a
|
|
1062
|
+
clip that's already playing in the same direction leaves it running; playing
|
|
1063
|
+
it the other way turns it around where it is.
|
|
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
|
+
|
|
953
1149
|
## Presets and grouped props
|
|
954
1150
|
|
|
955
1151
|
`preset` sets `ModelViewer` up for a common kind of page:
|
|
956
1152
|
|
|
957
1153
|
| Preset | For | What it sets |
|
|
958
1154
|
| --- | --- | --- |
|
|
959
|
-
| `minimal` | Just the model | Hides both panels, the download and reset buttons,
|
|
1155
|
+
| `minimal` | Just the model | Hides both panels, the download and reset buttons, tour controls, and the material variant buttons |
|
|
960
1156
|
| `showcase` | A product on display | Everything `minimal` does, plus auto-rotation |
|
|
961
|
-
| `configurator` | Customers changing parts | Shows the selected object's panel and the
|
|
962
|
-
| `inspector` | Checking a model | Every panel, the view gizmo, the measure, UV checker,
|
|
1157
|
+
| `configurator` | Customers changing parts | Shows the selected object's panel, the reset button, and the material variant buttons; hides the scene objects panel and download |
|
|
1158
|
+
| `inspector` | Checking a model | Every panel, the view gizmo, the measure, UV checker, exploded-view, section-cut, and isolate tools, and keyboard navigation |
|
|
963
1159
|
|
|
964
1160
|
`MODEL_VIEWER_PRESETS` lists exactly which props each preset sets.
|
|
965
1161
|
|
|
@@ -981,10 +1177,10 @@ readable:
|
|
|
981
1177
|
|
|
982
1178
|
| Group | Fields, and the flat prop each one sets |
|
|
983
1179
|
| --- | --- |
|
|
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` |
|
|
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`) |
|
|
1181
|
+
| `tools` | `measure` (`showMeasureTools`; `{ unit }` also sets `measurementUnit`), `uvChecker` (`showUvCheckerButton`), `explode` (`showExplodeControls`), `section` (`showSectionTools`), `isolate` (`showIsolateControls`), `texturePositioning` (`texturePositioning`) |
|
|
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` |
|
|
988
1184
|
| `tiles` | `url` (`tilesetUrl`), `errorTarget` (`tilesErrorTarget`), `cacheSize` (`tilesCacheSize`) |
|
|
989
1185
|
| `xr` | `enabled` (`enableXR`), `usdzUrl`, `scaleMode` (`arScaleMode`), `mobileHandoffUrl` |
|
|
990
1186
|
| `decoders` | `draco` (`dracoDecoderPath`), `ktx2` (`ktx2TranscoderPath`), `meshopt` |
|
|
@@ -1007,11 +1203,14 @@ type Props = {
|
|
|
1007
1203
|
xr?: ModelViewerXrOptions;
|
|
1008
1204
|
decoders?: ModelViewerDecoderOptions;
|
|
1009
1205
|
modelUrl: string;
|
|
1206
|
+
poster?: string;
|
|
1207
|
+
loading?: "lazy" | "eager"; // default "lazy"
|
|
1208
|
+
reveal?: "auto" | "interaction"; // default "auto"
|
|
1010
1209
|
tilesetUrl?: string | null;
|
|
1011
1210
|
tilesErrorTarget?: number;
|
|
1012
1211
|
tilesCacheSize?: number;
|
|
1013
1212
|
onObjectCatalog?: (objects: ViewerObjectInfo[]) => void;
|
|
1014
|
-
modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz";
|
|
1213
|
+
modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz" | "stl" | "ply";
|
|
1015
1214
|
licenseKey: string;
|
|
1016
1215
|
objectBindings?: Record<string, ObjectBinding> | "auto"; // default "auto"
|
|
1017
1216
|
selectedObject?: ObjectBinding | null;
|
|
@@ -1029,6 +1228,8 @@ type Props = {
|
|
|
1029
1228
|
controller: "orbit" | "pointerLock",
|
|
1030
1229
|
) => void;
|
|
1031
1230
|
onAction?: (event: ObjectActionEvent) => void;
|
|
1231
|
+
onEngagement?: (event: ViewerEngagementEvent) => void | Promise<void>;
|
|
1232
|
+
debugEngagement?: boolean;
|
|
1032
1233
|
onHiddenObjectsChange?: (next: Record<string, boolean>) => void;
|
|
1033
1234
|
onCameraChange?: (state: ViewerCameraState) => void;
|
|
1034
1235
|
onViewerReady?: (viewer: ViewerReadyState) => void;
|
|
@@ -1053,6 +1254,7 @@ type Props = {
|
|
|
1053
1254
|
showDownloadButton?: boolean;
|
|
1054
1255
|
downloadFilename?: string;
|
|
1055
1256
|
showResetButton?: boolean;
|
|
1257
|
+
showFullscreenButton?: boolean;
|
|
1056
1258
|
showLoadingOverlay?: boolean;
|
|
1057
1259
|
showMouseController?: boolean;
|
|
1058
1260
|
mouseControllerPosition?:
|
|
@@ -1074,11 +1276,14 @@ type Props = {
|
|
|
1074
1276
|
moveModeEnabled?: boolean;
|
|
1075
1277
|
objectTransformSpace?: "local" | "world";
|
|
1076
1278
|
enableKeyboardNavigation?: boolean;
|
|
1279
|
+
showObjectNavigator?: boolean;
|
|
1280
|
+
reducedMotion?: "user" | "always" | "never";
|
|
1077
1281
|
onAutoFit?: () => Promise<boolean>;
|
|
1078
1282
|
refitOnResize?: boolean;
|
|
1079
1283
|
renderMode?: "always" | "demand";
|
|
1080
1284
|
maxDpr?: number;
|
|
1081
1285
|
preserveDrawingBuffer?: boolean;
|
|
1286
|
+
webgpu?: boolean;
|
|
1082
1287
|
performanceProfile?: "auto" | "high" | "low";
|
|
1083
1288
|
dracoDecoderPath?: string | false;
|
|
1084
1289
|
ktx2TranscoderPath?: string | false;
|
|
@@ -1086,6 +1291,11 @@ type Props = {
|
|
|
1086
1291
|
showMeasureTools?: boolean;
|
|
1087
1292
|
showUvCheckerButton?: boolean;
|
|
1088
1293
|
showExplodeControls?: boolean;
|
|
1294
|
+
showSectionTools?: boolean;
|
|
1295
|
+
isolation?: ViewerIsolation | null;
|
|
1296
|
+
onIsolationChange?: (isolation: ViewerIsolation | null) => void;
|
|
1297
|
+
showIsolateControls?: boolean;
|
|
1298
|
+
showMaterialVariants?: boolean;
|
|
1089
1299
|
cinematic?: boolean | CinematicConfig;
|
|
1090
1300
|
measurementUnit?: string;
|
|
1091
1301
|
enableXR?: boolean;
|
|
@@ -1105,7 +1315,7 @@ type Props = {
|
|
|
1105
1315
|
|
|
1106
1316
|
### `modelUrl`
|
|
1107
1317
|
|
|
1108
|
-
URL or public path to a `.glb`, `.gltf`, `.obj`, `.fbx`, or `.
|
|
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.
|
|
1109
1319
|
|
|
1110
1320
|
Example:
|
|
1111
1321
|
|
|
@@ -1116,7 +1326,33 @@ modelUrl = "/model.glb";
|
|
|
1116
1326
|
### `modelFormat`
|
|
1117
1327
|
|
|
1118
1328
|
Optional explicit format for signed or extensionless model URLs. Normal URLs
|
|
1119
|
-
ending in `.glb`, `.gltf`, `.obj`, `.fbx`, or `.
|
|
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.
|
|
1120
1356
|
|
|
1121
1357
|
### 3D Tiles streaming
|
|
1122
1358
|
|
|
@@ -1312,6 +1548,7 @@ type ObjectPointerEvent = {
|
|
|
1312
1548
|
clientX: number; // viewport coordinates
|
|
1313
1549
|
clientY: number;
|
|
1314
1550
|
pointerType: string; // "mouse" | "pen" | "touch"
|
|
1551
|
+
surface?: { position: Vec3; normal: Vec3 }; // where on the object, in its own space
|
|
1315
1552
|
};
|
|
1316
1553
|
```
|
|
1317
1554
|
|
|
@@ -1403,6 +1640,8 @@ count:
|
|
|
1403
1640
|
type ViewerModelInfo = {
|
|
1404
1641
|
bounds: { min: Vec3; max: Vec3; center: Vec3; size: Vec3 }; // world space
|
|
1405
1642
|
objectCount: number; // meshes in the loaded scene
|
|
1643
|
+
materialVariants: string[]; // glTF material variant names, if any
|
|
1644
|
+
materialVariantGroups: MaterialVariantGroup[]; // the same, as option groups
|
|
1406
1645
|
};
|
|
1407
1646
|
```
|
|
1408
1647
|
|
|
@@ -1450,6 +1689,60 @@ type ObjectActionEvent = {
|
|
|
1450
1689
|
};
|
|
1451
1690
|
```
|
|
1452
1691
|
|
|
1692
|
+
### `onEngagement` / `debugEngagement`
|
|
1693
|
+
|
|
1694
|
+
Optional interaction analytics. Set `onEngagement` to receive typed
|
|
1695
|
+
`ViewerEngagementEvent` objects, or set `debugEngagement` to log them in the
|
|
1696
|
+
browser's developer console with the `[react-immersive engagement]` prefix.
|
|
1697
|
+
Both are disabled by default. The viewer does not send telemetry or persist
|
|
1698
|
+
analytics identifiers; your callback decides where events go.
|
|
1699
|
+
|
|
1700
|
+
```tsx
|
|
1701
|
+
import { ModelViewer, type ViewerEngagementEvent } from "@liveroom-tech/react-immersive";
|
|
1702
|
+
|
|
1703
|
+
function handleEngagement(event: ViewerEngagementEvent) {
|
|
1704
|
+
if (event.type === "part-viewed") {
|
|
1705
|
+
console.info("Part viewed:", event.objectKey, event.source);
|
|
1706
|
+
}
|
|
1707
|
+
if (event.type === "ar-ended") {
|
|
1708
|
+
console.info("Time in AR:", event.durationMs);
|
|
1709
|
+
}
|
|
1710
|
+
// Connect this callback to your application's analytics integration.
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
<ModelViewer
|
|
1714
|
+
modelUrl="/car.glb"
|
|
1715
|
+
licenseKey={licenseKey}
|
|
1716
|
+
onEngagement={handleEngagement}
|
|
1717
|
+
debugEngagement
|
|
1718
|
+
/>
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
Every event includes `sessionId` (an ephemeral ID for this viewer/model),
|
|
1722
|
+
`timestamp` (ISO format), `elapsedMs`, `modelUrl`, and `tilesetUrl` (or `null`).
|
|
1723
|
+
Durations use a monotonic clock and are expressed in milliseconds. Session
|
|
1724
|
+
elapsed time includes loading and background time; it is not an attention metric.
|
|
1725
|
+
|
|
1726
|
+
| Event `type` | Details and trigger |
|
|
1727
|
+
| --- | --- |
|
|
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. |
|
|
1730
|
+
| `ar-started` | A confirmed immersive WebXR AR session begins. |
|
|
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. |
|
|
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. |
|
|
1733
|
+
| `tour-started` | `annotationCount`. Starting the guided tour. |
|
|
1734
|
+
| `tour-completed` | `annotationCount` and `durationMs`. Advancing past its final annotation; emitted once per completed tour. |
|
|
1735
|
+
| `tour-stopped` | `annotationCount`, `durationMs`, and `reason` (`"stopped"`, `"restarted"`, `"annotations-changed"`, or `"interrupted"`). Leaving a tour before completion. |
|
|
1736
|
+
|
|
1737
|
+
Hover, camera motion, renders, and externally supplied selection/variant updates
|
|
1738
|
+
do not create engagement events. WebXR duration measures the session interval;
|
|
1739
|
+
Quick Look runs outside the page and has no reliable duration callback. Turning
|
|
1740
|
+
tracking on during an existing AR session measures from attachment. Callback
|
|
1741
|
+
changes do not restart timing, and synchronous or asynchronous callback failures are logged without
|
|
1742
|
+
interrupting the viewer. A new model or re-enabling tracking creates a new
|
|
1743
|
+
analytics session; WebGL recovery keeps the session ID; context loss ends active intervals
|
|
1744
|
+
as interrupted.
|
|
1745
|
+
|
|
1453
1746
|
### `onViewerReady`
|
|
1454
1747
|
|
|
1455
1748
|
Optional callback fired once camera controls are available and again when the
|
|
@@ -1469,7 +1762,9 @@ type ViewerReadyState = {
|
|
|
1469
1762
|
objectBindings: Record<string, ObjectBinding>;
|
|
1470
1763
|
homeCameraState: { position: Vec3; target: Vec3 } | null;
|
|
1471
1764
|
captureImage: (options?: CaptureImageOptions) => Promise<string>;
|
|
1765
|
+
captureVideo: (options?: CaptureVideoOptions) => Promise<Blob>;
|
|
1472
1766
|
invalidate: () => void;
|
|
1767
|
+
rendererBackend: "webgl" | "webgpu"; // which renderer draws; see `webgpu`
|
|
1473
1768
|
raw: {
|
|
1474
1769
|
controls: CameraControls; // camera-controls
|
|
1475
1770
|
scene: Object3D | null; // three.js
|
|
@@ -1521,6 +1816,67 @@ const connection = useViewerConnection(camera, effects);
|
|
|
1521
1816
|
const dataUrl = await connection.captureImage({ width: 1920, height: 1080 });
|
|
1522
1817
|
```
|
|
1523
1818
|
|
|
1819
|
+
`captureVideo` records one complete cinematic pass or turntable revolution and
|
|
1820
|
+
resolves with a video `Blob`. It is also available on `useViewerConnection` and
|
|
1821
|
+
`useViewer().connection`. Call it from a button handler after the model has
|
|
1822
|
+
finished loading and framing; ready-state callbacks run repeatedly as the camera
|
|
1823
|
+
moves and should not start recordings automatically.
|
|
1824
|
+
|
|
1825
|
+
```tsx
|
|
1826
|
+
const viewer = useViewer();
|
|
1827
|
+
const abort = new AbortController();
|
|
1828
|
+
|
|
1829
|
+
<ModelViewer {...viewer.props} cinematic showDownloadButton />;
|
|
1830
|
+
|
|
1831
|
+
// Run in an event handler. Upload the Blob or preview it with an object URL.
|
|
1832
|
+
const clip = await viewer.connection.captureVideo({
|
|
1833
|
+
mode: "turntable", // or "cinematic" to use the enabled camera path
|
|
1834
|
+
format: "auto", // prefers WebM; falls back to MP4. "gif" for an animated GIF
|
|
1835
|
+
duration: 12, // seconds; defaults to path timing or a 24-second orbit
|
|
1836
|
+
fps: 30, // 1–60; actual throughput depends on rendering/encoding
|
|
1837
|
+
videoBitsPerSecond: 8_000_000,
|
|
1838
|
+
signal: abort.signal,
|
|
1839
|
+
});
|
|
1840
|
+
// abort.abort() cancels an in-progress export and discards its data.
|
|
1841
|
+
```
|
|
1842
|
+
|
|
1843
|
+
The download menu offers **Record turntable video** and, when cinematic is
|
|
1844
|
+
enabled, **Record cinematic video**, plus Auto/WebM/MP4/GIF format selection. A
|
|
1845
|
+
recording shows progress and a Cancel control. The filename and Blob MIME type
|
|
1846
|
+
match the encoded format; an explicitly requested unsupported format rejects
|
|
1847
|
+
instead of silently changing formats. With `loop: true`, a cinematic export
|
|
1848
|
+
includes the return segment and stops after one cycle.
|
|
1849
|
+
|
|
1850
|
+
Recording runs in real time at the canvas's current pixel resolution, includes
|
|
1851
|
+
post-processing, and hides editor gizmos. It pauses cinematic playback, blocks
|
|
1852
|
+
viewer gestures, then restores the previous camera pose and controls on success,
|
|
1853
|
+
cancellation, or failure. Keep the tab visible until recording completes.
|
|
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,
|
|
1857
|
+
during XR, during another recording, or for durations outside (0, 300] seconds.
|
|
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
|
+
|
|
1524
1880
|
### `onTextureUpload`
|
|
1525
1881
|
|
|
1526
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.
|
|
@@ -1605,22 +1961,39 @@ Current callback shape:
|
|
|
1605
1961
|
Where `AnimationControls` is:
|
|
1606
1962
|
|
|
1607
1963
|
```ts
|
|
1608
|
-
type
|
|
1609
|
-
|
|
1610
|
-
|
|
1964
|
+
type AnimationPlayOptions = {
|
|
1965
|
+
alongside?: boolean; // keep the clips already playing (default false: replace them)
|
|
1966
|
+
fade?: number; // seconds to fade in, and to fade out the clips it replaces
|
|
1967
|
+
reverse?: boolean; // play backwards, from where the clip is or from its end
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
type AnimationClipState = {
|
|
1971
|
+
clip: string;
|
|
1972
|
+
status: "playing" | "paused" | "finished"; // finished: a play-once clip holding its last pose
|
|
1973
|
+
time: number; // seconds
|
|
1974
|
+
duration: number; // seconds
|
|
1611
1975
|
speed: number;
|
|
1612
|
-
|
|
1613
|
-
duration: number; // length of the current clip, in seconds
|
|
1976
|
+
reverse: boolean;
|
|
1614
1977
|
};
|
|
1615
1978
|
|
|
1979
|
+
type AnimationPlaybackState = {
|
|
1980
|
+
currentClip: string | null; // the clip played most recently, while it's active
|
|
1981
|
+
isPlaying: boolean; // whether any clip is playing
|
|
1982
|
+
speed: number; // speed, position and length of currentClip
|
|
1983
|
+
time: number;
|
|
1984
|
+
duration: number;
|
|
1985
|
+
active: AnimationClipState[]; // every active clip, most recently played last
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
// Leave out clipName to act on every active clip.
|
|
1616
1989
|
type AnimationControls = {
|
|
1617
1990
|
clips: string[];
|
|
1618
1991
|
clipDetails?: { sourceName: string; duration: number }[];
|
|
1619
|
-
play: (clipName: string) => void;
|
|
1620
|
-
pause: () => void;
|
|
1621
|
-
stop: () => void;
|
|
1622
|
-
setSpeed: (speed: number) => void;
|
|
1623
|
-
seek?: (time: number) => void; //
|
|
1992
|
+
play: (clipName: string, options?: AnimationPlayOptions) => void;
|
|
1993
|
+
pause: (clipName?: string) => void;
|
|
1994
|
+
stop: (clipName?: string) => void; // stops and resets the pose
|
|
1995
|
+
setSpeed: (speed: number, clipName?: string) => void;
|
|
1996
|
+
seek?: (time: number, clipName?: string) => void; // absolute time, in seconds
|
|
1624
1997
|
getState?: () => AnimationPlaybackState;
|
|
1625
1998
|
subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void;
|
|
1626
1999
|
};
|
|
@@ -1628,8 +2001,8 @@ type AnimationControls = {
|
|
|
1628
2001
|
|
|
1629
2002
|
Wire this to `useViewerAnimations().handleAnimationsReady` for the simplest integration.
|
|
1630
2003
|
When `sceneConfig.animations.autoplayClip` is set, `ModelViewer` will auto-play
|
|
1631
|
-
that clip on load and respect per-clip `loopMode
|
|
1632
|
-
soft-delete (`hidden`) settings.
|
|
2004
|
+
that clip on load and respect per-clip `loopMode` (`"repeat"`, `"once"`, or
|
|
2005
|
+
`"ping-pong"`), `speed`, `displayName`, and soft-delete (`hidden`) settings.
|
|
1633
2006
|
|
|
1634
2007
|
### Lighting
|
|
1635
2008
|
|
|
@@ -1679,6 +2052,81 @@ An attached light follows the object, offset from the object's origin in its
|
|
|
1679
2052
|
local space (the origin is often at its base, not its center). Lighting set up
|
|
1680
2053
|
in `BindingBuilder`'s Scene tab exports as the same `sceneConfig`.
|
|
1681
2054
|
|
|
2055
|
+
### Style rules
|
|
2056
|
+
|
|
2057
|
+
`sceneConfig.styleRules` restyles objects from their own data while the app
|
|
2058
|
+
runs: a colour for a temperature, grey for a disabled part, a pulsing glow for
|
|
2059
|
+
an alarm. Each rule picks objects (`target`), checks their data (`when`), and
|
|
2060
|
+
sets a look (`material`, `colorScale`, `pulse`):
|
|
2061
|
+
|
|
2062
|
+
```tsx
|
|
2063
|
+
const viewer = useViewer({
|
|
2064
|
+
bindings,
|
|
2065
|
+
sceneConfig: {
|
|
2066
|
+
...DEFAULT_SCENE_CONFIG,
|
|
2067
|
+
styleRules: [
|
|
2068
|
+
{
|
|
2069
|
+
id: "temperature",
|
|
2070
|
+
target: { group: "pumps" },
|
|
2071
|
+
colorScale: {
|
|
2072
|
+
metric: "temperature",
|
|
2073
|
+
stops: [[18, "#3b82f6"], [24, "#22c55e"], [32, "#ef4444"]],
|
|
2074
|
+
},
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
id: "offline",
|
|
2078
|
+
when: { status: "disabled" },
|
|
2079
|
+
material: { baseColor: "#9ca3af", opacity: 0.4 },
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
id: "alarm",
|
|
2083
|
+
when: [{ metric: "temperature", above: 32 }, { metadata: "alarm.acknowledged", equals: false }],
|
|
2084
|
+
pulse: { color: "#ff3b30", speed: 1.5 },
|
|
2085
|
+
},
|
|
2086
|
+
],
|
|
2087
|
+
},
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
<ModelViewer modelUrl="/plant.glb" licenseKey="your-license-key" {...viewer.props} />;
|
|
2091
|
+
|
|
2092
|
+
// Live data: update the bindings and the looks follow.
|
|
2093
|
+
viewer.bindings.updateObjectBindings("pump-3", { metrics: { temperature: 34 } });
|
|
2094
|
+
viewer.bindings.updateObjectBindings("valve-1", { status: "disabled" });
|
|
2095
|
+
```
|
|
2096
|
+
|
|
2097
|
+
- `target` takes the same targets as the binding helpers: an id, a list of
|
|
2098
|
+
ids, `{ group }`, `{ tag }`, or `{ type }`. Left out, the rule considers every
|
|
2099
|
+
bound object.
|
|
2100
|
+
- `when` is a condition, or a list that must all match; left out, the rule
|
|
2101
|
+
always applies. Conditions: `{ metric, above?, below? }` (the value must be
|
|
2102
|
+
greater than `above` and less than `below`; an object without the metric
|
|
2103
|
+
doesn't match), `{ status }` with one status or a list (a binding without
|
|
2104
|
+
one counts as `"normal"`), and `{ metadata, equals }`, where dots in the key
|
|
2105
|
+
reach into nested metadata.
|
|
2106
|
+
- `material` accepts any `style.material` field. `colorScale` sets
|
|
2107
|
+
`baseColor` (or `emissive`, with `channel: "emissive"`) from a metric,
|
|
2108
|
+
blending between hex colour stops and taking the nearest stop outside them.
|
|
2109
|
+
`pulse` makes the glow fade in and out (`true`, or `{ color, speed }` in
|
|
2110
|
+
pulses per second).
|
|
2111
|
+
- Rules apply in order over each object's own `style.material`, so a later
|
|
2112
|
+
rule wins where two set the same field. Hover and selection highlighting
|
|
2113
|
+
still show on top.
|
|
2114
|
+
- Rules only change how objects look. `objectBindings`, `onObjectBindingsChange`,
|
|
2115
|
+
and every callback keep the object's own values, so nothing a rule sets is
|
|
2116
|
+
saved with the bindings.
|
|
2117
|
+
- The pulse isn't drawn on streamed 3D Tiles models (`tilesetUrl`); the other
|
|
2118
|
+
looks are.
|
|
2119
|
+
|
|
2120
|
+
BindingBuilder's Scene tab has a **Rules** sub-tab for building these rules
|
|
2121
|
+
visually. It suggests your groups, tags, types, metric names, and metadata
|
|
2122
|
+
keys, shows how many objects each rule matches right now, and previews the
|
|
2123
|
+
result live. Rules export and import with the rest of `sceneConfig`.
|
|
2124
|
+
|
|
2125
|
+
`applyStyleRules(bindings, rules)` (from the main entry or `/utils`) returns
|
|
2126
|
+
the styled bindings, for example to colour your own list the way the viewer
|
|
2127
|
+
does, and `colorScaleValue(stops, value)` the colour a scale gives a value, for
|
|
2128
|
+
a legend.
|
|
2129
|
+
|
|
1682
2130
|
### `camera`
|
|
1683
2131
|
|
|
1684
2132
|
Optional starting camera: where it is and its lens.
|
|
@@ -1942,7 +2390,7 @@ SVG texture template per material. Meshes without a `uv` attribute are omitted;
|
|
|
1942
2390
|
when the model has no UV coordinates, the viewer reports that no layout is
|
|
1943
2391
|
available.
|
|
1944
2392
|
|
|
1945
|
-
The bottom-right action bar renders when at least one of `showResetButton`, `showDownloadButton`, or (`showAnnotationNavigation` with annotations present) is `true`.
|
|
2393
|
+
The bottom-right action bar renders when at least one of `showResetButton`, `showDownloadButton`, `showFullscreenButton` (when supported), or (`showAnnotationNavigation` with annotations present) is `true`.
|
|
1946
2394
|
|
|
1947
2395
|
### `downloadFilename`
|
|
1948
2396
|
|
|
@@ -1964,6 +2412,26 @@ Default:
|
|
|
1964
2412
|
true;
|
|
1965
2413
|
```
|
|
1966
2414
|
|
|
2415
|
+
### `showFullscreenButton`
|
|
2416
|
+
|
|
2417
|
+
Optional boolean that adds an enter/exit fullscreen toggle to the bottom-right
|
|
2418
|
+
action bar. Its grouped form is `ui.fullscreen`. Default: `false`.
|
|
2419
|
+
|
|
2420
|
+
```tsx
|
|
2421
|
+
<ModelViewer
|
|
2422
|
+
modelUrl="/model.glb"
|
|
2423
|
+
licenseKey={licenseKey}
|
|
2424
|
+
ui={{ fullscreen: true }}
|
|
2425
|
+
/>
|
|
2426
|
+
```
|
|
2427
|
+
|
|
2428
|
+
Fullscreen expands the whole viewer, including its panels and overlays. The
|
|
2429
|
+
button reflects browser exits such as Escape, and is hidden when fullscreen is
|
|
2430
|
+
unavailable. It is temporarily disabled during video recording. Embedded viewers
|
|
2431
|
+
need an iframe with `allow="fullscreen"` (or `allowFullScreen` in React). A rejected
|
|
2432
|
+
request displays an error beside the button; see the
|
|
2433
|
+
[Fullscreen API permissions](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#security).
|
|
2434
|
+
|
|
1967
2435
|
### `showLoadingOverlay`
|
|
1968
2436
|
|
|
1969
2437
|
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)).
|
|
@@ -2199,13 +2667,76 @@ Enables automatic orbiting and controls its speed. Defaults are `false` and
|
|
|
2199
2667
|
<ModelViewer {...props} autoRotate autoRotateSpeed={0.75} />
|
|
2200
2668
|
```
|
|
2201
2669
|
|
|
2670
|
+
Auto-rotate doesn't start for visitors who ask their device to reduce motion.
|
|
2671
|
+
See [`reducedMotion`](#reducedmotion).
|
|
2672
|
+
|
|
2202
2673
|
### `enableKeyboardNavigation`
|
|
2203
2674
|
|
|
2204
2675
|
Optional boolean enabling camera shortcuts after the viewer canvas is clicked
|
|
2205
2676
|
or focused. Default: `false`. Use `W`/`S` to move forward/back, `A`/`D` to
|
|
2206
2677
|
truck left/right, Space/`C` to move up/down, and the arrow keys to orbit.
|
|
2207
2678
|
`Shift` + Up/Down moves forward/back. Keyboard navigation is inactive while
|
|
2208
|
-
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.
|
|
2209
2740
|
|
|
2210
2741
|
### `showViewGizmo`
|
|
2211
2742
|
|
|
@@ -2228,6 +2759,8 @@ Current callback shape:
|
|
|
2228
2759
|
|
|
2229
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.
|
|
2230
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
|
+
|
|
2231
2764
|
Default:
|
|
2232
2765
|
|
|
2233
2766
|
```ts
|
|
@@ -2293,6 +2826,89 @@ Default:
|
|
|
2293
2826
|
false;
|
|
2294
2827
|
```
|
|
2295
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
|
+
|
|
2296
2912
|
### `performanceProfile`
|
|
2297
2913
|
|
|
2298
2914
|
Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.
|
|
@@ -2311,6 +2927,20 @@ Default:
|
|
|
2311
2927
|
"auto";
|
|
2312
2928
|
```
|
|
2313
2929
|
|
|
2930
|
+
If the WebGL context is lost, rendering pauses and the viewer displays a
|
|
2931
|
+
**Reload viewer** button. This message appears even with
|
|
2932
|
+
`ui={{ loadingOverlay: false }}`. When the browser fires `webglcontextrestored`,
|
|
2933
|
+
the viewer automatically rebuilds its canvas and scene; the reload button starts
|
|
2934
|
+
the same rebuild without waiting for browser restoration.
|
|
2935
|
+
|
|
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.
|
|
2941
|
+
`onSceneLoaded`, `onModelLoaded`, `onAnimationsReady`, and `onViewerReady` run again
|
|
2942
|
+
for the rebuilt scene, so integrations receive fresh controls and object refs.
|
|
2943
|
+
|
|
2314
2944
|
### `dracoDecoderPath` / `ktx2TranscoderPath` / `meshopt`
|
|
2315
2945
|
|
|
2316
2946
|
Optional controls for the compressed-asset decoders used when loading the GLB/GLTF asset.
|
|
@@ -2375,7 +3005,33 @@ false;
|
|
|
2375
3005
|
|
|
2376
3006
|
### `showMeasureTools`
|
|
2377
3007
|
|
|
2378
|
-
Optional boolean that shows
|
|
3008
|
+
Optional boolean that shows measurement tools and a toggleable bounding-box
|
|
3009
|
+
dimensions overlay. Start measuring, then choose a tool:
|
|
3010
|
+
|
|
3011
|
+
- **Distance:** pick points for distances between consecutive picks.
|
|
3012
|
+
- **Angle:** pick an arm, the angle's vertex, and the other arm. The readout is
|
|
3013
|
+
in degrees, with an arc around the middle pick.
|
|
3014
|
+
- **Area:** pick polygon corners in boundary order, then **Finish area**. The
|
|
3015
|
+
closed polygon is shaded and labeled in square units. Concave polygons are
|
|
3016
|
+
supported; nonplanar, crossing, touching, duplicate, or collinear boundaries
|
|
3017
|
+
show an error. Use Undo to correct a pick.
|
|
3018
|
+
- **Point to plane:** pick a reference face, then a point. The tool highlights
|
|
3019
|
+
the picked triangle and shows the shortest distance to its infinite plane,
|
|
3020
|
+
with a perpendicular segment to the projected point.
|
|
3021
|
+
|
|
3022
|
+
**No snapping** keeps picks on the surface. **Snap: vertex**, **Snap: edge**,
|
|
3023
|
+
and **Snap: auto** snap within 12 CSS pixels to vertices or triangle edges on
|
|
3024
|
+
the picked face. Auto prioritizes nearby vertices. Hover markers identify the
|
|
3025
|
+
snap target; snapped picks are yellow. Snapping respects world transforms,
|
|
3026
|
+
instances, and the current pose of skinned/morph geometry. Mesh triangulation
|
|
3027
|
+
edges can also be snap targets.
|
|
3028
|
+
|
|
3029
|
+
Undo removes the last pick; Clear removes the current measurement. Switching
|
|
3030
|
+
tools clears picks. A new pick after a completed angle, area, or point-to-plane
|
|
3031
|
+
measurement starts another measurement. Picks are snapshots in world space;
|
|
3032
|
+
model changes, display rotation, and binding transforms clear them. Pause
|
|
3033
|
+
animated models before measuring a pose. Hidden objects, decals, and editor
|
|
3034
|
+
helpers are excluded from picking.
|
|
2379
3035
|
|
|
2380
3036
|
Default:
|
|
2381
3037
|
|
|
@@ -2385,7 +3041,10 @@ false;
|
|
|
2385
3041
|
|
|
2386
3042
|
### `measurementUnit`
|
|
2387
3043
|
|
|
2388
|
-
Optional unit suffix appended to
|
|
3044
|
+
Optional unit suffix appended to distance and dimension readouts; area uses its
|
|
3045
|
+
squared form (for example `m²`). Angles always use degrees. glTF models are
|
|
3046
|
+
authored in meters by spec, so values are not converted; this only changes the
|
|
3047
|
+
displayed label.
|
|
2389
3048
|
|
|
2390
3049
|
Default:
|
|
2391
3050
|
|
|
@@ -2403,6 +3062,272 @@ Default:
|
|
|
2403
3062
|
false;
|
|
2404
3063
|
```
|
|
2405
3064
|
|
|
3065
|
+
### `showMaterialVariants`
|
|
3066
|
+
|
|
3067
|
+
Shows swatch buttons, centered at the bottom of the viewer, for switching
|
|
3068
|
+
between the model's glTF material variants (the `KHR_materials_variants`
|
|
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:
|
|
3077
|
+
|
|
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:
|
|
3087
|
+
|
|
3088
|
+
```tsx
|
|
3089
|
+
import { chooseMaterialVariant, useViewer } from "@liveroom-tech/react-immersive";
|
|
3090
|
+
|
|
3091
|
+
const viewer = useViewer();
|
|
3092
|
+
|
|
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
|
+
// ]
|
|
3102
|
+
|
|
3103
|
+
viewer.scene.updateSceneConfig({ model: { activeVariants: ["Paint: Red", "Wheels: Sport"] } });
|
|
3104
|
+
viewer.scene.updateSceneConfig({ model: { activeVariants: [] } }); // the defaults
|
|
3105
|
+
|
|
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
|
+
});
|
|
3113
|
+
```
|
|
3114
|
+
|
|
3115
|
+
- A variant sets each object's starting material. `objectBindings` material
|
|
3116
|
+
overrides (`style.material`) still apply on top of it, and clearing an
|
|
3117
|
+
override returns to the variant's material rather than the file's default.
|
|
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.
|
|
3120
|
+
- A variant's materials and textures load the first time it's chosen; the
|
|
3121
|
+
current look stays until they're ready.
|
|
3122
|
+
- With `onSceneConfigChange`, a pick goes to your scene config (`useViewer`
|
|
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.
|
|
3127
|
+
- An action can switch variants with a scene-config effect:
|
|
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.
|
|
3140
|
+
|
|
3141
|
+
### Decals
|
|
3142
|
+
|
|
3143
|
+
`binding.decals` prints images and text onto an object's surface: a logo on a
|
|
3144
|
+
shirt, a name on a mug, a label on a machine. Each decal is projected onto the
|
|
3145
|
+
object and follows it as it moves, and it's saved with the binding like any
|
|
3146
|
+
other edit.
|
|
3147
|
+
|
|
3148
|
+
```tsx
|
|
3149
|
+
const viewer = useViewer();
|
|
3150
|
+
|
|
3151
|
+
<ModelViewer
|
|
3152
|
+
modelUrl="/mug.glb"
|
|
3153
|
+
licenseKey="your-license-key"
|
|
3154
|
+
{...viewer.props}
|
|
3155
|
+
// Put the customer's text where they double-click.
|
|
3156
|
+
onObjectDoubleClick={({ binding, surface }) => {
|
|
3157
|
+
if (!surface) return;
|
|
3158
|
+
viewer.bindings.addDecal(binding.id, {
|
|
3159
|
+
text: customerName,
|
|
3160
|
+
color: "#1e293b",
|
|
3161
|
+
...surface, // position and normal, in the object's own space
|
|
3162
|
+
size: 0.06, // width in model units
|
|
3163
|
+
});
|
|
3164
|
+
return true; // skip the viewer's own double-click behavior
|
|
3165
|
+
}}
|
|
3166
|
+
/>;
|
|
3167
|
+
|
|
3168
|
+
const id = viewer.bindings.addDecal("mug", { image: "/logos/acme.png", position, normal, size: 0.05 });
|
|
3169
|
+
viewer.bindings.updateDecal("mug", id, { size: 0.08, rotation: Math.PI / 12 });
|
|
3170
|
+
viewer.bindings.removeDecal("mug", id);
|
|
3171
|
+
```
|
|
3172
|
+
|
|
3173
|
+
```ts
|
|
3174
|
+
type ObjectBindingDecal = {
|
|
3175
|
+
id: string;
|
|
3176
|
+
image?: string; // a URL or public path
|
|
3177
|
+
text?: string; // shown when there's no image
|
|
3178
|
+
color?: string; // text colour, default "#ffffff"
|
|
3179
|
+
fontFamily?: string; // CSS font family, default "sans-serif"
|
|
3180
|
+
fontWeight?: "normal" | "bold"; // default "bold"
|
|
3181
|
+
position: [number, number, number]; // on the object, in its own space
|
|
3182
|
+
normal: [number, number, number]; // the way the surface faces there
|
|
3183
|
+
size: number; // width in model units; the height follows the image or text
|
|
3184
|
+
rotation?: number; // radians around the normal, default 0
|
|
3185
|
+
opacity?: number; // 0–1, default 1
|
|
3186
|
+
};
|
|
3187
|
+
```
|
|
3188
|
+
|
|
3189
|
+
- `onObjectDoubleClick` and `onObjectContextMenu` report `surface`: the point
|
|
3190
|
+
under the pointer and the way the surface faces, in the object's own space,
|
|
3191
|
+
which is exactly what a decal's `position` and `normal` take.
|
|
3192
|
+
- `addDecal` returns the decal's id (pass your own `id` to choose it), and
|
|
3193
|
+
takes the same targets as the other binding helpers, so
|
|
3194
|
+
`addDecal({ group: "panels" }, …)` prints on each. Decals are plain binding
|
|
3195
|
+
data: you can also set `decals` directly.
|
|
3196
|
+
- A decal wraps onto the surface around its position, reaching about half
|
|
3197
|
+
its size in depth, so it follows gentle curves without printing on the far
|
|
3198
|
+
side of a thin part.
|
|
3199
|
+
- A decal fades with its object when that object is ghosted by isolation or
|
|
3200
|
+
made see-through by a style rule, and is cut by a section cut.
|
|
3201
|
+
- To let the people using your viewer add their own, give a binding the
|
|
3202
|
+
built-in `add-text` and `add-image` actions:
|
|
3203
|
+
|
|
3204
|
+
```ts
|
|
3205
|
+
actions: [
|
|
3206
|
+
{ id: "add-text", label: "Add Text", type: "command" },
|
|
3207
|
+
{ id: "add-image", label: "Add Image", type: "command" },
|
|
3208
|
+
]
|
|
3209
|
+
```
|
|
3210
|
+
|
|
3211
|
+
Each opens a control under its button in the object panel. **Add Text**
|
|
3212
|
+
takes the text and a colour; **Add Image** takes an image file (sent through
|
|
3213
|
+
`onTextureUpload` when you pass it, so the saved URL lasts; otherwise a
|
|
3214
|
+
blob URL for this session). The decal appears automatically at the centre
|
|
3215
|
+
of the camera-facing surface, ready to drag, resize, turn, or remove. **Move** enables dragging across
|
|
3216
|
+
the object's surface; release to save, or press Escape to cancel. The
|
|
3217
|
+
**Position** arrows move a decal in small steps relative to the current
|
|
3218
|
+
view, without needing to grab it. Click for a fine adjustment or hold an
|
|
3219
|
+
arrow for continuous movement. Each step stays on the nearby visible surface.
|
|
3220
|
+
Every change is saved to the
|
|
3221
|
+
binding's `decals` and reported through `onObjectBindingsChange`. While a
|
|
3222
|
+
decal is in Move mode, a hint shows over the viewer and Escape cancels
|
|
3223
|
+
the current drag without removing the centred decal.
|
|
3224
|
+
- On a rigged (skinned) mesh a decal bends with the animation: it takes on
|
|
3225
|
+
the bone weights of the surface under it and is driven by the same skeleton.
|
|
3226
|
+
A click on a posed mesh is stored at the matching point of its rest shape, so
|
|
3227
|
+
a decal lands where you clicked whatever pose the model is in. Over a joint
|
|
3228
|
+
that bends sharply across few polygons, the decal's outer edge can sit a
|
|
3229
|
+
hair off the surface. Decals don't follow morph targets (blend shapes).
|
|
3230
|
+
- Decals need the binding's object to be a mesh, and aren't drawn on streamed
|
|
3231
|
+
3D Tiles models.
|
|
3232
|
+
|
|
3233
|
+
### Isolating objects
|
|
3234
|
+
|
|
3235
|
+
`isolation` focuses the view on some objects: everything else is ghosted
|
|
3236
|
+
(drawn faintly) or hidden, and clicks and hovers pass through to what's
|
|
3237
|
+
isolated. Nothing about the bindings changes, and clearing the isolation puts
|
|
3238
|
+
everything back. It's view state, like the selection, so it isn't part of
|
|
3239
|
+
`sceneConfig`.
|
|
3240
|
+
|
|
3241
|
+
```tsx
|
|
3242
|
+
const viewer = useViewer();
|
|
3243
|
+
|
|
3244
|
+
<ModelViewer
|
|
3245
|
+
modelUrl="/engine.glb"
|
|
3246
|
+
licenseKey="your-license-key"
|
|
3247
|
+
{...viewer.props}
|
|
3248
|
+
tools={{ isolate: true }}
|
|
3249
|
+
/>;
|
|
3250
|
+
|
|
3251
|
+
viewer.isolation.isolate({ group: "gearbox" }); // ghost everything else
|
|
3252
|
+
viewer.isolation.isolate(["piston-1", "piston-2"], { mode: "hide" });
|
|
3253
|
+
viewer.isolation.clearIsolation();
|
|
3254
|
+
```
|
|
3255
|
+
|
|
3256
|
+
```ts
|
|
3257
|
+
type ViewerIsolation = {
|
|
3258
|
+
target: ObjectBindingTarget; // an id, a list of ids, { group }, { tag }, or { type }
|
|
3259
|
+
mode?: "ghost" | "hide"; // default "ghost"
|
|
3260
|
+
ghostOpacity?: number; // 0–1, default 0.12
|
|
3261
|
+
};
|
|
3262
|
+
|
|
3263
|
+
isolation?: ViewerIsolation | null;
|
|
3264
|
+
onIsolationChange?: (isolation: ViewerIsolation | null) => void;
|
|
3265
|
+
showIsolateControls?: boolean; // tools.isolate
|
|
3266
|
+
```
|
|
3267
|
+
|
|
3268
|
+
- Pass `isolation` (including `null`) to control it, and update it from
|
|
3269
|
+
`onIsolationChange`; `useViewer` wires both, as `viewer.isolation`. Leave
|
|
3270
|
+
`isolation` out and `ModelViewer` keeps it itself, clearing it when
|
|
3271
|
+
`modelUrl` changes. `useViewerIsolation` is the same state on its own.
|
|
3272
|
+
- `showIsolateControls` (`tools.isolate`, default `false`) adds an isolate
|
|
3273
|
+
button to each row of the scene objects panel (click again to show all) and
|
|
3274
|
+
a "Show all" pill at the bottom of the viewer while anything is isolated.
|
|
3275
|
+
- An action can isolate with an `isolation` effect:
|
|
3276
|
+
`{ isolation: { target: { group: "engine" } } }`, or `{ isolation: null }`
|
|
3277
|
+
to show everything again. Pass `isolation` to `useViewerActions` for it
|
|
3278
|
+
(`useViewer` does).
|
|
3279
|
+
- A target that matches no object isolates nothing, rather than fading out
|
|
3280
|
+
the whole model. Ghosting never raises an object's own lower opacity, and
|
|
3281
|
+
style rules still apply underneath.
|
|
3282
|
+
- `applyIsolation` (main entry and `/utils`) applies an isolation to bindings
|
|
3283
|
+
the same way, for rendering your own lists.
|
|
3284
|
+
|
|
3285
|
+
### `showSectionTools`
|
|
3286
|
+
|
|
3287
|
+
Shows a section-cut button over the canvas (`tools.section`). It cuts the model
|
|
3288
|
+
open along an axis, hiding everything on one side of a plane, to look inside
|
|
3289
|
+
assemblies, equipment, and buildings. Default `false`. Not available with
|
|
3290
|
+
[`webgpu`](#webgpu).
|
|
3291
|
+
|
|
3292
|
+
With the cut on, the toolbar picks the axis (X, Y, or Z), the position along
|
|
3293
|
+
it, and which side stays, and a plane appears across the model that you can
|
|
3294
|
+
drag. The cut is stored in `sceneConfig.section`, so you can also set it
|
|
3295
|
+
yourself, author it in BindingBuilder, or switch it from an action:
|
|
3296
|
+
|
|
3297
|
+
```tsx
|
|
3298
|
+
viewer.scene.updateSceneConfig({
|
|
3299
|
+
section: { enabled: true, axis: "y", position: 0.4, flip: false }, // a floor-plan cut
|
|
3300
|
+
});
|
|
3301
|
+
```
|
|
3302
|
+
|
|
3303
|
+
```ts
|
|
3304
|
+
type SceneSectionConfig = {
|
|
3305
|
+
enabled: boolean;
|
|
3306
|
+
axis: "x" | "y" | "z"; // world axis the cut runs across
|
|
3307
|
+
position: number; // 0 at the model's minimum along the axis, 1 at its maximum
|
|
3308
|
+
flip: boolean; // keep the higher side instead of the lower one
|
|
3309
|
+
};
|
|
3310
|
+
```
|
|
3311
|
+
|
|
3312
|
+
- The cut applies to the model only; lights, helpers, and gizmos stay whole.
|
|
3313
|
+
It also shows without the toolbar whenever `sceneConfig.section.enabled` is
|
|
3314
|
+
set; the draggable plane appears only with `showSectionTools`.
|
|
3315
|
+
- While the cut is on, the model's materials render both sides of each
|
|
3316
|
+
surface, so a cut part shows its inside surfaces instead of disappearing.
|
|
3317
|
+
There are no solid caps over the cut.
|
|
3318
|
+
- Clicking and hovering ignore whatever is cut away, so the parts you can see
|
|
3319
|
+
behind the cut are the ones you select.
|
|
3320
|
+
- Toolbar changes go to `onSceneConfigChange` when you pass it (`useViewer`
|
|
3321
|
+
does); otherwise `ModelViewer` keeps them until `sceneConfig.section` or
|
|
3322
|
+
`modelUrl` changes.
|
|
3323
|
+
- The measuring tool can still pick points on the hidden side of a cut.
|
|
3324
|
+
|
|
3325
|
+
Default:
|
|
3326
|
+
|
|
3327
|
+
```ts
|
|
3328
|
+
false;
|
|
3329
|
+
```
|
|
3330
|
+
|
|
2406
3331
|
### `cinematic`
|
|
2407
3332
|
|
|
2408
3333
|
Optional cinematic auto-camera. The camera glides along a path on its own, like a film, ideal for showing off a gallery, apartment, or product with no user interaction. Renders a play/pause button over the canvas; grabbing the camera (or selecting a part) pauses it.
|
|
@@ -2414,7 +3339,7 @@ type CinematicConfig = {
|
|
|
2414
3339
|
waypoints?: CinematicWaypoint[]; // camera keyframes to glide through
|
|
2415
3340
|
duration?: number; // seconds for one full pass (auto-scales when omitted)
|
|
2416
3341
|
loop?: boolean; // default true
|
|
2417
|
-
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
|
|
2418
3343
|
};
|
|
2419
3344
|
|
|
2420
3345
|
type CinematicWaypoint = {
|
|
@@ -2562,7 +3487,10 @@ See the full variable list in the [`ModelViewer` docs](https://react-immersive.l
|
|
|
2562
3487
|
```ts
|
|
2563
3488
|
type SimpleModelViewerProps = {
|
|
2564
3489
|
modelUrl: string;
|
|
2565
|
-
|
|
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";
|
|
2566
3494
|
dracoDecoderPath?: string | false;
|
|
2567
3495
|
ktx2TranscoderPath?: string | false;
|
|
2568
3496
|
meshopt?: boolean;
|
|
@@ -2574,6 +3502,7 @@ type SimpleModelViewerProps = {
|
|
|
2574
3502
|
highlightOnHover?: boolean;
|
|
2575
3503
|
showLoadingOverlay?: boolean;
|
|
2576
3504
|
refitOnResize?: boolean;
|
|
3505
|
+
reducedMotion?: "user" | "always" | "never"; // see ModelViewer's reducedMotion
|
|
2577
3506
|
// Initial values for the scene settings (environment) panel
|
|
2578
3507
|
backgroundEnabled?: boolean;
|
|
2579
3508
|
autoRotate?: boolean;
|
|
@@ -2592,13 +3521,21 @@ type SimpleModelViewerProps = {
|
|
|
2592
3521
|
|
|
2593
3522
|
### `modelUrl`
|
|
2594
3523
|
|
|
2595
|
-
URL or public path to the `.glb`, `.gltf`, `.obj`, `.fbx`, or `.
|
|
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.
|
|
2596
3525
|
|
|
2597
3526
|
### `modelFormat`
|
|
2598
3527
|
|
|
2599
3528
|
Optional explicit format for signed or extensionless model URLs. Normal URLs
|
|
2600
3529
|
ending in a supported extension are detected automatically.
|
|
2601
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
|
+
|
|
2602
3539
|
### `backgroundColor`
|
|
2603
3540
|
|
|
2604
3541
|
Optional canvas background color.
|
|
@@ -2700,7 +3637,7 @@ true;
|
|
|
2700
3637
|
|
|
2701
3638
|
### `enableModelUpload`
|
|
2702
3639
|
|
|
2703
|
-
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.
|
|
2704
3641
|
|
|
2705
3642
|
Default:
|
|
2706
3643
|
|
|
@@ -2762,6 +3699,7 @@ type ObjectBinding = {
|
|
|
2762
3699
|
hoverable?: boolean;
|
|
2763
3700
|
transform?: ObjectBindingTransform;
|
|
2764
3701
|
style?: ObjectBindingStyle;
|
|
3702
|
+
decals?: ObjectBindingDecal[];
|
|
2765
3703
|
actions?: ObjectBindingAction[];
|
|
2766
3704
|
metrics?: Record<string, number>;
|
|
2767
3705
|
metadata?: Record<string, unknown>;
|
|
@@ -2789,9 +3727,9 @@ Notes:
|
|
|
2789
3727
|
|
|
2790
3728
|
## Supported Built-In Actions
|
|
2791
3729
|
|
|
2792
|
-
`ModelViewer` handles
|
|
2793
|
-
`change-material`, `
|
|
2794
|
-
and `set-brightness`. Any other id does something only when you handle it in
|
|
3730
|
+
`ModelViewer` handles eight action ids itself: `change-color`,
|
|
3731
|
+
`change-material`, `add-text`, `add-image`, `toggle-visibility`,
|
|
3732
|
+
`toggle-light`, `change-light-color`, and `set-brightness`. Any other id does something only when you handle it in
|
|
2795
3733
|
`onAction` (pass `useViewerActions().handleAction` there to run an action's
|
|
2796
3734
|
declared `effects`). If a binding has such an action and `ModelViewer` has no
|
|
2797
3735
|
`onAction`, its button does nothing, and the viewer logs a console warning
|
|
@@ -2809,6 +3747,10 @@ findCatalogAction("toggle-light")?.handling; // "viewer"
|
|
|
2809
3747
|
|
|
2810
3748
|
### Implemented behaviors
|
|
2811
3749
|
|
|
3750
|
+
- `add-text`
|
|
3751
|
+
Opens a text box and colour inline beneath the action; the text appears at the centre of the camera-facing surface, ready to drag, resize, turn, or remove. Saved to `binding.decals` (see [Decals](#decals))
|
|
3752
|
+
- `add-image`
|
|
3753
|
+
Opens an image chooser inline beneath the action; the image is placed and edited the same way. Uses `onTextureUpload` for a durable URL when provided, otherwise a session-scoped blob URL
|
|
2812
3754
|
- `toggle-visibility`
|
|
2813
3755
|
Hides or shows the selected mesh by setting `binding.visible` and calling `onObjectBindingsChange`
|
|
2814
3756
|
- `change-color`
|
|
@@ -2882,8 +3824,8 @@ To use this library successfully, your model asset should follow these expectati
|
|
|
2882
3824
|
- the mesh node names must match the keys in `objectBindings` (or be referenced via `modelObjectId`)
|
|
2883
3825
|
- target nodes should have geometry and a supported Three.js mesh material; imported materials are normalized into the editable PBR pipeline
|
|
2884
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
|
|
2885
|
-
- OBJ
|
|
2886
|
-
- OBJ
|
|
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
|
|
2887
3829
|
|
|
2888
3830
|
If a node name or material name does not match, that object will not render through the interactive binding flow.
|
|
2889
3831
|
|
|
@@ -2923,3 +3865,32 @@ Two related picking notes that apply to models of every size:
|
|
|
2923
3865
|
## License
|
|
2924
3866
|
|
|
2925
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.
|