mujoco-react 8.2.1 → 8.3.3

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **Beta** — This library is under active development. The API may change between minor versions until 10.0.
8
8
 
9
- Composable [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) wrapper around [mujoco-js](https://www.npmjs.com/package/mujoco-js). Load any MuJoCo model, step physics, render bodies, and write controllers as React components.
9
+ Composable [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) wrapper around the official [@mujoco/mujoco](https://www.npmjs.com/package/@mujoco/mujoco) WASM bindings. Load any MuJoCo model, step physics, render bodies, and write controllers as React components.
10
10
 
11
11
  [![npm](https://img.shields.io/npm/v/mujoco-react)](https://www.npmjs.com/package/mujoco-react)
12
12
 
@@ -104,6 +104,78 @@ function useMyController(gain: number) {
104
104
  }
105
105
  ```
106
106
 
107
+ ### Applying Forces
108
+
109
+ Write directly to `xfrc_applied` in a physics callback for per-frame forces. The layout is `[torque_x, torque_y, torque_z, force_x, force_y, force_z]` per body:
110
+
111
+ ```tsx
112
+ import { useBeforePhysicsStep } from "mujoco-react";
113
+
114
+ function useSpringForce(bodyId: number, target: [number, number, number], stiffness = 100) {
115
+ useBeforePhysicsStep((_model, data) => {
116
+ const i3 = bodyId * 3;
117
+ const i6 = bodyId * 6;
118
+ data.xfrc_applied[i6 + 3] = (target[0] - data.xpos[i3]) * stiffness;
119
+ data.xfrc_applied[i6 + 4] = (target[1] - data.xpos[i3 + 1]) * stiffness;
120
+ data.xfrc_applied[i6 + 5] = (target[2] - data.xpos[i3 + 2]) * stiffness;
121
+ });
122
+ }
123
+ ```
124
+
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
+
127
+ ### WebSocket Joint Control
128
+
129
+ Stream joint commands over a WebSocket and send simulation state back:
130
+
131
+ ```tsx
132
+ import { useEffect, useRef } from "react";
133
+ import { useMujoco, useBeforePhysicsStep, useAfterPhysicsStep } from "mujoco-react";
134
+
135
+ function useWebSocketJoints(url: string) {
136
+ const { api } = useMujoco();
137
+ const wsRef = useRef<WebSocket | null>(null);
138
+ const latestJointsRef = useRef<number[] | null>(null);
139
+
140
+ useEffect(() => {
141
+ const ws = new WebSocket(url);
142
+ wsRef.current = ws;
143
+
144
+ ws.onmessage = (evt) => {
145
+ const msg = JSON.parse(evt.data);
146
+ if (msg.type === "joint_command") {
147
+ latestJointsRef.current = msg.qpos;
148
+ }
149
+ };
150
+
151
+ return () => ws.close();
152
+ }, [url]);
153
+
154
+ // Apply incoming joint positions each physics step
155
+ 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];
160
+ }
161
+ });
162
+
163
+ // Send sensor feedback back after physics
164
+ useAfterPhysicsStep((model, data) => {
165
+ const ws = wsRef.current;
166
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
167
+
168
+ ws.send(JSON.stringify({
169
+ type: "feedback",
170
+ time: data.time,
171
+ qpos: Array.from(data.qpos),
172
+ qvel: Array.from(data.qvel),
173
+ sensordata: Array.from(data.sensordata),
174
+ }));
175
+ });
176
+ }
177
+ ```
178
+
107
179
  For reusable controllers with typed config, default merging, and children, use the `createController` factory:
108
180
 
109
181
  ```tsx
@@ -445,7 +517,7 @@ import { useMujocoWasm } from "mujoco-react";
445
517
  const { mujoco, status } = useMujocoWasm();
446
518
 
