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/dist/index.js CHANGED
@@ -956,7 +956,7 @@ function MujocoSimProvider({
956
956
  const arr = values instanceof Float64Array ? values : new Float64Array(values);
957
957
  data.qvel.set(arr.subarray(0, Math.min(arr.length, mjModelRef.current?.nv ?? 0)));
958
958
  }, []);
959
- const getQpos = useCallback(() => {
959
+ const getQpos2 = useCallback(() => {
960
960
  return mjDataRef.current ? new Float64Array(mjDataRef.current.qpos) : new Float64Array(0);
961
961
  }, []);
962
962
  const getQvel = useCallback(() => {
@@ -976,7 +976,7 @@ function MujocoSimProvider({
976
976
  }
977
977
  }
978
978
  }, []);
979
- const getCtrl = useCallback(() => {
979
+ const getCtrl2 = useCallback(() => {
980
980
  return mjDataRef.current ? new Float64Array(mjDataRef.current.ctrl) : new Float64Array(0);
981
981
  }, []);
982
982
  const applyForce = useCallback((bodyName, force, point) => {
@@ -1384,10 +1384,10 @@ function MujocoSimProvider({
1384
1384
  restoreState,
1385
1385
  setQpos,
1386
1386
  setQvel,
1387
- getQpos,
1387
+ getQpos: getQpos2,
1388
1388
  getQvel,
1389
1389
  setCtrl,
1390
- getCtrl,
1390
+ getCtrl: getCtrl2,
1391
1391
  applyForce,
1392
1392
  applyTorque: applyTorqueApi,
1393
1393
  setExternalForce,
@@ -1430,10 +1430,10 @@ function MujocoSimProvider({
1430
1430
  restoreState,
1431
1431
  setQpos,
1432
1432
  setQvel,
1433
- getQpos,
1433
+ getQpos2,
1434
1434
  getQvel,
1435
1435
  setCtrl,
1436
- getCtrl,
1436
+ getCtrl2,
1437
1437
  applyForce,
1438
1438
  applyTorqueApi,
1439
1439
  setExternalForce,
@@ -3098,60 +3098,133 @@ function ContactListener({
3098
3098
  });
3099
3099
  return null;
3100
3100
  }
3101
+ function getQpos(input, idx) {
3102
+ const item = input[idx];
3103
+ if (!item) return null;
3104
+ if (Array.isArray(item)) return item;
3105
+ return item.qpos;
3106
+ }
3107
+ function getCtrl(input, idx) {
3108
+ const item = input[idx];
3109
+ if (!item || Array.isArray(item)) return null;
3110
+ return item.ctrl ?? null;
3111
+ }
3101
3112
  function useTrajectoryPlayer(trajectory, options = {}) {
3102
3113
  const { mjModelRef, mjDataRef, mujocoRef, pausedRef } = useMujocoContext();
3103
- const fps = options.fps ?? 30;
3104
- const loop = options.loop ?? false;
3105
- const playingRef = useRef(false);
3114
+ const optionsRef = useRef(options);
3115
+ optionsRef.current = options;
3116
+ const stateRef = useRef("idle");
3106
3117
  const frameRef = useRef(0);
3107
3118
  const lastFrameTimeRef = useRef(0);
3119
+ const speedRef = useRef(options.speed ?? 1);
3120
+ const wasPausedRef = useRef(false);
3121
+ const trajectoryRef = useRef(trajectory);
3122
+ trajectoryRef.current = trajectory;
3123
+ const setState = useCallback((next) => {
3124
+ if (stateRef.current === next) return;
3125
+ stateRef.current = next;
3126
+ optionsRef.current.onStateChange?.(next);
3127
+ }, []);
3108
3128
  const play = useCallback(() => {
3109
- playingRef.current = true;
3110
- pausedRef.current = true;
3129
+ const traj = trajectoryRef.current;
3130
+ if (traj.length === 0) return;
3131
+ const mode = optionsRef.current.mode ?? "kinematic";
3132
+ if (stateRef.current === "completed") {
3133
+ frameRef.current = 0;
3134
+ }
3135
+ if (mode === "kinematic") {
3136
+ wasPausedRef.current = pausedRef.current;
3137
+ pausedRef.current = true;
3138
+ }
3111
3139
  lastFrameTimeRef.current = performance.now();
3112
- }, [pausedRef]);
3140
+ setState("playing");
3141
+ }, [pausedRef, setState]);
3113
3142
  const pause = useCallback(() => {
3114
- playingRef.current = false;
3115
- }, []);
3143
+ if (stateRef.current !== "playing") return;
3144
+ setState("paused");
3145
+ }, [setState]);
3116
3146
  const seek = useCallback((frameIdx) => {
3117
- frameRef.current = Math.max(0, Math.min(frameIdx, trajectory.length - 1));
3147
+ const traj = trajectoryRef.current;
3148
+ if (traj.length === 0) return;
3149
+ frameRef.current = Math.max(0, Math.min(frameIdx, traj.length - 1));
3118
3150
  const model = mjModelRef.current;
3119
3151
  const data = mjDataRef.current;
3120
- if (!model || !data || !trajectory[frameRef.current]) return;
3121
- const qpos = trajectory[frameRef.current];
3152
+ if (!model || !data) return;
3153
+ const qpos = getQpos(traj, frameRef.current);
3154
+ if (!qpos) return;
3122
3155
  for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
3123
3156
  data.qpos[i] = qpos[i];
3124
3157
  }
3125
3158
  mujocoRef.current.mj_forward(model, data);
3126
- }, [trajectory, mjModelRef, mjDataRef, mujocoRef]);
3159
+ }, [mjModelRef, mjDataRef, mujocoRef]);
3127
3160
  const reset = useCallback(() => {
3161
+ const mode = optionsRef.current.mode ?? "kinematic";
3162
+ if (mode === "kinematic" && stateRef.current !== "idle") {
3163
+ pausedRef.current = wasPausedRef.current;
3164
+ }
3128
3165
  frameRef.current = 0;
3129
- playingRef.current = false;
3130
- pausedRef.current = false;
3131
- }, [pausedRef]);
3166
+ setState("idle");
3167
+ }, [pausedRef, setState]);
3168
+ const setSpeed = useCallback((s) => {
3169
+ speedRef.current = s;
3170
+ }, []);
3171
+ const complete = useCallback(() => {
3172
+ const mode = optionsRef.current.mode ?? "kinematic";
3173
+ if (mode === "kinematic") {
3174
+ pausedRef.current = wasPausedRef.current;
3175
+ }
3176
+ setState("completed");
3177
+ optionsRef.current.onComplete?.();
3178
+ }, [pausedRef, setState]);
3132
3179
  useFrame(() => {
3133
- if (!playingRef.current || trajectory.length === 0) return;
3180
+ if (stateRef.current !== "playing") return;
3181
+ if ((optionsRef.current.mode ?? "kinematic") !== "kinematic") return;
3182
+ const traj = trajectoryRef.current;
3183
+ if (traj.length === 0) return;
3134
3184
  const now = performance.now();
3185
+ const fps = optionsRef.current.fps ?? 30;
3186
+ const frameInterval = 1e3 / (fps * speedRef.current);
3135
3187
  const elapsed = now - lastFrameTimeRef.current;
3136
- const frameInterval = 1e3 / fps;
3137
3188
  if (elapsed < frameInterval) return;
3138
3189
  lastFrameTimeRef.current = now;
3139
3190
  const model = mjModelRef.current;
3140
3191
  const data = mjDataRef.current;
3141
3192
  if (!model || !data) return;
3142
- const qpos = trajectory[frameRef.current];
3193
+ const qpos = getQpos(traj, frameRef.current);
3143
3194
  if (!qpos) return;
3144
3195
  for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
3145
3196
  data.qpos[i] = qpos[i];
3146
3197
  }
3147
3198
  mujocoRef.current.mj_forward(model, data);
3148
3199
  frameRef.current++;
3149
- if (frameRef.current >= trajectory.length) {
3150
- if (loop) {
3200
+ if (frameRef.current >= traj.length) {
3201
+ if (optionsRef.current.loop) {
3202
+ frameRef.current = 0;
3203
+ } else {
3204
+ complete();
3205
+ }
3206
+ }
3207
+ });
3208
+ useBeforePhysicsStep((model, data) => {
3209
+ if (stateRef.current !== "playing") return;
3210
+ if ((optionsRef.current.mode ?? "kinematic") !== "physics") return;
3211
+ const traj = trajectoryRef.current;
3212
+ if (traj.length === 0) return;
3213
+ const fps = optionsRef.current.fps ?? 30;
3214
+ const targetFrame = Math.floor(data.time * fps * speedRef.current);
3215
+ frameRef.current = Math.min(targetFrame, traj.length - 1);
3216
+ const ctrl = getCtrl(traj, frameRef.current);
3217
+ if (ctrl) {
3218
+ for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
3219
+ data.ctrl[i] = ctrl[i];
3220
+ }
3221
+ }
3222
+ if (frameRef.current >= traj.length - 1) {
3223
+ if (optionsRef.current.loop) {
3224
+ data.time = 0;
3151
3225
  frameRef.current = 0;
3152
3226
  } else {
3153
- playingRef.current = false;
3154
- pausedRef.current = false;
3227
+ complete();
3155
3228
  }
3156
3229
  }
3157
3230
  });
@@ -3160,14 +3233,21 @@ function useTrajectoryPlayer(trajectory, options = {}) {
3160
3233
  pause,
3161
3234
  seek,
3162
3235
  reset,
3236
+ setSpeed,
3237
+ get state() {
3238
+ return stateRef.current;
3239
+ },
3163
3240
  get frame() {
3164
3241
  return frameRef.current;
3165
3242
  },
3166
3243
  get playing() {
3167
- return playingRef.current;
3244
+ return stateRef.current === "playing";
3168
3245
  },
3169
3246
  get totalFrames() {
3170
3247
  return trajectory.length;
3248
+ },
3249
+ get progress() {
3250
+ return trajectory.length > 1 ? frameRef.current / (trajectory.length - 1) : 0;
3171
3251
  }
3172
3252
  };
3173
3253
  }
@@ -3176,14 +3256,28 @@ function useTrajectoryPlayer(trajectory, options = {}) {
3176
3256
  function TrajectoryPlayer({
3177
3257
  trajectory,
3178
3258
  fps = 30,
3259
+ speed = 1,
3179
3260
  loop = false,
3180
3261
  playing = false,
3181
- onFrame
3262
+ mode = "kinematic",
3263
+ onFrame,
3264
+ onComplete,
3265
+ onStateChange
3182
3266
  }) {
3183
- const player = useTrajectoryPlayer(trajectory, { fps, loop });
3267
+ const player = useTrajectoryPlayer(trajectory, {
3268
+ fps,
3269
+ speed,
3270
+ loop,
3271
+ mode,
3272
+ onComplete,
3273
+ onStateChange
3274
+ });
3184
3275
  const onFrameRef = useRef(onFrame);
3185
3276
  onFrameRef.current = onFrame;
3186
3277
  const lastReportedFrameRef = useRef(-1);
3278
+ useEffect(() => {
3279
+ player.setSpeed(speed);
3280
+ }, [speed, player]);
3187
3281
  useEffect(() => {
3188
3282
  if (playing) {
3189
3283
  player.play();
@@ -3305,7 +3399,15 @@ function useSensor(name) {
3305
3399
  valueRef.current[i] = data.sensordata[adr + i];
3306
3400
  }
3307
3401
  });
3308
- return { value: valueRef, size: sensorDimRef.current };
3402
+ return useMemo(() => ({
3403
+ read() {
3404
+ return valueRef.current;
3405
+ },
3406
+ get dim() {
3407
+ return sensorDimRef.current;
3408
+ },
3409
+ name
3410
+ }), [name]);
3309
3411
  }
3310
3412
  function useSensors() {
3311
3413
  const { mjModelRef, status } = useMujocoContext();
@@ -3439,19 +3541,35 @@ function useBodyState(name) {
3439
3541
  function useCtrl(name) {
3440
3542
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
3441
3543
  const actuatorIdRef = useRef(-1);
3442
- const valueRef = useRef(0);
3544
+ const rangeRef = useRef([0, 0]);
3443
3545
  useEffect(() => {
3444
3546
  const model = mjModelRef.current;
3445
3547
  if (!model || status !== "ready") return;
3446
- actuatorIdRef.current = findActuatorByName(model, name);
3548
+ const id = findActuatorByName(model, name);
3549
+ actuatorIdRef.current = id;
3550
+ if (id >= 0) {
3551
+ rangeRef.current = [
3552
+ model.actuator_ctrlrange[id * 2],
3553
+ model.actuator_ctrlrange[id * 2 + 1]
3554
+ ];
3555
+ }
3447
3556
  }, [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];
3557
+ return useMemo(() => ({
3558
+ read() {
3559
+ const data = mjDataRef.current;
3560
+ if (!data || actuatorIdRef.current < 0) return 0;
3561
+ return data.ctrl[actuatorIdRef.current];
3562
+ },
3563
+ write(value) {
3564
+ const data = mjDataRef.current;
3565
+ if (!data || actuatorIdRef.current < 0) return;
3566
+ data.ctrl[actuatorIdRef.current] = value;
3567
+ },
3568
+ name,
3569
+ get range() {
3570
+ return rangeRef.current;
3571
+ }
3572
+ }), [name, mjDataRef]);
3455
3573
  }
3456
3574
  function useKeyboardTeleop(config) {
3457
3575
  const { mjModelRef, mjDataRef, status } = useMujocoContext();
@@ -3975,7 +4093,7 @@ function useCameraAnimation() {
3975
4093
  * @license
3976
4094
  * SPDX-License-Identifier: Apache-2.0
3977
4095
  *
3978
- * useCtrl — clean read/write access to a named actuator's ctrl value (spec 3.1)
4096
+ * useCtrl — handle-based read/write access to a named actuator's ctrl value (spec 3.1)
3979
4097
  */
3980
4098
  /**
3981
4099
  * @license