mujoco-react 9.1.0 → 9.3.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
@@ -165,11 +165,24 @@ visual scenarios as data, pass the scenario directly; the component resolves the
165
165
  splat asset and paired MJCF collision proxy metadata for you.
166
166
 
167
167
  ```tsx
168
- import { SplatEnvironment, withSplatEnvironment } from "mujoco-react";
168
+ import { MujocoCanvas, SplatEnvironment, useSplatSceneConfig } from "mujoco-react";
169
169
 
170
- <SplatEnvironment scenario={scenario} renderer="custom" />;
170
+ const splat = useSplatSceneConfig({ sceneConfig, scenario });
171
+
172
+ <MujocoCanvas config={splat.sceneConfig}>
173
+ {splat.environment ? (
174
+ <SplatEnvironment environment={splat.environment} renderer="custom">
175
+ <MySplatRenderer src={splat.environment.splat.src} />
176
+ </SplatEnvironment>
177
+ ) : null}
178
+ </MujocoCanvas>;
171
179
  ```
172
180
 
181
+ Use `splat.readiness` or `getSplatEnvironmentReadiness(scenario)` to gate
182
+ authoring and import flows. The status distinguishes disabled scenarios,
183
+ missing splat assets, missing MJCF collision proxies, unsupported Spark formats,
184
+ and ready paired environments.
185
+
173
186
  For MuJoCo + 3DGS composition, derive the collision environment from the same
174
187
  splat metadata and pass the resulting config to `<MujocoCanvas>`:
175
188
 
@@ -192,16 +205,18 @@ npm install @sparkjsdev/spark
192
205
  ```tsx
193
206
  import {
194
207
  SparkSplatEnvironment,
195
- useSparkSplatLifecycle,
208
+ useSparkSplatEnvironment,
196
209
  } from "mujoco-react/spark";
197
210
 
198
211
  function Scene() {
199
- const splat = useSparkSplatLifecycle();
212
+ const splat = useSparkSplatEnvironment({ sceneConfig, scenario });
200
213
 
201
214
  return (
202
- <MujocoCanvas config={sceneConfig} gl={{ preserveDrawingBuffer: true }}>
203
- <SparkSplatEnvironment scenario={scenario} hideGroundMeshes {...splat.props} />
204
- <StatusBadge status={splat.status} error={splat.error} />
215
+ <MujocoCanvas config={splat.sceneConfig} gl={{ preserveDrawingBuffer: true }}>
216
+ {splat.environment ? (
217
+ <SparkSplatEnvironment hideGroundMeshes {...splat.props} />
218
+ ) : null}
219
+ <StatusBadge status={splat.lifecycle.status} error={splat.lifecycle.error} />
205
220
  </MujocoCanvas>
206
221
  );
207
222
  }
@@ -213,18 +228,20 @@ function Scene() {
213
228
  ## Write Controllers
214
229
 
215
230
  ```tsx
216
- import { useBeforePhysicsStep } from "mujoco-react";
231
+ import { RobotActuators, useBeforePhysicsStep, useCtrl } from "mujoco-react";
217
232
 
218
233
  function MyController() {
234
+ const shoulder = useCtrl(RobotActuators.franka.actuator1);
235
+
219
236
  useBeforePhysicsStep(({ data }) => {
220
- data.ctrl[0] = Math.sin(data.time);
237
+ shoulder.write(Math.sin(data.time));
221
238
  });
222
239
 
223
240
  return null;
224
241
  }
225
242
  ```
226
243
 
227
- Controllers are just React children that read sensors, write `data.ctrl`, apply forces, or call the `MujocoSimAPI` at physics-step time.
244
+ Controllers are just React children that read sensors, write named controls, apply forces, or call the `MujocoSimAPI` at physics-step time.
228
245
 
229
246
  With generated resource values, reusable controllers can be scoped to one robot without hand-typing names:
230
247
 
@@ -388,15 +405,20 @@ function useWebSocketControls(url: string) {
388
405
  For reusable controllers with typed config, default merging, and children, use the `createController` factory:
389
406
 
390
407
  ```tsx
391
- import { createController, useCtrl, useBeforePhysicsStep } from "mujoco-react";
408
+ import {
409
+ RobotActuators,
410
+ createController,
411
+ useBeforePhysicsStep,
412
+ useCtrl,
413
+ } from "mujoco-react";
392
414
 
