mujoco-react 8.10.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +81 -44
  2. package/dist/chunk-33CV6HSV.js +400 -0
  3. package/dist/chunk-33CV6HSV.js.map +1 -0
  4. package/dist/index.d.ts +92 -24
  5. package/dist/index.js +338 -54
  6. package/dist/index.js.map +1 -1
  7. package/dist/spark.d.ts +24 -3
  8. package/dist/spark.js +91 -6
  9. package/dist/spark.js.map +1 -1
  10. package/dist/{types-FFW7ykBu.d.ts → types-izZlUweI.d.ts} +109 -16
  11. package/package.json +1 -1
  12. package/src/components/Body.tsx +3 -1
  13. package/src/components/DragInteraction.tsx +1 -1
  14. package/src/components/IkGizmo.tsx +2 -2
  15. package/src/components/SceneRenderer.tsx +1 -1
  16. package/src/components/TrajectoryPlayer.tsx +4 -1
  17. package/src/components/VisualScenario.tsx +343 -6
  18. package/src/core/MujocoCanvas.tsx +8 -1
  19. package/src/core/MujocoPhysics.tsx +10 -4
  20. package/src/core/MujocoSimProvider.tsx +15 -12
  21. package/src/core/SceneLoader.ts +182 -3
  22. package/src/core/createController.tsx +2 -2
  23. package/src/hooks/useBodyState.ts +1 -1
  24. package/src/hooks/useContacts.ts +1 -1
  25. package/src/hooks/useCtrlNoise.ts +1 -1
  26. package/src/hooks/useFrameCapture.ts +206 -0
  27. package/src/hooks/useGamepad.ts +1 -1
  28. package/src/hooks/useGravityCompensation.ts +1 -1
  29. package/src/hooks/useIkController.ts +22 -13
  30. package/src/hooks/useJointState.ts +1 -1
  31. package/src/hooks/useKeyboardTeleop.ts +1 -1
  32. package/src/hooks/usePolicy.ts +13 -9
  33. package/src/hooks/useSensor.ts +1 -1
  34. package/src/hooks/useTrajectoryPlayer.ts +4 -4
  35. package/src/hooks/useTrajectoryRecorder.ts +1 -1
  36. package/src/index.ts +35 -0
  37. package/src/spark.tsx +138 -4
  38. package/src/types.ts +128 -21
  39. package/dist/chunk-KGFRKPLS.js +0 -186
  40. package/dist/chunk-KGFRKPLS.js.map +0 -1
package/README.md CHANGED
@@ -145,20 +145,42 @@ Use it as a child of `<MujocoCanvas>`:
145
145
 
146
146
  Gaussian splats are visual context; MuJoCo XML remains the source of physics, contacts, and task fixtures. Pair each splat asset with collision proxy metadata so scene variants, rollouts, and datasets preserve both sides of the environment.
147
147
 
148
- Use the renderer-agnostic boundary from the main package:
148
+ Use `VisualScenarioEffects` when the same MuJoCo task should render under
149
+ different camera exposure, fog/background, and deterministic material variants:
149
150
 
150
151
  ```tsx
151
- import { SplatEnvironment } from "mujoco-react";
152
-
153
- <SplatEnvironment
154
- src="/models/lab/scene.spz"
155
- format="spz"
156
- collisionProxyMetadata={{
157
- xmlPath: "/models/lab/collision.xml",
158
- status: "validated",
159
- primitives: ["plane", "box"],
160
- }}
161
- />;
152
+ import { ScenarioLighting, VisualScenarioEffects } from "mujoco-react";
153
+
154
+ <MujocoCanvas config={sceneConfig}>
155
+ <VisualScenarioEffects
156
+ scenario={scenario}
157
+ materialFilter={({ object }) => object.name.startsWith("prop_")}
158
+ />
159
+ <ScenarioLighting preset={scenario.lighting} />
160
+ </MujocoCanvas>;
161
+ ```
162
+
163
+ Use the renderer-agnostic boundary from the main package. If your app stores
164
+ visual scenarios as data, pass the scenario directly; the component resolves the
165
+ splat asset and paired MJCF collision proxy metadata for you.
166
+
167
+ ```tsx
168
+ import { SplatEnvironment, withSplatEnvironment } from "mujoco-react";
169
+
170
+ <SplatEnvironment scenario={scenario} renderer="custom" />;
171
+ ```
172
+
173
+ For MuJoCo + 3DGS composition, derive the collision environment from the same
174
+ splat metadata and pass the resulting config to `<MujocoCanvas>`:
175
+
176
+ ```tsx
177
+ const sceneConfig = withSplatEnvironment(
178
+ {
179
+ src: "/models/xlerobot/",
180
+ sceneFile: "xlerobot.xml",
181
+ },
182
+ kitchenScenario
183
+ );
162
184
  ```
