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.
Files changed (42) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +510 -0
  3. package/dist/index.d.ts +1080 -0
  4. package/dist/index.js +3518 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +64 -0
  7. package/src/components/ContactListener.tsx +26 -0
  8. package/src/components/ContactMarkers.tsx +81 -0
  9. package/src/components/Debug.tsx +227 -0
  10. package/src/components/DragInteraction.tsx +227 -0
  11. package/src/components/FlexRenderer.tsx +102 -0
  12. package/src/components/IkGizmo.tsx +146 -0
  13. package/src/components/SceneLights.tsx +131 -0
  14. package/src/components/SceneRenderer.tsx +104 -0
  15. package/src/components/SelectionHighlight.tsx +69 -0
  16. package/src/components/TendonRenderer.tsx +84 -0
  17. package/src/components/TrajectoryPlayer.tsx +44 -0
  18. package/src/core/GenericIK.ts +339 -0
  19. package/src/core/MujocoCanvas.tsx +72 -0
  20. package/src/core/MujocoProvider.tsx +78 -0
  21. package/src/core/MujocoSimProvider.tsx +1201 -0
  22. package/src/core/SceneLoader.ts +275 -0
  23. package/src/hooks/useActuators.ts +36 -0
  24. package/src/hooks/useBodyState.ts +56 -0
  25. package/src/hooks/useContacts.ts +125 -0
  26. package/src/hooks/useCtrl.ts +40 -0
  27. package/src/hooks/useCtrlNoise.ts +59 -0
  28. package/src/hooks/useGamepad.ts +77 -0
  29. package/src/hooks/useGravityCompensation.ts +22 -0
  30. package/src/hooks/useJointState.ts +64 -0
  31. package/src/hooks/useKeyboardTeleop.ts +97 -0
  32. package/src/hooks/usePolicy.ts +56 -0
  33. package/src/hooks/useSensor.ts +83 -0
  34. package/src/hooks/useSitePosition.ts +62 -0
  35. package/src/hooks/useTrajectoryPlayer.ts +105 -0
  36. package/src/hooks/useTrajectoryRecorder.ts +97 -0
  37. package/src/hooks/useVideoRecorder.ts +82 -0
  38. package/src/index.ts +108 -0
  39. package/src/rendering/CapsuleGeometry.ts +35 -0
  40. package/src/rendering/GeomBuilder.ts +140 -0
  41. package/src/rendering/Reflector.ts +225 -0
  42. 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
+ }