393
415
  export const MyController = createController<{ gain: number }>(
394
416
  { name: "MyController", defaultConfig: { gain: 1.0 } },
395
417
  ({ config, children }) => {
396
- const shoulder = useCtrl("shoulder");
418
+ const shoulder = useCtrl(RobotActuators.franka.actuator1);
397
419
 
398
- useBeforePhysicsStep(() => {
399
- shoulder.write(config.gain * Math.sin(Date.now() / 1000));
420
+ useBeforePhysicsStep(({ data }) => {
421
+ shoulder.write(config.gain * Math.sin(data.time));
400
422
  });
401
423
 
402
424
  return <>{children}</>;
@@ -826,8 +848,12 @@ if (mujoco) {
826
848
  Run logic **before** `mj_step` each frame. Write to `data.ctrl`, apply forces, drive automation.
827
849
 
828
850
  ```tsx
851
+ import { RobotActuators, useBeforePhysicsStep, useCtrl } from "mujoco-react";
852
+
853
+ const shoulder = useCtrl(RobotActuators.franka.actuator1);
854
+
829
855
  useBeforePhysicsStep(({ data }) => {
830
- data.ctrl[0] = Math.sin(data.time);
856
+ shoulder.write(Math.sin(data.time));
831
857
  });
832
858
  ```
833
859
 
@@ -861,8 +887,8 @@ Read sensor values by name. Returns a `SensorHandle` with `read()`, `dim`, and `
861
887
  ```tsx
862
888
  import { RobotSensors, useSensor } from "mujoco-react";
863
889
 
864
- const force = useSensor(RobotSensors.franka.force_sensor);
865
- // force.read() -> Float64Array, force.dim -> number
890
+ const imu = useSensor(RobotSensors.g1["imu-torso-angular-velocity"]);
891
+ // imu.read() -> Float64Array, imu.dim -> number
866
892
  ```
867
893
 
868
894
  ### `useBodyState(name)`
@@ -997,6 +1023,84 @@ const blob = await apiRef.current?.captureFrameBlob({ type: "image/png" });
997
1023
  Use `useFrameCapture()` or the standalone `captureFrame()` helpers when you own
998
1024
  the canvas or want to capture a custom container.
999
1025
 
1026
+ Use `captureCameraFrame()` / `captureCameraFrameBlob()` when dataset generation
1027
+ needs an offscreen camera render at a stable resolution without moving the
1028
+ user's interactive viewport. Pass `cameraName`, `siteName`, or `bodyName` to
1029
+ record true MuJoCo-mounted camera frames; the returned image includes
1030
+ `source.kind` so dataset pipelines can reject fallback or synthetic fixed poses.
1031
+
1032
+ Use `recordCameraSequence()` / `useCameraSequenceRecorder()` to step policy
1033
+ rollouts and capture synchronized per-camera frames from one or more MuJoCo
1034
+ camera configs. Sequence recording requires mounted MuJoCo camera, site, or
1035
+ body selectors by default; use still capture APIs for synthetic debug poses.
1036
+
1037
+ For LeRobot-style datasets, prefer the named-camera wrapper. It resolves task
1038
+ camera keys to MuJoCo cameras/sites/bodies, records the sequence, and returns
1039
+ the plan and readiness summary alongside frame provenance:
1040
+
1041
+ ```tsx
1042
+ const sequence = await recordMountedCameraFrameSequence(api, {
1043
+ cameraKeys: ["head", "left_wrist", "right_wrist"],
1044
+ aliases: {
1045
+ head: [{ siteName: "head_camera_rgb_optical_frame" }],
1046
+ left_wrist: [{ siteName: "left_wrist_camera_optical_frame" }],
1047
+ right_wrist: [{ siteName: "right_wrist_camera_optical_frame" }],
1048
+ },
1049
+ defaults: {
1050
+ width: 640,
1051
+ height: 480,
1052
+ type: "image/png",
1053
+ fov: 45,
1054
+ near: 0.01,
1055
+ far: 100,
1056
+ },
1057
+ frames: 16,
1058
+ stepsPerFrame: 1,
1059
+ retainFrames: false,
1060
+ requireMountedSources: true,
1061
+ onFrame: ({ frameIndex, cameras }) => {
1062
+ queueLeRobotImages(frameIndex, cameras);
1063
+ },
1064
+ });
1065
+
1066
+ sequence.readiness.ready; // true when every requested stream resolved
1067
+ sequence.plan.missingKeys; // unresolved task cameras, if requireAll is false
1068
+ sequence.cameraSummaries.head.source; // mounted source provenance
1069
+ ```
1070
+
1071
+ `recordMountedCameraFrameSequence()` requires all requested `cameraKeys` by
1072
+ default so dataset recording cannot silently omit a camera stream. Set
1073
+ `requireAll: false` only for exploratory tooling that can tolerate partial
1074
+ camera coverage.
1075
+
1076
+ Inside `<MujocoCanvas>` children, `useMountedCameraSequenceRecorder()` exposes
1077
+ the same planning and recording surface with React status/error state.
1078
+
1079
+ Use `resolveMountedCameraFrameSource()` when dataset feature names need to map
1080
+ to named MuJoCo cameras, sites, or bodies before recording. The helper accepts
1081
+ the model resource lists plus app-level aliases and returns both the capture
1082
+ selector and the mounted-source provenance that should be stored beside the
1083
+ dataset:
1084
+
1085
+ ```tsx
1086
+ const resolved = resolveMountedCameraFrameSource("head", {
1087
+ cameras: api.getCameras(),
1088
+ sites: api.getSites(),
1089
+ bodies: api.getBodies(),
1090
+ aliases: {
1091
+ head: [{ siteName: "head_camera_rgb_optical_frame" }],
1092
+ },
1093
+ });
1094
+
1095
+ if (!resolved) throw new Error("head does not resolve to a MuJoCo source");
1096
+
1097
+ await api.recordCameraSequence({
1098
+ frames: 16,
1099
+ requireMountedSources: true,
1100
+ cameras: [{ key: "head", width: 640, height: 480, ...resolved.selector }],
1101
+ });
1102
+ ```
1103
+
1000
1104
  ### `useCtrlNoise(config)`
1001
1105
 
1002
1106
  Apply Gaussian noise to controls for robustness testing:
@@ -3,7 +3,88 @@ import { useEffect, useMemo } 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",
@@ -205,21 +286,151 @@ function useSplatEnvironment({
205
286
  const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;
206
287
  const resolvedFormat = format ?? scenarioEnvironment?.splat.format ?? scenario?.splat?.format ?? "spz";
207
288
  const resolvedCollisionProxy = collisionProxy ?? scenarioEnvironment?.collisionProxy ?? scenario?.splat?.collisionProxy ?? void 0;
289
+ const readiness = useMemo(
290
+ () => getSplatEnvironmentReadiness({
291
+ environment: scenarioEnvironment,
292
+ scenario,
293
+ renderer,
294
+ src: resolvedSrc,
295
+ format: resolvedFormat,
296
+ collisionProxy: resolvedCollisionProxy
297
+ }),
298
+ [
299
+ collisionProxy,
300
+ renderer,
301
+ resolvedCollisionProxy,
302
+ resolvedFormat,
303
+ resolvedSrc,
304
+ scenario,
305
+ scenarioEnvironment
306
+ ]
307
+ );
208
308
  return useMemo(
209
309
  () => ({
210
310
  src: resolvedSrc,
211
311
  format: resolvedFormat,
212
312
  collisionProxy: resolvedCollisionProxy,
313
+ readiness,
213
314
  userData: createSplatEnvironmentUserData({
214
315
  environment: scenarioEnvironment,
215
316
  src: resolvedSrc,
216
317
  format: resolvedFormat,
217
- collisionProxy: resolvedCollisionProxy
318
+ collisionProxy: resolvedCollisionProxy,
319
+ readiness
218
320
  })
219
321
  }),
220
- [scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
322
+ [
323
+ scenarioEnvironment,
324
+ resolvedSrc,
325
+ resolvedFormat,
326
+ resolvedCollisionProxy,
327
+ readiness
328
+ ]
329
+ );
330
+ }
331
+ function useSplatSceneConfig({
332
+ sceneConfig,
333
+ scenario,
334
+ environment,
335
+ enabled = true,
336
+ renderer
337
+ }) {
338
+ const resolvedEnvironment = useMemo(
339
+ () => enabled ? environment ?? (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : void 0) : void 0,
340
+ [enabled, environment, renderer, scenario]
341
+ );
342
+ const readiness = useMemo(
343
+ () => getSplatEnvironmentReadiness({
344
+ environment: resolvedEnvironment,
345
+ scenario,
346
+ renderer,
347
+ enabled
348
+ }),
349
+ [enabled, renderer, resolvedEnvironment, scenario]
350
+ );
351
+ const resolvedSceneConfig = useMemo(
352
+ () => resolvedEnvironment ? withSplatEnvironment(sceneConfig, resolvedEnvironment) : sceneConfig,
353
+ [resolvedEnvironment, sceneConfig]
354
+ );
355
+ return useMemo(
356
+ () => ({
357
+ environment: resolvedEnvironment,
358
+ sceneConfig: resolvedSceneConfig,
359
+ enabled: enabled && readiness.status !== SplatEnvironmentReadinessStatus.Disabled,
360
+ readiness
361
+ }),
362
+ [enabled, readiness, resolvedEnvironment, resolvedSceneConfig]
221
363
  );
222
364
  }
365
+ function getSplatEnvironmentReadiness({
366
+ environment,
367
+ scenario,
368
+ renderer,
369
+ src,
370
+ format,
371
+ collisionProxy,
372
+ enabled = true
373
+ }) {
374
+ const splat = scenario?.splat;
375
+ const resolvedSrc = src ?? environment?.splat.src ?? splat?.src;
376
+ const resolvedFormat = format ?? environment?.splat.format ?? splat?.format ?? "spz";
377
+ const resolvedRenderer = renderer ?? environment?.splat.renderer;
378
+ const resolvedCollisionProxy = collisionProxy ?? environment?.collisionProxy ?? splat?.collisionProxy ?? void 0;
379
+ const requiresCollisionProxy = splat?.requiresCollisionProxy ?? true;
380
+ if (!enabled || splat && splat.enabled === false && !environment) {
381
+ return {
382
+ status: SplatEnvironmentReadinessStatus.Disabled,
383
+ ready: false,
384
+ requiresCollisionProxy,
385
+ missing: [],
386
+ format: resolvedFormat,
387
+ renderer: resolvedRenderer,
388
+ message: "Splat environment is disabled."
389
+ };
390
+ }
391
+ if (!resolvedSrc) {
392
+ return {
393
+ status: SplatEnvironmentReadinessStatus.MissingSplat,
394
+ ready: false,
395
+ requiresCollisionProxy,
396
+ missing: ["splat"],
397
+ format: resolvedFormat,
398
+ renderer: resolvedRenderer,
399
+ message: "Splat environment is missing a visual asset source."
400
+ };
401
+ }
402
+ if (resolvedRenderer === "spark" && resolvedFormat !== "spz") {
403
+ return {
404
+ status: SplatEnvironmentReadinessStatus.UnsupportedFormat,
405
+ ready: false,
406
+ requiresCollisionProxy,
407
+ missing: [],
408
+ format: resolvedFormat,
409
+ renderer: resolvedRenderer,
410
+ message: `Spark splat rendering requires .spz assets; received ${resolvedFormat}.`
411
+ };
412
+ }
413
+ if (requiresCollisionProxy && !resolvedCollisionProxy?.xmlPath) {
414
+ return {
415
+ status: SplatEnvironmentReadinessStatus.MissingCollisionProxy,
416
+ ready: false,
417
+ requiresCollisionProxy,
418
+ missing: ["collisionProxy"],
419
+ format: resolvedFormat,
420
+ renderer: resolvedRenderer,
421
+ message: "Splat environment is missing paired MJCF collision proxy XML."
422
+ };
423
+ }
424
+ return {
425
+ status: SplatEnvironmentReadinessStatus.Ready,
426
+ ready: true,
427
+ requiresCollisionProxy,
428
+ missing: [],
429
+ format: resolvedFormat,
430
+ renderer: resolvedRenderer,
431
+ message: requiresCollisionProxy ? "Splat environment has visual asset and collision proxy metadata." : "Splat environment has a visual asset and does not require collision proxy metadata."
432
+ };
433
+ }
223
434
  function createPairedSplatEnvironment(scenario, options = {}) {
224
435
  const splat = scenario.splat;
225
436
  const collisionProxy = splat?.collisionProxy;
@@ -277,7 +488,8 @@ function createSplatEnvironmentUserData({
277
488
  environment,
278
489
  src,
279
490
  format = "spz",
280
- collisionProxy
491
+ collisionProxy,
492
+ readiness
281
493
  }) {
282
494
  return {
283
495
  role: "splat-environment",
@@ -288,7 +500,9 @@ function createSplatEnvironmentUserData({
288
500
  splatRenderer: environment?.splat.renderer,
289
501
  collisionProxyStatus: collisionProxy?.status ?? "missing",
290
502
  collisionProxyXmlPath: collisionProxy?.xmlPath,
291
- collisionProxyPrimitives: collisionProxy?.primitives ?? []
503
+ collisionProxyPrimitives: collisionProxy?.primitives ?? [],
504
+ readinessStatus: readiness?.status,
505
+ readinessMessage: readiness?.message
292
506
  };
293
507
  }
294
508
  function createSparkSplatViewerUrl({
@@ -395,6 +609,6 @@ function clamp01(value) {
395
609
  * SPDX-License-Identifier: Apache-2.0
396
610
  */
397
611
 
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
612
+ export { RobotActuators, RobotBodies, RobotCameras, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, ScenarioLighting, SplatEnvironment, SplatEnvironmentReadinessStatus, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getContact, getScenarioBackground, getScenarioCameraPosition, getSplatEnvironmentReadiness, registerRobotResources, useSplatEnvironment, useSplatSceneConfig, useVisualScenarioEffects, withContacts, withSplatEnvironment };
613
+ //# sourceMappingURL=chunk-T3GVZJ4F.js.map
614
+ //# sourceMappingURL=chunk-T3GVZJ4F.js.map