163
185
 
164
186
  For first-class Spark rendering, install Spark and import the optional adapter:
@@ -168,30 +190,33 @@ npm install @sparkjsdev/spark
168
190
  ```
169
191
 
170
192
  ```tsx
171
- import { SparkSplatEnvironment } from "mujoco-react/spark";
172
-
173
- <MujocoCanvas config={sceneConfig} gl={{ preserveDrawingBuffer: true }}>
174
- <SparkSplatEnvironment
175
- src="/models/lab/scene.spz"
176
- format="spz"
177
- collisionProxyMetadata={{
178
- xmlPath: "/models/lab/collision.xml",
179
- status: "validated",
180
- primitives: ["plane", "box"],
181
- }}
182
- hideGroundMeshes
183
- onStatusChange={(status) => console.log(status)}
184
- />
185
- </MujocoCanvas>;
193
+ import {
194
+ SparkSplatEnvironment,
195
+ useSparkSplatLifecycle,
196
+ } from "mujoco-react/spark";
197
+
198
+ function Scene() {
199
+ const splat = useSparkSplatLifecycle();
200
+
201
+ return (
202
+ <MujocoCanvas config={sceneConfig} gl={{ preserveDrawingBuffer: true }}>
203
+ <SparkSplatEnvironment scenario={scenario} hideGroundMeshes {...splat.props} />
204
+ <StatusBadge status={splat.status} error={splat.error} />
205
+ </MujocoCanvas>
206
+ );
207
+ }
186
208
  ```
187
209
 
210
+ `SparkSplatEnvironment` currently renders `.spz` assets. Use the renderer-agnostic
211
+ `SplatEnvironment` for `.ply`/`.splat` metadata or when wiring a different renderer.
212
+
188
213
  ## Write Controllers
189
214
 
190
215
  ```tsx
191
216
  import { useBeforePhysicsStep } from "mujoco-react";
192
217
 