447
519
  if (mujoco) {
448
- const model = mujoco.MjModel.loadFromXML("/path/to/scene.xml");
520
+ const model = mujoco.MjModel.from_xml_path("/path/to/scene.xml");
449
521
  const data = new mujoco.MjData(model);
450
522
  mujoco.mj_step(model, data);
451
523
  console.log(data.qpos); // joint positions after one step
@@ -769,7 +841,7 @@ Features planned but not yet implemented:
769
841
  | **Web Worker physics** | P2 | Run `mj_step` off main thread via SharedArrayBuffer |
770
842
  | **Register codegen** | P2 | CLI to auto-generate `Register` type augmentation from MJCF XML |
771
843
 
772
- ### WASM Limitations (mujoco-js 0.0.7)
844
+ ### WASM Limitations (@mujoco/mujoco)
773
845
 
774
846
  These MuJoCo features are not yet exposed in the WASM binding:
775
847
 
package/dist/index.d.ts CHANGED
@@ -64,12 +64,13 @@ interface MujocoContact {
64
64
  */
65
65
  interface MujocoContactArray {
66
66
  get(i: number): MujocoContact | undefined;
67
+ delete?: () => void;
67
68
  }
68
69
  /**
69
- * Read a single contact from the WASM contact array.
70
+ * Read a single contact from an already-acquired WASM contact array.
70
71
  * Returns undefined if the access fails (WASM heap issue, bad index, etc.).
71
72
  */
72
- declare function getContact(data: MujocoData, i: number): MujocoContact | undefined;
73
+ declare function getContact(contacts: MujocoContactArray, i: number): MujocoContact | undefined;
73
74
  /**
74
75
  * Minimal interface for MuJoCo Model to avoid 'any'.
75
76
  */
@@ -217,7 +218,9 @@ interface MujocoData {
217
218
  */
218
219
  interface MujocoModule {
219
220
  MjModel: {
220
- loadFromXML: (path: string) => MujocoModel;
221
+ from_xml_path?: (path: string) => MujocoModel;
222
+ from_xml_string?: (xml: string, vfs?: unknown) => MujocoModel;
223
+ loadFromXML?: (path: string) => MujocoModel;
221
224
  [key: string]: unknown;
222
225
  };
223
226
  MjData: new (model: MujocoModel) => MujocoData;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import loadMujoco from 'mujoco-js';
1
+ import loadMujoco from '@mujoco/mujoco';
2
+ import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
2
3
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
3
4
  import { jsx, jsxs } from 'react/jsx-runtime';
4
5
  import { Canvas, useThree, useFrame } from '@react-three/fiber';
@@ -22,7 +23,7 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
22
23
  useEffect(() => {
23
24
  isMounted.current = true;
24
25
  const wasmPromise = loadMujoco({
25
- ...wasmUrl ? { locateFile: (path) => path.endsWith(".wasm") ? wasmUrl : path } : {},
26
+ locateFile: (path) => path.endsWith(".wasm") ? wasmUrl ?? defaultMujocoWasmUrl : path,
26
27
  printErr: (text) => {
27
28
  if (text.includes("Aborted") && isMounted.current) {
28
29
  setError("Simulation crashed. Reload page.");
@@ -60,13 +61,21 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
60
61
  }
61
62
 
62
63
  // src/types.ts
63
- function getContact(data, i) {
64
+ function getContact(contacts, i) {
64
65
  try {
65
- return data.contact.get(i);
66
+ return contacts.get(i);
66
67
  } catch {
67
68
  return void 0;
68
69
  }
69
70
  }
71
+ function withContacts(data, read) {
72
+ const contacts = data.contact;
73
+ try {
74
+ return read(contacts);
75
+ } finally {
76
+ contacts.delete?.();
77
+ }
78
+ }
70
79
  var CapsuleGeometry = class extends THREE11.BufferGeometry {
71
80
  parameters;
72
81
  constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
@@ -403,6 +412,15 @@ function ensureDir(mujoco, fname) {
403
412
  }
404
413
  }
405
414
  }
415
+ function loadModelFromPath(mujoco, path) {
416
+ if (mujoco.MjModel.from_xml_path) {
417
+ return mujoco.MjModel.from_xml_path(path);
418
+ }
419
+ if (mujoco.MjModel.loadFromXML) {
420
+ return mujoco.MjModel.loadFromXML(path);
421
+ }
422
+ throw new Error("MuJoCo WASM module does not expose an XML path loader");
423
+ }
406
424
  async function loadScene(mujoco, config, onProgress) {
407
425
  try {
408
426
  mujoco.FS.unmount("/working");
@@ -486,7 +504,7 @@ async function loadScene(mujoco, config, onProgress) {
486
504
  }
487
505
  }
488
506
  onProgress?.("Loading model...");
489
- const mjModel = mujoco.MjModel.loadFromXML(`/working/${config.sceneFile}`);
507
+ const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
490
508
  const mjData = new mujoco.MjData(mjModel);
491
509
  if (config.homeJoints) {
492
510
  const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
@@ -1080,18 +1098,20 @@ function MujocoSimProvider({
1080
1098
  if (!model || !data) return [];
1081
1099
  const contacts = [];
1082
1100
  const ncon = data.ncon;
1083
- for (let i = 0; i < ncon; i++) {
1084
- const c = getContact(data, i);
1085
- if (!c) break;
1086
- contacts.push({
1087
- geom1: c.geom1,
1088
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
1089
- geom2: c.geom2,
1090
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
1091
- pos: [c.pos[0], c.pos[1], c.pos[2]],
1092
- depth: c.dist
1093
- });
1094
- }
1101
+ withContacts(data, (contactArray) => {
1102
+ for (let i = 0; i < ncon; i++) {
1103
+ const c = getContact(contactArray, i);
1104
+ if (!c) break;
1105
+ contacts.push({
1106
+ geom1: c.geom1,
1107
+ geom1Name: getName(model, model.name_geomadr[c.geom1]),
1108
+ geom2: c.geom2,
1109
+ geom2Name: getName(model, model.name_geomadr[c.geom2]),
1110
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
1111
+ depth: c.dist
1112
+ });
1113
+ }
1114
+ });
1095
1115
  return contacts;
1096
1116
  }, []);
1097
1117
  const getBodies = useCallback(() => {
@@ -2255,18 +2275,20 @@ function ContactMarkers({
2255
2275
  }
2256
2276
  const ncon = data.ncon;
2257
2277
  const count = Math.min(ncon, maxContacts);
2258
- for (let i = 0; i < count; i++) {
2259
- const c = getContact(data, i);
2260
- if (!c) {
2261
- mesh.count = i;
2262
- mesh.instanceMatrix.needsUpdate = true;
2263
- return;
2278
+ let resolvedCount = count;
2279
+ withContacts(data, (contactArray) => {
2280
+ for (let i = 0; i < count; i++) {
2281
+ const c = getContact(contactArray, i);
2282
+ if (!c) {
2283
+ resolvedCount = i;
2284
+ return;
2285
+ }
2286
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2287
+ _dummy.updateMatrix();
2288
+ mesh.setMatrixAt(i, _dummy.matrix);
2264
2289
  }
2265
- _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2266
- _dummy.updateMatrix();
2267
- mesh.setMatrixAt(i, _dummy.matrix);
2268
- }
2269
- mesh.count = count;
2290
+ });
2291
+ mesh.count = resolvedCount;
2270
2292
  mesh.instanceMatrix.needsUpdate = true;
2271
2293
  });
2272
2294
  if (status !== "ready") return null;
@@ -2807,22 +2829,24 @@ function Debug({
2807
2829
  if (!data || pool.length === 0) return;
2808
2830
  const ncon = data.ncon;
2809
2831
  let arrowIdx = 0;
2810
- for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
2811
- const c = getContact(data, i);
2812
- if (!c) break;
2813
- _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
2814
- _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
2815
- const force = Math.abs(c.dist) * 100;
2816
- const length = Math.min(force * 0.01, 0.1);
2817
- if (length > 1e-3 && arrowIdx < pool.length) {
2818
- const arrow = pool[arrowIdx];
2819
- arrow.position.copy(_contactPos);
2820
- arrow.setDirection(_contactNormal);
2821
- arrow.setLength(length, length * 0.3, length * 0.15);
2822
- arrow.visible = true;
2823
- arrowIdx++;
2832
+ withContacts(data, (contactArray) => {
2833
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
2834
+ const c = getContact(contactArray, i);
2835
+ if (!c) break;
2836
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
2837
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
2838
+ const force = Math.abs(c.dist) * 100;
2839
+ const length = Math.min(force * 0.01, 0.1);
2840
+ if (length > 1e-3 && arrowIdx < pool.length) {
2841
+ const arrow = pool[arrowIdx];
2842
+ arrow.position.copy(_contactPos);
2843
+ arrow.setDirection(_contactNormal);
2844
+ arrow.setLength(length, length * 0.3, length * 0.15);
2845
+ arrow.visible = true;
2846
+ arrowIdx++;
2847
+ }
2824
2848
  }
2825
- }
2849
+ });
2826
2850
  for (let i = arrowIdx; i < pool.length; i++) {
2827
2851
  pool[i].visible = false;
2828
2852
  }
@@ -3053,23 +3077,25 @@ function useContacts(bodyName, callback) {
3053
3077
  }
3054
3078
  const contacts = [];
3055
3079
  const filterBody = bodyIdRef.current;
3056
- for (let i = 0; i < ncon; i++) {
3057
- const c = getContact(data, i);
3058
- if (!c) break;
3059
- if (filterBody >= 0) {
3060
- const b1 = model.geom_bodyid[c.geom1];
3061
- const b2 = model.geom_bodyid[c.geom2];
3062
- if (b1 !== filterBody && b2 !== filterBody) continue;
3080
+ withContacts(data, (contactArray) => {
3081
+ for (let i = 0; i < ncon; i++) {
3082
+ const c = getContact(contactArray, i);
3083
+ if (!c) break;
3084
+ if (filterBody >= 0) {
3085
+ const b1 = model.geom_bodyid[c.geom1];
3086
+ const b2 = model.geom_bodyid[c.geom2];
3087
+ if (b1 !== filterBody && b2 !== filterBody) continue;
3088
+ }
3089
+ contacts.push({
3090
+ geom1: c.geom1,
3091
+ geom1Name: getGeomNameCached(model, c.geom1),
3092
+ geom2: c.geom2,
3093
+ geom2Name: getGeomNameCached(model, c.geom2),
3094
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
3095
+ depth: c.dist
3096
+ });
3063
3097
  }
3064
- contacts.push({
3065
- geom1: c.geom1,
3066
- geom1Name: getGeomNameCached(model, c.geom1),
3067
- geom2: c.geom2,
3068
- geom2Name: getGeomNameCached(model, c.geom2),
3069
- pos: [c.pos[0], c.pos[1], c.pos[2]],
3070
- depth: c.dist
3071
- });
3072
- }
3098
+ });
3073
3099
  contactsRef.current = contacts;
3074
3100
  callbackRef.current?.(contacts);
3075
3101
  });
@@ -4058,7 +4084,7 @@ function useCameraAnimation() {
4058
4084
  * WASM fields used: model.ntendon, model.ten_wrapadr, model.ten_wrapnum
4059
4085
  * data.wrap_xpos, data.ten_wrapadr (runtime)
4060
4086
  *
4061
- * Note: ten_rgba and ten_width are NOT available in mujoco-js 0.0.7.
4087
+ * Note: ten_rgba and ten_width may not be available in all MuJoCo WASM builds.
4062
4088
  * Tendons use a default color and width.
4063
4089
  */
4064
4090
  /**