mujoco-react 8.2.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "8.2.0",
3
+ "version": "8.3.3",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "license": "Apache-2.0",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "https://github.com/noah-wardlow/mujoco-react"
37
+ "url": "git+https://github.com/noah-wardlow/mujoco-react.git"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsup",
@@ -48,7 +48,7 @@
48
48
  "three": ">=0.160.0"
49
49
  },
50
50
  "dependencies": {
51
- "mujoco-js": "0.0.7"
51
+ "@mujoco/mujoco": "^3.9.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@react-three/drei": "^10.7.7",
@@ -13,7 +13,7 @@ import { useFrame } from '@react-three/fiber';
13
13
  import type { ThreeElements } from '@react-three/fiber';
14
14
  import * as THREE from 'three';
15
15
  import { useMujocoContext } from '../core/MujocoSimProvider';
16
- import { getContact } from '../types';
16
+ import { getContact, withContacts } from '../types';
17
17
 
18
18
  const _dummy = new THREE.Object3D();
19
19
 
@@ -49,19 +49,21 @@ export function ContactMarkers({
49
49
  const ncon = data.ncon;
50
50
  const count = Math.min(ncon, maxContacts);
51
51
 
52
- for (let i = 0; i < count; i++) {
53
- const c = getContact(data, i);
54
- if (!c) {
55
- mesh.count = i;
56
- mesh.instanceMatrix.needsUpdate = true;
57
- return;
52
+ let resolvedCount = count;
53
+ withContacts(data, (contactArray) => {
54
+ for (let i = 0; i < count; i++) {
55
+ const c = getContact(contactArray, i);
56
+ if (!c) {
57
+ resolvedCount = i;
58
+ return;
59
+ }
60
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
61
+ _dummy.updateMatrix();
62
+ mesh.setMatrixAt(i, _dummy.matrix);
58
63
  }
59
- _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
60
- _dummy.updateMatrix();
61
- mesh.setMatrixAt(i, _dummy.matrix);
62
- }
64
+ });
63
65
 
64
- mesh.count = count;
66
+ mesh.count = resolvedCount;
65
67
  mesh.instanceMatrix.needsUpdate = true;
66
68
  });
67
69
 
@@ -11,7 +11,7 @@ import type { ThreeElements } from '@react-three/fiber';
11
11
  import * as THREE from 'three';
12
12
  import { useMujocoContext } from '../core/MujocoSimProvider';
13
13
  import { getName } from '../core/SceneLoader';
14
- import { getContact } from '../types';
14
+ import { getContact, withContacts } from '../types';
15
15
  import type { DebugProps } from '../types';
16
16
 
17
17
  const JOINT_COLORS: Record<number, number> = {
@@ -330,22 +330,24 @@ export function Debug({
330
330
  const ncon = data.ncon;
331
331
  let arrowIdx = 0;
332
332
 
333
- for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
334
- const c = getContact(data, i);
335
- if (!c) break;
336
- _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
337
- _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
338
- const force = Math.abs(c.dist) * 100;
339
- const length = Math.min(force * 0.01, 0.1);
340
- if (length > 0.001 && arrowIdx < pool.length) {
341
- const arrow = pool[arrowIdx];
342
- arrow.position.copy(_contactPos);
343
- arrow.setDirection(_contactNormal);
344
- arrow.setLength(length, length * 0.3, length * 0.15);
345
- arrow.visible = true;
346
- arrowIdx++;
333
+ withContacts(data, (contactArray) => {
334
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
335
+ const c = getContact(contactArray, i);
336
+ if (!c) break;
337
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
338
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
339
+ const force = Math.abs(c.dist) * 100;
340
+ const length = Math.min(force * 0.01, 0.1);
341
+ if (length > 0.001 && arrowIdx < pool.length) {
342
+ const arrow = pool[arrowIdx];
343
+ arrow.position.copy(_contactPos);
344
+ arrow.setDirection(_contactNormal);
345
+ arrow.setLength(length, length * 0.3, length * 0.15);
346
+ arrow.visible = true;
347
+ arrowIdx++;
348
+ }
347
349
  }
348
- }
350
+ });
349
351
 
350
352
  // Hide unused arrows
351
353
  for (let i = arrowIdx; i < pool.length; i++) {
@@ -39,7 +39,7 @@ export function FlexRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
39
39
  const positions = new Float32Array(vertNum * 3);
40
40
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
41
41
 
42
- // Note: flex_faceadr/flex_facenum/flex_face are NOT available in mujoco-js WASM.
42
+ // Note: flex_faceadr/flex_facenum/flex_face may not be available in all MuJoCo WASM builds.
43
43
  // Without face data we render as a point cloud. If future WASM versions expose
44
44
  // face arrays, index-based triangle rendering can be added here.
45
45
 
@@ -7,7 +7,7 @@
7
7
  * WASM fields used: model.ntendon, model.ten_wrapadr, model.ten_wrapnum
8
8
  * data.wrap_xpos, data.ten_wrapadr (runtime)
9
9
  *
10
- * Note: ten_rgba and ten_width are NOT available in mujoco-js 0.0.7.
10
+ * Note: ten_rgba and ten_width may not be available in all MuJoCo WASM builds.
11
11
  * Tendons use a default color and width.
12
12
  */
13
13
 
@@ -3,7 +3,8 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
- import loadMujoco from 'mujoco-js';
6
+ import loadMujoco from '@mujoco/mujoco';
7
+ import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
7
8
  import { createContext, useContext, useEffect, useRef, useState } from 'react';
8
9
  import { MujocoModule, MujocoContextValue } from '../types';
9
10
 
@@ -42,7 +43,7 @@ export function MujocoProvider({ wasmUrl, timeout = 30000, children, onError }:
42
43
  isMounted.current = true;
43
44
 
44
45
  const wasmPromise = loadMujoco({
45
- ...(wasmUrl ? { locateFile: (path: string) => path.endsWith('.wasm') ? wasmUrl : path } : {}),
46
+ locateFile: (path: string) => path.endsWith('.wasm') ? (wasmUrl ?? defaultMujocoWasmUrl) : path,
46
47
  printErr: (text: string) => {
47
48
  if (text.includes('Aborted') && isMounted.current) {
48
49
  setError('Simulation crashed. Reload page.');
@@ -14,7 +14,7 @@ import {
14
14
  useState,
15
15
  } from 'react';
16
16
  import * as THREE from 'three';
17
- import { MujocoData, MujocoModel, MujocoModule, getContact } from '../types';
17
+ import { MujocoData, MujocoModel, MujocoModule, getContact, withContacts } from '../types';
18
18
  import { SceneRenderer } from '../components/SceneRenderer';
19
19
  import {
20
20
  ActuatorInfo,
@@ -576,18 +576,20 @@ export function MujocoSimProvider({
576
576
  if (!model || !data) return [];
577
577
  const contacts: ContactInfo[] = [];
578
578
  const ncon = data.ncon;
579
- for (let i = 0; i < ncon; i++) {
580
- const c = getContact(data, i);
581
- if (!c) break;
582
- contacts.push({
583
- geom1: c.geom1,
584
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
585
- geom2: c.geom2,
586
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
587
- pos: [c.pos[0], c.pos[1], c.pos[2]],
588
- depth: c.dist,
589
- });
590
- }
579
+ withContacts(data, (contactArray) => {
580
+ for (let i = 0; i < ncon; i++) {
581
+ const c = getContact(contactArray, i);
582
+ if (!c) break;
583
+ contacts.push({
584
+ geom1: c.geom1,
585
+ geom1Name: getName(model, model.name_geomadr[c.geom1]),
586
+ geom2: c.geom2,
587
+ geom2Name: getName(model, model.name_geomadr[c.geom2]),
588
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
589
+ depth: c.dist,
590
+ });
591
+ }
592
+ });
591
593
  return contacts;
592
594
  }, []);
593
595
 
@@ -153,6 +153,16 @@ interface LoadResult {
153
153
  mjData: MujocoData;
154
154
  }
155
155
 
156
+ function loadModelFromPath(mujoco: MujocoModule, path: string): MujocoModel {
157
+ if (mujoco.MjModel.from_xml_path) {
158
+ return mujoco.MjModel.from_xml_path(path);
159
+ }
160
+ if (mujoco.MjModel.loadFromXML) {
161
+ return mujoco.MjModel.loadFromXML(path);
162
+ }
163
+ throw new Error('MuJoCo WASM module does not expose an XML path loader');
164
+ }
165
+
156
166
  /**
157
167
  * Config-driven scene loader — replaces the old RobotLoader + patchSingleRobot approach.
158
168
  */
@@ -260,7 +270,7 @@ export async function loadScene(
260
270
 
261
271
  // 5. Load model
262
272
  onProgress?.('Loading model...');
263
- const mjModel = mujoco.MjModel.loadFromXML(`/working/${config.sceneFile}`);
273
+ const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
264
274
  const mjData = new mujoco.MjData(mjModel);
265
275
 
266
276
  // 6. Set initial pose — set both ctrl and qpos so robot starts at home.
@@ -9,7 +9,7 @@
9
9
  import { useCallback, useEffect, useRef } from 'react';
10
10
  import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
11
11
  import { findBodyByName, getName } from '../core/SceneLoader';
12
- import { getContact } from '../types';
12
+ import { getContact, withContacts } from '../types';
13
13
  import type { Bodies, ContactInfo, MujocoModel } from '../types';
14
14
 
15
15
  // Cache geom names per model to avoid cross-model id collisions.
@@ -77,24 +77,26 @@ export function useContacts(
77
77
  const contacts: ContactInfo[] = [];
78
78
  const filterBody = bodyIdRef.current;
79
79
 
80
- for (let i = 0; i < ncon; i++) {
81
- const c = getContact(data, i);
82
- if (!c) break;
83
- // Filter by body if specified
84
- if (filterBody >= 0) {
85
- const b1 = model.geom_bodyid[c.geom1];
86
- const b2 = model.geom_bodyid[c.geom2];
87
- if (b1 !== filterBody && b2 !== filterBody) continue;
80
+ withContacts(data, (contactArray) => {
81
+ for (let i = 0; i < ncon; i++) {
82
+ const c = getContact(contactArray, i);
83
+ if (!c) break;
84
+ // Filter by body if specified
85
+ if (filterBody >= 0) {
86
+ const b1 = model.geom_bodyid[c.geom1];
87
+ const b2 = model.geom_bodyid[c.geom2];
88
+ if (b1 !== filterBody && b2 !== filterBody) continue;
89
+ }
90
+ contacts.push({
91
+ geom1: c.geom1,
92
+ geom1Name: getGeomNameCached(model, c.geom1),
93
+ geom2: c.geom2,
94
+ geom2Name: getGeomNameCached(model, c.geom2),
95
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
96
+ depth: c.dist,
97
+ });
88
98
  }
89
- contacts.push({
90
- geom1: c.geom1,
91
- geom1Name: getGeomNameCached(model, c.geom1),
92
- geom2: c.geom2,
93
- geom2Name: getGeomNameCached(model, c.geom2),
94
- pos: [c.pos[0], c.pos[1], c.pos[2]],
95
- depth: c.dist,
96
- });
97
- }
99
+ });
98
100
  contactsRef.current = contacts;
99
101
  callbackRef.current?.(contacts);
100
102
  });
@@ -61,7 +61,7 @@ export class GeomBuilder {
61
61
  const MG = this.mujoco.mjtGeom; // Short alias for MuJoCo Geometry Types enum
62
62
  let geo: THREE.BufferGeometry | null = null;
63
63
 
64
- // The '.value ?? MG.XYZ' pattern handles slightly different versions of the mujoco-js bindings.
64
+ // The '.value ?? MG.XYZ' pattern handles slightly different MuJoCo WASM binding versions.
65
65
  const getVal = (v: unknown) => (v as { value: number })?.value ?? v;
66
66
 
67
67
  if (type === getVal(MG.mjGEOM_PLANE)) {
package/src/types.ts CHANGED
@@ -55,20 +55,33 @@ export interface MujocoContact {
55
55
  */
56
56
  export interface MujocoContactArray {
57
57
  get(i: number): MujocoContact | undefined;
58
+ delete?: () => void;
58
59
  }
59
60
 
60
61
  /**
61
- * Read a single contact from the WASM contact array.
62
+ * Read a single contact from an already-acquired WASM contact array.
62
63
  * Returns undefined if the access fails (WASM heap issue, bad index, etc.).
63
64
  */
64
- export function getContact(data: MujocoData, i: number): MujocoContact | undefined {
65
+ export function getContact(contacts: MujocoContactArray, i: number): MujocoContact | undefined {
65
66
  try {
66
- return data.contact.get(i);
67
+ return contacts.get(i);
67
68
  } catch {
68
69
  return undefined;
69
70
  }
70
71
  }
71
72
 
73
+ /**
74
+ * Access the current contact vector and release the copied WASM handle afterwards.
75
+ */
76
+ export function withContacts<T>(data: MujocoData, read: (contacts: MujocoContactArray) => T): T {
77
+ const contacts = data.contact;
78
+ try {
79
+ return read(contacts);
80
+ } finally {
81
+ contacts.delete?.();
82
+ }
83
+ }
84
+
72
85
  /**
73
86
  * Minimal interface for MuJoCo Model to avoid 'any'.
74
87
  */
@@ -249,7 +262,12 @@ export interface MujocoData {
249
262
  * Minimal interface for the MuJoCo WASM Module.
250
263
  */
251
264
  export interface MujocoModule {
252
- MjModel: { loadFromXML: (path: string) => MujocoModel; [key: string]: unknown };
265
+ MjModel: {
266
+ from_xml_path?: (path: string) => MujocoModel;
267
+ from_xml_string?: (xml: string, vfs?: unknown) => MujocoModel;
268
+ loadFromXML?: (path: string) => MujocoModel;
269
+ [key: string]: unknown;
270
+ };
253
271
  MjData: new (model: MujocoModel) => MujocoData;
254
272
  MjvOption: new () => { delete: () => void; [key: string]: unknown };
255
273
  mj_forward: (m: MujocoModel, d: MujocoData) => void;
@@ -0,0 +1,4 @@
1
+ declare module '*.wasm?url' {
2
+ const url: string;
3
+ export default url;
4
+ }