193
218
  function MyController() {
194
- useBeforePhysicsStep((_model, data) => {
219
+ useBeforePhysicsStep(({ data }) => {
195
220
  data.ctrl[0] = Math.sin(data.time);
196
221
  });
197
222
 
@@ -259,7 +284,7 @@ import type { ControlGroupInfo } from "mujoco-react";
259
284
  function HoldTcpPose() {
260
285
  const armRef = useRef<ControlGroupInfo | null>(null);
261
286
 
262
- useBeforePhysicsStep((model, data) => {
287
+ useBeforePhysicsStep(({ model, data }) => {
263
288
  armRef.current ??= resolveControlGroup(model, { siteName: RobotSites.franka.tcp });
264
289
  if (!armRef.current) return;
265
290
 
@@ -280,7 +305,7 @@ Build policy-ready observation vectors from common MuJoCo state without hard-cod
280
305
  import { buildObservation, useBeforePhysicsStep } from "mujoco-react";
281
306
 
282
307
  function PolicyDriver() {
283
- useBeforePhysicsStep((model, data) => {
308
+ useBeforePhysicsStep(({ model, data }) => {
284
309
  const obs = buildObservation(model, data, {
285
310
  qpos: true,
286
311
  qvel: true,
@@ -336,7 +361,7 @@ function useWebSocketControls(url: string) {
336
361
  }, [url]);
337
362
 
338
363
  // Apply incoming actuator controls each physics step.
339
- useBeforePhysicsStep((model, data) => {
364
+ useBeforePhysicsStep(({ model, data }) => {
340
365
  const ctrl = latestCtrlRef.current;
341
366
  if (!ctrl) return;
342
367
  for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
@@ -345,7 +370,7 @@ function useWebSocketControls(url: string) {
345
370
  });
346
371
 
347
372
  // Send simulation feedback back after physics.
348
- useAfterPhysicsStep((model, data) => {
373
+ useAfterPhysicsStep(({ data }) => {
349
374
  const ws = wsRef.current;
350
375
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
351
376
 
@@ -408,8 +433,8 @@ The built-in `useIkController()` uses Damped Least-Squares. Pass `ikSolveFn` to
408
433
  import { RobotSites } from "mujoco-react";
409
434
  import type { IKSolveFn } from "mujoco-react";
410
435
 
411
- const myIK: IKSolveFn = (pos, quat, currentQ) => {
412
- return myAnalyticalSolver(pos, currentQ); // return joint angles or null
436
+ const myIK: IKSolveFn = ({ position, currentQ }) => {
437
+ return myAnalyticalSolver(position, currentQ); // return joint angles or null
413
438
  };
414
439
 
415
440
  const ik = useIkController({ siteName: RobotSites.franka.tcp, ikSolveFn: myIK });
@@ -497,13 +522,24 @@ interface SceneConfig {
497
522
  src: string; // Base URL for model files
498
523
  sceneFile: string; // Entry XML/URDF file, e.g. "scene.xml"
499
524
  files?: File[]; // Local files for browser upload workflows
525
+ environmentFiles?: string[]; // Static MJCF environment XMLs merged before compile
500
526
  sceneObjects?: SceneObject[]; // Objects injected into scene XML at load time
501
527
  homeJoints?: number[]; // Initial joint positions
502
528
  xmlPatches?: XmlPatch[]; // Patches applied to XML files during loading
503
- onReset?: (model, data) => void; // Called during reset after mj_resetData
529
+ onReset?: ({ model, data }) => void; // Called during reset after mj_resetData
504
530
  }
505
531
  ```
506
532
 
533
+ Use `environmentFiles` to compose reusable physics/collision layers with a robot model. For Gaussian splat scenes, keep the `.spz` as a parallel visual layer and point `environmentFiles` at the paired MJCF proxy scene:
534
+
535
+ ```tsx
536
+ const kitchenRobot: SceneConfig = {
537
+ src: "/models/xlerobot/",
538
+ sceneFile: "xlerobot.xml",
539
+ environmentFiles: ["splats/tabletop/scene.xml"],
540
+ };
541
+ ```
542
+
507
543
  ### Local Files and URDF
508
544
 
509
545
  Load browser-selected MJCF or URDF files directly. Folder uploads preserve `webkitRelativePath`, and flat uploads fall back to matching referenced mesh/texture assets by basename:
@@ -600,10 +636,10 @@ Thin wrapper around R3F `<Canvas>`. Accepts all R3F Canvas props plus:
600
636
  | Prop | Type | Description |
601
637
  |------|------|-------------|
602
638
  | `config` | `SceneConfig` | **Required.** Scene/robot configuration |
603
- | `onReady` | `(api: MujocoSimAPI) => void` | Fires when model is loaded |
639
+ | `onReady` | `({ api }) => void` | Fires when model is loaded |
604
640
  | `onError` | `(error: Error) => void` | Fires on scene load failure |
605
- | `onStep` | `(time: number) => void` | Called each physics step |
606
- | `onSelection` | `(bodyId: number, name: string) => void` | Called on double-click |
641
+ | `onStep` | `({ time, model, data }) => void` | Called each physics step |
642
+ | `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
607
643
  | `gravity` | `[number, number, number]` | Override model gravity |
608
644
  | `timestep` | `number` | Override model.opt.timestep |
609
645
  | `substeps` | `number` | mj_step calls per frame |
@@ -628,10 +664,10 @@ Physics provider for use inside your own R3F `<Canvas>`. Same physics props as `
628
664
  | Prop | Type | Description |
629
665
  |------|------|-------------|
630
666
  | `config` | `SceneConfig` | **Required.** Scene/robot configuration |
631
- | `onReady` | `(api: MujocoSimAPI) => void` | Fires when model is loaded |
667
+ | `onReady` | `({ api }) => void` | Fires when model is loaded |
632
668
  | `onError` | `(error: Error) => void` | Fires on scene load failure |
633
- | `onStep` | `(time: number) => void` | Called each physics step |
634
- | `onSelection` | `(bodyId: number, name: string) => void` | Called on double-click |
669
+ | `onStep` | `({ time, model, data }) => void` | Called each physics step |
670
+ | `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
635
671
  | `gravity` | `[number, number, number]` | Override model gravity |
636
672
  | `timestep` | `number` | Override model.opt.timestep |
637
673
  | `substeps` | `number` | mj_step calls per frame |
@@ -679,7 +715,7 @@ drei PivotControls gizmo that tracks a MuJoCo site and drives IK on drag. Requir
679
715
  | `controller` | `IkContextValue` | **required** | Controller from `useIkController()` |
680
716
  | `siteName` | `string?` | controller's site | MuJoCo site to track |
681
717
  | `scale` | `number?` | `0.18` | Gizmo handle scale |
682
- | `onDrag` | `(pos, quat) => void` | -- | Custom drag handler (disables auto-IK) |
718
+ | `onDrag` | `({ position, quaternion }) => void` | -- | Custom drag handler (disables auto-IK) |
683
719
 
684
720
  ### `<DragInteraction />`
685
721
 
@@ -790,7 +826,7 @@ if (mujoco) {
790
826
  Run logic **before** `mj_step` each frame. Write to `data.ctrl`, apply forces, drive automation.
791
827
 
792
828
  ```tsx
793
- useBeforePhysicsStep((model, data) => {
829
+ useBeforePhysicsStep(({ data }) => {
794
830
  data.ctrl[0] = Math.sin(data.time);
795
831
  });
796
832
  ```
@@ -905,7 +941,8 @@ const obs = useObservation({ qpos: true, qvel: true, projectedGravity: "torso" }
905
941
  const policy = usePolicy({
906
942
  frequency: 50,
907
943
  onObservation: () => obs.readValues(),
908
- onAction: (action, model, data) => applyAction(action, data),
944
+ infer: ({ observation }) => policySession.run(observation),
945
+ onAction: ({ action, data }) => applyAction(action, data),
909
946
  });
910
947
  ```
911
948
 
@@ -0,0 +1,400 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { useEffect, useMemo } from 'react';
3
+ import * as THREE from 'three';
4
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/components/VisualScenario.tsx
7
+ var DEFAULT_BACKGROUND = "#181a1f";
8
+ function ScenarioLighting({
9
+ preset = "studio",
10
+ castShadow = true,
11
+ intensity = 1
12
+ }) {
13
+ if (preset === "warehouse") {
14
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
15
+ /* @__PURE__ */ jsx("ambientLight", { intensity: 0.18 * intensity }),
16
+ /* @__PURE__ */ jsx(
17
+ "directionalLight",
18
+ {
19
+ position: [3.5, -2, 5],
20
+ intensity: 2.2 * intensity,
21
+ castShadow
22
+ }
23
+ ),
24
+ /* @__PURE__ */ jsx("directionalLight", { position: [-2, 1.5, 2.5], intensity: 0.25 * intensity })
25
+ ] });
26
+ }
27
+ if (preset === "low-light") {
28
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ /* @__PURE__ */ jsx("ambientLight", { intensity: 0.08 * intensity }),
30
+ /* @__PURE__ */ jsx(
31
+ "directionalLight",
32
+ {
33
+ position: [2, -2, 3],
34
+ intensity: 0.75 * intensity,
35
+ castShadow
36
+ }
37
+ ),
38
+ /* @__PURE__ */ jsx("pointLight", { position: [-0.5, -0.8, 1.3], intensity: 0.6 * intensity })
39
+ ] });
40
+ }
41
+ if (preset === "splat") {
42
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
43
+ /* @__PURE__ */ jsx("ambientLight", { intensity: 0.42 * intensity }),
44
+ /* @__PURE__ */ jsx(
45
+ "directionalLight",
46
+ {
47
+ position: [1.8, -2.4, 3.5],
48
+ intensity: 1.2 * intensity,
49
+ castShadow
50
+ }
51
+ ),
52
+ /* @__PURE__ */ jsx("pointLight", { position: [0.4, 0.2, 1.4], intensity: 0.35 * intensity })
53
+ ] });
54
+ }
55
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
56
+ /* @__PURE__ */ jsx("ambientLight", { intensity: 0.35 * intensity }),
57
+ /* @__PURE__ */ jsx(
58
+ "directionalLight",
59
+ {
60
+ position: [2.5, -3, 4],
61
+ intensity: 1.6 * intensity,
62
+ castShadow
63
+ }
64
+ )
65
+ ] });
66
+ }
67
+ function getScenarioBackground(preset, fallback = DEFAULT_BACKGROUND) {
68
+ if (preset === "warehouse") return "#20242b";
69
+ if (preset === "low-light") return "#0f1115";
70
+ if (preset === "splat") return "#1b1f24";
71
+ return fallback;
72
+ }
73
+ function getScenarioCameraPosition(basePosition, scenario) {
74
+ const [x, y, z] = basePosition;
75
+ const jitter = scenario?.camera?.jitter ?? 0;
76
+ return [
77
+ Number((x + jitter * 0.6).toFixed(3)),
78
+ Number((y - jitter * 0.4).toFixed(3)),
79
+ Number((z + jitter * 0.25).toFixed(3))
80
+ ];
81
+ }
82
+ function VisualScenarioEffects(props) {
83
+ useVisualScenarioEffects(props);
84
+ return null;
85
+ }
86
+ function useVisualScenarioEffects({
87
+ scenario,
88
+ enabled = true,
89
+ applyBackground = true,
90
+ applyFog = true,
91
+ applyRenderer = true,
92
+ applyMaterials = true,
93
+ background,
94
+ fogNear,
95
+ fogFar,
96
+ materialFilter
97
+ }) {
98
+ const { gl, scene, invalidate } = useThree();
99
+ useEffect(() => {
100
+ if (!enabled || !scenario) {
101
+ return void 0;
102
+ }
103
+ const previousExposure = gl.toneMappingExposure;
104
+ const previousBackground = scene.background;
105
+ const previousFog = scene.fog;
106
+ const materialSnapshots = /* @__PURE__ */ new Map();
107
+ if (applyRenderer) {
108
+ gl.toneMappingExposure = scenario.camera?.exposure ?? 1;
109
+ }
110
+ if (applyBackground) {
111
+ scene.background = new THREE.Color(
112
+ background ?? getScenarioBackground(scenario.lighting)
113
+ );
114
+ }
115
+ if (applyFog) {
116
+ scene.fog = createScenarioFog(scenario, background, fogNear, fogFar);
117
+ }
118
+ if (applyMaterials && scenario.materials) {
119
+ applyScenarioMaterials(scene, scenario, materialSnapshots, materialFilter);
120
+ }
121
+ invalidate();
122
+ return () => {
123
+ gl.toneMappingExposure = previousExposure;
124
+ scene.background = previousBackground;
125
+ scene.fog = previousFog;
126
+ for (const [material, snapshot] of materialSnapshots) {
127
+ const mutable = getMutableScenarioMaterial(material);
128
+ if (!mutable) continue;
129
+ if (snapshot.color) mutable.color.copy(snapshot.color);
130
+ if (typeof snapshot.roughness === "number") {
131
+ mutable.roughness = snapshot.roughness;
132
+ }
133
+ if (typeof snapshot.metalness === "number") {
134
+ mutable.metalness = snapshot.metalness;
135
+ }
136
+ mutable.needsUpdate = true;
137
+ }
138
+ invalidate();
139
+ };
140
+ }, [
141
+ applyBackground,
142
+ applyFog,
143
+ applyMaterials,
144
+ applyRenderer,
145
+ background,
146
+ enabled,
147
+ fogFar,
148
+ fogNear,
149
+ gl,
150
+ invalidate,
151
+ materialFilter,
152
+ scenario,
153
+ scene
154
+ ]);
155
+ }
156
+ function SplatEnvironment({
157
+ environment,
158
+ scenario,
159
+ renderer,
160
+ src,
161
+ format,
162
+ collisionProxy,
163
+ collisionProxyMetadata,
164
+ children,
165
+ showPlaceholder = true,
166
+ ...groupProps
167
+ }) {
168
+ const metadata = useSplatEnvironment({
169
+ environment,
170
+ scenario,
171
+ renderer,
172
+ src,
173
+ format,
174
+ collisionProxy: collisionProxyMetadata
175
+ });
176
+ const existingUserData = typeof groupProps.userData === "object" && groupProps.userData !== null ? groupProps.userData : {};
177
+ return /* @__PURE__ */ jsxs(
178
+ "group",
179
+ {
180
+ ...groupProps,
181
+ userData: {
182
+ ...existingUserData,
183
+ ...metadata.userData
184
+ },
185
+ children: [
186
+ children,
187
+ children || !showPlaceholder ? null : /* @__PURE__ */ jsx(SplatPlaceholder, {}),
188
+ collisionProxy
189
+ ]
190
+ }
191
+ );
192
+ }
193
+ function useSplatEnvironment({
194
+ environment,
195
+ scenario,
196
+ renderer,
197
+ src,
198
+ format,
199
+ collisionProxy
200
+ }) {
201
+ const scenarioEnvironment = useMemo(
202
+ () => environment ?? (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : void 0),
203
+ [environment, renderer, scenario]
204
+ );
205
+ const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;
206
+ const resolvedFormat = format ?? scenarioEnvironment?.splat.format ?? scenario?.splat?.format ?? "spz";
207
+ const resolvedCollisionProxy = collisionProxy ?? scenarioEnvironment?.collisionProxy ?? scenario?.splat?.collisionProxy ?? void 0;
208
+ return useMemo(
209
+ () => ({
210
+ src: resolvedSrc,
211
+ format: resolvedFormat,
212
+ collisionProxy: resolvedCollisionProxy,
213
+ userData: createSplatEnvironmentUserData({
214
+ environment: scenarioEnvironment,
215
+ src: resolvedSrc,
216
+ format: resolvedFormat,
217
+ collisionProxy: resolvedCollisionProxy
218
+ })
219
+ }),
220
+ [scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
221
+ );
222
+ }
223
+ function createPairedSplatEnvironment(scenario, options = {}) {
224
+ const splat = scenario.splat;
225
+ const collisionProxy = splat?.collisionProxy;
226
+ if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {
227
+ return void 0;
228
+ }
229
+ return {
230
+ id: options.id ?? scenario.id ?? "splat-environment",
231
+ label: options.label ?? scenario.label ?? "Gaussian splat environment",
232
+ description: options.description ?? (scenario.environment ? `Visual ${scenario.environment} splat paired with MJCF collision proxy.` : void 0),
233
+ splat: {
234
+ src: splat.src,
235
+ format: splat.format ?? "spz",
236
+ renderer: options.renderer
237
+ },
238
+ collisionProxy: {
239
+ ...collisionProxy,
240
+ xmlPath: collisionProxy.xmlPath
241
+ }
242
+ };
243
+ }
244
+ function isPairedSplatEnvironment(input) {
245
+ return !!input && "collisionProxy" in input && "splat" in input;
246
+ }
247
+ function sceneRelativePath(sceneConfig, path) {
248
+ const src = sceneConfig.src;
249
+ if (!src) return path;
250
+ const base = src.endsWith("/") ? src : src + "/";
251
+ if (path.startsWith(base)) return path.slice(base.length);
252
+ return path;
253
+ }
254
+ function uniquePaths(paths) {
255
+ const seen = /* @__PURE__ */ new Set();
256
+ const result = [];
257
+ for (const path of paths) {
258
+ if (seen.has(path)) continue;
259
+ seen.add(path);
260
+ result.push(path);
261
+ }
262
+ return result;
263
+ }
264
+ function withSplatEnvironment(sceneConfig, input, options = {}) {
265
+ const environment = isPairedSplatEnvironment(input) ? input : input ? createPairedSplatEnvironment(input, options) : void 0;
266
+ const xmlPath = environment?.collisionProxy.xmlPath;
267
+ if (!xmlPath) return sceneConfig;
268
+ return {
269
+ ...sceneConfig,
270
+ environmentFiles: uniquePaths([
271
+ ...sceneConfig.environmentFiles ?? [],
272
+ sceneRelativePath(sceneConfig, xmlPath)
273
+ ])
274
+ };
275
+ }
276
+ function createSplatEnvironmentUserData({
277
+ environment,
278
+ src,
279
+ format = "spz",
280
+ collisionProxy
281
+ }) {
282
+ return {
283
+ role: "splat-environment",
284
+ environmentId: environment?.id,
285
+ environmentLabel: environment?.label,
286
+ splatSrc: src,
287
+ splatFormat: format,
288
+ splatRenderer: environment?.splat.renderer,
289
+ collisionProxyStatus: collisionProxy?.status ?? "missing",
290
+ collisionProxyXmlPath: collisionProxy?.xmlPath,
291
+ collisionProxyPrimitives: collisionProxy?.primitives ?? []
292
+ };
293
+ }
294
+ function createSparkSplatViewerUrl({
295
+ viewerUrl,
296
+ splatSrc
297
+ }) {
298
+ const url = new URL(viewerUrl, "http://mujoco-react.local");
299
+ url.searchParams.set("splat", splatSrc);
300
+ return viewerUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
301
+ }
302
+ function SplatPlaceholder() {
303
+ return /* @__PURE__ */ jsx("group", { children: /* @__PURE__ */ jsxs("mesh", { position: [0, 0, 1.2], children: [
304
+ /* @__PURE__ */ jsx("boxGeometry", { args: [2.4, 2.4, 2.4] }),
305
+ /* @__PURE__ */ jsx(
306
+ "meshBasicMaterial",
307
+ {
308
+ color: "#8b8b8b",
309
+ transparent: true,
310
+ opacity: 0.06,
311
+ wireframe: true,
312
+ side: THREE.DoubleSide
313
+ }
314
+ )
315
+ ] }) });
316
+ }
317
+ function createScenarioFog(scenario, background, fogNear, fogFar) {
318
+ if (scenario.lighting === "low-light") {
319
+ return new THREE.Fog(
320
+ background ?? getScenarioBackground(scenario.lighting),
321
+ fogNear ?? 2.5,
322
+ fogFar ?? 9
323
+ );
324
+ }
325
+ if (scenario.lighting === "warehouse") {
326
+ return new THREE.Fog(
327
+ background ?? getScenarioBackground(scenario.lighting),
328
+ fogNear ?? 5,
329
+ fogFar ?? 16
330
+ );
331
+ }
332
+ return null;
333
+ }
334
+ function applyScenarioMaterials(scene, scenario, snapshots, materialFilter) {
335
+ const materials = scenario.materials;
336
+ if (!materials) return;
337
+ scene.traverse((object) => {
338
+ if (!(object instanceof THREE.Mesh)) {
339
+ return;
340
+ }
341
+ for (const material of normalizeMaterials(object.material)) {
342
+ const mutable = getMutableScenarioMaterial(material);
343
+ if (!mutable) continue;
344
+ if (materialFilter && !materialFilter({ object, material })) continue;
345
+ if (!snapshots.has(material)) {
346
+ snapshots.set(material, {
347
+ color: mutable.color.clone(),
348
+ roughness: mutable.roughness,
349
+ metalness: mutable.metalness
350
+ });
351
+ }
352
+ applyScenarioMaterial(mutable, object, scenario, materials);
353
+ }
354
+ });
355
+ }
356
+ function applyScenarioMaterial(material, object, scenario, materials) {
357
+ const seed = scenario.seed ?? 0;
358
+ const objectKey = `${scenario.id ?? "scenario"}:${object.name}:${material.name}:${seed}`;
359
+ const variation = hashToUnitInterval(objectKey);
360
+ if (materials.randomizeObjectColors) {
361
+ material.color.setHSL(variation, 0.38, 0.42);
362
+ }
363
+ if (materials.randomizeTableMaterial) {
364
+ material.roughness = clamp01(
365
+ materials.roughness ?? 0.35 + variation * 0.45
366
+ );
367
+ material.metalness = clamp01(
368
+ materials.metalness ?? variation * 0.12
369
+ );
370
+ }
371
+ material.needsUpdate = true;
372
+ }
373
+ function normalizeMaterials(material) {
374
+ return Array.isArray(material) ? material : [material];
375
+ }
376
+ function getMutableScenarioMaterial(material) {
377
+ if (material instanceof THREE.MeshStandardMaterial || material instanceof THREE.MeshPhysicalMaterial) {
378
+ return material;
379
+ }
380
+ return null;
381
+ }
382
+ function hashToUnitInterval(value) {
383
+ let hash = 2166136261;
384
+ for (let index = 0; index < value.length; index += 1) {
385
+ hash ^= value.charCodeAt(index);
386
+ hash = Math.imul(hash, 16777619);
387
+ }
388
+ return (hash >>> 0) / 4294967295;
389
+ }
390
+ function clamp01(value) {
391
+ return Math.max(0, Math.min(1, value));
392
+ }
393
+ /**
394
+ * @license
395
+ * SPDX-License-Identifier: Apache-2.0
396
+ */
397
+
398
+ export { ScenarioLighting, SplatEnvironment, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getScenarioBackground, getScenarioCameraPosition, useSplatEnvironment, useVisualScenarioEffects, withSplatEnvironment };
399
+ //# sourceMappingURL=chunk-33CV6HSV.js.map
400
+ //# sourceMappingURL=chunk-33CV6HSV.js.map