mujoco-react 0.3.0 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "0.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,13 +34,12 @@
34
34
  "license": "Apache-2.0",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "https://github.com/noah/mujoco-react"
37
+ "url": "https://github.com/noah-wardlow/mujoco-react"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsup",
41
41
  "dev": "tsup --watch",
42
- "typecheck": "tsc --noEmit",
43
- "prepublishOnly": "npm run build"
42
+ "typecheck": "tsc --noEmit"
44
43
  },
45
44
  "peerDependencies": {
46
45
  "@react-three/drei": ">=9",
@@ -54,9 +53,12 @@
54
53
  "devDependencies": {
55
54
  "@react-three/drei": "^10.7.7",
56
55
  "@react-three/fiber": "^9.5.0",
56
+ "@semantic-release/changelog": "^6.0.3",
57
+ "@semantic-release/git": "^10.0.1",
57
58
  "@types/react": "^19.0.0",
58
59
  "@types/three": "^0.181.0",
59
60
  "react": "^19.2.0",
61
+ "semantic-release": "^25.0.3",
60
62
  "three": "^0.181.0",
61
63
  "tsup": "^8.4.0",
62
64
  "typescript": "~5.8.2"
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { useRef } from 'react';
12
12
  import { useFrame } from '@react-three/fiber';
13
+ import type { ThreeElements } from '@react-three/fiber';
13
14
  import * as THREE from 'three';
14
15
  import { useMujocoSim } from '../core/MujocoSimProvider';
15
16
  import { getContact } from '../types';
@@ -32,7 +33,8 @@ export function ContactMarkers({
32
33
  radius = 0.008,
33
34
  color = '#22d3ee',
34
35
  visible = true,
35
- }: ContactMarkersProps = {}) {
36
+ ...groupProps
37
+ }: ContactMarkersProps & Omit<ThreeElements['group'], 'ref' | 'visible'> = {}) {
36
38
  const { mjDataRef, status } = useMujocoSim();
37
39
  const meshRef = useRef<THREE.InstancedMesh>(null);
38
40
 
@@ -66,9 +68,11 @@ export function ContactMarkers({
66
68
  if (status !== 'ready') return null;
67
69
 
68
70
  return (
69
- <instancedMesh ref={meshRef} args={[undefined, undefined, maxContacts]} frustumCulled={false} renderOrder={999}>
70
- <sphereGeometry args={[radius, 8, 8]} />
71
- <meshBasicMaterial color={color} depthTest={false} />
72
- </instancedMesh>
71
+ <group {...groupProps}>
72
+ <instancedMesh ref={meshRef} args={[undefined, undefined, maxContacts]} frustumCulled={false} renderOrder={999}>
73
+ <sphereGeometry args={[radius, 8, 8]} />
74
+ <meshBasicMaterial color={color} depthTest={false} />
75
+ </instancedMesh>
76
+ </group>
73
77
  );
74
78
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { useEffect, useMemo, useRef } from 'react';
9
9
  import { useFrame, useThree } from '@react-three/fiber';
10
+ import type { ThreeElements } from '@react-three/fiber';
10
11
  import * as THREE from 'three';
11
12
  import { useMujocoSim } from '../core/MujocoSimProvider';
12
13
  import { getName } from '../core/SceneLoader';
@@ -40,7 +41,8 @@ export function Debug({
40
41
  showCOM = false,
41
42
  showInertia = false,
42
43
  showTendons = false,
43
- }: DebugProps) {
44
+ ...groupProps
45
+ }: DebugProps & Omit<ThreeElements['group'], 'ref'>) {
44
46
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
45
47
  const { scene } = useThree();
46
48
  const groupRef = useRef<THREE.Group>(null);
@@ -354,9 +356,9 @@ export function Debug({
354
356
  if (status !== 'ready') return null;
355
357
 
356
358
  return (
357
- <>
359
+ <group {...groupProps}>
358
360
  <group ref={groupRef} />
359
361
  {showContacts && <group ref={contactGroupRef} />}
360
- </>
362
+ </group>
361
363
  );
362
364
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { useFrame, useThree } from '@react-three/fiber';
7
+ import type { ThreeElements } from '@react-three/fiber';
7
8
  import { useEffect, useRef } from 'react';
8
9
  import * as THREE from 'three';
9
10
  import { useMujocoSim, useBeforePhysicsStep } from '../core/MujocoSimProvider';
@@ -35,7 +36,8 @@ const _mouse = new THREE.Vector2();
35
36
  export function DragInteraction({
36
37
  stiffness = 250,
37
38
  showArrow = true,
38
- }: DragInteractionProps) {
39
+ ...groupProps
40
+ }: DragInteractionProps & Omit<ThreeElements['group'], 'ref'>) {
39
41
  const { mjDataRef, mujocoRef, mjModelRef, status } = useMujocoSim();
40
42
  const { gl, camera, scene, controls } = useThree();
41
43
 
@@ -223,5 +225,5 @@ export function DragInteraction({
223
225
 
224
226
  if (status !== 'ready') return null;
225
227
 
226
- return <group ref={groupRef} />;
228
+ return <group {...groupProps} ref={groupRef} />;
227
229
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { useEffect, useRef } from 'react';
9
9
  import { useFrame } from '@react-three/fiber';
10
+ import type { ThreeElements } from '@react-three/fiber';
10
11
  import * as THREE from 'three';
11
12
  import { useMujocoSim } from '../core/MujocoSimProvider';
12
13
 
@@ -14,7 +15,7 @@ import { useMujocoSim } from '../core/MujocoSimProvider';
14
15
  * Renders MuJoCo flex (deformable) bodies as dynamic meshes.
15
16
  * Vertices are updated every frame from flexvert_xpos.
16
17
  */
17
- export function FlexRenderer() {
18
+ export function FlexRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
18
19
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
19
20
  const groupRef = useRef<THREE.Group>(null);
20
21
  const meshesRef = useRef<THREE.Mesh[]>([]);
@@ -98,5 +99,5 @@ export function FlexRenderer() {
98
99
  });
99
100
 
100
101
  if (status !== 'ready') return null;
101
- return <group ref={groupRef} />;
102
+ return <group {...props} ref={groupRef} />;
102
103
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { useFrame } from '@react-three/fiber';
7
+ import type { ThreeElements } from '@react-three/fiber';
7
8
  import { useEffect, useMemo, useRef } from 'react';
8
9
  import * as THREE from 'three';
9
10
  import { GeomBuilder } from '../rendering/GeomBuilder';
@@ -13,9 +14,9 @@ import { useMujocoSim } from '../core/MujocoSimProvider';
13
14
 
14
15
  /**
15
16
  * SceneRenderer — creates and syncs MuJoCo body meshes every frame.
16
- * Replaces RenderSystem.initScene() + RenderSystem.update() body loop.
17
+ * Accepts standard R3F group props (position, rotation, scale, visible, etc.).
17
18
  */
18
- export function SceneRenderer() {
19
+ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
19
20
  const { mjModelRef, mjDataRef, mujocoRef, onSelectionRef, status } = useMujocoSim();
20
21
  const groupRef = useRef<THREE.Group>(null);
21
22
  const bodyRefs = useRef<(THREE.Group | null)[]>([]);
@@ -85,8 +86,10 @@ export function SceneRenderer() {
85
86
 
86
87
  return (
87
88
  <group
89
+ {...props}
88
90
  ref={groupRef}
89
91
  onDoubleClick={(e) => {
92
+ if (typeof props.onDoubleClick === 'function') props.onDoubleClick(e);
90
93
  e.stopPropagation();
91
94
  let obj: THREE.Object3D | null = e.object;
92
95
  while (obj && obj.userData.bodyID === undefined && obj.parent) {
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { useEffect, useRef } from 'react';
15
15
  import { useFrame } from '@react-three/fiber';
16
+ import type { ThreeElements } from '@react-three/fiber';
16
17
  import * as THREE from 'three';
17
18
  import { useMujocoSim } from '../core/MujocoSimProvider';
18
19
 
@@ -22,7 +23,7 @@ const DEFAULT_TENDON_WIDTH = 0.002;
22
23
  // Preallocated temp vector to avoid per-frame allocations
23
24
  const _tmpVec = new THREE.Vector3();
24
25
 
25
- export function TendonRenderer() {
26
+ export function TendonRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
26
27
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
27
28
  const groupRef = useRef<THREE.Group>(null);
28
29
  const meshesRef = useRef<THREE.Mesh[]>([]);
@@ -144,5 +145,5 @@ export function TendonRenderer() {
144
145
  });
145
146
 
146
147
  if (status !== 'ready') return null;
147
- return <group ref={groupRef} />;
148
+ return <group {...props} ref={groupRef} />;
148
149
  }
@@ -24,15 +24,12 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
24
24
  onError,
25
25
  onStep,
26
26
  onSelection,
27
- // Declarative physics config (spec 1.1)
27
+ // Declarative physics config
28
28
  gravity,
29
29
  timestep,
30
30
  substeps,
31
31
  paused,
32
32
  speed,
33
- interpolate,
34
- gravityCompensation,
35
- mjcfLights,
36
33
  children,
37
34
  ...canvasProps
38
35
  },
@@ -65,7 +62,6 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
65
62
  substeps={substeps}
66
63
  paused={paused}
67
64
  speed={speed}
68
- interpolate={interpolate}
69
65
  >
70
66
  {children}
71
67
  </MujocoSimProvider>
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { forwardRef, useEffect } from 'react';
7
+ import { useMujoco } from './MujocoProvider';
8
+ import { MujocoSimProvider } from './MujocoSimProvider';
9
+ import type { MujocoSimAPI, SceneConfig } from '../types';
10
+
11
+ export interface MujocoPhysicsProps {
12
+ /** Scene/robot configuration. */
13
+ config: SceneConfig;
14
+ /** Fires when model is loaded and API is ready. */
15
+ onReady?: (api: MujocoSimAPI) => void;
16
+ /** Fires on scene load failure. */
17
+ onError?: (error: Error) => void;
18
+ /** Called each physics step. */
19
+ onStep?: (time: number) => void;
20
+ /** Called on body double-click selection. */
21
+ onSelection?: (bodyId: number, name: string) => void;
22
+ /** Override model gravity. */
23
+ gravity?: [number, number, number];
24
+ /** Override model.opt.timestep. */
25
+ timestep?: number;
26
+ /** mj_step calls per frame. */
27
+ substeps?: number;
28
+ /** Declarative pause. */
29
+ paused?: boolean;
30
+ /** Simulation speed multiplier. */
31
+ speed?: number;
32
+ children: React.ReactNode;
33
+ }
34
+
35
+ /**
36
+ * MujocoPhysics — physics provider for use inside a user-owned R3F Canvas.
37
+ *
38
+ * This is the R3F-idiomatic alternative to MujocoCanvas. Instead of wrapping
39
+ * the Canvas, place this inside your own <Canvas>:
40
+ *
41
+ * ```tsx
42
+ * <MujocoProvider>
43
+ * <Canvas shadows camera={...}>
44
+ * <MujocoPhysics config={config} paused={paused}>
45
+ * <SceneRenderer />
46
+ * <OrbitControls />
47
+ * </MujocoPhysics>
48
+ * </Canvas>
49
+ * </MujocoProvider>
50
+ * ```
51
+ *
52
+ * Forward ref exposes MujocoSimAPI.
53
+ */
54
+ export const MujocoPhysics = forwardRef<MujocoSimAPI, MujocoPhysicsProps>(
55
+ function MujocoPhysics({ onError, children, ...props }, ref) {
56
+ const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
57
+
58
+ useEffect(() => {
59
+ if (wasmStatus === 'error' && onError) {
60
+ onError(new Error(wasmError ?? 'WASM load failed'));
61
+ }
62
+ }, [wasmStatus, wasmError, onError]);
63
+
64
+ if (wasmStatus === 'error' || wasmStatus === 'loading' || !mujoco) {
65
+ return null;
66
+ }
67
+
68
+ return (
69
+ <MujocoSimProvider
70
+ mujoco={mujoco}
71
+ apiRef={ref}
72
+ onError={onError}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </MujocoSimProvider>
77
+ );
78
+ }
79
+ );
@@ -15,6 +15,7 @@ import {
15
15
  } from 'react';
16
16
  import * as THREE from 'three';
17
17
  import { MujocoData, MujocoModel, MujocoModule, getContact } from '../types';
18
+ import { SceneRenderer } from '../components/SceneRenderer';
18
19
  import {
19
20
  ActuatorInfo,
20
21
  BodyInfo,
@@ -140,7 +141,6 @@ interface MujocoSimProviderProps {
140
141
  substeps?: number;
141
142
  paused?: boolean;
142
143
  speed?: number;
143
- interpolate?: boolean;
144
144
  children: React.ReactNode;
145
145
  }
146
146
 
@@ -157,7 +157,6 @@ export function MujocoSimProvider({
157
157
  substeps,
158
158
  paused,
159
159
  speed,
160
- interpolate,
161
160
  children,
162
161
  }: MujocoSimProviderProps) {
163
162
  const { gl, camera } = useThree();
@@ -171,15 +170,9 @@ export function MujocoSimProvider({
171
170
  const pausedRef = useRef(paused ?? false);
172
171
  const speedRef = useRef(speed ?? 1);
173
172
  const substepsRef = useRef(substeps ?? 1);
174
- const interpolateRef = useRef(interpolate ?? false);
175
173
  const stepsToRunRef = useRef(0);
176
174
  const loadGenRef = useRef(0);
177
175
 
178
- // Interpolation state
179
- const prevXposRef = useRef<Float64Array | null>(null);
180
- const prevXquatRef = useRef<Float64Array | null>(null);
181
- const interpAlphaRef = useRef(0);
182
-
183
176
  const onSelectionRef = useRef(onSelection);
184
177
  onSelectionRef.current = onSelection;
185
178
  const onStepRef = useRef(onStep);
@@ -195,7 +188,6 @@ export function MujocoSimProvider({
195
188
  useEffect(() => { pausedRef.current = paused ?? false; }, [paused]);
196
189
  useEffect(() => { speedRef.current = speed ?? 1; }, [speed]);
197
190
  useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
198
- useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
199
191
 
200
192
  // Sync gravity prop
201
193
  useEffect(() => {
@@ -277,7 +269,7 @@ export function MujocoSimProvider({
277
269
  }, [status]);
278
270
 
279
271
  // --- Physics step (priority -1) ---
280
- useFrame(() => {
272
+ useFrame((_state, delta) => {
281
273
  const model = mjModelRef.current;
282
274
  const data = mjDataRef.current;
283
275
  if (!model || !data) return;
@@ -305,7 +297,8 @@ export function MujocoSimProvider({
305
297
  stepsToRunRef.current = 0;
306
298
  } else {
307
299
  const startSimTime = data.time;
308
- const frameTime = (1.0 / 60.0) * speedRef.current;
300
+ const clampedDelta = Math.min(delta, 1 / 15); // cap to avoid spiral of death
301
+ const frameTime = clampedDelta * speedRef.current;
309
302
  while (data.time - startSimTime < frameTime) {
310
303
  for (let s = 0; s < numSubsteps; s++) {
311
304
  mujoco.mj_step(model, data);
@@ -930,6 +923,7 @@ export function MujocoSimProvider({
930
923
 
931
924
  return (
932
925
  <MujocoSimContext.Provider value={contextValue}>
926
+ <SceneRenderer />
933
927
  {children}
934
928
  </MujocoSimContext.Provider>
935
929
  );
@@ -140,8 +140,6 @@ function sceneObjectToXml(obj: SceneObject): string {
140
140
  interface LoadResult {
141
141
  mjModel: MujocoModel;
142
142
  mjData: MujocoData;
143
- siteId: number;
144
- gripperId: number;
145
143
  }
146
144
 
147
145
  /**
@@ -241,11 +239,7 @@ export async function loadScene(
241
239
  const mjModel = mujoco.MjModel.loadFromXML(`/working/${config.sceneFile}`);
242
240
  const mjData = new mujoco.MjData(mjModel);
243
241
 
244
- // 6. Find TCP site and gripper actuator
245
- const siteId = findSiteByName(mjModel, config.tcpSiteName ?? 'tcp');
246
- const gripperId = findActuatorByName(mjModel, config.gripperActuatorName ?? 'gripper');
247
-
248
- // 7. Set initial pose — set both ctrl and qpos so robot starts at home.
242
+ // 6. Set initial pose — set both ctrl and qpos so robot starts at home.
249
243
  // If homeJoints is not provided, keep raw MuJoCo defaults.
250
244
  if (config.homeJoints) {
251
245
  const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
@@ -260,7 +254,7 @@ export async function loadScene(
260
254
 
261
255
  mujoco.mj_forward(mjModel, mjData);
262
256
 
263
- return { mjModel, mjData, siteId, gripperId };
257
+ return { mjModel, mjData };
264
258
  }
265
259
 
266
260
  /**
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  // Core
7
7
  export { MujocoProvider, useMujoco } from './core/MujocoProvider';
8
8
  export { MujocoCanvas } from './core/MujocoCanvas';
9
+ export { MujocoPhysics } from './core/MujocoPhysics';
10
+ export type { MujocoPhysicsProps } from './core/MujocoPhysics';
9
11
  export { MujocoSimProvider, useMujocoSim, useBeforePhysicsStep, useAfterPhysicsStep } from './core/MujocoSimProvider';
10
12
  export {
11
13
  loadScene,
@@ -30,7 +32,6 @@ export { useIk } from './core/IkContext';
30
32
  export type { IkContextValue } from './core/IkContext';
31
33
 
32
34
  // Components
33
- export { SceneRenderer } from './components/SceneRenderer';
34
35
  export { IkGizmo } from './components/IkGizmo';
35
36
  export { ContactMarkers } from './components/ContactMarkers';
36
37
  export { DragInteraction } from './components/DragInteraction';
package/src/types.ts CHANGED
@@ -291,12 +291,6 @@ export interface SceneConfig {
291
291
  homeJoints?: number[];
292
292
  xmlPatches?: XmlPatch[];
293
293
  onReset?: (model: MujocoModel, data: MujocoData) => void;
294
- /** @deprecated Use IkController config.siteName instead. */
295
- tcpSiteName?: string;
296
- /** @deprecated Use your own gripper control logic instead. */
297
- gripperActuatorName?: string;
298
- /** @deprecated Use IkController config.numJoints instead. */
299
- numArmJoints?: number;
300
294
  }
301
295
 
302
296
  // ---- IK Controller Config ----
@@ -328,7 +322,6 @@ export interface PhysicsConfig {
328
322
  substeps?: number;
329
323
  paused?: boolean;
330
324
  speed?: number;
331
- interpolate?: boolean;
332
325
  }
333
326
 
334
327
  // ---- IK ----
@@ -620,9 +613,6 @@ export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
620
613
  substeps?: number;
621
614
  paused?: boolean;
622
615
  speed?: number;
623
- interpolate?: boolean;
624
- gravityCompensation?: boolean;
625
- mjcfLights?: boolean;
626
616
  };
627
617
 
628
618
  // ---- Hook Return Types ----