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