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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "7.0.1",
3
+ "version": "8.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",
@@ -9,13 +9,13 @@ import { useEffect, useRef } from 'react';
9
9
  import * as THREE from 'three';
10
10
  import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
11
11
  import { findBodyByName } from '../core/SceneLoader';
12
- import type { BodyStateResult } from '../types';
12
+ import type { Bodies, BodyStateResult } from '../types';
13
13
 
14
14
  /**
15
15
  * Track a MuJoCo body's world position, quaternion, and velocities.
16
16
  * All values are ref-based — updated every physics frame without re-renders.
17
17
  */
18
- export function useBodyState(name: string): BodyStateResult {
18
+ export function useBodyState(name: Bodies): BodyStateResult {
19
19
  const { mjModelRef, status } = useMujocoContext();
20
20
  const bodyIdRef = useRef(-1);
21
21
  const position = useRef(new THREE.Vector3());
@@ -10,7 +10,7 @@ import { useCallback, useEffect, useRef } from 'react';
10
10
  import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
11
11
  import { findBodyByName, getName } from '../core/SceneLoader';
12
12
  import { getContact } from '../types';
13
- import type { ContactInfo, MujocoModel } from '../types';
13
+ import type { Bodies, ContactInfo, MujocoModel } from '../types';
14
14
 
15
15
  // Cache geom names per model to avoid cross-model id collisions.
16
16
  const geomNameCacheByModel = new WeakMap<MujocoModel, Map<number, string>>();
@@ -36,7 +36,7 @@ function getGeomNameCached(model: MujocoModel, geomId: number): string {
36
36
  * Reads `data.ncon` first to avoid allocating for zero contacts.
37
37
  */
38
38
  export function useContacts(
39
- bodyName?: string,
39
+ bodyName?: Bodies,
40
40
  callback?: (contacts: ContactInfo[]) => void,
41
41
  ): React.RefObject<ContactInfo[]> {
42
42
  const { mjModelRef, status } = useMujocoContext();
@@ -108,7 +108,7 @@ export function useContacts(
108
108
  * onEnter/onExit callbacks on transitions.
109
109
  */
110
110
  export function useContactEvents(
111
- bodyName: string,
111
+ bodyName: Bodies,
112
112
  handlers: {
113
113
  onEnter?: (info: ContactInfo) => void;
114
114
  onExit?: (info: ContactInfo) => void;
@@ -2,39 +2,52 @@
2
2
  * @license
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  *
5
- * useCtrl — clean read/write access to a named actuator's ctrl value (spec 3.1)
5
+ * useCtrl — handle-based read/write access to a named actuator's ctrl value (spec 3.1)
6
6
  */
7
7
 
8
- import { useCallback, useEffect, useRef } from 'react';
8
+ import { useEffect, useRef, useMemo } from 'react';
9
9
  import { useMujocoContext } from '../core/MujocoSimProvider';
10
10
  import { findActuatorByName } from '../core/SceneLoader';
11
+ import type { Actuators, CtrlHandle } from '../types';
11
12
 
12
13
  /**
13
14
  * Access a single actuator's control value by name.
14
15
  *
15
- * Returns [currentValue, setValue]:
16
- * - `currentValue` is a ref updated every frame (no re-renders).
17
- * - `setValue` writes directly to `data.ctrl[actuatorId]`.
16
+ * Returns a `CtrlHandle` with `read()` and `write()` methods that
17
+ * operate directly on `data.ctrl` without causing React re-renders.
18
18
  */
19
- export function useCtrl(name: string): [React.RefObject<number>, (value: number) => void] {
19
+ export function useCtrl(name: Actuators): CtrlHandle {
20
20
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
21
21
  const actuatorIdRef = useRef(-1);
22
- const valueRef = useRef(0);
22
+ const rangeRef = useRef<[number, number]>([0, 0]);
23
23
 
24
24
  useEffect(() => {
25
25
  const model = mjModelRef.current;
26
26
  if (!model || status !== 'ready') return;
27
- actuatorIdRef.current = findActuatorByName(model, name);
27
+ const id = findActuatorByName(model, name);
28
+ actuatorIdRef.current = id;
29
+ if (id >= 0) {
30
+ rangeRef.current = [
31
+ model.actuator_ctrlrange[id * 2],
32
+ model.actuator_ctrlrange[id * 2 + 1],
33
+ ];
34
+ }
28
35
  }, [name, status, mjModelRef]);
29
36
 
30
- // Read current value each frame (via afterStep would be ideal but
31
- // useCtrl is primarily for writing; reading can use the ref)
32
- const setValue = useCallback((value: number) => {
33
- const data = mjDataRef.current;
34
- if (!data || actuatorIdRef.current < 0) return;
35
- data.ctrl[actuatorIdRef.current] = value;
36
- valueRef.current = value;
37
- }, [mjDataRef]);
38
-
39
- return [valueRef, setValue];
37
+ return useMemo<CtrlHandle>(() => ({
38
+ read() {
39
+ const data = mjDataRef.current;
40
+ if (!data || actuatorIdRef.current < 0) return 0;
41
+ return data.ctrl[actuatorIdRef.current];
42
+ },
43
+ write(value: number) {
44
+ const data = mjDataRef.current;
45
+ if (!data || actuatorIdRef.current < 0) return;
46
+ data.ctrl[actuatorIdRef.current] = value;
47
+ },
48
+ name,
49
+ get range(): [number, number] {
50
+ return rangeRef.current;
51
+ },
52
+ }), [name, mjDataRef]);
40
53
  }
@@ -8,7 +8,7 @@
8
8
  import { useEffect, useRef } from 'react';
9
9
  import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
10
10
  import { getName } from '../core/SceneLoader';
11
- import type { JointStateResult } from '../types';
11
+ import type { Joints, JointStateResult } from '../types';
12
12
 
13
13
  /**
14
14
  * Track a MuJoCo joint's position and velocity by name.
@@ -18,7 +18,7 @@ import type { JointStateResult } from '../types';
18
18
  * For ball joints, position is quat (4), velocity is angular vel (3).
19
19
  * For free joints, position is pos+quat (7), velocity is lin+ang vel (6).
20
20
  */
21
- export function useJointState(name: string): JointStateResult {
21
+ export function useJointState(name: Joints): JointStateResult {
22
22
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
23
23
  const jointIdRef = useRef(-1);
24
24
  const qposAdrRef = useRef(0);
@@ -8,13 +8,14 @@
8
8
  import { useEffect, useRef, useMemo } from 'react';
9
9
  import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
10
10
  import { getName } from '../core/SceneLoader';
11
- import type { SensorInfo, SensorResult } from '../types';
11
+ import type { Sensors, SensorHandle, SensorInfo } from '../types';
12
12
 
13
13
  /**
14
- * Access a single MuJoCo sensor by name. Returns a ref-based value
15
- * updated every physics frame without causing React re-renders.
14
+ * Access a single MuJoCo sensor by name. Returns a `SensorHandle` with
15
+ * `read()`, `dim`, and `name`. The backing array is updated every physics
16
+ * frame without causing React re-renders.
16
17
  */
17
- export function useSensor(name: string): SensorResult {
18
+ export function useSensor(name: Sensors): SensorHandle {
18
19
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
19
20
  const sensorIdRef = useRef(-1);
20
21
  const sensorAdrRef = useRef(0);
@@ -47,7 +48,15 @@ export function useSensor(name: string): SensorResult {
47
48
  }
48
49
  });
49
50
 
50
- return { value: valueRef, size: sensorDimRef.current };
51
+ return useMemo<SensorHandle>(() => ({
52
+ read() {
53
+ return valueRef.current;
54
+ },
55
+ get dim() {
56
+ return sensorDimRef.current;
57
+ },
58
+ name,
59
+ }), [name]);
51
60
  }
52
61
 
53
62
  /**
@@ -8,7 +8,7 @@ import { useFrame } from '@react-three/fiber';
8
8
  import * as THREE from 'three';
9
9
  import { useMujocoContext } from '../core/MujocoSimProvider';
10
10
  import { findSiteByName } from '../core/SceneLoader';
11
- import type { SitePositionResult } from '../types';
11
+ import type { Sites, SitePositionResult } from '../types';
12
12
 
13
13
  // Preallocated temp for rotation matrix extraction
14
14
  const _mat4 = new THREE.Matrix4();
@@ -17,7 +17,7 @@ const _mat4 = new THREE.Matrix4();
17
17
  * Returns reactive refs for a MuJoCo site's world position and orientation.
18
18
  * Refs are updated every frame without triggering React re-renders.
19
19
  */
20
- export function useSitePosition(siteName: string): SitePositionResult {
20
+ export function useSitePosition(siteName: Sites): SitePositionResult {
21
21
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
22
22
  const siteIdRef = useRef(-1);
23
23
  const positionRef = useRef(new THREE.Vector3());
package/src/index.ts CHANGED
@@ -115,8 +115,19 @@ export type {
115
115
  // Hook return types
116
116
  SitePositionResult,
117
117
  SensorResult,
118
+ CtrlHandle,
119
+ SensorHandle,
118
120
  BodyStateResult,
119
121
  JointStateResult,
122
+ // Register (type-safe named resources)
123
+ Register,
124
+ Actuators,
125
+ Sensors,
126
+ Bodies,
127
+ Joints,
128
+ Sites,
129
+ Geoms,
130
+ Keyframes,
120
131
  } from './types';
121
132
 
122
133
  // Re-export MuJoCo types for convenience
package/src/types.ts CHANGED
@@ -8,6 +8,34 @@ import type { ReactNode } from 'react';
8
8
  import type { CanvasProps } from '@react-three/fiber';
9
9
  import * as THREE from 'three';
10
10
 
11
+ // ---- Register (type-safe named resources) ----
12
+
13
+ /**
14
+ * Module augmentation interface for type-safe resource names.
15
+ *
16
+ * Declare your model's resource names via module augmentation:
17
+ * ```ts
18
+ * declare module 'mujoco-react' {
19
+ * interface Register {
20
+ * actuators: 'joint1' | 'joint2' | 'gripper';
21
+ * sensors: 'force_sensor' | 'torque_sensor';
22
+ * bodies: 'link0' | 'link1' | 'hand';
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * When no augmentation is declared, all names fall back to `string`.
28
+ */
29
+ export interface Register {}
30
+
31
+ export type Actuators = Register extends { actuators: infer T extends string } ? T : string;
32
+ export type Sensors = Register extends { sensors: infer T extends string } ? T : string;
33
+ export type Bodies = Register extends { bodies: infer T extends string } ? T : string;
34
+ export type Joints = Register extends { joints: infer T extends string } ? T : string;
35
+ export type Sites = Register extends { sites: infer T extends string } ? T : string;
36
+ export type Geoms = Register extends { geoms: infer T extends string } ? T : string;
37
+ export type Keyframes = Register extends { keyframes: infer T extends string } ? T : string;
38
+
11
39
  // ---- MuJoCo WASM Types ----
12
40
 
13
41
  /**
@@ -300,7 +328,7 @@ export interface SceneConfig {
300
328
 
301
329
  export interface IkConfig {
302
330
  /** MuJoCo site name for IK target. */
303
- siteName: string;
331
+ siteName: Sites;
304
332
  /** Number of joints to solve for. */
305
333
  numJoints: number;
306
334
  /** Custom IK solver. When omitted, uses built-in Damped Least-Squares solver. */
@@ -462,7 +490,7 @@ export interface TrajectoryData {
462
490
  // ---- Keyboard Teleop (spec 12.1) ----
463
491
 
464
492
  export interface KeyBinding {
465
- actuator: string;
493
+ actuator: Actuators;
466
494
  delta?: number;
467
495
  toggle?: [number, number];
468
496
  set?: number;
@@ -527,13 +555,13 @@ export interface SelectionHighlightProps {
527
555
  }
528
556
 
529
557
  export interface ContactListenerProps {
530
- body: string;
558
+ body: Bodies;
531
559
  onContactEnter?: (info: ContactInfo) => void;
532
560
  onContactExit?: (info: ContactInfo) => void;
533
561
  }
534
562
 
535
563
  export interface BodyProps {
536
- name: string;
564
+ name: Bodies;
537
565
  type: 'box' | 'sphere' | 'cylinder';
538
566
  size: [number, number, number];
539
567
  position?: [number, number, number];
@@ -562,7 +590,7 @@ export interface MujocoSimAPI {
562
590
  step(n?: number): void;
563
591
  getTime(): number;
564
592
  getTimestep(): number;
565
- applyKeyframe(nameOrIndex: string | number): void;
593
+ applyKeyframe(nameOrIndex: Keyframes | number): void;
566
594
 
567
595
  // State management (spec 4.1, 4.2, 4.3)
568
596
  saveState(): StateSnapshot;
@@ -573,17 +601,17 @@ export interface MujocoSimAPI {
573
601
  getQvel(): Float64Array;
574
602
 
575
603
  // Actuator / control (spec 3.1)
576
- setCtrl(nameOrValues: string | Record<string, number>, value?: number): void;
604
+ setCtrl(nameOrValues: Actuators | Record<Actuators, number>, value?: number): void;
577
605
  getCtrl(): Float64Array;
578
606
 
579
607
  // Force application (spec 8.1)
580
- applyForce(bodyName: string, force: THREE.Vector3, point?: THREE.Vector3): void;
581
- applyTorque(bodyName: string, torque: THREE.Vector3): void;
582
- setExternalForce(bodyName: string, force: THREE.Vector3, torque: THREE.Vector3): void;
608
+ applyForce(bodyName: Bodies, force: THREE.Vector3, point?: THREE.Vector3): void;
609
+ applyTorque(bodyName: Bodies, torque: THREE.Vector3): void;
610
+ setExternalForce(bodyName: Bodies, force: THREE.Vector3, torque: THREE.Vector3): void;
583
611
  applyGeneralizedForce(values: Float64Array | number[]): void;
584
612
 
585
613
  // Sensors (spec 2.1)
586
- getSensorData(name: string): Float64Array | null;
614
+ getSensorData(name: Sensors): Float64Array | null;
587
615
 
588
616
  // Contacts (spec 2.4)
589
617
  getContacts(): ContactInfo[];
@@ -621,9 +649,9 @@ export interface MujocoSimAPI {
621
649
  ): { point: THREE.Vector3; bodyId: number; geomId: number } | null;
622
650
 
623
651
  // Domain randomization (spec 10.3)
624
- setBodyMass(name: string, mass: number): void;
625
- setGeomFriction(name: string, friction: [number, number, number]): void;
626
- setGeomSize(name: string, size: [number, number, number]): void;
652
+ setBodyMass(name: Bodies, mass: number): void;
653
+ setGeomFriction(name: Geoms, friction: [number, number, number]): void;
654
+ setGeomSize(name: Geoms, size: [number, number, number]): void;
627
655
 
628
656
  // Internal refs for advanced use
629
657
  readonly mjModelRef: React.RefObject<MujocoModel | null>;
@@ -659,11 +687,32 @@ export interface MujocoContextValue {
659
687
  error: string | null;
660
688
  }
661
689
 
690
+ /** @deprecated Use `SensorHandle` instead. */
662
691
  export interface SensorResult {
663
692
  value: React.RefObject<Float64Array>;
664
693
  size: number;
665
694
  }
666
695
 
696
+ export interface CtrlHandle {
697
+ /** Read the current ctrl value. */
698
+ read(): number;
699
+ /** Write a ctrl value (goes directly to data.ctrl). */
700
+ write(value: number): void;
701
+ /** Actuator name. */
702
+ name: Actuators;
703
+ /** Actuator control range [min, max]. */
704
+ range: [number, number];
705
+ }
706
+
707
+ export interface SensorHandle {
708
+ /** Read the current sensor data. */
709
+ read(): Float64Array;
710
+ /** Sensor dimensionality. */
711
+ dim: number;
712
+ /** Sensor name. */
713
+ name: Sensors;
714
+ }
715
+
667
716
  export interface BodyStateResult {
668
717
  position: React.RefObject<THREE.Vector3>;
669
718
  quaternion: React.RefObject<THREE.Quaternion>;