mujoco-react 8.5.0 → 8.6.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
@@ -742,7 +742,152 @@ function loadModelFromPath(mujoco, path) {
742
742
  }
743
743
  throw new Error("MuJoCo WASM module does not expose an XML path loader");
744
744
  }
745
+ function isModelTextFile(fname) {
746
+ const lower = fname.toLowerCase();
747
+ return lower.endsWith(".xml") || lower.endsWith(".urdf") || lower.endsWith(".mjcf");
748
+ }
749
+ function normalizeVfsPath(path) {
750
+ const parts = path.replace(/\\/g, "/").split("/");
751
+ const norm = [];
752
+ for (const part of parts) {
753
+ if (!part || part === ".") continue;
754
+ if (part === "..") norm.pop();
755
+ else norm.push(part);
756
+ }
757
+ return norm.join("/");
758
+ }
759
+ function localFilePath(file) {
760
+ return normalizeVfsPath(file.webkitRelativePath || file.name);
761
+ }
762
+ function inferSceneFile(files, options) {
763
+ if (options?.sceneFile) return normalizeVfsPath(options.sceneFile);
764
+ const paths = files.map(localFilePath);
765
+ const preferred = ["scene.xml", "model.xml", "robot.xml", "scene.urdf", "model.urdf", "robot.urdf"];
766
+ for (const name of preferred) {
767
+ const match = paths.find((path) => path.endsWith(name));
768
+ if (match) return match;
769
+ }
770
+ const firstModel = paths.find(isModelTextFile);
771
+ if (!firstModel) throw new Error("No MJCF XML or URDF file found in FileList");
772
+ return firstModel;
773
+ }
774
+ function createSceneConfigFromFiles(files, options = {}) {
775
+ const fileArray = Array.from(files);
776
+ return {
777
+ src: "",
778
+ sceneFile: inferSceneFile(fileArray, options),
779
+ files: fileArray,
780
+ homeJoints: options.homeJoints,
781
+ xmlPatches: options.xmlPatches,
782
+ sceneObjects: options.sceneObjects,
783
+ onReset: options.onReset
784
+ };
785
+ }
786
+ function applyXmlPatches(text, fname, config) {
787
+ let result = text;
788
+ for (const patch of config.xmlPatches ?? []) {
789
+ if (fname.endsWith(patch.target) || fname === patch.target) {
790
+ if (patch.replace) {
791
+ const [from, to] = patch.replace;
792
+ if (result.includes(from)) {
793
+ result = result.replace(from, to);
794
+ } else {
795
+ const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
796
+ console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
797
+ }
798
+ }
799
+ if (patch.inject && patch.injectAfter) {
800
+ const idx = result.indexOf(patch.injectAfter);
801
+ if (idx !== -1) {
802
+ const tagEnd = result.indexOf(">", idx + patch.injectAfter.length);
803
+ if (tagEnd !== -1) {
804
+ result = result.slice(0, tagEnd + 1) + patch.inject + result.slice(tagEnd + 1);
805
+ } else {
806
+ console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
807
+ }
808
+ } else {
809
+ const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
810
+ console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
811
+ }
812
+ }
813
+ }
814
+ }
815
+ if (fname === config.sceneFile && config.sceneObjects?.length && result.includes("</worldbody>")) {
816
+ const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
817
+ result = result.replace("</worldbody>", xml + "</worldbody>");
818
+ }
819
+ return result;
820
+ }
821
+ async function loadSceneFromFiles(mujoco, config, onProgress) {
822
+ const files = config.files ?? [];
823
+ if (!files.length) throw new Error("loadFromFiles requires at least one File");
824
+ try {
825
+ mujoco.FS.unmount("/working");
826
+ } catch {
827
+ }
828
+ try {
829
+ mujoco.FS.mkdir("/working");
830
+ } catch {
831
+ }
832
+ const parser = new DOMParser();
833
+ const byPath = /* @__PURE__ */ new Map();
834
+ const byBasename = /* @__PURE__ */ new Map();
835
+ const written = /* @__PURE__ */ new Set();
836
+ const textByPath = /* @__PURE__ */ new Map();
837
+ for (const file of files) {
838
+ const path = localFilePath(file);
839
+ byPath.set(path, file);
840
+ byBasename.set(path.split("/").pop() ?? path, file);
841
+ }
842
+ for (const [path, file] of byPath) {
843
+ onProgress?.(`Reading ${path}...`);
844
+ ensureDir(mujoco, path);
845
+ if (isModelTextFile(path)) {
846
+ const text = applyXmlPatches(await file.text(), path, config);
847
+ textByPath.set(path, text);
848
+ mujoco.FS.writeFile(`/working/${path}`, text);
849
+ } else {
850
+ mujoco.FS.writeFile(`/working/${path}`, new Uint8Array(await file.arrayBuffer()));
851
+ }
852
+ written.add(path);
853
+ }
854
+ for (const [path, text] of textByPath) {
855
+ const deps = collectDependencyPaths(text, path, parser);
856
+ for (const dep of deps) {
857
+ if (written.has(dep)) continue;
858
+ const file = byPath.get(dep) ?? byBasename.get(dep.split("/").pop() ?? dep);
859
+ if (!file) continue;
860
+ ensureDir(mujoco, dep);
861
+ if (isModelTextFile(dep)) {
862
+ mujoco.FS.writeFile(`/working/${dep}`, applyXmlPatches(await file.text(), dep, config));
863
+ } else {
864
+ mujoco.FS.writeFile(`/working/${dep}`, new Uint8Array(await file.arrayBuffer()));
865
+ }
866
+ written.add(dep);
867
+ }
868
+ }
869
+ onProgress?.("Loading model...");
870
+ const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
871
+ const mjData = new mujoco.MjData(mjModel);
872
+ applyInitialPose(mjModel, mjData, config);
873
+ mujoco.mj_forward(mjModel, mjData);
874
+ return { mjModel, mjData };
875
+ }
876
+ function applyInitialPose(mjModel, mjData, config) {
877
+ if (!config.homeJoints) return;
878
+ const homeCount = Math.min(config.homeJoints.length, Math.max(mjModel.nu, mjModel.nq));
879
+ for (let i = 0; i < homeCount; i++) {
880
+ if (i < mjModel.nu) mjData.ctrl[i] = config.homeJoints[i];
881
+ if (i < mjModel.nq) {
882
+ const qposAdr = i < mjModel.nu ? getActuatedScalarQposAdr(mjModel, i) : -1;
883
+ mjData.qpos[qposAdr !== -1 ? qposAdr : i] = config.homeJoints[i];
884
+ }
885
+ }
886
+ }
745
887
  async function loadScene(mujoco, config, onProgress) {
888
+ if (config.files?.length) {
889
+ return loadSceneFromFiles(mujoco, config, onProgress);
890
+ }
746
891
  try {
747
892
  mujoco.FS.unmount("/working");
748
893
  } catch {
@@ -760,7 +905,7 @@ async function loadScene(mujoco, config, onProgress) {
760
905
  const fname = xmlQueue.shift();
761
906
  if (downloaded.has(fname)) continue;
762
907
  downloaded.add(fname);
763
- if (!fname.endsWith(".xml")) {
908
+ if (!isModelTextFile(fname)) {
764
909
  assetFiles.push(fname);
765
910
  continue;
766
911
  }
@@ -770,38 +915,7 @@ async function loadScene(mujoco, config, onProgress) {
770
915
  console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
771
916
  continue;
772
917
  }
773
- let text = await res.text();
774
- for (const patch of config.xmlPatches ?? []) {
775
- if (fname.endsWith(patch.target) || fname === patch.target) {
776
- if (patch.replace) {
777
- const [from, to] = patch.replace;
778
- if (text.includes(from)) {
779
- text = text.replace(from, to);
780
- } else {
781
- const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
782
- console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
783
- }
784
- }
785
- if (patch.inject && patch.injectAfter) {
786
- const idx = text.indexOf(patch.injectAfter);
787
- if (idx !== -1) {
788
- const tagEnd = text.indexOf(">", idx + patch.injectAfter.length);
789
- if (tagEnd !== -1) {
790
- text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
791
- } else {
792
- console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
793
- }
794
- } else {
795
- const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
796
- console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
797
- }
798
- }
799
- }
800
- }
801
- if (fname === config.sceneFile && config.sceneObjects?.length) {
802
- const xml = config.sceneObjects.map((obj) => sceneObjectToXml(obj)).join("");
803
- text = text.replace("</worldbody>", xml + "</worldbody>");
804
- }
918
+ const text = applyXmlPatches(await res.text(), fname, config);
805
919
  ensureDir(mujoco, fname);
806
920
  mujoco.FS.writeFile(`/working/${fname}`, text);
807
921
  scanDependencies(text, fname, parser, downloaded, xmlQueue);
@@ -827,48 +941,49 @@ async function loadScene(mujoco, config, onProgress) {
827
941
  onProgress?.("Loading model...");
828
942
  const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
829
943
  const mjData = new mujoco.MjData(mjModel);
830
- if (config.homeJoints) {
831
- const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
832
- for (let i = 0; i < homeCount; i++) {
833
- mjData.ctrl[i] = config.homeJoints[i];
834
- const qposAdr = getActuatedScalarQposAdr(mjModel, i);
835
- if (qposAdr !== -1) {
836
- mjData.qpos[qposAdr] = config.homeJoints[i];
837
- }
838
- }
839
- }
944
+ applyInitialPose(mjModel, mjData, config);
840
945
  mujoco.mj_forward(mjModel, mjData);
841
946
  return { mjModel, mjData };
842
947
  }
843
948
  function scanDependencies(xmlString, currentFile, parser, downloaded, queue) {
949
+ for (const fullPath of collectDependencyPaths(xmlString, currentFile, parser)) {
950
+ if (!downloaded.has(fullPath)) queue.push(fullPath);
951
+ }
952
+ }
953
+ function collectDependencyPaths(xmlString, currentFile, parser) {
844
954
  const xmlDoc = parser.parseFromString(xmlString, "text/xml");
845
955
  const compiler = xmlDoc.querySelector("compiler");
846
956
  const assetDir = compiler?.getAttribute("assetdir") || "";
847
957
  const meshDir = compiler?.getAttribute("meshdir") || assetDir;
848
958
  const textureDir = compiler?.getAttribute("texturedir") || assetDir;
849
959
  const currentDir = currentFile.includes("/") ? currentFile.substring(0, currentFile.lastIndexOf("/") + 1) : "";
850
- xmlDoc.querySelectorAll("[file]").forEach((el) => {
851
- const fileAttr = el.getAttribute("file");
960
+ const paths = [];
961
+ xmlDoc.querySelectorAll("[file], [filename]").forEach((el) => {
962
+ const fileAttr = el.getAttribute("file") ?? el.getAttribute("filename");
852
963
  if (!fileAttr) return;
964
+ if (/^[a-z]+:\/\//i.test(fileAttr) || fileAttr.startsWith("package://")) return;
853
965
  let prefix = "";
854
966
  if (el.tagName.toLowerCase() === "mesh") {
855
967
  prefix = meshDir ? meshDir + "/" : "";
856
968
  } else if (["texture", "hfield"].includes(el.tagName.toLowerCase())) {
857
969
  prefix = textureDir ? textureDir + "/" : "";
858
970
  }
859
- let fullPath = (currentDir + prefix + fileAttr).replace(/\/\//g, "/");
860
- const parts = fullPath.split("/");
861
- const norm = [];
862
- for (const p of parts) {
863
- if (p === "..") norm.pop();
864
- else if (p !== ".") norm.push(p);
865
- }
866
- fullPath = norm.join("/");
867
- if (!downloaded.has(fullPath)) queue.push(fullPath);
971
+ const fullPath = normalizeVfsPath(currentDir + prefix + fileAttr);
972
+ paths.push(fullPath);
868
973
  });
974
+ return paths;
869
975
  }
870
976
  function SceneRenderer(props) {
871
- const { mjModelRef, mjDataRef, mujocoRef, onSelectionRef, hiddenBodiesRef, status } = useMujocoContext();
977
+ const {
978
+ mjModelRef,
979
+ mjDataRef,
980
+ mujocoRef,
981
+ onSelectionRef,
982
+ hiddenBodiesRef,
983
+ interpolateRef,
984
+ interpolationStateRef,
985
+ status
986
+ } = useMujocoContext();
872
987
  const groupRef = useRef(null);
873
988
  const bodyRefs = useRef([]);
874
989
  const prevModelRef = useRef(null);
@@ -908,20 +1023,46 @@ function SceneRenderer(props) {
908
1023
  const data = mjDataRef.current;
909
1024
  if (!data) return;
910
1025
  const bodies = bodyRefs.current;
1026
+ const interpolation = interpolationStateRef.current;
1027
+ const useInterpolation = interpolateRef.current && interpolation.valid;
911
1028
  for (let i = 0; i < bodies.length; i++) {
912
1029
  const ref = bodies[i];
913
1030
  if (!ref) continue;
914
- ref.position.set(
915
- data.xpos[i * 3],
916
- data.xpos[i * 3 + 1],
917
- data.xpos[i * 3 + 2]
918
- );
919
- ref.quaternion.set(
920
- data.xquat[i * 4 + 1],
921
- data.xquat[i * 4 + 2],
922
- data.xquat[i * 4 + 3],
923
- data.xquat[i * 4]
924
- );
1031
+ if (useInterpolation) {
1032
+ const alpha = interpolation.alpha;
1033
+ const i3 = i * 3;
1034
+ ref.position.set(
1035
+ THREE11.MathUtils.lerp(interpolation.previousXpos[i3], interpolation.currentXpos[i3], alpha),
1036
+ THREE11.MathUtils.lerp(interpolation.previousXpos[i3 + 1], interpolation.currentXpos[i3 + 1], alpha),
1037
+ THREE11.MathUtils.lerp(interpolation.previousXpos[i3 + 2], interpolation.currentXpos[i3 + 2], alpha)
1038
+ );
1039
+ const i4 = i * 4;
1040
+ _previousQuat.set(
1041
+ interpolation.previousXquat[i4 + 1],
1042
+ interpolation.previousXquat[i4 + 2],
1043
+ interpolation.previousXquat[i4 + 3],
1044
+ interpolation.previousXquat[i4]
1045
+ );
1046
+ _currentQuat.set(
1047
+ interpolation.currentXquat[i4 + 1],
1048
+ interpolation.currentXquat[i4 + 2],
1049
+ interpolation.currentXquat[i4 + 3],
1050
+ interpolation.currentXquat[i4]
1051
+ );
1052
+ ref.quaternion.copy(_previousQuat).slerp(_currentQuat, alpha);
1053
+ } else {
1054
+ ref.position.set(
1055
+ data.xpos[i * 3],
1056
+ data.xpos[i * 3 + 1],
1057
+ data.xpos[i * 3 + 2]
1058
+ );
1059
+ ref.quaternion.set(
1060
+ data.xquat[i * 4 + 1],
1061
+ data.xquat[i * 4 + 2],
1062
+ data.xquat[i * 4 + 3],
1063
+ data.xquat[i * 4]
1064
+ );
1065
+ }
925
1066
  }
926
1067
  });
927
1068
  return /* @__PURE__ */ jsx(
@@ -948,6 +1089,8 @@ function SceneRenderer(props) {
948
1089
  }
949
1090
  );
950
1091
  }
1092
+ var _previousQuat = new THREE11.Quaternion();
1093
+ var _currentQuat = new THREE11.Quaternion();
951
1094
  var JOINT_TYPE_NAMES2 = ["free", "ball", "slide", "hinge"];
952
1095
  var GEOM_TYPE_NAMES = ["plane", "hfield", "sphere", "capsule", "ellipsoid", "cylinder", "box", "mesh"];
953
1096
  var SENSOR_TYPE_NAMES = {
@@ -1106,6 +1249,7 @@ function MujocoSimProvider({
1106
1249
  substeps,
1107
1250
  paused,
1108
1251
  speed,
1252
+ interpolate,
1109
1253
  children
1110
1254
  }) {
1111
1255
  const { gl, camera } = useThree();
@@ -1117,6 +1261,16 @@ function MujocoSimProvider({
1117
1261
  const pausedRef = useRef(paused ?? false);
1118
1262
  const speedRef = useRef(speed ?? 1);
1119
1263
  const substepsRef = useRef(substeps ?? 1);
1264
+ const interpolateRef = useRef(interpolate ?? false);
1265
+ const interpolationStateRef = useRef({
1266
+ alpha: 1,
1267
+ previousXpos: new Float64Array(0),
1268
+ previousXquat: new Float64Array(0),
1269
+ currentXpos: new Float64Array(0),
1270
+ currentXquat: new Float64Array(0),
1271
+ valid: false
1272
+ });
1273
+ const physicsAccumulatorRef = useRef(0);
1120
1274
  const stepsToRunRef = useRef(0);
1121
1275
  const loadGenRef = useRef(0);
1122
1276
  const onSelectionRef = useRef(onSelection);
@@ -1130,7 +1284,9 @@ function MujocoSimProvider({
1130
1284
  const bodyRegistryRef = useRef(/* @__PURE__ */ new Map());
1131
1285
  const hiddenBodiesRef = useRef(/* @__PURE__ */ new Set());
1132
1286
  const bodyReloadTimerRef = useRef(null);
1133
- configRef.current = config;
1287
+ useEffect(() => {
1288
+ configRef.current = config;
1289
+ }, [config]);
1134
1290
  useEffect(() => {
1135
1291
  pausedRef.current = paused ?? false;
1136
1292
  }, [paused]);
@@ -1140,6 +1296,9 @@ function MujocoSimProvider({
1140
1296
  useEffect(() => {
1141
1297
  substepsRef.current = substeps ?? 1;
1142
1298
  }, [substeps]);
1299
+ useEffect(() => {
1300
+ interpolateRef.current = interpolate ?? false;
1301
+ }, [interpolate]);
1143
1302
  useEffect(() => {
1144
1303
  if (!gravity) return;
1145
1304
  const model = mjModelRef.current;
@@ -1177,6 +1336,8 @@ function MujocoSimProvider({
1177
1336
  }
1178
1337
  mjModelRef.current = result.mjModel;
1179
1338
  mjDataRef.current = result.mjData;
1339
+ physicsAccumulatorRef.current = 0;
1340
+ interpolationStateRef.current.valid = false;
1180
1341
  if (gravity && result.mjModel.opt?.gravity) {
1181
1342
  result.mjModel.opt.gravity[0] = gravity[0];
1182
1343
  result.mjModel.opt.gravity[1] = gravity[1];
@@ -1201,6 +1362,8 @@ function MujocoSimProvider({
1201
1362
  mjDataRef.current?.delete();
1202
1363
  mjModelRef.current = null;
1203
1364
  mjDataRef.current = null;
1365
+ physicsAccumulatorRef.current = 0;
1366
+ interpolationStateRef.current.valid = false;
1204
1367
  try {
1205
1368
  mujoco.FS.unmount("/working");
1206
1369
  } catch {
@@ -1233,19 +1396,56 @@ function MujocoSimProvider({
1233
1396
  cb(model, data);
1234
1397
  }
1235
1398
  const numSubsteps = substepsRef.current;
1236
- if (stepsToRunRef.current > 0) {
1399
+ if (!interpolateRef.current) {
1400
+ if (stepsToRunRef.current > 0) {
1401
+ for (let s = 0; s < stepsToRunRef.current; s++) {
1402
+ mujoco.mj_step(model, data);
1403
+ }
1404
+ stepsToRunRef.current = 0;
1405
+ } else {
1406
+ const startSimTime = data.time;
1407
+ const clampedDelta = Math.min(delta, 1 / 15);
1408
+ const frameTime = clampedDelta * speedRef.current;
1409
+ while (data.time - startSimTime < frameTime) {
1410
+ for (let s = 0; s < numSubsteps; s++) {
1411
+ mujoco.mj_step(model, data);
1412
+ }
1413
+ }
1414
+ }
1415
+ } else if (stepsToRunRef.current > 0) {
1416
+ ensureInterpolationBuffers(model);
1417
+ copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
1237
1418
  for (let s = 0; s < stepsToRunRef.current; s++) {
1238
1419
  mujoco.mj_step(model, data);
1239
1420
  }
1421
+ copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
1422
+ interpolationStateRef.current.alpha = 1;
1423
+ interpolationStateRef.current.valid = true;
1240
1424
  stepsToRunRef.current = 0;
1241
1425
  } else {
1242
- const startSimTime = data.time;
1426
+ ensureInterpolationBuffers(model);
1243
1427
  const clampedDelta = Math.min(delta, 1 / 15);
1244
- const frameTime = clampedDelta * speedRef.current;
1245
- while (data.time - startSimTime < frameTime) {
1428
+ physicsAccumulatorRef.current += clampedDelta * speedRef.current;
1429
+ const stepDt = Math.max((model.opt?.timestep ?? 2e-3) * Math.max(1, numSubsteps), 1e-6);
1430
+ let stepped = false;
1431
+ while (physicsAccumulatorRef.current >= stepDt) {
1432
+ copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
1246
1433
  for (let s = 0; s < numSubsteps; s++) {
1247
1434
  mujoco.mj_step(model, data);
1248
1435
  }
1436
+ copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
1437
+ physicsAccumulatorRef.current -= stepDt;
1438
+ stepped = true;
1439
+ }
1440
+ if (!interpolationStateRef.current.valid) {
1441
+ copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
1442
+ copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
1443
+ }
1444
+ interpolationStateRef.current.alpha = Math.min(Math.max(physicsAccumulatorRef.current / stepDt, 0), 1);
1445
+ interpolationStateRef.current.valid = true;
1446
+ if (!stepped) {
1447
+ onStepRef.current?.(data.time);
1448
+ return;
1249
1449
  }
1250
1450
  }
1251
1451
  for (const cb of afterStepCallbacks.current) {
@@ -1253,6 +1453,19 @@ function MujocoSimProvider({
1253
1453
  }
1254
1454
  onStepRef.current?.(data.time);
1255
1455
  }, -1);
1456
+ function ensureInterpolationBuffers(model) {
1457
+ const state = interpolationStateRef.current;
1458
+ const xposLength = model.nbody * 3;
1459
+ const xquatLength = model.nbody * 4;
1460
+ if (state.previousXpos.length !== xposLength) state.previousXpos = new Float64Array(xposLength);
1461
+ if (state.currentXpos.length !== xposLength) state.currentXpos = new Float64Array(xposLength);
1462
+ if (state.previousXquat.length !== xquatLength) state.previousXquat = new Float64Array(xquatLength);
1463
+ if (state.currentXquat.length !== xquatLength) state.currentXquat = new Float64Array(xquatLength);
1464
+ }
1465
+ function copyBodyPose(data, xpos, xquat) {
1466
+ xpos.set(data.xpos.subarray(0, xpos.length));
1467
+ xquat.set(data.xquat.subarray(0, xquat.length));
1468
+ }
1256
1469
  const reset = useCallback(() => {
1257
1470
  const model = mjModelRef.current;
1258
1471
  const data = mjDataRef.current;
@@ -1660,7 +1873,7 @@ function MujocoSimProvider({
1660
1873
  mjModelRef.current = null;
1661
1874
  mjDataRef.current = null;
1662
1875
  setStatus("loading");
1663
- const result = await loadScene(mujoco, newConfig);
1876
+ const result = await loadScene(mujoco, buildMergedConfig(newConfig));
1664
1877
  if (gen !== loadGenRef.current) {
1665
1878
  result.mjModel.delete();
1666
1879
  result.mjData.delete();
@@ -1668,6 +1881,8 @@ function MujocoSimProvider({
1668
1881
  }
1669
1882
  mjModelRef.current = result.mjModel;
1670
1883
  mjDataRef.current = result.mjData;
1884
+ physicsAccumulatorRef.current = 0;
1885
+ interpolationStateRef.current.valid = false;
1671
1886
  configRef.current = newConfig;
1672
1887
  setStatus("ready");
1673
1888
  } catch (e) {
@@ -1681,9 +1896,36 @@ function MujocoSimProvider({
1681
1896
  if (bodyReloadTimerRef.current) clearTimeout(bodyReloadTimerRef.current);
1682
1897
  bodyReloadTimerRef.current = setTimeout(() => {
1683
1898
  bodyReloadTimerRef.current = null;
1684
- loadSceneApi(buildMergedConfig(configRef.current));
1899
+ loadSceneApi(configRef.current);
1685
1900
  }, 0);
1686
1901
  }, [loadSceneApi]);
1902
+ const loadFromFilesApi = useCallback(
1903
+ async (files, options) => {
1904
+ await loadSceneApi(createSceneConfigFromFiles(files, options));
1905
+ },
1906
+ [loadSceneApi]
1907
+ );
1908
+ const addBodyApi = useCallback(async (body) => {
1909
+ const current = configRef.current;
1910
+ const sceneObjects = [
1911
+ ...(current.sceneObjects ?? []).filter((obj) => obj.name !== body.name),
1912
+ body
1913
+ ];
1914
+ await loadSceneApi({ ...current, sceneObjects });
1915
+ }, [loadSceneApi]);
1916
+ const removeBodyApi = useCallback(async (name) => {
1917
+ const current = configRef.current;
1918
+ bodyRegistryRef.current.delete(name);
1919
+ const sceneObjects = (current.sceneObjects ?? []).filter((obj) => obj.name !== name);
1920
+ await loadSceneApi({ ...current, sceneObjects });
1921
+ }, [loadSceneApi]);
1922
+ const recompileApi = useCallback(async (patches = []) => {
1923
+ const current = configRef.current;
1924
+ await loadSceneApi({
1925
+ ...current,
1926
+ xmlPatches: patches.length ? [...current.xmlPatches ?? [], ...patches] : current.xmlPatches
1927
+ });
1928
+ }, [loadSceneApi]);
1687
1929
  const getCanvasSnapshot = useCallback(
1688
1930
  (width, height, mimeType = "image/jpeg") => {
1689
1931
  if (width && height) {
@@ -1761,7 +2003,9 @@ function MujocoSimProvider({
1761
2003
  get status() {
1762
2004
  return status;
1763
2005
  },
1764
- config,
2006
+ get config() {
2007
+ return configRef.current;
2008
+ },
1765
2009
  reset,
1766
2010
  setSpeed,
1767
2011
  togglePause,
@@ -1800,6 +2044,10 @@ function MujocoSimProvider({
1800
2044
  getKeyframeNames,
1801
2045
  getKeyframeCount,
1802
2046
  loadScene: loadSceneApi,
2047
+ loadFromFiles: loadFromFilesApi,
2048
+ addBody: addBodyApi,
2049
+ removeBody: removeBodyApi,
2050
+ recompile: recompileApi,
1803
2051
  getCanvasSnapshot,
1804
2052
  project2DTo3D,
1805
2053
  setBodyMass,
@@ -1810,7 +2058,6 @@ function MujocoSimProvider({
1810
2058
  }),
1811
2059
  [
1812
2060
  status,
1813
- config,
1814
2061
  reset,
1815
2062
  setSpeed,
1816
2063
  togglePause,
@@ -1849,6 +2096,10 @@ function MujocoSimProvider({
1849
2096
  getKeyframeNames,
1850
2097
  getKeyframeCount,
1851
2098
  loadSceneApi,
2099
+ loadFromFilesApi,
2100
+ addBodyApi,
2101
+ removeBodyApi,
2102
+ recompileApi,
1852
2103
  getCanvasSnapshot,
1853
2104
  project2DTo3D,
1854
2105
  setBodyMass,
@@ -1868,6 +2119,8 @@ function MujocoSimProvider({
1868
2119
  pausedRef,
1869
2120
  speedRef,
1870
2121
  substepsRef,
2122
+ interpolateRef,
2123
+ interpolationStateRef,
1871
2124
  onSelectionRef,
1872
2125
  beforeStepCallbacks,
1873
2126
  afterStepCallbacks,
@@ -1898,6 +2151,7 @@ var MujocoCanvas = forwardRef(
1898
2151
  substeps,
1899
2152
  paused,
1900
2153
  speed,
2154
+ interpolate,
1901
2155
  children,
1902
2156
  ...canvasProps
1903
2157
  }, ref) {
@@ -1925,6 +2179,7 @@ var MujocoCanvas = forwardRef(
1925
2179
  substeps,
1926
2180
  paused,
1927
2181
  speed,
2182
+ interpolate,
1928
2183
  children
1929
2184
  }
1930
2185
  ) });
@@ -3476,6 +3731,128 @@ function FlexRenderer(props) {
3476
3731
  if (status !== "ready") return null;
3477
3732
  return /* @__PURE__ */ jsx("group", { ...props, ref: groupRef });
3478
3733
  }
3734
+ var GEOM_TYPE_NAMES2 = ["plane", "hfield", "sphere", "capsule", "ellipsoid", "cylinder", "box", "mesh"];
3735
+ var _matrix = new THREE11.Matrix4();
3736
+ function getGeomInfo(model, geomId) {
3737
+ const size = model.geom_size.subarray(geomId * 3, geomId * 3 + 3);
3738
+ const type = model.geom_type[geomId];
3739
+ return {
3740
+ id: geomId,
3741
+ name: getName(model, model.name_geomadr[geomId]),
3742
+ type,
3743
+ typeName: GEOM_TYPE_NAMES2[type] ?? `type-${type}`,
3744
+ size: [size[0], size[1], size[2]],
3745
+ bodyId: model.geom_bodyid[geomId]
3746
+ };
3747
+ }
3748
+ function geomSignature(model, geomId) {
3749
+ const type = model.geom_type[geomId];
3750
+ const size = Array.from(model.geom_size.subarray(geomId * 3, geomId * 3 + 3)).join(",");
3751
+ const mat = model.geom_matid[geomId];
3752
+ const data = model.geom_dataid[geomId];
3753
+ const rgba = Array.from(model.geom_rgba.subarray(geomId * 4, geomId * 4 + 4)).join(",");
3754
+ return [type, size, mat, data, rgba].join("|");
3755
+ }
3756
+ function firstMesh(object) {
3757
+ if (object instanceof THREE11.Mesh) return object;
3758
+ let mesh = null;
3759
+ object.traverse((child) => {
3760
+ if (!mesh && child instanceof THREE11.Mesh) mesh = child;
3761
+ });
3762
+ return mesh;
3763
+ }
3764
+ function InstancedGeomRenderer({
3765
+ geomGroup,
3766
+ filter,
3767
+ material,
3768
+ visible = true,
3769
+ castShadow = true,
3770
+ receiveShadow = true,
3771
+ ...groupProps
3772
+ } = {}) {
3773
+ const { mjModelRef, mjDataRef, mujocoRef, status } = useMujocoContext();
3774
+ const meshRefs = useRef([]);
3775
+ const batches = useMemo(() => {
3776
+ if (status !== "ready") return [];
3777
+ const model = mjModelRef.current;
3778
+ if (!model) return [];
3779
+ const builder = new GeomBuilder(mujocoRef.current);
3780
+ const grouped = /* @__PURE__ */ new Map();
3781
+ for (let geomId = 0; geomId < model.ngeom; geomId++) {
3782
+ if (model.geom_group[geomId] === 3) continue;
3783
+ if (geomGroup !== void 0 && model.geom_group[geomId] !== geomGroup) continue;
3784
+ const info = getGeomInfo(model, geomId);
3785
+ if (filter && !filter(info)) continue;
3786
+ const key = geomSignature(model, geomId);
3787
+ const ids = grouped.get(key);
3788
+ if (ids) ids.push(geomId);
3789
+ else grouped.set(key, [geomId]);
3790
+ }
3791
+ const next = [];
3792
+ for (const [key, geomIds] of grouped) {
3793
+ if (geomIds.length < 2) continue;
3794
+ const object = builder.create(model, geomIds[0]);
3795
+ if (!object) continue;
3796
+ const mesh = firstMesh(object);
3797
+ if (!mesh) continue;
3798
+ const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
3799
+ next.push({
3800
+ key,
3801
+ geomIds,
3802
+ geometry: mesh.geometry.clone(),
3803
+ material: material ?? sourceMaterial.clone()
3804
+ });
3805
+ }
3806
+ return next;
3807
+ }, [filter, geomGroup, material, mjModelRef, mujocoRef, status]);
3808
+ useFrame(() => {
3809
+ const data = mjDataRef.current;
3810
+ if (!data || !visible) return;
3811
+ batches.forEach((batch, batchIndex) => {
3812
+ const mesh = meshRefs.current[batchIndex];
3813
+ if (!mesh) return;
3814
+ batch.geomIds.forEach((geomId, instanceId) => {
3815
+ const p = geomId * 3;
3816
+ const r = geomId * 9;
3817
+ _matrix.set(
3818
+ data.geom_xmat[r],
3819
+ data.geom_xmat[r + 1],
3820
+ data.geom_xmat[r + 2],
3821
+ data.geom_xpos[p],
3822
+ data.geom_xmat[r + 3],
3823
+ data.geom_xmat[r + 4],
3824
+ data.geom_xmat[r + 5],
3825
+ data.geom_xpos[p + 1],
3826
+ data.geom_xmat[r + 6],
3827
+ data.geom_xmat[r + 7],
3828
+ data.geom_xmat[r + 8],
3829
+ data.geom_xpos[p + 2],
3830
+ 0,
3831
+ 0,
3832
+ 0,
3833
+ 1
3834
+ );
3835
+ mesh.setMatrixAt(instanceId, _matrix);
3836
+ });
3837
+ mesh.count = batch.geomIds.length;
3838
+ mesh.instanceMatrix.needsUpdate = true;
3839
+ });
3840
+ });
3841
+ if (status !== "ready" || batches.length === 0) return null;
3842
+ return /* @__PURE__ */ jsx("group", { ...groupProps, visible, children: batches.map((batch, index) => /* @__PURE__ */ jsx(
3843
+ "instancedMesh",
3844
+ {
3845
+ ref: (mesh) => {
3846
+ meshRefs.current[index] = mesh;
3847
+ },
3848
+ args: [batch.geometry, batch.material, batch.geomIds.length],
3849
+ castShadow,
3850
+ receiveShadow,
3851
+ frustumCulled: false
3852
+ },
3853
+ batch.key
3854
+ )) });
3855
+ }
3479
3856
  var geomNameCacheByModel = /* @__PURE__ */ new WeakMap();
3480
3857
  function getGeomNameCached(model, geomId) {
3481
3858
  let perModel = geomNameCacheByModel.get(model);
@@ -4668,6 +5045,6 @@ function useCameraAnimation() {
4668
5045
  * useCameraAnimation — composable camera animation hook.
4669
5046
  */
4670
5047
 
4671
- export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
5048
+ export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, InstancedGeomRenderer, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
4672
5049
  //# sourceMappingURL=index.js.map
4673
5050
  //# sourceMappingURL=index.js.map