mujoco-react 9.2.0 → 9.4.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 CHANGED
@@ -143,7 +143,7 @@ Use it as a child of `<MujocoCanvas>`:
143
143
 
144
144
  ## Gaussian Splat Environments
145
145
 
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.
146
+ Gaussian splats are visual context; MuJoCo XML remains the source of physics, contacts, and task fixtures. Use visual-only splats when you only need rendered environment context, and add collision proxy metadata when a workflow needs simplified contact geometry.
147
147
 
148
148
  Use `VisualScenarioEffects` when the same MuJoCo task should render under
149
149
  different camera exposure, fog/background, and deterministic material variants:
@@ -162,16 +162,102 @@ import { ScenarioLighting, VisualScenarioEffects } from "mujoco-react";
162
162
 
163
163
  Use the renderer-agnostic boundary from the main package. If your app stores
164
164
  visual scenarios as data, pass the scenario directly; the component resolves the
165
- splat asset and paired MJCF collision proxy metadata for you.
165
+ splat asset and any paired MJCF collision proxy metadata for you. Visual-only
166
+ splats are valid, and readiness tells you whether a collision proxy is required
167
+ for your training/physics handoff.
166
168
 
167
169
  ```tsx
168
- import { SplatEnvironment, withSplatEnvironment } from "mujoco-react";
170
+ import { MujocoCanvas, SplatEnvironment, useSplatSceneConfig } from "mujoco-react";
169
171
 
170
- <SplatEnvironment scenario={scenario} renderer="custom" />;
172
+ const splat = useSplatSceneConfig({ sceneConfig, scenario });
173
+
174
+ <MujocoCanvas config={splat.sceneConfig}>
175
+ {splat.environment ? (
176
+ <SplatEnvironment environment={splat.environment} renderer="custom">
177
+ <MySplatRenderer src={splat.environment.splat.src} />
178
+ </SplatEnvironment>
179
+ ) : null}
180
+ </MujocoCanvas>;
181
+ ```
182
+
183
+ When a splat scenario includes paired MJCF collision proxy metadata, render a
184
+ generic wireframe preview from that XML with `SplatCollisionProxyPreview`. The
185
+ component parses MJCF primitives such as planes, boxes, spheres, capsules, and
186
+ mesh placeholders from any fetchable proxy XML; the tabletop example is just one
187
+ possible environment.
188
+
189
+ ```tsx
190
+ import {
191
+ SplatCollisionProxyPreview,
192
+ SplatEnvironment,
193
+ useSplatCollisionProxyGeoms,
194
+ useSplatSceneConfig,
195
+ } from "mujoco-react";
196
+
197
+ const splat = useSplatSceneConfig({ sceneConfig, scenario });
198
+ const proxy = splat.environment?.collisionProxy;
199
+
200
+ <SplatEnvironment
201
+ environment={splat.environment}
202
+ collisionProxy={
203
+ proxy ? <SplatCollisionProxyPreview collisionProxy={proxy} /> : undefined
204
+ }
205
+ />;
206
+ ```
207
+
208
+ Use `useSplatCollisionProxyGeoms()` when your app wants to inspect or style the
209
+ proxy primitives itself:
210
+
211
+ ```tsx
212
+ const proxyPreview = useSplatCollisionProxyGeoms({
213
+ collisionProxy: splat.environment?.collisionProxy,
214
+ });
215
+
216
+ proxyPreview.geoms.map((geom) => geom.type);
171
217
  ```
172
218
 
