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/README.md +77 -64
- package/dist/index.d.ts +27 -7
- package/dist/index.js +191 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/TrajectoryPlayer.tsx +16 -2
- package/src/core/SceneLoader.ts +77 -51
- package/src/hooks/useTrajectoryPlayer.ts +152 -29
- package/src/index.ts +2 -0
- package/src/types.ts +9 -1
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
|
|
417
|
+
const xmlQueue = [config.sceneFile];
|
|
418
|
+
const assetFiles = [];
|
|
406
419
|
const parser = new DOMParser();
|
|
407
|
-
while (
|
|
408
|
-
const fname =
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1454
|
+
getQpos2,
|
|
1434
1455
|
getQvel,
|
|
1435
1456
|
setCtrl,
|
|
1436
|
-
|
|
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
|
|
3104
|
-
|
|
3105
|
-
const
|
|
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
|
-
|
|
3110
|
-
|
|
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
|
-
|
|
3161
|
+
setState("playing");
|
|
3162
|
+
}, [pausedRef, setState]);
|
|
3113
3163
|
const pause = useCallback(() => {
|
|
3114
|
-
|
|
3115
|
-
|
|
3164
|
+
if (stateRef.current !== "playing") return;
|
|
3165
|
+
setState("paused");
|
|
3166
|
+
}, [setState]);
|
|
3116
3167
|
const seek = useCallback((frameIdx) => {
|
|
3117
|
-
|
|
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
|
|
3121
|
-
const qpos =
|
|
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
|
-
}, [
|
|
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
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
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 (
|
|
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 =
|
|
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 >=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3283
|
+
mode = "kinematic",
|
|
3284
|
+
onFrame,
|
|
3285
|
+
onComplete,
|
|
3286
|
+
onStateChange
|
|
3182
3287
|
}) {
|
|
3183
|
-
const player = useTrajectoryPlayer(trajectory, {
|
|
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();
|