mujoco-react 7.0.1 → 8.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/README.md CHANGED
@@ -85,15 +85,18 @@ function MyComponent() {
85
85
 
86
86
  ## Writing a Controller
87
87
 
88
- A controller is a React component that calls `useBeforePhysicsStep` to write `data.ctrl` each frame:
88
+ A controller is a React component that uses handle-based hooks for type-safe actuator and sensor access:
89
89
 
90
90
  ```tsx
91
- import { useBeforePhysicsStep } from 'mujoco-react';
91
+ import { useCtrl, useSensor, useBeforePhysicsStep } from 'mujoco-react';
92
92
 
93
93
  function MyController() {
94
- useBeforePhysicsStep((_model, data) => {
95
- data.ctrl[0] = Math.sin(data.time); // sine wave on actuator 0
96
- data.ctrl[1] = data.sensordata[0] * -0.5; // feedback from a sensor
94
+ const joint1 = useCtrl('joint1');
95
+ const force = useSensor('force_sensor');
96
+
97
+ useBeforePhysicsStep(() => {
98
+ joint1.write(Math.sin(Date.now() / 1000));
99
+ joint1.write(force.read()[0] * -0.5);
97
100
  });
98
101
  return null;
99
102
  }
@@ -175,6 +178,23 @@ Returns `IkContextValue | null` with methods like `setIkEnabled`, `moveTarget`,
175
178
 
176
179
  Pass the returned value to `<IkGizmo controller={ik} />` or to your own controller as a prop.
177
180
 
181
+ ## Type-Safe Resource Names
182
+
183
+ Use TypeScript module augmentation to get autocomplete and type checking for actuator, sensor, body, joint, site, geom, and keyframe names:
184
+
185
+ ```ts
186
+ // e.g. in src/mujoco-register.d.ts
187
+ declare module 'mujoco-react' {
188
+ interface Register {
189
+ actuators: 'joint1' | 'joint2' | 'joint3' | 'gripper';
190
+ sensors: 'force_sensor' | 'torque_sensor';
191
+ bodies: 'link0' | 'link1' | 'hand';
192
+ }
193
+ }
194
+ ```
195
+
196
+ Once declared, hooks like `useCtrl`, `useSensor`, `useBodyState`, and API methods like `setCtrl`, `applyForce`, `getSensorData` will only accept the declared names. When no `Register` augmentation is provided, all names fall back to `string`.
197
+
178
198
  ## Loading Models
179
199
 
180
200
  The loader fetches `src + sceneFile`, parses the XML for dependencies (meshes, textures, includes), recursively fetches those too, and writes everything to MuJoCo's in-memory WASM filesystem.
@@ -466,10 +486,11 @@ await moveCameraTo(
466
486
 
467
487
  ### `useSensor(name)` / `useSensors()`
468
488
 
469
- Read sensor values by name (ref-based, no re-renders):
489
+ Read sensor values by name. Returns a `SensorHandle` with `read()`, `dim`, and `name`:
470
490
 
471
491
  ```tsx
472
- const { value, size, type } = useSensor('force_sensor_1');
492
+ const force = useSensor('force_sensor_1');
493
+ // force.read() → Float64Array, force.dim → number
473
494
  ```
474
495
 
475
496
  ### `useBodyState(name)`
@@ -490,10 +511,11 @@ const { position, velocity } = useJointState('joint1');
490
511
 
491
512
  ### `useCtrl(name)`
492
513
 
493
- Read/write actuator control by name:
514
+ Read/write actuator control by name. Returns a `CtrlHandle` with `read()`, `write()`, `name`, and `range`:
494
515
 
495
516
  ```tsx
496
- const [value, setValue] = useCtrl('gripper');
517
+ const gripper = useCtrl('gripper');
518
+ // gripper.read() → number, gripper.write(0.04), gripper.range → [min, max]
497
519
  ```
498
520
 
499
521
  ### `useContacts(bodyName?)` / `useContactEvents(bodyName, handlers)`
@@ -732,6 +754,7 @@ Features planned but not yet implemented:
732
754
  | **Physics interpolation** | P1 | Smooth rendering between physics ticks for very high refresh displays |
733
755
  | **Instanced geom rendering** | P2 | `<InstancedGeomRenderer />` for particle/granular sims |
734
756
  | **Web Worker physics** | P2 | Run `mj_step` off main thread via SharedArrayBuffer |
757
+ | **Register codegen** | P2 | CLI to auto-generate `Register` type augmentation from MJCF XML |
735
758
 
736
759
  ### WASM Limitations (mujoco-js 0.0.7)
737
760
 
package/dist/index.d.ts CHANGED
@@ -9,6 +9,45 @@ import * as THREE from 'three';
9
9
  * SPDX-License-Identifier: Apache-2.0
10
10
  */
11
11
 
12
+ /**
13
+ * Module augmentation interface for type-safe resource names.
14
+ *
15
+ * Declare your model's resource names via module augmentation:
16
+ * ```ts
17
+ * declare module 'mujoco-react' {
18
+ * interface Register {
19
+ * actuators: 'joint1' | 'joint2' | 'gripper';
20
+ * sensors: 'force_sensor' | 'torque_sensor';
21
+ * bodies: 'link0' | 'link1' | 'hand';
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * When no augmentation is declared, all names fall back to `string`.
27
+ */
28
+ interface Register {
29
+ }
30
+ type Actuators = Register extends {
31
+ actuators: infer T extends string;
32
+ } ? T : string;
33
+ type Sensors = Register extends {
34
+ sensors: infer T extends string;
35
+ } ? T : string;
36
+ type Bodies = Register extends {
37
+ bodies: infer T extends string;
38
+ } ? T : string;
39
+ type Joints = Register extends {
40
+ joints: infer T extends string;
41
+ } ? T : string;
42
+ type Sites = Register extends {
43
+ sites: infer T extends string;
44
+ } ? T : string;
45
+ type Geoms = Register extends {
46
+ geoms: infer T extends string;
47
+ } ? T : string;
48
+ type Keyframes = Register extends {
49
+ keyframes: infer T extends string;
50
+ } ? T : string;
12
51
  /**
13
52
  * A single MuJoCo contact from the WASM module.
14
53
  * Accessed via `data.contact.get(i)`.
@@ -245,7 +284,7 @@ interface SceneConfig {
245
284
  }
246
285
  interface IkConfig {
247
286
  /** MuJoCo site name for IK target. */
248
- siteName: string;
287
+ siteName: Sites;
249
288
  /** Number of joints to solve for. */
250
289
  numJoints: number;
251
290
  /** Custom IK solver. When omitted, uses built-in Damped Least-Squares solver. */
@@ -365,7 +404,7 @@ interface TrajectoryData {
365
404
  fps: number;
366
405
  }
367
406
  interface KeyBinding {
368
- actuator: string;
407
+ actuator: Actuators;
369
408
  delta?: number;
370
409
  toggle?: [number, number];
371
410
  set?: number;
@@ -410,12 +449,12 @@ interface TrajectoryPlayerProps {
410
449
  onFrame?: (frameIdx: number) => void;
411
450
  }
412
451
  interface ContactListenerProps {
413
- body: string;
452
+ body: Bodies;
414
453
  onContactEnter?: (info: ContactInfo) => void;
415
454
  onContactExit?: (info: ContactInfo) => void;
416
455
  }
417
456
  interface BodyProps {
418
- name: string;
457
+ name: Bodies;
419
458
  type: 'box' | 'sphere' | 'cylinder';
420
459
  size: [number, number, number];
421
460
  position?: [number, number, number];
@@ -438,20 +477,20 @@ interface MujocoSimAPI {
438
477
  step(n?: number): void;
439
478
  getTime(): number;
440
479
  getTimestep(): number;
441
- applyKeyframe(nameOrIndex: string | number): void;
480
+ applyKeyframe(nameOrIndex: Keyframes | number): void;
442
481
  saveState(): StateSnapshot;
443
482
  restoreState(snapshot: StateSnapshot): void;
444
483
  setQpos(values: Float64Array | number[]): void;
445
484
  setQvel(values: Float64Array | number[]): void;
446
485
  getQpos(): Float64Array;
447
486
  getQvel(): Float64Array;
448
- setCtrl(nameOrValues: string | Record<string, number>, value?: number): void;
487
+ setCtrl(nameOrValues: Actuators | Record<Actuators, number>, value?: number): void;
449
488
  getCtrl(): Float64Array;
450
- applyForce(bodyName: string, force: THREE.Vector3, point?: THREE.Vector3): void;
451
- applyTorque(bodyName: string, torque: THREE.Vector3): void;
452
- setExternalForce(bodyName: string, force: THREE.Vector3, torque: THREE.Vector3): void;
489
+ applyForce(bodyName: Bodies, force: THREE.Vector3, point?: THREE.Vector3): void;
490
+ applyTorque(bodyName: Bodies, torque: THREE.Vector3): void;
491
+ setExternalForce(bodyName: Bodies, force: THREE.Vector3, torque: THREE.Vector3): void;
453
492
  applyGeneralizedForce(values: Float64Array | number[]): void;
454
- getSensorData(name: string): Float64Array | null;
493
+ getSensorData(name: Sensors): Float64Array | null;
455
494
  getContacts(): ContactInfo[];
456
495
  getBodies(): BodyInfo[];
457
496
  getJoints(): JointInfo[];
@@ -472,9 +511,9 @@ interface MujocoSimAPI {
472
511
  bodyId: number;
473
512
  geomId: number;
474
513
  } | null;
475
- setBodyMass(name: string, mass: number): void;
476
- setGeomFriction(name: string, friction: [number, number, number]): void;
477
- setGeomSize(name: string, size: [number, number, number]): void;
514
+ setBodyMass(name: Bodies, mass: number): void;
515
+ setGeomFriction(name: Geoms, friction: [number, number, number]): void;
516
+ setGeomSize(name: Geoms, size: [number, number, number]): void;
478
517
  readonly mjModelRef: React__default.RefObject<MujocoModel | null>;
479
518
  readonly mjDataRef: React__default.RefObject<MujocoData | null>;
480
519
  }
@@ -499,10 +538,29 @@ interface MujocoContextValue {
499
538
  status: 'loading' | 'ready' | 'error';
500
539
  error: string | null;
501
540
  }
541
+ /** @deprecated Use `SensorHandle` instead. */
502
542
  interface SensorResult {
503
543
  value: React__default.RefObject<Float64Array>;
504
544
  size: number;
505
545
  }
546
+ interface CtrlHandle {
547
+ /** Read the current ctrl value. */
548
+ read(): number;
549
+ /** Write a ctrl value (goes directly to data.ctrl). */
550
+ write(value: number): void;
551
+ /** Actuator name. */
552
+ name: Actuators;
553
+ /** Actuator control range [min, max]. */
554
+ range: [number, number];
555
+ }
556
+ interface SensorHandle {
557
+ /** Read the current sensor data. */
558
+ read(): Float64Array;
559
+ /** Sensor dimensionality. */
560
+ dim: number;
561
+ /** Sensor name. */
562
+ name: Sensors;
563
+ }
506
564
  interface BodyStateResult {
507
565
  position: React__default.RefObject<THREE.Vector3>;
508
566
  quaternion: React__default.RefObject<THREE.Quaternion>;
@@ -862,7 +920,7 @@ declare function useActuators(): ActuatorInfo[];
862
920
  * Returns reactive refs for a MuJoCo site's world position and orientation.
863
921
  * Refs are updated every frame without triggering React re-renders.
864
922
  */
865
- declare function useSitePosition(siteName: string): SitePositionResult;
923
+ declare function useSitePosition(siteName: Sites): SitePositionResult;
866
924
 
867
925
  /**
868
926
  * @license
@@ -885,10 +943,11 @@ declare function useGravityCompensation(enabled?: boolean): void;
885
943
  */
886
944
 
887
945
  /**
888
- * Access a single MuJoCo sensor by name. Returns a ref-based value
889
- * updated every physics frame without causing React re-renders.
946
+ * Access a single MuJoCo sensor by name. Returns a `SensorHandle` with
947
+ * `read()`, `dim`, and `name`. The backing array is updated every physics
948
+ * frame without causing React re-renders.
890
949
  */
891
- declare function useSensor(name: string): SensorResult;
950
+ declare function useSensor(name: Sensors): SensorHandle;
892
951
  /**
893
952
  * Enumerate all sensors in the loaded MuJoCo model.
894
953
  * Returns a stable array recomputed only when the model changes.
@@ -910,7 +969,7 @@ declare function useSensors(): SensorInfo[];
910
969
  * For ball joints, position is quat (4), velocity is angular vel (3).
911
970
  * For free joints, position is pos+quat (7), velocity is lin+ang vel (6).
912
971
  */
913
- declare function useJointState(name: string): JointStateResult;
972
+ declare function useJointState(name: Joints): JointStateResult;
914
973
 
915
974
  /**
916
975
  * @license
@@ -923,22 +982,22 @@ declare function useJointState(name: string): JointStateResult;
923
982
  * Track a MuJoCo body's world position, quaternion, and velocities.
924
983
  * All values are ref-based — updated every physics frame without re-renders.
925
984
  */
926
- declare function useBodyState(name: string): BodyStateResult;
985
+ declare function useBodyState(name: Bodies): BodyStateResult;
927
986
 
928
987
  /**
929
988
  * @license
930
989
  * SPDX-License-Identifier: Apache-2.0
931
990
  *
932
- * useCtrl — clean read/write access to a named actuator's ctrl value (spec 3.1)
991
+ * useCtrl — handle-based read/write access to a named actuator's ctrl value (spec 3.1)
933
992
  */
993
+
934
994
  /**
935
995
  * Access a single actuator's control value by name.
936
996
  *
937
- * Returns [currentValue, setValue]:
938
- * - `currentValue` is a ref updated every frame (no re-renders).
939
- * - `setValue` writes directly to `data.ctrl[actuatorId]`.
997
+ * Returns a `CtrlHandle` with `read()` and `write()` methods that
998
+ * operate directly on `data.ctrl` without causing React re-renders.
940
999
  */
941
- declare function useCtrl(name: string): [React.RefObject<number>, (value: number) => void];
1000
+ declare function useCtrl(name: Actuators): CtrlHandle;
942
1001
 
943
1002
  /**
944
1003
  * @license
@@ -953,13 +1012,13 @@ declare function useCtrl(name: string): [React.RefObject<number>, (value: number
953
1012
  * Calls the callback every physics frame with current contact list.
954
1013
  * Reads `data.ncon` first to avoid allocating for zero contacts.
955
1014
  */
956
- declare function useContacts(bodyName?: string, callback?: (contacts: ContactInfo[]) => void): React.RefObject<ContactInfo[]>;
1015
+ declare function useContacts(bodyName?: Bodies, callback?: (contacts: ContactInfo[]) => void): React.RefObject<ContactInfo[]>;
957
1016
  /**
958
1017
  * Contact enter/exit events for a specific body (spec 2.5).
959
1018
  * Tracks which geom pairs are in contact frame-to-frame and fires
960
1019
  * onEnter/onExit callbacks on transitions.
961
1020
  */
962
- declare function useContactEvents(bodyName: string, handlers: {
1021
+ declare function useContactEvents(bodyName: Bodies, handlers: {
963
1022
  onEnter?: (info: ContactInfo) => void;
964
1023
  onExit?: (info: ContactInfo) => void;
965
1024
  }): void;
@@ -1195,4 +1254,4 @@ interface CameraAnimationAPI {
1195
1254
  */
1196
1255
  declare function useCameraAnimation(): CameraAnimationAPI;
1197
1256
 
1198
- export { type ActuatorInfo, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControllerComponent, type ControllerOptions, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type KeyBinding, type KeyboardTeleopConfig, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoSimAPI, MujocoSimProvider, type PhysicsConfig, type PhysicsStepCallback, type PolicyConfig, type RayHit, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorInfo, type SensorResult, type SiteInfo, type SitePositionResult, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, createController, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getContact, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
1257
+ export { type ActuatorInfo, type Actuators, type Bodies, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControllerComponent, type ControllerOptions, type CtrlHandle, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type Geoms, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type Joints, type KeyBinding, type KeyboardTeleopConfig, type Keyframes, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoSimAPI, MujocoSimProvider, type PhysicsConfig, type PhysicsStepCallback, type PolicyConfig, type RayHit, type Register, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorHandle, type SensorInfo, type SensorResult, type Sensors, type SiteInfo, type SitePositionResult, type Sites, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, createController, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getContact, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
package/dist/index.js CHANGED
@@ -3305,7 +3305,15 @@ function useSensor(name) {
3305
3305
  valueRef.current[i] = data.sensordata[adr + i];
3306
3306
  }
3307
3307
  });
3308
- return { value: valueRef, size: sensorDimRef.current };
3308
+ return useMemo(() => ({
3309
+ read() {
3310
+ return valueRef.current;
3311
+ },
3312
+ get dim() {
3313
+ return sensorDimRef.current;
3314
+ },
3315
+ name
3316
+ }), [name]);
3309
3317
  }
3310
3318
  function useSensors() {
3311
3319
  const { mjModelRef, status } = useMujocoContext();
@@ -3439,19 +3447,35 @@ function useBodyState(name) {
3439
3447
  function useCtrl(name) {
3440
3448
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
3441
3449
  const actuatorIdRef = useRef(-1);
3442
- const valueRef = useRef(0);
3450
+ const rangeRef = useRef([0, 0]);
3443
3451
  useEffect(() => {
3444
3452
  const model = mjModelRef.current;
3445
3453
  if (!model || status !== "ready") return;
3446
- actuatorIdRef.current = findActuatorByName(model, name);
3454
+ const id = findActuatorByName(model, name);
3455
+ actuatorIdRef.current = id;
3456
+ if (id >= 0) {
3457
+ rangeRef.current = [
3458
+ model.actuator_ctrlrange[id * 2],
3459
+ model.actuator_ctrlrange[id * 2 + 1]
3460
+ ];
3461
+ }
3447
3462
  }, [name, status, mjModelRef]);
3448
- const setValue = useCallback((value) => {
3449
- const data = mjDataRef.current;
3450
- if (!data || actuatorIdRef.current < 0) return;
3451
- data.ctrl[actuatorIdRef.current] = value;
3452
- valueRef.current = value;
3453
- }, [mjDataRef]);
3454
- return [valueRef, setValue];
3463
+ return useMemo(() => ({
3464
+ read() {
3465
+ const data = mjDataRef.current;
3466
+ if (!data || actuatorIdRef.current < 0) return 0;
3467
+ return data.ctrl[actuatorIdRef.current];
3468
+ },
3469
+ write(value) {
3470
+ const data = mjDataRef.current;
3471
+ if (!data || actuatorIdRef.current < 0) return;
3472
+ data.ctrl[actuatorIdRef.current] = value;
3473
+ },
3474
+ name,
3475
+ get range() {
3476
+ return rangeRef.current;
3477
+ }
3478
+ }), [name, mjDataRef]);
3455
3479
  }
3456
3480
  function useKeyboardTeleop(config) {
3457
3481
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
@@ -3975,7 +3999,7 @@ function useCameraAnimation() {
3975
3999
  * @license
3976
4000
  * SPDX-License-Identifier: Apache-2.0
3977
4001
  *
3978
- * useCtrl — clean read/write access to a named actuator's ctrl value (spec 3.1)
4002
+ * useCtrl — handle-based read/write access to a named actuator's ctrl value (spec 3.1)
3979
4003
  */
3980
4004
  /**
3981
4005
  * @license