mujoco-react 6.0.1 → 7.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 +84 -46
- package/dist/index.d.ts +88 -57
- package/dist/index.js +372 -243
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Body.tsx +102 -0
- package/src/components/ContactMarkers.tsx +2 -2
- package/src/components/Debug.tsx +2 -2
- package/src/components/DragInteraction.tsx +2 -2
- package/src/components/FlexRenderer.tsx +2 -2
- package/src/components/IkGizmo.tsx +7 -11
- package/src/components/SceneRenderer.tsx +9 -6
- package/src/components/TendonRenderer.tsx +2 -2
- package/src/core/MujocoSimProvider.tsx +87 -6
- package/src/core/createController.tsx +54 -2
- package/src/hooks/useActuators.ts +2 -2
- package/src/hooks/useBodyState.ts +2 -2
- package/src/hooks/useContacts.ts +2 -2
- package/src/hooks/useCtrl.ts +2 -2
- package/src/hooks/useCtrlNoise.ts +2 -2
- package/src/hooks/useGamepad.ts +2 -2
- package/src/hooks/useIkController.ts +242 -0
- package/src/hooks/useJointState.ts +2 -2
- package/src/hooks/useKeyboardTeleop.ts +2 -2
- package/src/hooks/usePolicy.ts +2 -2
- package/src/hooks/useSceneLights.ts +2 -2
- package/src/hooks/useSensor.ts +3 -3
- package/src/hooks/useSitePosition.ts +2 -2
- package/src/hooks/useTrajectoryPlayer.ts +2 -2
- package/src/hooks/useTrajectoryRecorder.ts +2 -2
- package/src/index.ts +5 -4
- package/src/types.ts +30 -0
- package/src/components/IkController.tsx +0 -262
- package/src/core/IkContext.tsx +0 -40
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useFrame } from '@react-three/fiber';
|
|
7
|
+
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
10
|
+
import { findBodyByName } from '../core/SceneLoader';
|
|
11
|
+
import type { BodyProps, SceneObject } from '../types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Declarative physics body component. Registers a body definition in the
|
|
15
|
+
* provider-level registry so it gets injected into the MJCF XML at load time.
|
|
16
|
+
*
|
|
17
|
+
* Bodies present at initial mount cause zero extra reloads (useLayoutEffect
|
|
18
|
+
* runs before the provider's loadScene useEffect). Bodies added/removed after
|
|
19
|
+
* the initial load trigger a debounced scene reload.
|
|
20
|
+
*/
|
|
21
|
+
export function Body({
|
|
22
|
+
name,
|
|
23
|
+
type,
|
|
24
|
+
size,
|
|
25
|
+
position = [0, 0, 0],
|
|
26
|
+
rgba = [0.5, 0.5, 0.5, 1],
|
|
27
|
+
mass,
|
|
28
|
+
freejoint,
|
|
29
|
+
friction,
|
|
30
|
+
solref,
|
|
31
|
+
solimp,
|
|
32
|
+
condim,
|
|
33
|
+
children,
|
|
34
|
+
}: BodyProps) {
|
|
35
|
+
const { bodyRegistryRef, hiddenBodiesRef, requestBodyReload, mjDataRef, mjModelRef, status } =
|
|
36
|
+
useMujocoContext();
|
|
37
|
+
const bodyIdRef = useRef(-1);
|
|
38
|
+
const groupRef = useRef<THREE.Group>(null);
|
|
39
|
+
const initialLoadRef = useRef(true);
|
|
40
|
+
const hasChildren = children != null;
|
|
41
|
+
|
|
42
|
+
// Register in body registry BEFORE the provider's loadScene useEffect fires.
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
const definition: SceneObject = {
|
|
45
|
+
name,
|
|
46
|
+
type,
|
|
47
|
+
size,
|
|
48
|
+
position,
|
|
49
|
+
rgba,
|
|
50
|
+
mass,
|
|
51
|
+
freejoint,
|
|
52
|
+
friction,
|
|
53
|
+
solref,
|
|
54
|
+
solimp,
|
|
55
|
+
condim,
|
|
56
|
+
};
|
|
57
|
+
bodyRegistryRef.current.set(name, { definition, hasCustomChildren: hasChildren });
|
|
58
|
+
if (hasChildren) {
|
|
59
|
+
hiddenBodiesRef.current.add(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
bodyRegistryRef.current.delete(name);
|
|
64
|
+
hiddenBodiesRef.current.delete(name);
|
|
65
|
+
if (!initialLoadRef.current) {
|
|
66
|
+
requestBodyReload();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
|
|
70
|
+
|
|
71
|
+
// Resolve body ID once the scene is ready
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (status !== 'ready') return;
|
|
74
|
+
const model = mjModelRef.current;
|
|
75
|
+
if (!model) return;
|
|
76
|
+
bodyIdRef.current = findBodyByName(model, name);
|
|
77
|
+
initialLoadRef.current = false;
|
|
78
|
+
}, [status, name, mjModelRef]);
|
|
79
|
+
|
|
80
|
+
// Sync group transform to body pose each frame (only when children are provided)
|
|
81
|
+
useFrame(() => {
|
|
82
|
+
if (!hasChildren) return;
|
|
83
|
+
const data = mjDataRef.current;
|
|
84
|
+
const id = bodyIdRef.current;
|
|
85
|
+
const group = groupRef.current;
|
|
86
|
+
if (!data || id < 0 || !group) return;
|
|
87
|
+
|
|
88
|
+
const i3 = id * 3;
|
|
89
|
+
const i4 = id * 4;
|
|
90
|
+
group.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
|
|
91
|
+
group.quaternion.set(
|
|
92
|
+
data.xquat[i4 + 1],
|
|
93
|
+
data.xquat[i4 + 2],
|
|
94
|
+
data.xquat[i4 + 3],
|
|
95
|
+
data.xquat[i4],
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!hasChildren) return null;
|
|
100
|
+
|
|
101
|
+
return <group ref={groupRef}>{children}</group>;
|
|
102
|
+
}
|
|
@@ -12,7 +12,7 @@ import { useRef } from 'react';
|
|
|
12
12
|
import { useFrame } from '@react-three/fiber';
|
|
13
13
|
import type { ThreeElements } from '@react-three/fiber';
|
|
14
14
|
import * as THREE from 'three';
|
|
15
|
-
import {
|
|
15
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
16
16
|
import { getContact } from '../types';
|
|
17
17
|
|
|
18
18
|
const _dummy = new THREE.Object3D();
|
|
@@ -35,7 +35,7 @@ export function ContactMarkers({
|
|
|
35
35
|
visible = true,
|
|
36
36
|
...groupProps
|
|
37
37
|
}: ContactMarkersProps & Omit<ThreeElements['group'], 'ref' | 'visible'> = {}) {
|
|
38
|
-
const { mjDataRef, status } =
|
|
38
|
+
const { mjDataRef, status } = useMujocoContext();
|
|
39
39
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
40
40
|
|
|
41
41
|
useFrame(() => {
|
package/src/components/Debug.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { useEffect, useMemo, useRef } from 'react';
|
|
|
9
9
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
10
10
|
import type { ThreeElements } from '@react-three/fiber';
|
|
11
11
|
import * as THREE from 'three';
|
|
12
|
-
import {
|
|
12
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
13
13
|
import { getName } from '../core/SceneLoader';
|
|
14
14
|
import { getContact } from '../types';
|
|
15
15
|
import type { DebugProps } from '../types';
|
|
@@ -43,7 +43,7 @@ export function Debug({
|
|
|
43
43
|
showTendons = false,
|
|
44
44
|
...groupProps
|
|
45
45
|
}: DebugProps & Omit<ThreeElements['group'], 'ref'>) {
|
|
46
|
-
const { mjModelRef, mjDataRef, status } =
|
|
46
|
+
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
47
47
|
const { scene } = useThree();
|
|
48
48
|
const groupRef = useRef<THREE.Group>(null);
|
|
49
49
|
|
|
@@ -7,7 +7,7 @@ import { useFrame, useThree } from '@react-three/fiber';
|
|
|
7
7
|
import type { ThreeElements } from '@react-three/fiber';
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import * as THREE from 'three';
|
|
10
|
-
import {
|
|
10
|
+
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import type { DragInteractionProps } from '../types';
|
|
12
12
|
|
|
13
13
|
// Preallocated temps to avoid GC pressure
|
|
@@ -38,7 +38,7 @@ export function DragInteraction({
|
|
|
38
38
|
showArrow = true,
|
|
39
39
|
...groupProps
|
|
40
40
|
}: DragInteractionProps & Omit<ThreeElements['group'], 'ref'>) {
|
|
41
|
-
const { mjDataRef, mujocoRef, mjModelRef, status } =
|
|
41
|
+
const { mjDataRef, mujocoRef, mjModelRef, status } = useMujocoContext();
|
|
42
42
|
const { gl, camera, scene, controls } = useThree();
|
|
43
43
|
|
|
44
44
|
const draggingRef = useRef(false);
|
|
@@ -9,14 +9,14 @@ import { useEffect, useRef } from 'react';
|
|
|
9
9
|
import { useFrame } from '@react-three/fiber';
|
|
10
10
|
import type { ThreeElements } from '@react-three/fiber';
|
|
11
11
|
import * as THREE from 'three';
|
|
12
|
-
import {
|
|
12
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Renders MuJoCo flex (deformable) bodies as dynamic meshes.
|
|
16
16
|
* Vertices are updated every frame from flexvert_xpos.
|
|
17
17
|
*/
|
|
18
18
|
export function FlexRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
19
|
-
const { mjModelRef, mjDataRef, status } =
|
|
19
|
+
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
20
20
|
const groupRef = useRef<THREE.Group>(null);
|
|
21
21
|
const meshesRef = useRef<THREE.Mesh[]>([]);
|
|
22
22
|
|
|
@@ -7,8 +7,7 @@ import { PivotControls } from '@react-three/drei';
|
|
|
7
7
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import * as THREE from 'three';
|
|
10
|
-
import {
|
|
11
|
-
import { useIk } from '../core/IkContext';
|
|
10
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
12
11
|
import { findSiteByName } from '../core/SceneLoader';
|
|
13
12
|
import type { IkGizmoProps } from '../types';
|
|
14
13
|
|
|
@@ -21,18 +20,19 @@ const _scale = new THREE.Vector3(1, 1, 1);
|
|
|
21
20
|
/**
|
|
22
21
|
* IkGizmo — drei PivotControls that tracks a MuJoCo site.
|
|
23
22
|
*
|
|
24
|
-
*
|
|
23
|
+
* Requires a `controller` from `useIkController()`.
|
|
25
24
|
*
|
|
26
25
|
* Props:
|
|
27
|
-
* - `
|
|
26
|
+
* - `controller` — IkContextValue from `useIkController()`.
|
|
27
|
+
* - `siteName` — MuJoCo site to track. Defaults to the controller's configured site.
|
|
28
28
|
* - `scale` — Gizmo handle scale. Default: 0.18.
|
|
29
29
|
* - `onDrag` — Custom drag callback `(pos, quat) => void`.
|
|
30
30
|
* When omitted, dragging enables IK and writes to the IK target.
|
|
31
31
|
* When provided, the consumer handles what happens during drag.
|
|
32
32
|
*/
|
|
33
|
-
export function IkGizmo({ siteName, scale = 0.18, onDrag }: IkGizmoProps) {
|
|
34
|
-
const { mjModelRef, mjDataRef, status } =
|
|
35
|
-
const { ikTargetRef, siteIdRef, ikEnabledRef, setIkEnabled } =
|
|
33
|
+
export function IkGizmo({ controller, siteName, scale = 0.18, onDrag }: IkGizmoProps) {
|
|
34
|
+
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
35
|
+
const { ikTargetRef, siteIdRef, ikEnabledRef, setIkEnabled } = controller;
|
|
36
36
|
|
|
37
37
|
const wrapperRef = useRef<THREE.Group>(null);
|
|
38
38
|
const pivotRef = useRef<THREE.Group>(null);
|
|
@@ -53,9 +53,6 @@ export function IkGizmo({ siteName, scale = 0.18, onDrag }: IkGizmoProps) {
|
|
|
53
53
|
// Every frame: sync the visual wrapper to the tracked site (when not dragging)
|
|
54
54
|
useFrame(() => {
|
|
55
55
|
const data = mjDataRef.current;
|
|
56
|
-
// Read IkController's siteIdRef directly in useFrame — avoids useEffect timing
|
|
57
|
-
// issues (React runs child effects before parent effects, so reading siteIdRef
|
|
58
|
-
// in a useEffect would see -1 before IkController resolves it).
|
|
59
56
|
const sid = siteName ? localSiteIdRef.current : siteIdRef.current;
|
|
60
57
|
if (!data || sid < 0 || !wrapperRef.current) return;
|
|
61
58
|
|
|
@@ -67,7 +64,6 @@ export function IkGizmo({ siteName, scale = 0.18, onDrag }: IkGizmoProps) {
|
|
|
67
64
|
|
|
68
65
|
// Position wrapper at the site
|
|
69
66
|
wrapperRef.current.position.set(p[i3], p[i3 + 1], p[i3 + 2]);
|
|
70
|
-
// MuJoCo site_xmat is row-major 3x3; THREE.Matrix4.set() is row-major
|
|
71
67
|
_mat4.set(
|
|
72
68
|
m[i9], m[i9 + 1], m[i9 + 2], 0,
|
|
73
69
|
m[i9 + 3], m[i9 + 4], m[i9 + 5], 0,
|
|
@@ -10,14 +10,14 @@ import * as THREE from 'three';
|
|
|
10
10
|
import { GeomBuilder } from '../rendering/GeomBuilder';
|
|
11
11
|
import { MujocoModel } from '../types';
|
|
12
12
|
import { getName } from '../core/SceneLoader';
|
|
13
|
-
import {
|
|
13
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* SceneRenderer — creates and syncs MuJoCo body meshes every frame.
|
|
17
17
|
* Accepts standard R3F group props (position, rotation, scale, visible, etc.).
|
|
18
18
|
*/
|
|
19
19
|
export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
20
|
-
const { mjModelRef, mjDataRef, mujocoRef, onSelectionRef, status } =
|
|
20
|
+
const { mjModelRef, mjDataRef, mujocoRef, onSelectionRef, hiddenBodiesRef, status } = useMujocoContext();
|
|
21
21
|
const groupRef = useRef<THREE.Group>(null);
|
|
22
22
|
const bodyRefs = useRef<(THREE.Group | null)[]>([]);
|
|
23
23
|
const prevModelRef = useRef<MujocoModel | null>(null);
|
|
@@ -48,11 +48,14 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
48
48
|
for (let i = 0; i < model.nbody; i++) {
|
|
49
49
|
const bodyGroup = new THREE.Group();
|
|
50
50
|
bodyGroup.userData.bodyID = i;
|
|
51
|
+
const bodyName = getName(model, model.name_bodyadr[i]);
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if (!hiddenBodiesRef.current.has(bodyName)) {
|
|
54
|
+
for (let g = 0; g < model.ngeom; g++) {
|
|
55
|
+
if (model.geom_bodyid[g] === i) {
|
|
56
|
+
const mesh = geomBuilder.create(model, g);
|
|
57
|
+
if (mesh) bodyGroup.add(mesh);
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -15,7 +15,7 @@ import { useEffect, useRef } from 'react';
|
|
|
15
15
|
import { useFrame } from '@react-three/fiber';
|
|
16
16
|
import type { ThreeElements } from '@react-three/fiber';
|
|
17
17
|
import * as THREE from 'three';
|
|
18
|
-
import {
|
|
18
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
19
19
|
|
|
20
20
|
const DEFAULT_TENDON_COLOR = new THREE.Color(0.3, 0.3, 0.8);
|
|
21
21
|
const DEFAULT_TENDON_WIDTH = 0.002;
|
|
@@ -24,7 +24,7 @@ const DEFAULT_TENDON_WIDTH = 0.002;
|
|
|
24
24
|
const _tmpVec = new THREE.Vector3();
|
|
25
25
|
|
|
26
26
|
export function TendonRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
27
|
-
const { mjModelRef, mjDataRef, status } =
|
|
27
|
+
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
28
28
|
const groupRef = useRef<THREE.Group>(null);
|
|
29
29
|
const meshesRef = useRef<THREE.Mesh[]>([]);
|
|
30
30
|
const curvesRef = useRef<THREE.CatmullRomCurve3[]>([]);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
PhysicsStepCallback,
|
|
28
28
|
RayHit,
|
|
29
29
|
SceneConfig,
|
|
30
|
+
SceneObject,
|
|
30
31
|
SensorInfo,
|
|
31
32
|
SiteInfo,
|
|
32
33
|
StateSnapshot,
|
|
@@ -91,20 +92,68 @@ export interface MujocoSimContextValue {
|
|
|
91
92
|
beforeStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
92
93
|
afterStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
93
94
|
resetCallbacks: React.RefObject<Set<() => void>>;
|
|
95
|
+
errorRef: React.RefObject<string | null>;
|
|
96
|
+
bodyRegistryRef: React.RefObject<Map<string, { definition: SceneObject; hasCustomChildren: boolean }>>;
|
|
97
|
+
hiddenBodiesRef: React.RefObject<Set<string>>;
|
|
98
|
+
requestBodyReload: () => void;
|
|
94
99
|
status: 'loading' | 'ready' | 'error';
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
const MujocoSimContext = createContext<MujocoSimContextValue | null>(null);
|
|
98
103
|
|
|
99
|
-
export
|
|
104
|
+
export type UseMujocoResult =
|
|
105
|
+
| { status: 'loading'; isPending: true; isReady: false; isError: false; error: null; api: null; mjModelRef: null; mjDataRef: null }
|
|
106
|
+
| { status: 'error'; isPending: false; isReady: false; isError: true; error: string; api: null; mjModelRef: null; mjDataRef: null }
|
|
107
|
+
| { status: 'ready'; isPending: false; isReady: true; isError: false; error: null;
|
|
108
|
+
api: MujocoSimAPI; mjModelRef: React.RefObject<MujocoModel | null>; mjDataRef: React.RefObject<MujocoData | null> };
|
|
109
|
+
|
|
110
|
+
export function useMujocoContext(): MujocoSimContextValue {
|
|
100
111
|
const ctx = useContext(MujocoSimContext);
|
|
101
112
|
if (!ctx)
|
|
102
113
|
throw new Error('useMujoco must be used inside <MujocoSimProvider>');
|
|
103
114
|
return ctx;
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
export function useMujoco(): UseMujocoResult {
|
|
118
|
+
const ctx = useMujocoContext();
|
|
119
|
+
if (ctx.status === 'ready') {
|
|
120
|
+
return {
|
|
121
|
+
status: 'ready',
|
|
122
|
+
isPending: false,
|
|
123
|
+
isReady: true,
|
|
124
|
+
isError: false,
|
|
125
|
+
error: null,
|
|
126
|
+
api: ctx.api,
|
|
127
|
+
mjModelRef: ctx.mjModelRef,
|
|
128
|
+
mjDataRef: ctx.mjDataRef,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (ctx.status === 'error') {
|
|
132
|
+
return {
|
|
133
|
+
status: 'error',
|
|
134
|
+
isPending: false,
|
|
135
|
+
isReady: false,
|
|
136
|
+
isError: true,
|
|
137
|
+
error: ctx.errorRef.current ?? 'Unknown error',
|
|
138
|
+
api: null,
|
|
139
|
+
mjModelRef: null,
|
|
140
|
+
mjDataRef: null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
status: 'loading',
|
|
145
|
+
isPending: true,
|
|
146
|
+
isReady: false,
|
|
147
|
+
isError: false,
|
|
148
|
+
error: null,
|
|
149
|
+
api: null,
|
|
150
|
+
mjModelRef: null,
|
|
151
|
+
mjDataRef: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
export function useBeforePhysicsStep(callback: PhysicsStepCallback) {
|
|
107
|
-
const { beforeStepCallbacks } =
|
|
156
|
+
const { beforeStepCallbacks } = useMujocoContext();
|
|
108
157
|
const callbackRef = useRef(callback);
|
|
109
158
|
callbackRef.current = callback;
|
|
110
159
|
|
|
@@ -116,7 +165,7 @@ export function useBeforePhysicsStep(callback: PhysicsStepCallback) {
|
|
|
116
165
|
}
|
|
117
166
|
|
|
118
167
|
export function useAfterPhysicsStep(callback: PhysicsStepCallback) {
|
|
119
|
-
const { afterStepCallbacks } =
|
|
168
|
+
const { afterStepCallbacks } = useMujocoContext();
|
|
120
169
|
const callbackRef = useRef(callback);
|
|
121
170
|
callbackRef.current = callback;
|
|
122
171
|
|
|
@@ -181,6 +230,10 @@ export function MujocoSimProvider({
|
|
|
181
230
|
const beforeStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
182
231
|
const afterStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
183
232
|
const resetCallbacks = useRef(new Set<() => void>());
|
|
233
|
+
const errorRef = useRef<string | null>(null);
|
|
234
|
+
const bodyRegistryRef = useRef(new Map<string, { definition: SceneObject; hasCustomChildren: boolean }>());
|
|
235
|
+
const hiddenBodiesRef = useRef(new Set<string>());
|
|
236
|
+
const bodyReloadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
184
237
|
|
|
185
238
|
configRef.current = config;
|
|
186
239
|
|
|
@@ -207,13 +260,26 @@ export function MujocoSimProvider({
|
|
|
207
260
|
model.opt.timestep = timestep;
|
|
208
261
|
}, [timestep]);
|
|
209
262
|
|
|
263
|
+
// --- Build merged config (base + body registry) ---
|
|
264
|
+
function buildMergedConfig(baseConfig: SceneConfig): SceneConfig {
|
|
265
|
+
if (bodyRegistryRef.current.size === 0) return baseConfig;
|
|
266
|
+
const registeredNames = new Set(bodyRegistryRef.current.keys());
|
|
267
|
+
const baseObjects = (baseConfig.sceneObjects ?? []).filter(o => !registeredNames.has(o.name));
|
|
268
|
+
const registeredBodies = Array.from(bodyRegistryRef.current.values()).map(e => e.definition);
|
|
269
|
+
hiddenBodiesRef.current.clear();
|
|
270
|
+
for (const [name, entry] of bodyRegistryRef.current) {
|
|
271
|
+
if (entry.hasCustomChildren) hiddenBodiesRef.current.add(name);
|
|
272
|
+
}
|
|
273
|
+
return { ...baseConfig, sceneObjects: [...baseObjects, ...registeredBodies] };
|
|
274
|
+
}
|
|
275
|
+
|
|
210
276
|
// --- Load scene on mount ---
|
|
211
277
|
useEffect(() => {
|
|
212
278
|
let disposed = false;
|
|
213
279
|
|
|
214
280
|
(async () => {
|
|
215
281
|
try {
|
|
216
|
-
const result = await loadScene(mujoco, config);
|
|
282
|
+
const result = await loadScene(mujoco, buildMergedConfig(config));
|
|
217
283
|
if (disposed) {
|
|
218
284
|
result.mjModel.delete();
|
|
219
285
|
result.mjData.delete();
|
|
@@ -236,8 +302,10 @@ export function MujocoSimProvider({
|
|
|
236
302
|
setStatus('ready');
|
|
237
303
|
} catch (e: unknown) {
|
|
238
304
|
if (!disposed) {
|
|
305
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
306
|
+
errorRef.current = err.message;
|
|
239
307
|
setStatus('error');
|
|
240
|
-
onError?.(
|
|
308
|
+
onError?.(err);
|
|
241
309
|
}
|
|
242
310
|
}
|
|
243
311
|
})();
|
|
@@ -755,11 +823,20 @@ export function MujocoSimProvider({
|
|
|
755
823
|
setStatus('ready');
|
|
756
824
|
} catch (e) {
|
|
757
825
|
if (gen !== loadGenRef.current) return;
|
|
826
|
+
errorRef.current = e instanceof Error ? e.message : String(e);
|
|
758
827
|
setStatus('error');
|
|
759
828
|
throw e;
|
|
760
829
|
}
|
|
761
830
|
}, [mujoco]);
|
|
762
831
|
|
|
832
|
+
const requestBodyReload = useCallback(() => {
|
|
833
|
+
if (bodyReloadTimerRef.current) clearTimeout(bodyReloadTimerRef.current);
|
|
834
|
+
bodyReloadTimerRef.current = setTimeout(() => {
|
|
835
|
+
bodyReloadTimerRef.current = null;
|
|
836
|
+
loadSceneApi(buildMergedConfig(configRef.current));
|
|
837
|
+
}, 0);
|
|
838
|
+
}, [loadSceneApi]);
|
|
839
|
+
|
|
763
840
|
const getCanvasSnapshot = useCallback(
|
|
764
841
|
(width?: number, height?: number, mimeType = 'image/jpeg'): string => {
|
|
765
842
|
if (width && height) {
|
|
@@ -916,9 +993,13 @@ export function MujocoSimProvider({
|
|
|
916
993
|
beforeStepCallbacks,
|
|
917
994
|
afterStepCallbacks,
|
|
918
995
|
resetCallbacks,
|
|
996
|
+
errorRef,
|
|
997
|
+
bodyRegistryRef,
|
|
998
|
+
hiddenBodiesRef,
|
|
999
|
+
requestBodyReload,
|
|
919
1000
|
status,
|
|
920
1001
|
}),
|
|
921
|
-
[api, status]
|
|
1002
|
+
[api, status, requestBodyReload]
|
|
922
1003
|
);
|
|
923
1004
|
|
|
924
1005
|
return (
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*
|
|
5
|
-
* createController — typed factory for BYOC (Bring Your Own Controller) plugins.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import { useMemo, useRef } from 'react';
|
|
@@ -89,3 +87,57 @@ export function createController<TConfig>(
|
|
|
89
87
|
|
|
90
88
|
return Controller as ControllerComponent<TConfig>;
|
|
91
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Factory that produces a typed controller hook.
|
|
93
|
+
*
|
|
94
|
+
* Same config stabilization and default merging as `createController`,
|
|
95
|
+
* but returns a hook instead of a component. Pass `null` to disable.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* const useMyController = createControllerHook<MyConfig, MyValue>(
|
|
100
|
+
* { name: 'useMyController', defaultConfig: { gain: 1.0 } },
|
|
101
|
+
* function useMyControllerImpl(config) {
|
|
102
|
+
* // config is MyConfig | null — hooks must be called unconditionally
|
|
103
|
+
* useBeforePhysicsStep((_model, data) => {
|
|
104
|
+
* if (!config) return;
|
|
105
|
+
* data.ctrl[0] = config.gain * Math.sin(data.time);
|
|
106
|
+
* });
|
|
107
|
+
* if (!config) return null;
|
|
108
|
+
* return { /* value *\/ };
|
|
109
|
+
* },
|
|
110
|
+
* );
|
|
111
|
+
*
|
|
112
|
+
* // Usage:
|
|
113
|
+
* const value = useMyController({ gain: 2.0 });
|
|
114
|
+
* const disabled = useMyController(null); // returns null
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function createControllerHook<TConfig, TValue>(
|
|
118
|
+
options: ControllerOptions<TConfig>,
|
|
119
|
+
useImpl: (config: TConfig | null) => TValue | null,
|
|
120
|
+
): (config: TConfig | null) => TValue | null {
|
|
121
|
+
const useController = (config: TConfig | null): TValue | null => {
|
|
122
|
+
const configObj = config as Record<string, unknown> | null;
|
|
123
|
+
const stableRef = useRef(configObj);
|
|
124
|
+
if (configObj && stableRef.current) {
|
|
125
|
+
if (!shallowEqual(stableRef.current, configObj)) {
|
|
126
|
+
stableRef.current = configObj;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
stableRef.current = configObj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const mergedConfig = useMemo(
|
|
133
|
+
() => stableRef.current
|
|
134
|
+
? ({ ...options.defaultConfig, ...stableRef.current } as TConfig)
|
|
135
|
+
: null,
|
|
136
|
+
[stableRef.current],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return useImpl(mergedConfig);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return useController;
|
|
143
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useMemo } from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
8
8
|
import { getName } from '../core/SceneLoader';
|
|
9
9
|
import type { ActuatorInfo } from '../types';
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ import type { ActuatorInfo } from '../types';
|
|
|
13
13
|
* Computed once when the model loads. Consumer reads/writes data.ctrl[id] directly.
|
|
14
14
|
*/
|
|
15
15
|
export function useActuators(): ActuatorInfo[] {
|
|
16
|
-
const { mjModelRef, status } =
|
|
16
|
+
const { mjModelRef, status } = useMujocoContext();
|
|
17
17
|
|
|
18
18
|
return useMemo(() => {
|
|
19
19
|
if (status !== 'ready') return [];
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import * as THREE from 'three';
|
|
10
|
-
import {
|
|
10
|
+
import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import { findBodyByName } from '../core/SceneLoader';
|
|
12
12
|
import type { BodyStateResult } from '../types';
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ import type { BodyStateResult } from '../types';
|
|
|
16
16
|
* All values are ref-based — updated every physics frame without re-renders.
|
|
17
17
|
*/
|
|
18
18
|
export function useBodyState(name: string): BodyStateResult {
|
|
19
|
-
const { mjModelRef, status } =
|
|
19
|
+
const { mjModelRef, status } = useMujocoContext();
|
|
20
20
|
const bodyIdRef = useRef(-1);
|
|
21
21
|
const position = useRef(new THREE.Vector3());
|
|
22
22
|
const quaternion = useRef(new THREE.Quaternion());
|
package/src/hooks/useContacts.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import { useMujocoContext, useAfterPhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import { findBodyByName, getName } from '../core/SceneLoader';
|
|
12
12
|
import { getContact } from '../types';
|
|
13
13
|
import type { ContactInfo, MujocoModel } from '../types';
|
|
@@ -39,7 +39,7 @@ export function useContacts(
|
|
|
39
39
|
bodyName?: string,
|
|
40
40
|
callback?: (contacts: ContactInfo[]) => void,
|
|
41
41
|
): React.RefObject<ContactInfo[]> {
|
|
42
|
-
const { mjModelRef, status } =
|
|
42
|
+
const { mjModelRef, status } = useMujocoContext();
|
|
43
43
|
const contactsRef = useRef<ContactInfo[]>([]);
|
|
44
44
|
const bodyIdRef = useRef(-1);
|
|
45
45
|
const bodyResolvedRef = useRef(false);
|
package/src/hooks/useCtrl.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useCallback, useEffect, useRef } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
10
10
|
import { findActuatorByName } from '../core/SceneLoader';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -17,7 +17,7 @@ import { findActuatorByName } from '../core/SceneLoader';
|
|
|
17
17
|
* - `setValue` writes directly to `data.ctrl[actuatorId]`.
|
|
18
18
|
*/
|
|
19
19
|
export function useCtrl(name: string): [React.RefObject<number>, (value: number) => void] {
|
|
20
|
-
const { mjModelRef, mjDataRef, status } =
|
|
20
|
+
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
21
21
|
const actuatorIdRef = useRef(-1);
|
|
22
22
|
const valueRef = useRef(0);
|
|
23
23
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useRef } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
10
10
|
|
|
11
11
|
interface CtrlNoiseConfig {
|
|
12
12
|
/** Exponential filter rate (0-1). Higher = faster noise changes. Default: 0.01. */
|
|
@@ -25,7 +25,7 @@ interface CtrlNoiseConfig {
|
|
|
25
25
|
* data.ctrl[i] += noise[i]
|
|
26
26
|
*/
|
|
27
27
|
export function useCtrlNoise(config: CtrlNoiseConfig = {}) {
|
|
28
|
-
const { mjModelRef } =
|
|
28
|
+
const { mjModelRef } = useMujocoContext();
|
|
29
29
|
const configRef = useRef(config);
|
|
30
30
|
configRef.current = config;
|
|
31
31
|
const noiseRef = useRef<Float64Array | null>(null);
|
package/src/hooks/useGamepad.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
10
10
|
import { findActuatorByName } from '../core/SceneLoader';
|
|
11
11
|
|
|
12
12
|
interface GamepadConfig {
|
|
@@ -29,7 +29,7 @@ interface GamepadConfig {
|
|
|
29
29
|
* Buttons map their 0..1 pressed value to the actuator.
|
|
30
30
|
*/
|
|
31
31
|
export function useGamepad(config: GamepadConfig) {
|
|
32
|
-
const { mjModelRef, status } =
|
|
32
|
+
const { mjModelRef, status } = useMujocoContext();
|
|
33
33
|
const configRef = useRef(config);
|
|
34
34
|
configRef.current = config;
|
|
35
35
|
|