173
- For MuJoCo + 3DGS composition, derive the collision environment from the same
174
- splat metadata and pass the resulting config to `<MujocoCanvas>`:
219
+ Use `splat.readiness` or `getSplatEnvironmentReadiness(scenario)` to gate
220
+ authoring and import flows. The status distinguishes disabled scenarios,
221
+ missing visual assets, missing collision proxies, unsupported renderer formats,
222
+ and ready environments.
223
+
224
+ Use `createSplatSceneConfig()` when the same scene composition needs to run
225
+ outside React, such as codegen, import validation, backend handoff metadata, or
226
+ tests:
227
+
228
+ ```tsx
229
+ import { createSplatSceneConfig } from "mujoco-react";
230
+
231
+ const splat = createSplatSceneConfig({
232
+ sceneConfig,
233
+ scenario,
234
+ renderer: "spark",
235
+ });
236
+ ```
237
+
238
+ Use `createVisualScenarioExecutionContext()` or
239
+ `useVisualScenarioExecutionContext()` when recording rollouts or exporting
240
+ LeRobot/HF Jobs handoff artifacts. It resolves the scenario seed, camera
241
+ exposure/noise/blur/jitter, material randomization, splat source, collision
242
+ proxy, and readiness into one serializable object.
243
+
244
+ ```tsx
245
+ import { createVisualScenarioExecutionContext } from "mujoco-react";
246
+
247
+ const visualContext = createVisualScenarioExecutionContext({
248
+ scenario,
249
+ renderer: "spark",
250
+ variantId,
251
+ });
252
+
253
+ writeEpisodeManifest({
254
+ task,
255
+ visualExecutionContext: visualContext,
256
+ });
257
+ ```
258
+
259
+ For MuJoCo + 3DGS composition, derive the optional collision environment from
260
+ the same splat metadata and pass the resulting config to `<MujocoCanvas>`:
175
261
 
176
262
  ```tsx
177
263
  const sceneConfig = withSplatEnvironment(
@@ -192,16 +278,18 @@ npm install @sparkjsdev/spark
192
278
  ```tsx
193
279
  import {
194
280
  SparkSplatEnvironment,
195
- useSparkSplatLifecycle,
281
+ useSparkSplatEnvironment,
196
282
  } from "mujoco-react/spark";
197
283
 
198
284
  function Scene() {
199
- const splat = useSparkSplatLifecycle();
285
+ const splat = useSparkSplatEnvironment({ sceneConfig, scenario });
200
286
 
201
287
  return (
202
- <MujocoCanvas config={sceneConfig} gl={{ preserveDrawingBuffer: true }}>
203
- <SparkSplatEnvironment scenario={scenario} hideGroundMeshes {...splat.props} />
204
- <StatusBadge status={splat.status} error={splat.error} />
288
+ <MujocoCanvas config={splat.sceneConfig} gl={{ preserveDrawingBuffer: true }}>
289
+ {splat.environment ? (
290
+ <SparkSplatEnvironment hideGroundMeshes {...splat.props} />
291
+ ) : null}
292
+ <StatusBadge status={splat.lifecycle.status} error={splat.lifecycle.error} />
205
293
  </MujocoCanvas>
206
294
  );
207
295
  }
@@ -537,7 +625,7 @@ interface SceneConfig {
537
625
  }
538
626
  ```
539
627
 
540
- 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:
628
+ 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 a paired MJCF proxy scene only when contact geometry is needed:
541
629
 
542
630
  ```tsx
