mujoco-react 0.1.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/LICENSE +177 -0
- package/README.md +510 -0
- package/dist/index.d.ts +1080 -0
- package/dist/index.js +3518 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/components/ContactListener.tsx +26 -0
- package/src/components/ContactMarkers.tsx +81 -0
- package/src/components/Debug.tsx +227 -0
- package/src/components/DragInteraction.tsx +227 -0
- package/src/components/FlexRenderer.tsx +102 -0
- package/src/components/IkGizmo.tsx +146 -0
- package/src/components/SceneLights.tsx +131 -0
- package/src/components/SceneRenderer.tsx +104 -0
- package/src/components/SelectionHighlight.tsx +69 -0
- package/src/components/TendonRenderer.tsx +84 -0
- package/src/components/TrajectoryPlayer.tsx +44 -0
- package/src/core/GenericIK.ts +339 -0
- package/src/core/MujocoCanvas.tsx +72 -0
- package/src/core/MujocoProvider.tsx +78 -0
- package/src/core/MujocoSimProvider.tsx +1201 -0
- package/src/core/SceneLoader.ts +275 -0
- package/src/hooks/useActuators.ts +36 -0
- package/src/hooks/useBodyState.ts +56 -0
- package/src/hooks/useContacts.ts +125 -0
- package/src/hooks/useCtrl.ts +40 -0
- package/src/hooks/useCtrlNoise.ts +59 -0
- package/src/hooks/useGamepad.ts +77 -0
- package/src/hooks/useGravityCompensation.ts +22 -0
- package/src/hooks/useJointState.ts +64 -0
- package/src/hooks/useKeyboardTeleop.ts +97 -0
- package/src/hooks/usePolicy.ts +56 -0
- package/src/hooks/useSensor.ts +83 -0
- package/src/hooks/useSitePosition.ts +62 -0
- package/src/hooks/useTrajectoryPlayer.ts +105 -0
- package/src/hooks/useTrajectoryRecorder.ts +97 -0
- package/src/hooks/useVideoRecorder.ts +82 -0
- package/src/index.ts +108 -0
- package/src/rendering/CapsuleGeometry.ts +35 -0
- package/src/rendering/GeomBuilder.ts +140 -0
- package/src/rendering/Reflector.ts +225 -0
- package/src/types.ts +619 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import { MujocoModule, MujocoModel, MujocoData } from '../types';
|
|
8
|
+
|
|
9
|
+
export interface GenericIKOptions {
|
|
10
|
+
maxIterations: number;
|
|
11
|
+
damping: number;
|
|
12
|
+
tolerance: number;
|
|
13
|
+
epsilon: number;
|
|
14
|
+
posWeight: number;
|
|
15
|
+
rotWeight: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULTS: GenericIKOptions = {
|
|
19
|
+
maxIterations: 50,
|
|
20
|
+
damping: 0.01,
|
|
21
|
+
tolerance: 1e-3,
|
|
22
|
+
epsilon: 1e-6,
|
|
23
|
+
posWeight: 1.0,
|
|
24
|
+
rotWeight: 0.3,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generic Damped Least-Squares IK solver.
|
|
29
|
+
* Uses finite-difference Jacobian via MuJoCo's mj_forward.
|
|
30
|
+
* Works for any MuJoCo model — no robot-specific parameters.
|
|
31
|
+
*/
|
|
32
|
+
export class GenericIK {
|
|
33
|
+
private mujoco: MujocoModule;
|
|
34
|
+
|
|
35
|
+
constructor(mujoco: MujocoModule) {
|
|
36
|
+
this.mujoco = mujoco;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Solve IK for a target 6-DOF pose.
|
|
41
|
+
* @param model MuJoCo model
|
|
42
|
+
* @param data MuJoCo data (qpos will be temporarily modified, then restored)
|
|
43
|
+
* @param siteId Index of the end-effector site to control
|
|
44
|
+
* @param numJoints Number of arm joints (assumes qpos[0..numJoints-1])
|
|
45
|
+
* @param targetPos Target position in world frame
|
|
46
|
+
* @param targetQuat Target orientation in world frame
|
|
47
|
+
* @param currentQ Current joint angles (length = numJoints)
|
|
48
|
+
* @param opts Optional solver parameters
|
|
49
|
+
* @returns Joint angles array, or null if solver diverged
|
|
50
|
+
*/
|
|
51
|
+
solve(
|
|
52
|
+
model: MujocoModel,
|
|
53
|
+
data: MujocoData,
|
|
54
|
+
siteId: number,
|
|
55
|
+
numJoints: number,
|
|
56
|
+
targetPos: THREE.Vector3,
|
|
57
|
+
targetQuat: THREE.Quaternion,
|
|
58
|
+
currentQ: number[],
|
|
59
|
+
opts?: Partial<GenericIKOptions>
|
|
60
|
+
): number[] | null {
|
|
61
|
+
const o = { ...DEFAULTS, ...opts };
|
|
62
|
+
const n = numJoints;
|
|
63
|
+
|
|
64
|
+
// Save full qpos so we can restore after solving
|
|
65
|
+
const savedQpos = new Float64Array(data.qpos.length);
|
|
66
|
+
savedQpos.set(data.qpos);
|
|
67
|
+
|
|
68
|
+
// Build target rotation matrix (3x3 row-major)
|
|
69
|
+
const R_target = quatToMat3(targetQuat);
|
|
70
|
+
|
|
71
|
+
// Working joint angles — start from current configuration
|
|
72
|
+
const q = new Float64Array(n);
|
|
73
|
+
for (let i = 0; i < n; i++) q[i] = currentQ[i];
|
|
74
|
+
|
|
75
|
+
// Pre-allocate work arrays
|
|
76
|
+
const J = new Float64Array(6 * n); // 6×n Jacobian (row-major)
|
|
77
|
+
const JJt = new Float64Array(36); // 6×6
|
|
78
|
+
const rhs = new Float64Array(6); // right-hand side
|
|
79
|
+
const x = new Float64Array(6); // solve result
|
|
80
|
+
const dq = new Float64Array(n); // joint update
|
|
81
|
+
const baseSitePos = new Float64Array(3);
|
|
82
|
+
const baseSiteMat = new Float64Array(9);
|
|
83
|
+
const pertSitePos = new Float64Array(3);
|
|
84
|
+
const pertSiteMat = new Float64Array(9);
|
|
85
|
+
|
|
86
|
+
let bestQ: number[] | null = null;
|
|
87
|
+
let bestErr = Infinity;
|
|
88
|
+
|
|
89
|
+
for (let iter = 0; iter < o.maxIterations; iter++) {
|
|
90
|
+
// Set joints and run FK
|
|
91
|
+
for (let i = 0; i < n; i++) data.qpos[i] = q[i];
|
|
92
|
+
this.mujoco.mj_forward(model, data);
|
|
93
|
+
|
|
94
|
+
// Read current site pose
|
|
95
|
+
const sp = data.site_xpos;
|
|
96
|
+
const sm = data.site_xmat;
|
|
97
|
+
const off3 = siteId * 3;
|
|
98
|
+
const off9 = siteId * 9;
|
|
99
|
+
for (let i = 0; i < 3; i++) baseSitePos[i] = sp[off3 + i];
|
|
100
|
+
for (let i = 0; i < 9; i++) baseSiteMat[i] = sm[off9 + i];
|
|
101
|
+
|
|
102
|
+
// Compute 6D error
|
|
103
|
+
const posErr0 = targetPos.x - baseSitePos[0];
|
|
104
|
+
const posErr1 = targetPos.y - baseSitePos[1];
|
|
105
|
+
const posErr2 = targetPos.z - baseSitePos[2];
|
|
106
|
+
const rotErr = orientationError(baseSiteMat, R_target);
|
|
107
|
+
|
|
108
|
+
const error = [
|
|
109
|
+
posErr0 * o.posWeight,
|
|
110
|
+
posErr1 * o.posWeight,
|
|
111
|
+
posErr2 * o.posWeight,
|
|
112
|
+
rotErr[0] * o.rotWeight,
|
|
113
|
+
rotErr[1] * o.rotWeight,
|
|
114
|
+
rotErr[2] * o.rotWeight,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const errNorm = Math.sqrt(
|
|
118
|
+
error[0] * error[0] + error[1] * error[1] + error[2] * error[2] +
|
|
119
|
+
error[3] * error[3] + error[4] * error[4] + error[5] * error[5]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Track best solution
|
|
123
|
+
if (errNorm < bestErr) {
|
|
124
|
+
bestErr = errNorm;
|
|
125
|
+
bestQ = Array.from(q);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Converged
|
|
129
|
+
if (errNorm < o.tolerance) break;
|
|
130
|
+
|
|
131
|
+
// Compute Jacobian via finite differences
|
|
132
|
+
for (let j = 0; j < n; j++) {
|
|
133
|
+
const saved = data.qpos[j];
|
|
134
|
+
data.qpos[j] = q[j] + o.epsilon;
|
|
135
|
+
this.mujoco.mj_forward(model, data);
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < 3; i++) pertSitePos[i] = sp[off3 + i];
|
|
138
|
+
for (let i = 0; i < 9; i++) pertSiteMat[i] = sm[off9 + i];
|
|
139
|
+
|
|
140
|
+
// Position Jacobian columns (rows 0-2)
|
|
141
|
+
J[0 * n + j] = ((pertSitePos[0] - baseSitePos[0]) / o.epsilon) * o.posWeight;
|
|
142
|
+
J[1 * n + j] = ((pertSitePos[1] - baseSitePos[1]) / o.epsilon) * o.posWeight;
|
|
143
|
+
J[2 * n + j] = ((pertSitePos[2] - baseSitePos[2]) / o.epsilon) * o.posWeight;
|
|
144
|
+
|
|
145
|
+
// Orientation Jacobian columns (rows 3-5)
|
|
146
|
+
// δR = R_perturbed * R_base^T, then extract angular velocity
|
|
147
|
+
const dRot = angularDelta(baseSiteMat, pertSiteMat);
|
|
148
|
+
J[3 * n + j] = (dRot[0] / o.epsilon) * o.rotWeight;
|
|
149
|
+
J[4 * n + j] = (dRot[1] / o.epsilon) * o.rotWeight;
|
|
150
|
+
J[5 * n + j] = (dRot[2] / o.epsilon) * o.rotWeight;
|
|
151
|
+
|
|
152
|
+
// Restore joint
|
|
153
|
+
data.qpos[j] = saved;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Restore base FK state for next iteration
|
|
157
|
+
for (let i = 0; i < n; i++) data.qpos[i] = q[i];
|
|
158
|
+
|
|
159
|
+
// Damped least squares: Δq = Jᵀ (J Jᵀ + λI)⁻¹ error
|
|
160
|
+
// 1. Compute JJᵀ (6×6)
|
|
161
|
+
for (let r = 0; r < 6; r++) {
|
|
162
|
+
for (let c = 0; c < 6; c++) {
|
|
163
|
+
let sum = 0;
|
|
164
|
+
for (let k = 0; k < n; k++) {
|
|
165
|
+
sum += J[r * n + k] * J[c * n + k];
|
|
166
|
+
}
|
|
167
|
+
JJt[r * 6 + c] = sum + (r === c ? o.damping : 0);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Solve (JJᵀ + λI) x = error
|
|
172
|
+
for (let i = 0; i < 6; i++) rhs[i] = error[i];
|
|
173
|
+
solve6x6(JJt, rhs, x);
|
|
174
|
+
|
|
175
|
+
// 3. Δq = Jᵀ x
|
|
176
|
+
for (let j = 0; j < n; j++) {
|
|
177
|
+
let sum = 0;
|
|
178
|
+
for (let r = 0; r < 6; r++) {
|
|
179
|
+
sum += J[r * n + j] * x[r];
|
|
180
|
+
}
|
|
181
|
+
dq[j] = sum;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update joints
|
|
185
|
+
for (let i = 0; i < n; i++) q[i] += dq[i];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Restore original qpos
|
|
189
|
+
data.qpos.set(savedQpos);
|
|
190
|
+
this.mujoco.mj_forward(model, data);
|
|
191
|
+
|
|
192
|
+
return bestQ;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Math utilities ---
|
|
197
|
+
|
|
198
|
+
/** Convert THREE.Quaternion to 3x3 rotation matrix (row-major Float64Array) */
|
|
199
|
+
function quatToMat3(q: THREE.Quaternion): Float64Array {
|
|
200
|
+
const m = new Float64Array(9);
|
|
201
|
+
const x = q.x, y = q.y, z = q.z, w = q.w;
|
|
202
|
+
const xx = x * x, yy = y * y, zz = z * z;
|
|
203
|
+
const xy = x * y, xz = x * z, yz = y * z;
|
|
204
|
+
const wx = w * x, wy = w * y, wz = w * z;
|
|
205
|
+
m[0] = 1 - 2 * (yy + zz); m[1] = 2 * (xy - wz); m[2] = 2 * (xz + wy);
|
|
206
|
+
m[3] = 2 * (xy + wz); m[4] = 1 - 2 * (xx + zz); m[5] = 2 * (yz - wx);
|
|
207
|
+
m[6] = 2 * (xz - wy); m[7] = 2 * (yz + wx); m[8] = 1 - 2 * (xx + yy);
|
|
208
|
+
return m;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Compute orientation error between current and target rotation matrices.
|
|
213
|
+
* Returns the axis-angle vector (log map of R_target * R_current^T).
|
|
214
|
+
* Uses the small-angle approximation: error ≈ 0.5 * [R32-R23, R13-R31, R21-R12]
|
|
215
|
+
* where R = R_target * R_current^T.
|
|
216
|
+
*/
|
|
217
|
+
function orientationError(R_cur: Float64Array, R_tgt: Float64Array): [number, number, number] {
|
|
218
|
+
// R_err = R_tgt * R_cur^T (both row-major 3x3)
|
|
219
|
+
// R_err[i][j] = sum_k R_tgt[i][k] * R_cur[j][k] (note: transposing R_cur)
|
|
220
|
+
const Re = new Float64Array(9);
|
|
221
|
+
for (let i = 0; i < 3; i++) {
|
|
222
|
+
for (let j = 0; j < 3; j++) {
|
|
223
|
+
let s = 0;
|
|
224
|
+
for (let k = 0; k < 3; k++) {
|
|
225
|
+
s += R_tgt[i * 3 + k] * R_cur[j * 3 + k];
|
|
226
|
+
}
|
|
227
|
+
Re[i * 3 + j] = s;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Extract axis-angle from rotation matrix
|
|
232
|
+
// For better accuracy than small-angle approx, use full log map
|
|
233
|
+
const trace = Re[0] + Re[4] + Re[8];
|
|
234
|
+
const cosAngle = Math.max(-1, Math.min(1, (trace - 1) * 0.5));
|
|
235
|
+
const angle = Math.acos(cosAngle);
|
|
236
|
+
|
|
237
|
+
// Near zero rotation — use small-angle approximation
|
|
238
|
+
if (angle < 1e-6) {
|
|
239
|
+
return [0, 0, 0];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Near π — degenerate, use small-angle approx of the skew-symmetric part
|
|
243
|
+
if (angle > Math.PI - 1e-6) {
|
|
244
|
+
return [
|
|
245
|
+
0.5 * (Re[7] - Re[5]),
|
|
246
|
+
0.5 * (Re[2] - Re[6]),
|
|
247
|
+
0.5 * (Re[3] - Re[1]),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// General case: axis = skew(R_err) / (2 sin(angle)), scaled by angle
|
|
252
|
+
const s = angle / (2 * Math.sin(angle));
|
|
253
|
+
return [
|
|
254
|
+
s * (Re[7] - Re[5]),
|
|
255
|
+
s * (Re[2] - Re[6]),
|
|
256
|
+
s * (Re[3] - Re[1]),
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compute angular velocity vector from R_base to R_perturbed.
|
|
262
|
+
* Returns the axis-angle of R_perturbed * R_base^T.
|
|
263
|
+
* (Small angle: the rotation caused by the perturbation.)
|
|
264
|
+
*/
|
|
265
|
+
function angularDelta(R_base: Float64Array, R_pert: Float64Array): [number, number, number] {
|
|
266
|
+
// δR = R_pert * R_base^T
|
|
267
|
+
// Small angle approx: ω ≈ 0.5 * [δR[7]-δR[5], δR[2]-δR[6], δR[3]-δR[1]]
|
|
268
|
+
// This is fine because the perturbation epsilon is tiny.
|
|
269
|
+
const dR = new Float64Array(9);
|
|
270
|
+
for (let i = 0; i < 3; i++) {
|
|
271
|
+
for (let j = 0; j < 3; j++) {
|
|
272
|
+
let s = 0;
|
|
273
|
+
for (let k = 0; k < 3; k++) {
|
|
274
|
+
s += R_pert[i * 3 + k] * R_base[j * 3 + k];
|
|
275
|
+
}
|
|
276
|
+
dR[i * 3 + j] = s;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return [
|
|
280
|
+
0.5 * (dR[7] - dR[5]),
|
|
281
|
+
0.5 * (dR[2] - dR[6]),
|
|
282
|
+
0.5 * (dR[3] - dR[1]),
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Solve 6×6 linear system Ax = b via Gaussian elimination with partial pivoting.
|
|
288
|
+
* Modifies A and b in place. Result written to x.
|
|
289
|
+
*/
|
|
290
|
+
function solve6x6(A: Float64Array, b: Float64Array, x: Float64Array): void {
|
|
291
|
+
const N = 6;
|
|
292
|
+
// Work on copies to avoid destroying originals needed elsewhere
|
|
293
|
+
const a = new Float64Array(A);
|
|
294
|
+
const r = new Float64Array(b);
|
|
295
|
+
|
|
296
|
+
// Forward elimination with partial pivoting
|
|
297
|
+
for (let col = 0; col < N; col++) {
|
|
298
|
+
// Find pivot
|
|
299
|
+
let maxVal = Math.abs(a[col * N + col]);
|
|
300
|
+
let maxRow = col;
|
|
301
|
+
for (let row = col + 1; row < N; row++) {
|
|
302
|
+
const val = Math.abs(a[row * N + col]);
|
|
303
|
+
if (val > maxVal) { maxVal = val; maxRow = row; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Swap rows
|
|
307
|
+
if (maxRow !== col) {
|
|
308
|
+
for (let k = 0; k < N; k++) {
|
|
309
|
+
const tmp = a[col * N + k]; a[col * N + k] = a[maxRow * N + k]; a[maxRow * N + k] = tmp;
|
|
310
|
+
}
|
|
311
|
+
const tmp = r[col]; r[col] = r[maxRow]; r[maxRow] = tmp;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const pivot = a[col * N + col];
|
|
315
|
+
if (Math.abs(pivot) < 1e-12) {
|
|
316
|
+
// Singular — return zeros
|
|
317
|
+
x.fill(0);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Eliminate below
|
|
322
|
+
for (let row = col + 1; row < N; row++) {
|
|
323
|
+
const factor = a[row * N + col] / pivot;
|
|
324
|
+
for (let k = col; k < N; k++) {
|
|
325
|
+
a[row * N + k] -= factor * a[col * N + k];
|
|
326
|
+
}
|
|
327
|
+
r[row] -= factor * r[col];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Back substitution
|
|
332
|
+
for (let row = N - 1; row >= 0; row--) {
|
|
333
|
+
let sum = r[row];
|
|
334
|
+
for (let k = row + 1; k < N; k++) {
|
|
335
|
+
sum -= a[row * N + k] * x[k];
|
|
336
|
+
}
|
|
337
|
+
x[row] = sum / a[row * N + row];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Canvas } from '@react-three/fiber';
|
|
7
|
+
import { forwardRef, useEffect } from 'react';
|
|
8
|
+
import { useMujoco } from './MujocoProvider';
|
|
9
|
+
import { MujocoSimProvider } from './MujocoSimProvider';
|
|
10
|
+
import { MujocoCanvasProps } from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MujocoCanvas — thin R3F Canvas wrapper for MuJoCo scenes.
|
|
14
|
+
* Accepts all R3F Canvas props and forwards them through.
|
|
15
|
+
* Supports declarative physics config props (spec 1.1).
|
|
16
|
+
*/
|
|
17
|
+
export const MujocoCanvas = forwardRef<HTMLCanvasElement, MujocoCanvasProps>(
|
|
18
|
+
function MujocoCanvas(
|
|
19
|
+
{
|
|
20
|
+
config,
|
|
21
|
+
onReady,
|
|
22
|
+
onError,
|
|
23
|
+
onStep,
|
|
24
|
+
onSelection,
|
|
25
|
+
// Declarative physics config (spec 1.1)
|
|
26
|
+
gravity,
|
|
27
|
+
timestep,
|
|
28
|
+
substeps,
|
|
29
|
+
paused,
|
|
30
|
+
speed,
|
|
31
|
+
interpolate,
|
|
32
|
+
gravityCompensation,
|
|
33
|
+
mjcfLights,
|
|
34
|
+
children,
|
|
35
|
+
...canvasProps
|
|
36
|
+
},
|
|
37
|
+
ref
|
|
38
|
+
) {
|
|
39
|
+
const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (wasmStatus === 'error' && onError) {
|
|
43
|
+
onError(new Error(wasmError ?? 'WASM load failed'));
|
|
44
|
+
}
|
|
45
|
+
}, [wasmStatus, wasmError, onError]);
|
|
46
|
+
|
|
47
|
+
if (wasmStatus === 'error' || wasmStatus === 'loading' || !mujoco) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Canvas ref={ref} {...canvasProps}>
|
|
53
|
+
<MujocoSimProvider
|
|
54
|
+
mujoco={mujoco}
|
|
55
|
+
config={config}
|
|
56
|
+
onReady={onReady}
|
|
57
|
+
onError={onError}
|
|
58
|
+
onStep={onStep}
|
|
59
|
+
onSelection={onSelection}
|
|
60
|
+
gravity={gravity}
|
|
61
|
+
timestep={timestep}
|
|
62
|
+
substeps={substeps}
|
|
63
|
+
paused={paused}
|
|
64
|
+
speed={speed}
|
|
65
|
+
interpolate={interpolate}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</MujocoSimProvider>
|
|
69
|
+
</Canvas>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import loadMujoco from 'mujoco-js';
|
|
7
|
+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { MujocoModule, MujocoContextValue } from '../types';
|
|
9
|
+
|
|
10
|
+
const MujocoContext = createContext<MujocoContextValue>({
|
|
11
|
+
mujoco: null,
|
|
12
|
+
status: 'loading',
|
|
13
|
+
error: null,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook to access the MuJoCo WASM module.
|
|
18
|
+
*/
|
|
19
|
+
export function useMujoco(): MujocoContextValue {
|
|
20
|
+
return useContext(MujocoContext);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MujocoProviderProps {
|
|
24
|
+
wasmUrl?: string;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* MujocoProvider — WASM / module lifecycle.
|
|
31
|
+
* Loads the MuJoCo WASM module on mount and provides it to children via context.
|
|
32
|
+
*/
|
|
33
|
+
export function MujocoProvider({ wasmUrl, children, onError }: MujocoProviderProps) {
|
|
34
|
+
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
const moduleRef = useRef<MujocoModule | null>(null);
|
|
37
|
+
const isMounted = useRef(true);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
isMounted.current = true;
|
|
41
|
+
|
|
42
|
+
loadMujoco({
|
|
43
|
+
...(wasmUrl ? { locateFile: (path: string) => path.endsWith('.wasm') ? wasmUrl : path } : {}),
|
|
44
|
+
printErr: (text: string) => {
|
|
45
|
+
if (text.includes('Aborted') && isMounted.current) {
|
|
46
|
+
setError('Simulation crashed. Reload page.');
|
|
47
|
+
setStatus('error');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
.then((inst: unknown) => {
|
|
52
|
+
if (isMounted.current) {
|
|
53
|
+
moduleRef.current = inst as MujocoModule;
|
|
54
|
+
setStatus('ready');
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.catch((err: Error) => {
|
|
58
|
+
if (isMounted.current) {
|
|
59
|
+
const msg = err.message || 'Failed to init spatial simulation';
|
|
60
|
+
setError(msg);
|
|
61
|
+
setStatus('error');
|
|
62
|
+
onError?.(new Error(msg));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
isMounted.current = false;
|
|
68
|
+
};
|
|
69
|
+
}, [wasmUrl]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<MujocoContext.Provider
|
|
73
|
+
value={{ mujoco: moduleRef.current, status, error }}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</MujocoContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
}
|