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/README.md +78 -31
- package/dist/index.d.ts +6 -3
- package/dist/index.js +86 -60
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ContactMarkers.tsx +14 -12
- package/src/components/Debug.tsx +18 -16
- package/src/components/FlexRenderer.tsx +1 -1
- package/src/components/TendonRenderer.tsx +1 -1
- package/src/core/MujocoProvider.tsx +3 -2
- package/src/core/MujocoSimProvider.tsx +15 -13
- package/src/core/SceneLoader.ts +11 -1
- package/src/hooks/useContacts.ts +20 -18
- package/src/rendering/GeomBuilder.ts +1 -1
- package/src/types.ts +22 -4
- package/src/wasm-url.d.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mujoco-react",
|
|
3
|
-
"version": "8.
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
_dummy.updateMatrix();
|
|
61
|
-
mesh.setMatrixAt(i, _dummy.matrix);
|
|
62
|
-
}
|
|
64
|
+
});
|
|
63
65
|
|
|
64
|
-
mesh.count =
|
|
66
|
+
mesh.count = resolvedCount;
|
|
65
67
|
mesh.instanceMatrix.needsUpdate = true;
|
|
66
68
|
});
|
|
67
69
|
|
package/src/components/Debug.tsx
CHANGED
|
@@ -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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
package/src/core/SceneLoader.ts
CHANGED
|
@@ -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
|
|
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.
|
package/src/hooks/useContacts.ts
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
65
|
+
export function getContact(contacts: MujocoContactArray, i: number): MujocoContact | undefined {
|
|
65
66
|
try {
|
|
66
|
-
return
|
|
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: {
|
|
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;
|