mujoco-react 8.0.0 → 8.1.1

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
@@ -391,6 +391,18 @@ function sceneObjectToXml(obj) {
391
391
  const condim = obj.condim ? ` condim="${obj.condim}"` : "";
392
392
  return `<body name="${obj.name}" pos="${pos}">${joint}<geom type="${obj.type}" size="${size}" rgba="${rgba}" contype="1" conaffinity="1"${mass}${friction}${solref}${solimp}${condim}/></body>`;
393
393
  }
394
+ function ensureDir(mujoco, fname) {
395
+ const dirParts = fname.split("/");
396
+ dirParts.pop();
397
+ let currentPath = "/working";
398
+ for (const part of dirParts) {
399
+ currentPath += "/" + part;
400
+ try {
401
+ mujoco.FS.mkdir(currentPath);
402
+ } catch {
403
+ }
404
+ }
405
+ }
394
406
  async function loadScene(mujoco, config, onProgress) {
395
407
  try {
396
408
  mujoco.FS.unmount("/working");
@@ -402,66 +414,75 @@ async function loadScene(mujoco, config, onProgress) {
402
414
  }
403
415
  const baseUrl = config.src.endsWith("/") ? config.src : config.src + "/";
404
416
  const downloaded = /* @__PURE__ */ new Set();
405
- const queue = [config.sceneFile];
417
+ const xmlQueue = [config.sceneFile];
418
+ const assetFiles = [];
406
419
  const parser = new DOMParser();
407
- while (queue.length > 0) {
408
- const fname = queue.shift();
420
+ while (xmlQueue.length > 0) {
421
+ const fname = xmlQueue.shift();
409
422
  if (downloaded.has(fname)) continue;
410
423
  downloaded.add(fname);
424
+ if (!fname.endsWith(".xml")) {
425
+ assetFiles.push(fname);
426
+ continue;
427
+ }
411
428
  onProgress?.(`Downloading ${fname}...`);
412
429
  const res = await fetch(baseUrl + fname);
413
430
  if (!res.ok) {
414
431
  console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
415
432
  continue;
416
433
  }
417
- const dirParts = fname.split("/");
418
- dirParts.pop();
419
- let currentPath = "/working";
420
- for (const part of dirParts) {
421
- currentPath += "/" + part;
422
- try {
423
- mujoco.FS.mkdir(currentPath);
424
- } catch {
425
- }
426
- }
427
- if (fname.endsWith(".xml")) {
428
- let text = await res.text();
429
- for (const patch of config.xmlPatches ?? []) {
430
- if (fname.endsWith(patch.target) || fname === patch.target) {
431
- if (patch.replace) {
432
- const [from, to] = patch.replace;
433
- if (text.includes(from)) {
434
- text = text.replace(from, to);
435
- } else {
436
- const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
437
- console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
438
- }
434
+ let text = await res.text();
435
+ for (const patch of config.xmlPatches ?? []) {
436
+ if (fname.endsWith(patch.target) || fname === patch.target) {
437
+ if (patch.replace) {
438
+ const [from, to] = patch.replace;
439
+ if (text.includes(from)) {
440
+ text = text.replace(from, to);
441
+ } else {
442
+ const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
443
+ console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
439
444
  }
440
- if (patch.inject && patch.injectAfter) {
441
- const idx = text.indexOf(patch.injectAfter);
442
- if (idx !== -1) {
443
- const tagEnd = text.indexOf(">", idx + patch.injectAfter.length);
444
- if (tagEnd !== -1) {
445
- text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
446
- } else {
447
- console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
448
- }
445
+ }
446
+ if (patch.inject && patch.injectAfter) {
447
+ const idx = text.indexOf(patch.injectAfter);
448
+ if (idx !== -1) {
449
+ const tagEnd = text.indexOf(">", idx + patch.injectAfter.length);
450
+ if (tagEnd !== -1) {
451
+ text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
449
452
  } else {
450
- const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
451
- console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
453
+ console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
452
454
  }
455
+ } else {
456
+ const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
457
+ console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
453
458
  }
454
459
  }
455
460
  }
456
- if (fname === config.sceneFile && config.sceneObjects?.length) {
457
- const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
458
- text = text.replace("</worldbody>", xml + "</worldbody>");
459
- }
460
- mujoco.FS.writeFile(`/working/${fname}`, text);
461
- scanDependencies(text, fname, parser, downloaded, queue);
462
- } else {
463
- const buffer = new Uint8Array(await res.arrayBuffer());
464
- mujoco.FS.writeFile(`/working/${fname}`, buffer);
461
+ }
462
+ if (fname === config.sceneFile && config.sceneObjects?.length) {
463
+ const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
464
+ text = text.replace("</worldbody>", xml + "</worldbody>");
465
+ }
466
+ ensureDir(mujoco, fname);
467
+ mujoco.FS.writeFile(`/working/${fname}`, text);
468
+ scanDependencies(text, fname, parser, downloaded, xmlQueue);
469
+ }
470
+ if (assetFiles.length > 0) {
471
+ onProgress?.(`Downloading ${assetFiles.length} assets...`);
472
+ const results = await Promise.all(
473
+ assetFiles.map(async (fname) => {
474
+ const res = await fetch(baseUrl + fname);
475
+ if (!res.ok) {
476
+ console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
477
+ return null;
478
+ }
479
+ return { fname, buffer: new Uint8Array(await res.arrayBuffer()) };
480
+ })
481
+ );
482
+ for (const result of results) {
483
+ if (!result) continue;
484
+ ensureDir(mujoco, result.fname);
485
+ mujoco.FS.writeFile(`/working/${result.fname}`, result.buffer);
465
486
  }
466
487
  }
467
488
  onProgress?.("Loading model...");
@@ -956,7 +977,7 @@ function MujocoSimProvider({
956
977
  const arr = values instanceof Float64Array ? values : new Float64Array(values);
957
978
  data.qvel.set(arr.subarray(0, Math.min(arr.length, mjModelRef.current?.nv ?? 0)));
958
979
  }, []);
959
- const getQpos = useCallback(() => {
980
+ const getQpos2 = useCallback(() => {
960
981
  return mjDataRef.current ? new Float64Array(mjDataRef.current.qpos) : new Float64Array(0);
961
982
  }, []);
962
983
  const getQvel = useCallback(() => {
@@ -976,7 +997,7 @@ function MujocoSimProvider({
976
997
  }
977
998
  }
978
999
  }, []);
979
- const getCtrl = useCallback(() => {
1000
+ const getCtrl2 = useCallback(() => {
980
1001
  return mjDataRef.current ? new Float64Array(mjDataRef.current.ctrl) : new Float64Array(0);
981
1002
  }, []);
982
1003
  const applyForce = useCallback((bodyName, force, point) => {
@@ -1384,10 +1405,10 @@ function MujocoSimProvider({
1384
1405
  restoreState,
1385
1406
  setQpos,
1386
1407
  setQvel,
1387
- getQpos,
1408
+ getQpos: getQpos2,
1388
1409
  getQvel,
1389
1410
  setCtrl,
1390
- getCtrl,
1411
+ getCtrl: getCtrl2,
1391
1412
  applyForce,
1392
1413
  applyTorque: applyTorqueApi,
1393
1414
  setExternalForce,
@@ -1430,10 +1451,10 @@ function MujocoSimProvider({
1430
1451
  restoreState,
1431
1452
  setQpos,
1432
1453
  setQvel,
1433
- getQpos,
1454
+ getQpos2,
1434
1455
  getQvel,
1435
1456
  setCtrl,
1436
- getCtrl,
1457
+ getCtrl2,
1437
1458
  applyForce,
1438
1459
  applyTorqueApi,
1439
1460
  setExternalForce,
@@ -3098,60 +3119,133 @@ function ContactListener({
3098
3119
  });
3099
3120
  return null;
3100
3121
  }
3122
+ function getQpos(input, idx) {
3123
+ const item = input[idx];
3124
+ if (!item) return null;
3125
+ if (Array.isArray(item)) return item;
3126
+ return item.qpos;
3127
+ }
3128
+ function getCtrl(input, idx) {
3129
+ const item = input[idx];
3130
+ if (!item || Array.isArray(item)) return null;
3131
+ return item.ctrl ?? null;
3132
+ }
3101
3133
  function useTrajectoryPlayer(trajectory, options = {}) {
3102
3134
  const { mjModelRef, mjDataRef, mujocoRef, pausedRef } = useMujocoContext();
3103
- const fps = options.fps ?? 30;
3104
- const loop = options.loop ?? false;
3105
- const playingRef = useRef(false);
3135
+ const optionsRef = useRef(options);
3136
+ optionsRef.current = options;
3137
+ const stateRef = useRef("idle");
3106
3138
  const frameRef = useRef(0);
3107
3139
  const lastFrameTimeRef = useRef(0);
3140
+ const speedRef = useRef(options.speed ?? 1);
3141
+ const wasPausedRef = useRef(false);
3142
+ const trajectoryRef = useRef(trajectory);
3143
+ trajectoryRef.current = trajectory;
3144
+ const setState = useCallback((next) => {
3145
+ if (stateRef.current === next) return;
3146
+ stateRef.current = next;
3147
+ optionsRef.current.onStateChange?.(next);
3148
+ }, []);
3108
3149
  const play = useCallback(() => {
3109
- playingRef.current = true;
3110
- pausedRef.current = true;
3150
+ const traj = trajectoryRef.current;
3151
+ if (traj.length === 0) return;
3152
+ const mode = optionsRef.current.mode ?? "kinematic";
3153
+ if (stateRef.current === "completed") {
3154
+ frameRef.current = 0;
3155
+ }
3156
+ if (mode === "kinematic") {
3157
+ wasPausedRef.current = pausedRef.current;
3158
+ pausedRef.current = true;
3159
+ }
3111
3160
  lastFrameTimeRef.current = performance.now();
3112
- }, [pausedRef]);
3161
+ setState("playing");
3162
+ }, [pausedRef, setState]);
3113
3163
  const pause = useCallback(() => {
3114
- playingRef.current = false;
3115
- }, []);
3164
+ if (stateRef.current !== "playing") return;
3165
+ setState("paused");
3166
+ }, [setState]);
3116
3167
  const seek = useCallback((frameIdx) => {
3117
- frameRef.current = Math.max(0, Math.min(frameIdx, trajectory.length - 1));
3168
+ const traj = trajectoryRef.current;
3169
+ if (traj.length === 0) return;
3170
+ frameRef.current = Math.max(0, Math.min(frameIdx, traj.length - 1));
3118
3171
  const model = mjModelRef.current;
3119
3172
  const data = mjDataRef.current;
3120
- if (!model || !data || !trajectory[frameRef.current]) return;
3121
- const qpos = trajectory[frameRef.current];
3173
+ if (!model || !data) return;
3174
+ const qpos = getQpos(traj, frameRef.current);
3175
+ if (!qpos) return;
3122
3176
  for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
3123
3177
  data.qpos[i] = qpos[i];
3124
3178
  }
3125
3179
  mujocoRef.current.mj_forward(model, data);
3126
- }, [trajectory, mjModelRef, mjDataRef, mujocoRef]);
3180
+ }, [mjModelRef, mjDataRef, mujocoRef]);
3127
3181
  const reset = useCallback(() => {
3182
+ const mode = optionsRef.current.mode ?? "kinematic";
3183
+ if (mode === "kinematic" && stateRef.current !== "idle") {
3184
+ pausedRef.current = wasPausedRef.current;
3185
+ }
3128
3186
  frameRef.current = 0;
3129
- playingRef.current = false;
3130
- pausedRef.current = false;
3131
- }, [pausedRef]);
3187
+ setState("idle");
3188
+ }, [pausedRef, setState]);
3189
+ const setSpeed = useCallback((s) => {
3190
+ speedRef.current = s;
3191
+ }, []);
3192
+ const complete = useCallback(() => {
3193
+ const mode = optionsRef.current.mode ?? "kinematic";
3194
+ if (mode === "kinematic") {
3195
+ pausedRef.current = wasPausedRef.current;
3196
+ }
3197
+ setState("completed");
3198
+ optionsRef.current.onComplete?.();
3199
+ }, [pausedRef, setState]);
3132
3200
  useFrame(() => {
3133
- if (!playingRef.current || trajectory.length === 0) return;
3201
+ if (stateRef.current !== "playing") return;
3202
+ if ((optionsRef.current.mode ?? "kinematic") !== "kinematic") return;
3203
+ const traj = trajectoryRef.current;
3204
+ if (traj.length === 0) return;
3134
3205
  const now = performance.now();
3206
+ const fps = optionsRef.current.fps ?? 30;
3207
+ const frameInterval = 1e3 / (fps * speedRef.current);
3135
3208
  const elapsed = now - lastFrameTimeRef.current;
3136
- const frameInterval = 1e3 / fps;
3137
3209
  if (elapsed < frameInterval) return;
3138
3210
  lastFrameTimeRef.current = now;
3139
3211
  const model = mjModelRef.current;
3140
3212
  const data = mjDataRef.current;
3141
3213
  if (!model || !data) return;
3142
- const qpos = trajectory[frameRef.current];
3214
+ const qpos = getQpos(traj, frameRef.current);
3143
3215
  if (!qpos) return;
3144
3216
  for (let i = 0; i < Math.min(qpos.length, model.nq); i++) {
3145
3217
  data.qpos[i] = qpos[i];
3146
3218
  }
3147
3219
  mujocoRef.current.mj_forward(model, data);
3148
3220
  frameRef.current++;
3149
- if (frameRef.current >= trajectory.length) {
3150
- if (loop) {
3221
+ if (frameRef.current >= traj.length) {
3222
+ if (optionsRef.current.loop) {
3223
+ frameRef.current = 0;
3224
+ } else {
3225
+ complete();
3226
+ }
3227
+ }
3228
+ });
3229
+ useBeforePhysicsStep((model, data) => {
3230
+ if (stateRef.current !== "playing") return;
3231
+ if ((optionsRef.current.mode ?? "kinematic") !== "physics") return;
3232
+ const traj = trajectoryRef.current;
3233
+ if (traj.length === 0) return;
3234
+ const fps = optionsRef.current.fps ?? 30;
3235
+ const targetFrame = Math.floor(data.time * fps * speedRef.current);
3236
+ frameRef.current = Math.min(targetFrame, traj.length - 1);
3237
+ const ctrl = getCtrl(traj, frameRef.current);
3238
+ if (ctrl) {
3239
+ for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
3240
+ data.ctrl[i] = ctrl[i];
3241
+ }
3242
+ }
3243
+ if (frameRef.current >= traj.length - 1) {
3244
+ if (optionsRef.current.loop) {
3245
+ data.time = 0;
3151
3246
  frameRef.current = 0;
3152
3247
  } else {
3153
- playingRef.current = false;
3154
- pausedRef.current = false;
3248
+ complete();
3155
3249
  }
3156
3250
  }
3157
3251
  });
@@ -3160,14 +3254,21 @@ function useTrajectoryPlayer(trajectory, options = {}) {
3160
3254
  pause,
3161
3255
  seek,
3162
3256
  reset,
3257
+ setSpeed,
3258
+ get state() {
3259
+ return stateRef.current;
3260
+ },
3163
3261
  get frame() {
3164
3262
  return frameRef.current;
3165
3263
  },
3166
3264
  get playing() {
3167
- return playingRef.current;
3265
+ return stateRef.current === "playing";
3168
3266
  },
3169
3267
  get totalFrames() {
3170
3268
  return trajectory.length;
3269
+ },
3270
+ get progress() {
3271
+ return trajectory.length > 1 ? frameRef.current / (trajectory.length - 1) : 0;
3171
3272
  }
3172
3273
  };
3173
3274
  }
@@ -3176,14 +3277,28 @@ function useTrajectoryPlayer(trajectory, options = {}) {
3176
3277
  function TrajectoryPlayer({
3177
3278
  trajectory,
3178
3279
  fps = 30,
3280
+ speed = 1,
3179
3281
  loop = false,
3180
3282
  playing = false,
3181
- onFrame
3283
+ mode = "kinematic",
3284
+ onFrame,
3285
+ onComplete,
3286
+ onStateChange
3182
3287
  }) {
3183
- const player = useTrajectoryPlayer(trajectory, { fps, loop });
3288
+ const player = useTrajectoryPlayer(trajectory, {
3289
+ fps,
3290
+ speed,
3291
+ loop,
3292
+ mode,
3293
+ onComplete,
3294
+ onStateChange
3295
+ });
3184
3296
  const onFrameRef = useRef(onFrame);
3185
3297
  onFrameRef.current = onFrame;
3186
3298
  const lastReportedFrameRef = useRef(-1);
3299
+ useEffect(() => {
3300
+ player.setSpeed(speed);
3301
+ }, [speed, player]);
3187
3302
  useEffect(() => {
3188
3303
  if (playing) {
3189
3304
  player.play();