mujoco-react 8.3.3 → 8.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
@@ -37,7 +37,7 @@ const config: SceneConfig = {
37
37
  };
38
38
 
39
39
  function Scene() {
40
- const ik = useIkController({ siteName: "tcp", numJoints: 7 });
40
+ const ik = useIkController({ siteName: "tcp" });
41
41
  return (
42
42
  <>
43
43
  <OrbitControls enableDamping makeDefault />
@@ -124,43 +124,56 @@ function useSpringForce(bodyId: number, target: [number, number, number], stiffn
124
124
 
125
125
  The `api.applyForce(bodyName, force)` convenience method also works for one-off interactions (e.g. button clicks) but does a name lookup each call.
126
126
 
127
- ### WebSocket Joint Control
127
+ ### WebSocket Control
128
128
 
129
- Stream joint commands over a WebSocket and send simulation state back:
129
+ Stream actuator commands over a WebSocket and send simulation state back. Use a schema validator such as Zod at this boundary because socket messages are untrusted app input (`npm install zod` for this example):
130
130
 
131
131
  ```tsx
132
132
  import { useEffect, useRef } from "react";
133
+ import { z } from "zod";
133
134
  import { useMujoco, useBeforePhysicsStep, useAfterPhysicsStep } from "mujoco-react";
134
135
 
135
- function useWebSocketJoints(url: string) {
136
- const { api } = useMujoco();
136
+ const CtrlCommand = z.object({
137
+ type: z.literal("ctrl_command"),
138
+ ctrl: z.array(z.number()),
139
+ });
140
+
141
+ type CtrlCommand = z.infer<typeof CtrlCommand>;
142
+
143
+ function parseSocketMessage(data: string): CtrlCommand | null {
144
+ try {
145
+ const parsed = CtrlCommand.safeParse(JSON.parse(data));
146
+ return parsed.success ? parsed.data : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function useWebSocketControls(url: string) {
137
153
  const wsRef = useRef<WebSocket | null>(null);
138
- const latestJointsRef = useRef<number[] | null>(null);
154
+ const latestCommandRef = useRef<CtrlCommand | null>(null);
139
155
 
140
156
  useEffect(() => {
141
157
  const ws = new WebSocket(url);
142
158
  wsRef.current = ws;
143
159
 
144
160
  ws.onmessage = (evt) => {
145
- const msg = JSON.parse(evt.data);
146
- if (msg.type === "joint_command") {
147
- latestJointsRef.current = msg.qpos;
148
- }
161
+ latestCommandRef.current = parseSocketMessage(evt.data);
149
162
  };
150
163
 
151
164
  return () => ws.close();
152
165
  }, [url]);
153
166
 
154
- // Apply incoming joint positions each physics step
167
+ // Apply incoming actuator controls each physics step.
155
168
  useBeforePhysicsStep((model, data) => {
156
- const joints = latestJointsRef.current;
157
- if (!joints) return;
158
- for (let i = 0; i < Math.min(joints.length, model.nu); i++) {
159
- data.ctrl[i] = joints[i];
169
+ const command = latestCommandRef.current;
170
+ if (!command) return;
171
+ for (let i = 0; i < Math.min(command.ctrl.length, model.nu); i++) {
172
+ data.ctrl[i] = command.ctrl[i];
160
173
  }
161
174
  });
162
175
 
163
- // Send sensor feedback back after physics
176
+ // Send simulation feedback back after physics.
164
177
  useAfterPhysicsStep((model, data) => {
165
178
  const ws = wsRef.current;
166
179
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
@@ -227,7 +240,7 @@ const myIK: IKSolveFn = (pos, quat, currentQ) => {
227
240
  return myAnalyticalSolver(pos, currentQ); // return joint angles or null
228
241
  };
229
242
 
230
- const ik = useIkController({ siteName: "tcp", numJoints: 7, ikSolveFn: myIK });
243
+ const ik = useIkController({ siteName: "tcp", ikSolveFn: myIK });
231
244
  ```
232
245
 
233
246
  ### `useIkController(config | null)`
@@ -235,14 +248,16 @@ const ik = useIkController({ siteName: "tcp", numJoints: 7, ikSolveFn: myIK });
235
248
  Hook for interactive end-effector control. Pass `null` to disable IK (safe to call unconditionally):
236
249
 
237
250
  ```tsx
238
- const ik = useIkController({ siteName: "tcp", numJoints: 7 });
251
+ const ik = useIkController({ siteName: "tcp" });
239
252
  return ik ? <IkGizmo controller={ik} /> : null;
240
253
  ```
241
254
 
242
255
  | Config | Type | Default | Description |
243
256
  |--------|------|---------|-------------|
244
257
  | `siteName` | `string` | **required** | MuJoCo site to track |
245
- | `numJoints` | `number` | **required** | Number of joints for IK |
258
+ | `joints` | `string \| string[] \| RegExp \| (joint) => boolean` | inferred | Explicit hinge/slide joints for IK |
259
+ | `actuators` | `string \| string[] \| RegExp \| (actuator) => boolean` | inferred | Explicit actuators for IK output |
260
+ | `numJoints` | `number` | legacy only | Contiguous qpos/ctrl count from older examples |
246
261
  | `ikSolveFn` | `IKSolveFn` | built-in DLS | Custom solver function |
247
262
  | `damping` | `number` | `0.01` | DLS damping |
248
263
  | `maxIterations` | `number` | `50` | Max solver iterations |
@@ -251,6 +266,20 @@ Returns `IkContextValue | null` with methods like `setIkEnabled`, `moveTarget`,
251
266
 
252
267
  Pass the returned value to `<IkGizmo controller={ik} />` or to your own controller as a prop.
253
268
 
269
+ By default the controller infers scalar hinge/slide joints by walking from the site body toward the model root. For robots where the MJCF control layout is not a simple chain, pass explicit names:
270
+
271
+ ```tsx
272
+ const leftArmIk = useIkController({
273
+ siteName: "left_tcp",
274
+ joints: ["left_shoulder", "left_elbow", "left_wrist"],
275
+ });
276
+
277
+ const gripperIk = useIkController({
278
+ siteName: "tcp",
279
+ actuators: /^actuator/,
280
+ });
281
+ ```
282
+
254
283
  ## Type-Safe Resource Names
255
284
 
256
285
  Use TypeScript module augmentation to get autocomplete and type checking for actuator, sensor, body, joint, site, geom, and keyframe names:
@@ -334,8 +363,29 @@ Loads the MuJoCo WASM module. Wrap your entire app in this.
334
363
  | Prop | Type | Description |
335
364
  |------|------|-------------|
336
365
  | `wasmUrl` | `string?` | Custom WASM URL override |
366
+ | `mtWasmUrl` | `string?` | Custom multi-threaded WASM URL override |
367
+ | `threadedLoader` | `(options?) => Promise<unknown>` | Optional loader imported from `@mujoco/mujoco/mt` |
368
+ | `wasmVariant` | `"single" \| "threaded" \| "auto"` | MuJoCo WASM build. Defaults to `"single"` |
369
+ | `timeout` | `number` | WASM load timeout in ms |
337
370
  | `onError` | `(error: Error) => void` | Called if WASM fails to load |
338
371
 
372
+ The official `@mujoco/mujoco` package also ships a multi-threaded WASM build. Import it only in apps that opt into it:
373
+
374
+ ```tsx
375
+ import loadMujocoMt from "@mujoco/mujoco/mt";
376
+ import mtWasmUrl from "@mujoco/mujoco/mt/mujoco.wasm?url";
377
+
378
+ <MujocoProvider
379
+ wasmVariant="auto"
380
+ threadedLoader={loadMujocoMt}
381
+ mtWasmUrl={mtWasmUrl}
382
+ >
383
+ <App />
384
+ </MujocoProvider>
385
+ ```
386
+
387
+ `"auto"` uses the threaded build only when `threadedLoader` and `mtWasmUrl` are provided and `globalThis.crossOriginIsolated` is true. Forced `"threaded"` mode requires `threadedLoader`, `mtWasmUrl`, `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`.
388
+
339
389
  ### `<MujocoCanvas>`
340
390
 
341
391
  Thin wrapper around R3F `<Canvas>`. Accepts all R3F Canvas props plus:
package/dist/index.d.ts CHANGED
@@ -285,11 +285,22 @@ interface SceneConfig {
285
285
  xmlPatches?: XmlPatch[];
286
286
  onReset?: (model: MujocoModel, data: MujocoData) => void;
287
287
  }
288
+ type ResourceSelector<TInfo, TName extends string = string> = TName | readonly TName[] | RegExp | ((info: TInfo) => boolean);
288
289
  interface IkConfig {
289
290
  /** MuJoCo site name for IK target. */
290
291
  siteName: Sites;
291
- /** Number of joints to solve for. */
292
- numJoints: number;
292
+ /**
293
+ * Explicit joints for IK. When omitted, the controller infers scalar hinge/slide
294
+ * joints by walking from the site body to the model root.
295
+ */
296
+ joints?: ResourceSelector<JointInfo, Joints>;
297
+ /** Explicit actuators for IK control output. */
298
+ actuators?: ResourceSelector<ActuatorInfo, Actuators>;
299
+ /**
300
+ * Number of joints to solve for, assuming legacy contiguous qpos/ctrl layout
301
+ * starting at index 0. Prefer inferred IK or `joints`/`actuators`.
302
+ */
303
+ numJoints?: number;
293
304
  /** Custom IK solver. When omitted, uses built-in Damped Least-Squares solver. */
294
305
  ikSolveFn?: IKSolveFn;
295
306
  /** DLS damping. Default: 0.01. */
@@ -323,7 +334,13 @@ interface PhysicsConfig {
323
334
  paused?: boolean;
324
335
  speed?: number;
325
336
  }
326
- type IKSolveFn = (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]) => number[] | null;
337
+ type IKSolveFn = (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[], context?: IKSolveContext) => number[] | null;
338
+ interface IKSolveContext {
339
+ model: MujocoModel;
340
+ data: MujocoData;
341
+ siteId: number;
342
+ controlGroup: ControlGroupInfo;
343
+ }
327
344
  type PhysicsStepCallback = (model: MujocoModel, data: MujocoData) => void;
328
345
  interface StateSnapshot {
329
346
  time: number;
@@ -368,6 +385,44 @@ interface ActuatorInfo {
368
385
  name: string;
369
386
  range: [number, number];
370
387
  }
388
+ interface ActuatedJointInfo extends JointInfo {
389
+ actuatorId: number;
390
+ actuatorName: string;
391
+ ctrlAdr: number;
392
+ ctrlRange: [number, number];
393
+ }
394
+ interface ControlJointInfo extends JointInfo {
395
+ actuatorId: number | null;
396
+ actuatorName: string | null;
397
+ ctrlAdr: number | null;
398
+ ctrlRange: [number, number] | null;
399
+ }
400
+ interface ControlGroupSelector {
401
+ /** Infer a kinematic chain from a MuJoCo site. */
402
+ siteName?: Sites;
403
+ /** Infer a kinematic chain from a body. */
404
+ bodyName?: Bodies;
405
+ /** Select joints by name, names, regex, or predicate. */
406
+ joints?: ResourceSelector<JointInfo, Joints>;
407
+ /** Select actuators by name, names, regex, or predicate. */
408
+ actuators?: ResourceSelector<ActuatorInfo, Actuators>;
409
+ }
410
+ interface ControlGroupInfo {
411
+ /** Joints in solve/control order. */
412
+ joints: ControlJointInfo[];
413
+ /** Actuators in control output order. */
414
+ actuators: ActuatorInfo[];
415
+ /** qpos addresses for scalar hinge/slide joints. */
416
+ qposAdr: number[];
417
+ /** dof addresses for scalar hinge/slide joints. */
418
+ dofAdr: number[];
419
+ /** ctrl addresses matching writable actuators. */
420
+ ctrlAdr: number[];
421
+ readQpos(data: MujocoData): Float64Array;
422
+ readCtrl(data: MujocoData): Float64Array;
423
+ writeQpos(data: MujocoData, values: ArrayLike<number>): void;
424
+ writeCtrl(data: MujocoData, values: ArrayLike<number>): void;
425
+ }
371
426
  interface SensorInfo {
372
427
  id: number;
373
428
  name: string;
@@ -495,6 +550,9 @@ interface MujocoSimAPI {
495
550
  getQvel(): Float64Array;
496
551
  setCtrl(nameOrValues: Actuators | Record<Actuators, number>, value?: number): void;
497
552
  getCtrl(): Float64Array;
553
+ getControlMap(): ControlGroupInfo;
554
+ getActuatedJoints(): ActuatedJointInfo[];
555
+ resolveControlGroup(selector: ControlGroupSelector): ControlGroupInfo | null;
498
556
  applyForce(bodyName: Bodies, force: THREE.Vector3, point?: THREE.Vector3): void;
499
557
  applyTorque(bodyName: Bodies, torque: THREE.Vector3): void;
500
558
  setExternalForce(bodyName: Bodies, force: THREE.Vector3, torque: THREE.Vector3): void;
@@ -585,8 +643,28 @@ interface JointStateResult {
585
643
  * Hook to access the MuJoCo WASM module.
586
644
  */
587
645
  declare function useMujocoWasm(): MujocoContextValue;
646
+ type MujocoWasmVariant = 'single' | 'threaded' | 'auto';
647
+ interface MujocoLoaderOptions {
648
+ locateFile?: (path: string) => string;
649
+ printErr?: (text: string) => void;
650
+ }
651
+ type MujocoLoader = (options?: MujocoLoaderOptions) => Promise<unknown>;
588
652
  interface MujocoProviderProps {
589
653
  wasmUrl?: string;
654
+ /** Optional URL for the multi-threaded WASM asset. */
655
+ mtWasmUrl?: string;
656
+ /**
657
+ * Optional official multi-threaded loader, usually imported from
658
+ * `@mujoco/mujoco/mt`. It is supplied by the app so the default package path
659
+ * does not force every bundler to process the threaded Emscripten build.
660
+ */
661
+ threadedLoader?: MujocoLoader;
662
+ /**
663
+ * MuJoCo WASM build to load. `single` is the default and works everywhere.
664
+ * `threaded` requires `threadedLoader` and cross-origin isolation. `auto`
665
+ * uses threaded only when both conditions are satisfied.
666
+ */
667
+ wasmVariant?: MujocoWasmVariant;
590
668
  /** Timeout in ms for WASM module load. Default: 30000. */
591
669
  timeout?: number;
592
670
  children: React.ReactNode;
@@ -596,7 +674,7 @@ interface MujocoProviderProps {
596
674
  * MujocoProvider — WASM / module lifecycle.
597
675
  * Loads the MuJoCo WASM module on mount and provides it to children via context.
598
676
  */
599
- declare function MujocoProvider({ wasmUrl, timeout, children, onError }: MujocoProviderProps): react_jsx_runtime.JSX.Element;
677
+ declare function MujocoProvider({ wasmUrl, mtWasmUrl, threadedLoader, wasmVariant, timeout, children, onError, }: MujocoProviderProps): react_jsx_runtime.JSX.Element;
600
678
 
601
679
  /**
602
680
  * MujocoCanvas — thin R3F Canvas wrapper for MuJoCo scenes.
@@ -740,6 +818,10 @@ declare function findSensorByName(mjModel: MujocoModel, name: string): number;
740
818
  * Find a tendon by name in the MuJoCo model. Returns -1 if not found.
741
819
  */
742
820
  declare function findTendonByName(mjModel: MujocoModel, name: string): number;
821
+ declare function getActuatedJoints(mjModel: MujocoModel): ActuatedJointInfo[];
822
+ declare function getControlMap(mjModel: MujocoModel): ControlGroupInfo;
823
+ declare function resolveControlGroup(mjModel: MujocoModel, selector: ControlGroupSelector): ControlGroupInfo | null;
824
+ declare function createContiguousControlGroup(mjModel: MujocoModel, count: number): ControlGroupInfo;
743
825
  interface LoadResult {
744
826
  mjModel: MujocoModel;
745
827
  mjData: MujocoData;
@@ -1304,4 +1386,4 @@ interface CameraAnimationAPI {
1304
1386
  */
1305
1387
  declare function useCameraAnimation(): CameraAnimationAPI;
1306
1388
 
1307
- export { type ActuatorInfo, type Actuators, type Bodies, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControllerComponent, type ControllerOptions, type CtrlHandle, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type Geoms, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type Joints, type KeyBinding, type KeyboardTeleopConfig, type Keyframes, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoSimAPI, MujocoSimProvider, type PhysicsConfig, type PhysicsStepCallback, type PlaybackState, type PolicyConfig, type RayHit, type Register, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorHandle, type SensorInfo, type SensorResult, type Sensors, type SiteInfo, type SitePositionResult, type Sites, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, type TrajectoryInput, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getContact, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
1389
+ export { type ActuatedJointInfo, type ActuatorInfo, type Actuators, type Bodies, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControlGroupInfo, type ControlGroupSelector, type ControlJointInfo, type ControllerComponent, type ControllerOptions, type CtrlHandle, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type Geoms, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type Joints, type KeyBinding, type KeyboardTeleopConfig, type Keyframes, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoLoader, type MujocoLoaderOptions, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoProviderProps, type MujocoSimAPI, MujocoSimProvider, type MujocoWasmVariant, type PhysicsConfig, type PhysicsStepCallback, type PlaybackState, type PolicyConfig, type RayHit, type Register, type ResourceSelector, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorHandle, type SensorInfo, type SensorResult, type Sensors, type SiteInfo, type SitePositionResult, type Sites, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, type TrajectoryInput, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };