mujoco-react 0.3.0 → 1.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": "1.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
+ );
@@ -140,7 +140,6 @@ interface MujocoSimProviderProps {
140
140
  substeps?: number;
141
141
  paused?: boolean;
142
142
  speed?: number;
143
- interpolate?: boolean;
144
143
  children: React.ReactNode;
145
144
  }
146
145
 
@@ -157,7 +156,6 @@ export function MujocoSimProvider({
157
156
  substeps,
158
157
  paused,
159
158
  speed,
160
- interpolate,
161
159
  children,
162
160
  }: MujocoSimProviderProps) {
163
161
  const { gl, camera } = useThree();
@@ -171,15 +169,9 @@ export function MujocoSimProvider({
171
169
  const pausedRef = useRef(paused ?? false);
172
170
  const speedRef = useRef(speed ?? 1);
173
171
  const substepsRef = useRef(substeps ?? 1);
174
- const interpolateRef = useRef(interpolate ?? false);
175
172
  const stepsToRunRef = useRef(0);
176
173
  const loadGenRef = useRef(0);
177
174
 
178
- // Interpolation state
179
- const prevXposRef = useRef<Float64Array | null>(null);
180
- const prevXquatRef = useRef<Float64Array | null>(null);
181
- const interpAlphaRef = useRef(0);
182
-
183
175
  const onSelectionRef = useRef(onSelection);
184
176
  onSelectionRef.current = onSelection;
185
177
  const onStepRef = useRef(onStep);
@@ -195,7 +187,6 @@ export function MujocoSimProvider({
195
187
  useEffect(() => { pausedRef.current = paused ?? false; }, [paused]);
196
188
  useEffect(() => { speedRef.current = speed ?? 1; }, [speed]);
197
189
  useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
198
- useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
199
190
 
200
191
  // Sync gravity prop
201
192
  useEffect(() => {
@@ -277,7 +268,7 @@ export function MujocoSimProvider({
277
268
  }, [status]);
278
269
 
279
270
  // --- Physics step (priority -1) ---
280
- useFrame(() => {
271
+ useFrame((_state, delta) => {
281
272
  const model = mjModelRef.current;
282
273
  const data = mjDataRef.current;
283
274
  if (!model || !data) return;
@@ -305,7 +296,8 @@ export function MujocoSimProvider({
305
296
  stepsToRunRef.current = 0;
306
297
  } else {
307
298
  const startSimTime = data.time;
308
- const frameTime = (1.0 / 60.0) * speedRef.current;
299
+ const clampedDelta = Math.min(delta, 1 / 15); // cap to avoid spiral of death
300
+ const frameTime = clampedDelta * speedRef.current;
309
301
  while (data.time - startSimTime < frameTime) {
310
302
  for (let s = 0; s < numSubsteps; s++) {
311
303
  mujoco.mj_step(model, data);
@@ -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,
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 ----