543
631
  const kitchenRobot: SceneConfig = {
@@ -1009,11 +1097,133 @@ Use `useFrameCapture()` or the standalone `captureFrame()` helpers when you own
1009
1097
  the canvas or want to capture a custom container.
1010
1098
 
1011
1099
  Use `captureCameraFrame()` / `captureCameraFrameBlob()` when dataset generation
1012
- needs a fixed offscreen camera pose or resolution without moving the user's
1013
- interactive viewport.
1100
+ needs an offscreen camera render at a stable resolution without moving the
1101
+ user's interactive viewport. Pass `cameraName`, `siteName`, or `bodyName` to
1102
+ record true MuJoCo-mounted camera frames; the returned image includes
1103
+ `source.kind` so dataset pipelines can reject fallback or synthetic fixed poses.
1014
1104
 
1015
1105
  Use `recordCameraSequence()` / `useCameraSequenceRecorder()` to step policy
1016
- rollouts and capture synchronized frames from one or more fixed camera configs.
1106
+ rollouts and capture synchronized per-camera frames from one or more MuJoCo
1107
+ camera configs. Sequence recording requires mounted MuJoCo camera, site, or
1108
+ body selectors by default; use still capture APIs for synthetic debug poses.
1109
+
1110
+ For LeRobot-style datasets, prefer the named-camera wrapper. It resolves task
1111
+ camera keys to MuJoCo cameras/sites/bodies, records the sequence, and returns
1112
+ the plan and readiness summary alongside frame provenance:
1113
+
1114
+ ```tsx
1115
+ const sequence = await recordMountedCameraFrameSequence(api, {
1116
+ cameraKeys: ["head", "left_wrist", "right_wrist"],
1117
+ aliases: {
1118
+ head: [{ siteName: "head_camera_rgb_optical_frame" }],
1119
+ left_wrist: [{ siteName: "left_wrist_camera_optical_frame" }],
1120
+ right_wrist: [{ siteName: "right_wrist_camera_optical_frame" }],
1121
+ },
1122
+ defaults: {
1123
+ width: 640,
1124
+ height: 480,
1125
+ type: "image/png",
1126
+ fov: 45,
1127
+ near: 0.01,
1128
+ far: 100,
1129
+ },
1130
+ frames: 16,
1131
+ stepsPerFrame: 1,
1132
+ retainFrames: false,
1133
+ requireMountedSources: true,
1134
+ onFrame: ({ frameIndex, cameras }) => {
1135
+ queueLeRobotImages(frameIndex, cameras);
1136
+ },
1137
+ });
1138
+
1139
+ sequence.readiness.ready; // true when every requested stream resolved
1140
+ sequence.plan.missingKeys; // unresolved task cameras, if requireAll is false
1141
+ sequence.cameraSummaries.head.source; // mounted source provenance
1142
+
1143
+ const manifest = createMountedCameraFrameSequenceManifest(sequence);
1144
+ manifest.streamSummaries.head.complete; // per-camera frame coverage
1145
+ manifest.status; // "complete", "partial", or "missing"
1146
+ ```
1147
+
1148
+ `recordMountedCameraFrameSequence()` requires all requested `cameraKeys` by
1149
+ default so dataset recording cannot silently omit a camera stream. Set
1150
+ `requireAll: false` only for exploratory tooling that can tolerate partial
1151
+ camera coverage.
1152
+
1153
+ Use `createMountedCameraFrameSequenceManifest()` after recording to persist a
1154
+ stable dataset-facing manifest with readiness, source targets, dimensions,
1155
+ first/last frame indices, timestamps, and per-camera missing-frame counts.
1156
+
1157
+ Inside `<MujocoCanvas>` children, `useMountedCameraSequenceRecorder()` exposes
1158
+ the same planning and recording surface with React status/error/result state.
1159
+ Use `checkReadiness()` before recording when the UI needs a preflight gate for
1160
+ LeRobot camera streams:
1161
+
1162
+ ```tsx
1163
+ function DatasetRecorder() {
1164
+ const recorder = useMountedCameraSequenceRecorder({
1165
+ defaults: { width: 640, height: 480, type: "image/png" },
1166
+ aliases: {
1167
+ head: { cameraName: "head" },
1168
+ left_wrist: { siteName: "left_wrist_camera_optical_frame" },
1169
+ right_wrist: { siteName: "right_wrist_camera_optical_frame" },
1170
+ },
1171
+ });
1172
+
1173
+ async function recordDatasetEpisode() {
1174
+ const cameraKeys = ["head", "left_wrist", "right_wrist"];
1175
+ const readiness = recorder.checkReadiness(cameraKeys);
1176
+ if (!readiness.ready) return;
1177
+
1178
+ await recorder.record({
1179
+ cameraKeys,
1180
+ frames: 16,
1181
+ retainFrames: false,
1182
+ onFrame: ({ frameIndex, cameras }) => {
1183
+ queueLeRobotImages(frameIndex, cameras);
1184
+ },
1185
+ });
1186
+ }
1187
+
1188
+ return (
1189
+ <button disabled={recorder.isRecording} onClick={recordDatasetEpisode}>
1190
+ Record camera streams
1191
+ </button>
1192
+ );
1193
+ }
1194
+ ```
1195
+
1196
+ `recorder.readiness` keeps the latest preflight result, and
1197
+ `recorder.result?.readiness` keeps the readiness that shipped with the most
1198
+ recent recording.
1199
+
1200
+ Use `resolveMountedCameraFrameSource()` when dataset feature names need to map
1201
+ to named MuJoCo cameras, sites, or bodies before recording. The helper first
1202
+ checks exact names and aliases, then falls back to normalized/prefix/suffix
1203
+ matches such as `left_wrist` -> `left_wrist_camera_optical_frame`, and
1204
+ token-contained imported-model names such as `observation.images.head` ->
1205
+ `robot_head_camera`. It returns both the capture selector and the
1206
+ mounted-source provenance that should be stored beside the dataset:
1207
+
1208
+ ```tsx
1209
+ const resolved = resolveMountedCameraFrameSource("head", {
1210
+ cameras: api.getCameras(),
1211
+ sites: api.getSites(),
1212
+ bodies: api.getBodies(),
1213
+ });
1214
+
1215
+ if (!resolved) throw new Error("head does not resolve to a MuJoCo source");
1216
+
1217
+ await api.recordCameraSequence({
1218
+ frames: 16,
1219
+ requireMountedSources: true,
1220
+ cameras: [{ key: "head", width: 640, height: 480, ...resolved.selector }],
1221
+ });
1222
+ ```
1223
+
1224
+ Pass `aliases` when multiple MuJoCo resources could match a dataset stream key
1225
+ or when the model uses names that do not share a normalized prefix/suffix with
1226
+ the LeRobot camera feature.
1017
1227
 
1018
1228
  ### `useCtrlNoise(config)`
1019
1229
 
@@ -1,9 +1,90 @@
1
1
  import { useThree } from '@react-three/fiber';
2
- import { useEffect, useMemo } from 'react';
2
+ import { useMemo, useEffect } from 'react';
3
3
  import * as THREE from 'three';
4
4
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
5
 
6
- // src/components/VisualScenario.tsx
6
+ // src/types.ts
7
+ var runtimeRobotResources = {};
8
+ var REGISTER_RESOURCE_KEYS = ["actuators", "sensors", "bodies", "joints", "sites", "geoms", "keyframes", "cameras"];
9
+ function createEmptyRuntimeResources() {
10
+ return {
11
+ actuators: {},
12
+ sensors: {},
13
+ bodies: {},
14
+ joints: {},
15
+ sites: {},
16
+ geoms: {},
17
+ keyframes: {},
18
+ cameras: {}
19
+ };
20
+ }
21
+ function registerRobotResources(resources) {
22
+ for (const [robot, robotResources] of Object.entries(resources)) {
23
+ const existing = runtimeRobotResources[robot] ?? createEmptyRuntimeResources();
24
+ for (const key of REGISTER_RESOURCE_KEYS) {
25
+ existing[key] = { ...existing[key], ...robotResources[key] ?? {} };
26
+ }
27
+ runtimeRobotResources[robot] = existing;
28
+ }
29
+ }
30
+ function createResourceCategory(key) {
31
+ return new Proxy({}, {
32
+ get(_target, robot) {
33
+ if (typeof robot !== "string") return void 0;
34
+ return runtimeRobotResources[robot]?.[key] ?? {};
35
+ },
36
+ ownKeys() {
37
+ return Reflect.ownKeys(runtimeRobotResources);
38
+ },
39
+ getOwnPropertyDescriptor(_target, robot) {
40
+ if (typeof robot !== "string" || !(robot in runtimeRobotResources)) return void 0;
41
+ return { enumerable: true, configurable: true };
42
+ }
43
+ });
44
+ }
45
+ var RobotResources = new Proxy(runtimeRobotResources, {
46
+ get(target, robot) {
47
+ if (typeof robot !== "string") return void 0;
48
+ return target[robot] ?? createEmptyRuntimeResources();
49
+ },
50
+ ownKeys(target) {
51
+ return Reflect.ownKeys(target);
52
+ },
53
+ getOwnPropertyDescriptor(target, robot) {
54
+ if (typeof robot !== "string" || !(robot in target)) return void 0;
55
+ return { enumerable: true, configurable: true };
56
+ }
57
+ });
58
+ var RobotActuators = createResourceCategory("actuators");
59
+ var RobotSensors = createResourceCategory("sensors");
60
+ var RobotBodies = createResourceCategory("bodies");
61
+ var RobotJoints = createResourceCategory("joints");
62
+ var RobotSites = createResourceCategory("sites");
63
+ var RobotGeoms = createResourceCategory("geoms");
64
+ var RobotKeyframes = createResourceCategory("keyframes");
65
+ var RobotCameras = createResourceCategory("cameras");
66
+ function getContact(contacts, i) {
67
+ try {
68
+ return contacts.get(i);
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ function withContacts(data, read) {
74
+ const contacts = data.contact;
75
+ try {
76
+ return read(contacts);
77
+ } finally {
78
+ contacts.delete?.();
79
+ }
80
+ }
81
+ var SplatEnvironmentReadinessStatus = {
82
+ Disabled: "disabled",
83
+ MissingSplat: "missing-splat",
84
+ MissingCollisionProxy: "missing-collision-proxy",
85
+ UnsupportedFormat: "unsupported-format",
86
+ Ready: "ready"
87
+ };
7
88
  var DEFAULT_BACKGROUND = "#181a1f";
8
89
  function ScenarioLighting({
9
90
  preset = "studio",
@@ -79,6 +160,75 @@ function getScenarioCameraPosition(basePosition, scenario) {
79
160
  Number((z + jitter * 0.25).toFixed(3))
80
161
  ];
81
162
  }
163
+ function useVisualScenarioExecutionContext({
164
+ scenario,
165
+ environment,
166
+ renderer,
167
+ variantId,
168
+ enabled
169
+ }) {
170
+ return useMemo(
171
+ () => createVisualScenarioExecutionContext({
172
+ scenario,
173
+ environment,
174
+ renderer,
175
+ variantId,
176
+ enabled
177
+ }),
178
+ [enabled, environment, renderer, scenario, variantId]
179
+ );
180
+ }
181
+ function createVisualScenarioExecutionContext({
182
+ scenario,
183
+ environment,
184
+ renderer,
185
+ variantId,
186
+ enabled = true
187
+ }) {
188
+ const pairedEnvironment = environment ?? (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : void 0);
189
+ const splat = scenario?.splat;
190
+ const collisionProxy = pairedEnvironment?.collisionProxy ?? splat?.collisionProxy ?? void 0;
191
+ const readiness = getSplatEnvironmentReadiness({
192
+ environment: pairedEnvironment,
193
+ scenario,
194
+ renderer,
195
+ enabled
196
+ });
197
+ const format = pairedEnvironment?.splat.format ?? splat?.format ?? readiness.format ?? "spz";
198
+ return {
199
+ scenarioId: scenario?.id ?? pairedEnvironment?.id ?? "visual-scenario",
200
+ scenarioLabel: scenario?.label ?? pairedEnvironment?.label ?? "Visual scenario",
201
+ variantId,
202
+ seed: scenario?.seed ?? 0,
203
+ lighting: scenario?.lighting ?? "studio",
204
+ environment: scenario?.environment,
205
+ camera: {
206
+ jitter: scenario?.camera?.jitter ?? 0,
207
+ exposure: scenario?.camera?.exposure ?? 1,
208
+ noise: scenario?.camera?.noise ?? 0,
209
+ blur: scenario?.camera?.blur ?? 0
210
+ },
211
+ materials: {
212
+ randomizeObjectColors: Boolean(
213
+ scenario?.materials?.randomizeObjectColors
214
+ ),
215
+ randomizeTableMaterial: Boolean(
216
+ scenario?.materials?.randomizeTableMaterial
217
+ ),
218
+ roughness: scenario?.materials?.roughness,
219
+ metalness: scenario?.materials?.metalness
220
+ },
221
+ splatEnabled: Boolean(splat?.enabled || pairedEnvironment),
222
+ splatSrc: pairedEnvironment?.splat.src ?? splat?.src,
223
+ splatFormat: format,
224
+ splatRenderer: renderer ?? pairedEnvironment?.splat.renderer,
225
+ collisionProxyXmlPath: collisionProxy?.xmlPath,
226
+ collisionProxyStatus: collisionProxy?.status,
227
+ collisionProxyPrimitives: collisionProxy?.primitives ?? [],
228
+ readiness,
229
+ transformSource: "visualScenario.camera"
230
+ };
231
+ }
82
232
  function VisualScenarioEffects(props) {
83
233
  useVisualScenarioEffects(props);
84
234
  return null;
@@ -205,25 +355,161 @@ function useSplatEnvironment({
205
355
  const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;
206
356
  const resolvedFormat = format ?? scenarioEnvironment?.splat.format ?? scenario?.splat?.format ?? "spz";
207
357
  const resolvedCollisionProxy = collisionProxy ?? scenarioEnvironment?.collisionProxy ?? scenario?.splat?.collisionProxy ?? void 0;
358
+ const readiness = useMemo(
359
+ () => getSplatEnvironmentReadiness({
360
+ environment: scenarioEnvironment,
361
+ scenario,
362
+ renderer,
363
+ src: resolvedSrc,
364
+ format: resolvedFormat,
365
+ collisionProxy: resolvedCollisionProxy
366
+ }),
367
+ [
368
+ collisionProxy,
369
+ renderer,
370
+ resolvedCollisionProxy,
371
+ resolvedFormat,
372
+ resolvedSrc,
373
+ scenario,
374
+ scenarioEnvironment
375
+ ]
376
+ );
208
377
  return useMemo(
209
378
  () => ({
210
379
  src: resolvedSrc,
211
380
  format: resolvedFormat,
212
381
  collisionProxy: resolvedCollisionProxy,
382
+ readiness,
213
383
  userData: createSplatEnvironmentUserData({
214
384
  environment: scenarioEnvironment,
215
385
  src: resolvedSrc,
216
386
  format: resolvedFormat,
217
- collisionProxy: resolvedCollisionProxy
387
+ collisionProxy: resolvedCollisionProxy,
388
+ readiness
218
389
  })
219
390
  }),
220
- [scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
391
+ [
392
+ scenarioEnvironment,
393
+ resolvedSrc,
394
+ resolvedFormat,
395
+ resolvedCollisionProxy,
396
+ readiness
397
+ ]
221
398
  );
222
399
  }
400
+ function useSplatSceneConfig({
401
+ sceneConfig,
402
+ scenario,
403
+ environment,
404
+ enabled = true,
405
+ renderer
406
+ }) {
407
+ return useMemo(
408
+ () => createSplatSceneConfig({
409
+ sceneConfig,
410
+ scenario,
411
+ environment,
412
+ enabled,
413
+ renderer
414
+ }),
415
+ [enabled, environment, renderer, scenario, sceneConfig]
416
+ );
417
+ }
418
+ function createSplatSceneConfig({
419
+ sceneConfig,
420
+ scenario,
421
+ environment,
422
+ enabled = true,
423
+ renderer
424
+ }) {
425
+ const resolvedEnvironment = enabled ? environment ?? (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : void 0) : void 0;
426
+ const readiness = getSplatEnvironmentReadiness({
427
+ environment: resolvedEnvironment,
428
+ scenario,
429
+ renderer,
430
+ enabled
431
+ });
432
+ const resolvedSceneConfig = resolvedEnvironment ? withSplatEnvironment(sceneConfig, resolvedEnvironment, { renderer }) : sceneConfig;
433
+ return {
434
+ environment: resolvedEnvironment,
435
+ sceneConfig: resolvedSceneConfig,
436
+ enabled: enabled && readiness.status !== SplatEnvironmentReadinessStatus.Disabled,
437
+ readiness
438
+ };
439
+ }
440
+ function getSplatEnvironmentReadiness({
441
+ environment,
442
+ scenario,
443
+ renderer,
444
+ src,
445
+ format,
446
+ collisionProxy,
447
+ enabled = true
448
+ }) {
449
+ const splat = scenario?.splat;
450
+ const resolvedSrc = src ?? environment?.splat.src ?? splat?.src;
451
+ const resolvedFormat = format ?? environment?.splat.format ?? splat?.format ?? "spz";
452
+ const resolvedRenderer = renderer ?? environment?.splat.renderer;
453
+ const resolvedCollisionProxy = collisionProxy ?? environment?.collisionProxy ?? splat?.collisionProxy ?? void 0;
454
+ const requiresCollisionProxy = splat?.requiresCollisionProxy ?? true;
455
+ if (!enabled || splat && splat.enabled === false && !environment) {
456
+ return {
457
+ status: SplatEnvironmentReadinessStatus.Disabled,
458
+ ready: false,
459
+ requiresCollisionProxy,
460
+ missing: [],
461
+ format: resolvedFormat,
462
+ renderer: resolvedRenderer,
463
+ message: "Splat environment is disabled."
464
+ };
465
+ }
466
+ if (!resolvedSrc) {
467
+ return {
468
+ status: SplatEnvironmentReadinessStatus.MissingSplat,
469
+ ready: false,
470
+ requiresCollisionProxy,
471
+ missing: ["splat"],
472
+ format: resolvedFormat,
473
+ renderer: resolvedRenderer,
474
+ message: "Splat environment is missing a visual asset source."
475
+ };
476
+ }
477
+ if (resolvedRenderer === "spark" && resolvedFormat !== "spz") {
478
+ return {
479
+ status: SplatEnvironmentReadinessStatus.UnsupportedFormat,
480
+ ready: false,
481
+ requiresCollisionProxy,
482
+ missing: [],
483
+ format: resolvedFormat,
484
+ renderer: resolvedRenderer,
485
+ message: `Spark splat rendering requires .spz assets; received ${resolvedFormat}.`
486
+ };
487
+ }
488
+ if (requiresCollisionProxy && !resolvedCollisionProxy?.xmlPath) {
489
+ return {
490
+ status: SplatEnvironmentReadinessStatus.MissingCollisionProxy,
491
+ ready: false,
492
+ requiresCollisionProxy,
493
+ missing: ["collisionProxy"],
494
+ format: resolvedFormat,
495
+ renderer: resolvedRenderer,
496
+ message: "Splat environment is missing paired MJCF collision proxy XML."
497
+ };
498
+ }
499
+ return {
500
+ status: SplatEnvironmentReadinessStatus.Ready,
501
+ ready: true,
502
+ requiresCollisionProxy,
503
+ missing: [],
504
+ format: resolvedFormat,
505
+ renderer: resolvedRenderer,
506
+ message: requiresCollisionProxy ? "Splat environment has visual asset and collision proxy metadata." : "Splat environment has a visual asset and does not require collision proxy metadata."
507
+ };
508
+ }
223
509
  function createPairedSplatEnvironment(scenario, options = {}) {
224
510
  const splat = scenario.splat;
225
511
  const collisionProxy = splat?.collisionProxy;
226
- if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {
512
+ if (!splat?.enabled || !splat.src) {
227
513
  return void 0;
228
514
  }
229
515
  return {
@@ -235,14 +521,14 @@ function createPairedSplatEnvironment(scenario, options = {}) {
235
521
  format: splat.format ?? "spz",
236
522
  renderer: options.renderer
237
523
  },
238
- collisionProxy: {
524
+ collisionProxy: collisionProxy?.xmlPath ? {
239
525
  ...collisionProxy,
240
526
  xmlPath: collisionProxy.xmlPath
241
- }
527
+ } : void 0
242
528
  };
243
529
  }
244
530
  function isPairedSplatEnvironment(input) {
245
- return !!input && "collisionProxy" in input && "splat" in input;
531
+ return !!input && "splat" in input && !!input.splat && !("enabled" in input.splat);
246
532
  }
247
533
  function sceneRelativePath(sceneConfig, path) {
248
534
  const src = sceneConfig.src;
@@ -263,7 +549,7 @@ function uniquePaths(paths) {
263
549
  }
264
550
  function withSplatEnvironment(sceneConfig, input, options = {}) {
265
551
  const environment = isPairedSplatEnvironment(input) ? input : input ? createPairedSplatEnvironment(input, options) : void 0;
266
- const xmlPath = environment?.collisionProxy.xmlPath;
552
+ const xmlPath = environment?.collisionProxy?.xmlPath;
267
553
  if (!xmlPath) return sceneConfig;
268
554
  return {
269
555
  ...sceneConfig,
@@ -277,7 +563,8 @@ function createSplatEnvironmentUserData({
277
563
  environment,
278
564
  src,
279
565
  format = "spz",
280
- collisionProxy
566
+ collisionProxy,
567
+ readiness
281
568
  }) {
282
569
  return {
283
570
  role: "splat-environment",
@@ -288,7 +575,9 @@ function createSplatEnvironmentUserData({
288
575
  splatRenderer: environment?.splat.renderer,
289
576
  collisionProxyStatus: collisionProxy?.status ?? "missing",
290
577
  collisionProxyXmlPath: collisionProxy?.xmlPath,
291
- collisionProxyPrimitives: collisionProxy?.primitives ?? []
578
+ collisionProxyPrimitives: collisionProxy?.primitives ?? [],
579
+ readinessStatus: readiness?.status,
580
+ readinessMessage: readiness?.message
292
581
  };
293
582
  }
294
583
  function createSparkSplatViewerUrl({
@@ -395,6 +684,6 @@ function clamp01(value) {
395
684
  * SPDX-License-Identifier: Apache-2.0
396
685
  */
397
686
 
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
687
+ export { RobotActuators, RobotBodies, RobotCameras, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, ScenarioLighting, SplatEnvironment, SplatEnvironmentReadinessStatus, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, createSplatSceneConfig, createVisualScenarioExecutionContext, getContact, getScenarioBackground, getScenarioCameraPosition, getSplatEnvironmentReadiness, registerRobotResources, useSplatEnvironment, useSplatSceneConfig, useVisualScenarioEffects, useVisualScenarioExecutionContext, withContacts, withSplatEnvironment };
688
+ //# sourceMappingURL=chunk-VDSEPZYQ.js.map
689
+ //# sourceMappingURL=chunk-VDSEPZYQ.js.map