mujoco-react 7.0.1 → 8.1.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 +96 -60
- package/dist/index.d.ts +112 -33
- package/dist/index.js +160 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/TrajectoryPlayer.tsx +16 -2
- package/src/hooks/useBodyState.ts +2 -2
- package/src/hooks/useContacts.ts +3 -3
- package/src/hooks/useCtrl.ts +31 -18
- package/src/hooks/useJointState.ts +2 -2
- package/src/hooks/useSensor.ts +14 -5
- package/src/hooks/useSitePosition.ts +2 -2
- package/src/hooks/useTrajectoryPlayer.ts +152 -29
- package/src/index.ts +13 -0
- package/src/types.ts +71 -14
package/package.json
CHANGED
|
@@ -17,15 +17,30 @@ import type { TrajectoryPlayerProps } from '../types';
|
|
|
17
17
|
export function TrajectoryPlayer({
|
|
18
18
|
trajectory,
|
|
19
19
|
fps = 30,
|
|
20
|
+
speed = 1.0,
|
|
20
21
|
loop = false,
|
|
21
22
|
playing = false,
|
|
23
|
+
mode = 'kinematic',
|
|
22
24
|
onFrame,
|
|
25
|
+
onComplete,
|
|
26
|
+
onStateChange,
|
|
23
27
|
}: TrajectoryPlayerProps) {
|
|
24
|
-
const player = useTrajectoryPlayer(trajectory, {
|
|
28
|
+
const player = useTrajectoryPlayer(trajectory, {
|
|
29
|
+
fps,
|
|
30
|
+
speed,
|
|
31
|
+
loop,
|
|
32
|
+
mode,
|
|
33
|
+
onComplete,
|
|
34
|
+
onStateChange,
|
|
35
|
+
});
|
|
25
36
|
const onFrameRef = useRef(onFrame);
|
|
26
37
|
onFrameRef.current = onFrame;
|
|
27
38
|
const lastReportedFrameRef = useRef(-1);
|
|
28
39
|
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
player.setSpeed(speed);
|
|
42
|
+
}, [speed, player]);
|
|
43
|
+
|
|
29
44
|
useEffect(() => {
|
|
30
45
|
if (playing) {
|
|
31
46
|
player.play();
|
|
@@ -34,7 +49,6 @@ export function TrajectoryPlayer({
|
|
|
34
49
|
}
|
|
35
50
|
}, [playing, player]);
|
|
36
51
|
|
|
37
|
-
// Use useFrame instead of setInterval to sync with the render loop
|
|
38
52
|
useFrame(() => {
|
|
39
53
|
if (!onFrameRef.current) return;
|
|
40
54
|
const currentFrame = player.frame;
|
|
@@ -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:
|
|
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());
|
package/src/hooks/useContacts.ts
CHANGED
|
@@ -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?:
|
|
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:
|
|
111
|
+
bodyName: Bodies,
|
|
112
112
|
handlers: {
|
|
113
113
|
onEnter?: (info: ContactInfo) => void;
|
|
114
114
|
onExit?: (info: ContactInfo) => void;
|
package/src/hooks/useCtrl.ts
CHANGED
|
@@ -2,39 +2,52 @@
|
|
|
2
2
|
* @license
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*
|
|
5
|
-
* useCtrl —
|
|
5
|
+
* useCtrl — handle-based read/write access to a named actuator's ctrl value (spec 3.1)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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
|
|
16
|
-
*
|
|
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:
|
|
19
|
+
export function useCtrl(name: Actuators): CtrlHandle {
|
|
20
20
|
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
21
21
|
const actuatorIdRef = useRef(-1);
|
|
22
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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:
|
|
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);
|
package/src/hooks/useSensor.ts
CHANGED
|
@@ -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 {
|
|
11
|
+
import type { Sensors, SensorHandle, SensorInfo } from '../types';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Access a single MuJoCo sensor by name. Returns a
|
|
15
|
-
*
|
|
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:
|
|
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
|
|
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:
|
|
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());
|
|
@@ -7,65 +7,153 @@
|
|
|
7
7
|
|
|
8
8
|
import { useCallback, useRef } from 'react';
|
|
9
9
|
import { useFrame } from '@react-three/fiber';
|
|
10
|
-
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
10
|
+
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
11
|
+
import type { PlaybackState, TrajectoryFrame, TrajectoryInput } from '../types';
|
|
11
12
|
|
|
12
|
-
interface TrajectoryPlayerOptions {
|
|
13
|
+
export interface TrajectoryPlayerOptions {
|
|
13
14
|
fps?: number;
|
|
15
|
+
speed?: number;
|
|
14
16
|
loop?: boolean;
|
|
17
|
+
mode?: 'kinematic' | 'physics';
|
|
18
|
+
onComplete?: () => void;
|
|
19
|
+
onStateChange?: (state: PlaybackState) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Check if input is TrajectoryFrame[] (vs number[][]) */
|
|
23
|
+
function isTrajectoryFrames(input: TrajectoryInput): input is TrajectoryFrame[] {
|
|
24
|
+
return input.length > 0 && typeof (input[0] as TrajectoryFrame).time === 'number'
|
|
25
|
+
&& 'qpos' in (input[0] as TrajectoryFrame);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extract qpos as plain number array from a frame */
|
|
29
|
+
function getQpos(input: TrajectoryInput, idx: number): ArrayLike<number> | null {
|
|
30
|
+
const item = input[idx];
|
|
31
|
+
if (!item) return null;
|
|
32
|
+
if (Array.isArray(item)) return item;
|
|
33
|
+
return (item as TrajectoryFrame).qpos;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extract ctrl values from a TrajectoryFrame, if available */
|
|
37
|
+
function getCtrl(input: TrajectoryInput, idx: number): ArrayLike<number> | null {
|
|
38
|
+
const item = input[idx];
|
|
39
|
+
if (!item || Array.isArray(item)) return null;
|
|
40
|
+
return (item as TrajectoryFrame).ctrl ?? null;
|
|
15
41
|
}
|
|
16
42
|
|
|
17
43
|
/**
|
|
18
|
-
* Play back a
|
|
44
|
+
* Play back a trajectory, overriding simulation state.
|
|
19
45
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
46
|
+
* Accepts either `TrajectoryFrame[]` (from useTrajectoryRecorder) or
|
|
47
|
+
* `number[][]` (raw qpos arrays).
|
|
48
|
+
*
|
|
49
|
+
* In `kinematic` mode (default), the simulation is paused and qpos is
|
|
50
|
+
* set directly each frame with mj_forward for rendering.
|
|
51
|
+
*
|
|
52
|
+
* In `physics` mode, the simulation keeps running and ctrl values from
|
|
53
|
+
* the trajectory are applied each physics step via useBeforePhysicsStep.
|
|
22
54
|
*/
|
|
23
55
|
export function useTrajectoryPlayer(
|
|
24
|
-
trajectory:
|
|
56
|
+
trajectory: TrajectoryInput,
|
|
25
57
|
options: TrajectoryPlayerOptions = {},
|
|
26
58
|
) {
|
|
27
59
|
const { mjModelRef, mjDataRef, mujocoRef, pausedRef } = useMujocoContext();
|
|
28
|
-
const fps = options.fps ?? 30;
|
|
29
|
-
const loop = options.loop ?? false;
|
|
30
60
|
|
|
31
|
-
const
|
|
61
|
+
const optionsRef = useRef(options);
|
|
62
|
+
optionsRef.current = options;
|
|
63
|
+
|
|
64
|
+
const stateRef = useRef<PlaybackState>('idle');
|
|
32
65
|
const frameRef = useRef(0);
|
|
33
66
|
const lastFrameTimeRef = useRef(0);
|
|
67
|
+
const speedRef = useRef(options.speed ?? 1.0);
|
|
68
|
+
const wasPausedRef = useRef(false);
|
|
69
|
+
|
|
70
|
+
// Stable ref to trajectory to avoid stale closures in useBeforePhysicsStep
|
|
71
|
+
const trajectoryRef = useRef(trajectory);
|
|
72
|
+
trajectoryRef.current = trajectory;
|
|
73
|
+
|
|
74
|
+
const setState = useCallback((next: PlaybackState) => {
|
|
75
|
+
if (stateRef.current === next) return;
|
|
76
|
+
stateRef.current = next;
|
|
77
|
+
optionsRef.current.onStateChange?.(next);
|
|
78
|
+
}, []);
|
|
34
79
|
|
|
35
80
|
const play = useCallback(() => {
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
const traj = trajectoryRef.current;
|
|
82
|
+
if (traj.length === 0) return;
|
|
83
|
+
|
|
84
|
+
const mode = optionsRef.current.mode ?? 'kinematic';
|
|
85
|
+
|
|
86
|
+
if (stateRef.current === 'completed') {
|
|
87
|
+
frameRef.current = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (mode === 'kinematic') {
|
|
91
|
+
wasPausedRef.current = pausedRef.current;
|
|
92
|
+
pausedRef.current = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
lastFrameTimeRef.current = performance.now();
|
|
39
|
-
|
|
96
|
+
setState('playing');
|
|
97
|
+
}, [pausedRef, setState]);
|
|
40
98
|
|
|
41
99
|
const pause = useCallback(() => {
|
|
42
|
-
|
|
43
|
-
|
|
100
|
+
if (stateRef.current !== 'playing') return;
|
|
101
|
+
setState('paused');
|
|
102
|
+
}, [setState]);
|
|
44
103
|
|
|
45
104
|
const seek = useCallback((frameIdx: number) => {
|
|
46
|
-
|
|
105
|
+
const traj = trajectoryRef.current;
|
|
106
|
+
if (traj.length === 0) return;
|
|
107
|
+
|
|
108
|
+
frameRef.current = Math.max(0, Math.min(frameIdx, traj.length - 1));
|
|
109
|
+
|
|
47
110
|
const model = mjModelRef.current;
|
|
48
111
|
const data = mjDataRef.current;
|
|
49
|
-
if (!model || !data
|
|
50
|
-
|
|
112
|
+
if (!model || !data) return;
|
|
113
|
+
|
|
114
|
+
const qpos = getQpos(traj, frameRef.current);
|
|
115
|
+
if (!qpos) return;
|
|
116
|
+
|
|
51
117
|
for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
|
|
52
118
|
data.qpos[i] = qpos[i];
|
|
53
119
|
}
|
|
54
120
|
mujocoRef.current.mj_forward(model, data);
|
|
55
|
-
}, [
|
|
121
|
+
}, [mjModelRef, mjDataRef, mujocoRef]);
|
|
56
122
|
|
|
57
123
|
const reset = useCallback(() => {
|
|
124
|
+
const mode = optionsRef.current.mode ?? 'kinematic';
|
|
125
|
+
if (mode === 'kinematic' && stateRef.current !== 'idle') {
|
|
126
|
+
pausedRef.current = wasPausedRef.current;
|
|
127
|
+
}
|
|
58
128
|
frameRef.current = 0;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
129
|
+
setState('idle');
|
|
130
|
+
}, [pausedRef, setState]);
|
|
131
|
+
|
|
132
|
+
const setSpeed = useCallback((s: number) => {
|
|
133
|
+
speedRef.current = s;
|
|
134
|
+
}, []);
|
|
62
135
|
|
|
136
|
+
const complete = useCallback(() => {
|
|
137
|
+
const mode = optionsRef.current.mode ?? 'kinematic';
|
|
138
|
+
if (mode === 'kinematic') {
|
|
139
|
+
pausedRef.current = wasPausedRef.current;
|
|
140
|
+
}
|
|
141
|
+
setState('completed');
|
|
142
|
+
optionsRef.current.onComplete?.();
|
|
143
|
+
}, [pausedRef, setState]);
|
|
144
|
+
|
|
145
|
+
// --- Kinematic mode: drive qpos directly from useFrame ---
|
|
63
146
|
useFrame(() => {
|
|
64
|
-
if (
|
|
147
|
+
if (stateRef.current !== 'playing') return;
|
|
148
|
+
if ((optionsRef.current.mode ?? 'kinematic') !== 'kinematic') return;
|
|
149
|
+
|
|
150
|
+
const traj = trajectoryRef.current;
|
|
151
|
+
if (traj.length === 0) return;
|
|
65
152
|
|
|
66
153
|
const now = performance.now();
|
|
154
|
+
const fps = optionsRef.current.fps ?? 30;
|
|
155
|
+
const frameInterval = 1000 / (fps * speedRef.current);
|
|
67
156
|
const elapsed = now - lastFrameTimeRef.current;
|
|
68
|
-
const frameInterval = 1000 / fps;
|
|
69
157
|
|
|
70
158
|
if (elapsed < frameInterval) return;
|
|
71
159
|
lastFrameTimeRef.current = now;
|
|
@@ -74,7 +162,7 @@ export function useTrajectoryPlayer(
|
|
|
74
162
|
const data = mjDataRef.current;
|
|
75
163
|
if (!model || !data) return;
|
|
76
164
|
|
|
77
|
-
const qpos =
|
|
165
|
+
const qpos = getQpos(traj, frameRef.current);
|
|
78
166
|
if (!qpos) return;
|
|
79
167
|
|
|
80
168
|
for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
|
|
@@ -83,12 +171,44 @@ export function useTrajectoryPlayer(
|
|
|
83
171
|
mujocoRef.current.mj_forward(model, data);
|
|
84
172
|
|
|
85
173
|
frameRef.current++;
|
|
86
|
-
if (frameRef.current >=
|
|
87
|
-
if (loop) {
|
|
174
|
+
if (frameRef.current >= traj.length) {
|
|
175
|
+
if (optionsRef.current.loop) {
|
|
176
|
+
frameRef.current = 0;
|
|
177
|
+
} else {
|
|
178
|
+
complete();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// --- Physics mode: set ctrl values each physics step ---
|
|
184
|
+
useBeforePhysicsStep((model, data) => {
|
|
185
|
+
if (stateRef.current !== 'playing') return;
|
|
186
|
+
if ((optionsRef.current.mode ?? 'kinematic') !== 'physics') return;
|
|
187
|
+
|
|
188
|
+
const traj = trajectoryRef.current;
|
|
189
|
+
if (traj.length === 0) return;
|
|
190
|
+
|
|
191
|
+
// Advance frame based on sim time vs trajectory time
|
|
192
|
+
const fps = optionsRef.current.fps ?? 30;
|
|
193
|
+
const targetFrame = Math.floor(data.time * fps * speedRef.current);
|
|
194
|
+
frameRef.current = Math.min(targetFrame, traj.length - 1);
|
|
195
|
+
|
|
196
|
+
// Apply ctrl from trajectory
|
|
197
|
+
const ctrl = getCtrl(traj, frameRef.current);
|
|
198
|
+
if (ctrl) {
|
|
199
|
+
for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
|
|
200
|
+
data.ctrl[i] = ctrl[i];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check completion
|
|
205
|
+
if (frameRef.current >= traj.length - 1) {
|
|
206
|
+
if (optionsRef.current.loop) {
|
|
207
|
+
// Reset sim time to restart
|
|
208
|
+
data.time = 0;
|
|
88
209
|
frameRef.current = 0;
|
|
89
210
|
} else {
|
|
90
|
-
|
|
91
|
-
pausedRef.current = false;
|
|
211
|
+
complete();
|
|
92
212
|
}
|
|
93
213
|
}
|
|
94
214
|
});
|
|
@@ -98,8 +218,11 @@ export function useTrajectoryPlayer(
|
|
|
98
218
|
pause,
|
|
99
219
|
seek,
|
|
100
220
|
reset,
|
|
221
|
+
setSpeed,
|
|
222
|
+
get state() { return stateRef.current; },
|
|
101
223
|
get frame() { return frameRef.current; },
|
|
102
|
-
get playing() { return
|
|
224
|
+
get playing() { return stateRef.current === 'playing'; },
|
|
103
225
|
get totalFrames() { return trajectory.length; },
|
|
226
|
+
get progress() { return trajectory.length > 1 ? frameRef.current / (trajectory.length - 1) : 0; },
|
|
104
227
|
};
|
|
105
228
|
}
|
package/src/index.ts
CHANGED
|
@@ -95,6 +95,8 @@ export type {
|
|
|
95
95
|
// Trajectory
|
|
96
96
|
TrajectoryFrame,
|
|
97
97
|
TrajectoryData,
|
|
98
|
+
TrajectoryInput,
|
|
99
|
+
PlaybackState,
|
|
98
100
|
// Keyboard teleop
|
|
99
101
|
KeyBinding,
|
|
100
102
|
KeyboardTeleopConfig,
|
|
@@ -115,8 +117,19 @@ export type {
|
|
|
115
117
|
// Hook return types
|
|
116
118
|
SitePositionResult,
|
|
117
119
|
SensorResult,
|
|
120
|
+
CtrlHandle,
|
|
121
|
+
SensorHandle,
|
|
118
122
|
BodyStateResult,
|
|
119
123
|
JointStateResult,
|
|
124
|
+
// Register (type-safe named resources)
|
|
125
|
+
Register,
|
|
126
|
+
Actuators,
|
|
127
|
+
Sensors,
|
|
128
|
+
Bodies,
|
|
129
|
+
Joints,
|
|
130
|
+
Sites,
|
|
131
|
+
Geoms,
|
|
132
|
+
Keyframes,
|
|
120
133
|
} from './types';
|
|
121
134
|
|
|
122
135
|
// Re-export MuJoCo types for convenience
|