mujoco-react 8.10.0 → 9.0.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.
Files changed (40) hide show
  1. package/README.md +81 -44
  2. package/dist/chunk-33CV6HSV.js +400 -0
  3. package/dist/chunk-33CV6HSV.js.map +1 -0
  4. package/dist/index.d.ts +92 -24
  5. package/dist/index.js +338 -54
  6. package/dist/index.js.map +1 -1
  7. package/dist/spark.d.ts +24 -3
  8. package/dist/spark.js +91 -6
  9. package/dist/spark.js.map +1 -1
  10. package/dist/{types-FFW7ykBu.d.ts → types-izZlUweI.d.ts} +109 -16
  11. package/package.json +1 -1
  12. package/src/components/Body.tsx +3 -1
  13. package/src/components/DragInteraction.tsx +1 -1
  14. package/src/components/IkGizmo.tsx +2 -2
  15. package/src/components/SceneRenderer.tsx +1 -1
  16. package/src/components/TrajectoryPlayer.tsx +4 -1
  17. package/src/components/VisualScenario.tsx +343 -6
  18. package/src/core/MujocoCanvas.tsx +8 -1
  19. package/src/core/MujocoPhysics.tsx +10 -4
  20. package/src/core/MujocoSimProvider.tsx +15 -12
  21. package/src/core/SceneLoader.ts +182 -3
  22. package/src/core/createController.tsx +2 -2
  23. package/src/hooks/useBodyState.ts +1 -1
  24. package/src/hooks/useContacts.ts +1 -1
  25. package/src/hooks/useCtrlNoise.ts +1 -1
  26. package/src/hooks/useFrameCapture.ts +206 -0
  27. package/src/hooks/useGamepad.ts +1 -1
  28. package/src/hooks/useGravityCompensation.ts +1 -1
  29. package/src/hooks/useIkController.ts +22 -13
  30. package/src/hooks/useJointState.ts +1 -1
  31. package/src/hooks/useKeyboardTeleop.ts +1 -1
  32. package/src/hooks/usePolicy.ts +13 -9
  33. package/src/hooks/useSensor.ts +1 -1
  34. package/src/hooks/useTrajectoryPlayer.ts +4 -4
  35. package/src/hooks/useTrajectoryRecorder.ts +1 -1
  36. package/src/index.ts +35 -0
  37. package/src/spark.tsx +138 -4
  38. package/src/types.ts +128 -21
  39. package/dist/chunk-KGFRKPLS.js +0 -186
  40. package/dist/chunk-KGFRKPLS.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { ScenarioLighting, SplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getScenarioBackground, getScenarioCameraPosition, useSplatEnvironment } from './chunk-KGFRKPLS.js';
1
+ export { ScenarioLighting, SplatEnvironment, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getScenarioBackground, getScenarioCameraPosition, useSplatEnvironment, useVisualScenarioEffects, withSplatEnvironment } from './chunk-33CV6HSV.js';
2
2
  import loadMujoco from '@mujoco/mujoco';
3
3
  import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
4
4
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
@@ -776,7 +776,8 @@ function sceneObjectToXml(obj) {
776
776
  const solref = obj.solref ? ` solref="${obj.solref}"` : "";
777
777
  const solimp = obj.solimp ? ` solimp="${obj.solimp}"` : "";
778
778
  const condim = obj.condim ? ` condim="${obj.condim}"` : "";
779
- 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>`;
779
+ const group = obj.group !== void 0 ? ` group="${obj.group}"` : "";
780
+ 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}${group}/></body>`;
780
781
  }
781
782
  function ensureDir(mujoco, fname) {
782
783
  const dirParts = fname.split("/");
@@ -816,6 +817,20 @@ function normalizeVfsPath(path) {
816
817
  function localFilePath(file) {
817
818
  return normalizeVfsPath(file.webkitRelativePath || file.name);
818
819
  }
820
+ function dirname(path) {
821
+ const normalized = normalizeVfsPath(path);
822
+ const idx = normalized.lastIndexOf("/");
823
+ return idx === -1 ? "" : normalized.slice(0, idx + 1);
824
+ }
825
+ function relativeVfsPath(fromDir, targetPath) {
826
+ const from = normalizeVfsPath(fromDir).split("/").filter(Boolean);
827
+ const target = normalizeVfsPath(targetPath).split("/").filter(Boolean);
828
+ while (from.length && target.length && from[0] === target[0]) {
829
+ from.shift();
830
+ target.shift();
831
+ }
832
+ return [...from.map(() => ".."), ...target].join("/") || ".";
833
+ }
819
834
  function inferSceneFile(files, options) {
820
835
  if (options?.sceneFile) return normalizeVfsPath(options.sceneFile);
821
836
  const paths = files.map(localFilePath);
@@ -834,12 +849,120 @@ function createSceneConfigFromFiles(files, options = {}) {
834
849
  src: "",
835
850
  sceneFile: inferSceneFile(fileArray, options),
836
851
  files: fileArray,
852
+ environmentFiles: options.environmentFiles?.map(normalizeVfsPath),
837
853
  homeJoints: options.homeJoints,
838
854
  xmlPatches: options.xmlPatches,
839
855
  sceneObjects: options.sceneObjects,
840
856
  onReset: options.onReset
841
857
  };
842
858
  }
859
+ var ENVIRONMENT_MERGE_SECTIONS = [
860
+ "asset",
861
+ "worldbody",
862
+ "contact",
863
+ "equality",
864
+ "tendon",
865
+ "sensor",
866
+ "keyframe",
867
+ "custom",
868
+ "extension"
869
+ ];
870
+ function directChild(parent, tagName) {
871
+ const lower = tagName.toLowerCase();
872
+ for (const child of Array.from(parent.children)) {
873
+ if (child.tagName.toLowerCase() === lower) return child;
874
+ }
875
+ return null;
876
+ }
877
+ function ensureTopLevelSection(doc, tagName) {
878
+ const root = doc.documentElement;
879
+ const existing = directChild(root, tagName);
880
+ if (existing) return existing;
881
+ const section = doc.createElement(tagName);
882
+ if (tagName === "asset") {
883
+ const worldbody = directChild(root, "worldbody");
884
+ if (worldbody) root.insertBefore(section, worldbody);
885
+ else root.appendChild(section);
886
+ } else {
887
+ root.appendChild(section);
888
+ }
889
+ return section;
890
+ }
891
+ function readCompilerDirs(doc) {
892
+ const compiler = directChild(doc.documentElement, "compiler");
893
+ const assetDir = compiler?.getAttribute("assetdir") || "";
894
+ return {
895
+ meshDir: compiler?.getAttribute("meshdir") || assetDir,
896
+ textureDir: compiler?.getAttribute("texturedir") || assetDir
897
+ };
898
+ }
899
+ function isExternalPath(path) {
900
+ return /^[a-z]+:\/\//i.test(path) || path.startsWith("package://") || path.startsWith("/");
901
+ }
902
+ function fileReferencePrefix(el, compilerDirs) {
903
+ const tag = el.tagName.toLowerCase();
904
+ if (tag === "mesh") return compilerDirs.meshDir ? compilerDirs.meshDir + "/" : "";
905
+ if (tag === "texture" || tag === "hfield") return compilerDirs.textureDir ? compilerDirs.textureDir + "/" : "";
906
+ return "";
907
+ }
908
+ function rewriteFileReferencesForMerge(node, sourceFile, targetFile, sourceDoc) {
909
+ const sourceDir = dirname(sourceFile);
910
+ const targetDir = dirname(targetFile);
911
+ const compilerDirs = readCompilerDirs(sourceDoc);
912
+ node.querySelectorAll("[file], [filename]").forEach((el) => {
913
+ const attr = el.hasAttribute("file") ? "file" : "filename";
914
+ const value = el.getAttribute(attr);
915
+ if (!value || isExternalPath(value)) return;
916
+ const sourceRelativePath = normalizeVfsPath(fileReferencePrefix(el, compilerDirs) + value);
917
+ const resolvedPath = normalizeVfsPath(sourceDir + sourceRelativePath);
918
+ el.setAttribute(attr, relativeVfsPath(targetDir, resolvedPath));
919
+ });
920
+ }
921
+ function hasParseError(doc) {
922
+ return doc.getElementsByTagName("parsererror").length > 0;
923
+ }
924
+ function composeEnvironmentXml(sceneXml, config, parser, environmentXmlByPath) {
925
+ const environmentFiles = config.environmentFiles?.map(normalizeVfsPath) ?? [];
926
+ if (!environmentFiles.length) return sceneXml;
927
+ const sceneDoc = parser.parseFromString(sceneXml, "text/xml");
928
+ if (hasParseError(sceneDoc)) {
929
+ console.warn(`Could not compose environments: failed to parse ${config.sceneFile}`);
930
+ return sceneXml;
931
+ }
932
+ for (const environmentFile of environmentFiles) {
933
+ const environmentXml = environmentXmlByPath.get(environmentFile);
934
+ if (!environmentXml) {
935
+ console.warn(`Environment XML not found: ${environmentFile}`);
936
+ continue;
937
+ }
938
+ const environmentDoc = parser.parseFromString(environmentXml, "text/xml");
939
+ if (hasParseError(environmentDoc)) {
940
+ console.warn(`Skipping environment XML with parse errors: ${environmentFile}`);
941
+ continue;
942
+ }
943
+ for (const sectionName of ENVIRONMENT_MERGE_SECTIONS) {
944
+ const environmentSection = directChild(environmentDoc.documentElement, sectionName);
945
+ if (!environmentSection?.children.length) continue;
946
+ const targetSection = ensureTopLevelSection(sceneDoc, sectionName);
947
+ for (const child of Array.from(environmentSection.children)) {
948
+ const imported = sceneDoc.importNode(child, true);
949
+ rewriteFileReferencesForMerge(imported, environmentFile, config.sceneFile, environmentDoc);
950
+ targetSection.appendChild(imported);
951
+ }
952
+ }
953
+ }
954
+ return new XMLSerializer().serializeToString(sceneDoc);
955
+ }
956
+ function findTextByConfiguredPath(textByPath, configuredPath) {
957
+ const normalized = normalizeVfsPath(configuredPath);
958
+ const direct = textByPath.get(normalized);
959
+ if (direct) return direct;
960
+ const suffix = "/" + normalized;
961
+ for (const [path, text] of textByPath) {
962
+ if (path.endsWith(suffix) || path === normalized.split("/").pop()) return text;
963
+ }
964
+ return void 0;
965
+ }
843
966
  function applyXmlPatches(text, fname, config) {
844
967
  let result = text;
845
968
  for (const patch of config.xmlPatches ?? []) {
@@ -902,10 +1025,21 @@ async function loadSceneFromFiles(mujoco, config, onProgress) {
902
1025
  if (isModelTextFile(path)) {
903
1026
  const text = applyXmlPatches(await file.text(), path, config);
904
1027
  textByPath.set(path, text);
905
- mujoco.FS.writeFile(`/working/${path}`, text);
906
1028
  } else {
907
1029
  mujoco.FS.writeFile(`/working/${path}`, new Uint8Array(await file.arrayBuffer()));
1030
+ written.add(path);
908
1031
  }
1032
+ }
1033
+ const environmentXmlByPath = /* @__PURE__ */ new Map();
1034
+ for (const environmentFile of config.environmentFiles?.map(normalizeVfsPath) ?? []) {
1035
+ const environmentXml = findTextByConfiguredPath(textByPath, environmentFile);
1036
+ if (environmentXml) environmentXmlByPath.set(environmentFile, environmentXml);
1037
+ }
1038
+ for (const [path, text] of textByPath) {
1039
+ const composedText = path === config.sceneFile ? composeEnvironmentXml(text, config, parser, environmentXmlByPath) : text;
1040
+ textByPath.set(path, composedText);
1041
+ ensureDir(mujoco, path);
1042
+ mujoco.FS.writeFile(`/working/${path}`, composedText);
909
1043
  written.add(path);
910
1044
  }
911
1045
  for (const [path, text] of textByPath) {
@@ -954,6 +1088,17 @@ async function loadScene(mujoco, config, onProgress) {
954
1088
  } catch {
955
1089
  }
956
1090
  const baseUrl = config.src.endsWith("/") ? config.src : config.src + "/";
1091
+ const environmentXmlByPath = /* @__PURE__ */ new Map();
1092
+ const environmentFiles = config.environmentFiles?.map(normalizeVfsPath) ?? [];
1093
+ for (const environmentFile of environmentFiles) {
1094
+ onProgress?.(`Downloading ${environmentFile}...`);
1095
+ const res = await fetch(baseUrl + environmentFile);
1096
+ if (!res.ok) {
1097
+ console.warn(`Failed to fetch environment XML ${environmentFile}: ${res.status} ${res.statusText}`);
1098
+ continue;
1099
+ }
1100
+ environmentXmlByPath.set(environmentFile, applyXmlPatches(await res.text(), environmentFile, config));
1101
+ }
957
1102
  const downloaded = /* @__PURE__ */ new Set();
958
1103
  const xmlQueue = [config.sceneFile];
959
1104
  const assetFiles = [];
@@ -972,7 +1117,8 @@ async function loadScene(mujoco, config, onProgress) {
972
1117
  console.warn(`Failed to fetch ${fname}: ${res.status} ${res.statusText}`);
973
1118
  continue;
974
1119
  }
975
- const text = applyXmlPatches(await res.text(), fname, config);
1120
+ const patchedText = applyXmlPatches(await res.text(), fname, config);
1121
+ const text = fname === config.sceneFile ? composeEnvironmentXml(patchedText, config, parser, environmentXmlByPath) : patchedText;
976
1122
  ensureDir(mujoco, fname);
977
1123
  mujoco.FS.writeFile(`/working/${fname}`, text);
978
1124
  scanDependencies(text, fname, parser, downloaded, xmlQueue);
@@ -1139,7 +1285,7 @@ function SceneRenderer(props) {
1139
1285
  const model = mjModelRef.current;
1140
1286
  if (model && bodyID < model.nbody && onSelectionRef.current) {
1141
1287
  const name = getName(model, model.name_bodyadr[bodyID]);
1142
- onSelectionRef.current(bodyID, name);
1288
+ onSelectionRef.current({ bodyId: bodyID, name });
1143
1289
  }
1144
1290
  }
1145
1291
  }
@@ -1274,7 +1420,7 @@ function useBeforePhysicsStep(callback) {
1274
1420
  const callbackRef = useRef(callback);
1275
1421
  callbackRef.current = callback;
1276
1422
  useEffect(() => {
1277
- const wrapped = (model, data) => callbackRef.current(model, data);
1423
+ const wrapped = (input) => callbackRef.current(input);
1278
1424
  beforeStepCallbacks.current.add(wrapped);
1279
1425
  return () => {
1280
1426
  beforeStepCallbacks.current.delete(wrapped);
@@ -1286,7 +1432,7 @@ function useAfterPhysicsStep(callback) {
1286
1432
  const callbackRef = useRef(callback);
1287
1433
  callbackRef.current = callback;
1288
1434
  useEffect(() => {
1289
- const wrapped = (model, data) => callbackRef.current(model, data);
1435
+ const wrapped = (input) => callbackRef.current(input);
1290
1436
  afterStepCallbacks.current.add(wrapped);
1291
1437
  return () => {
1292
1438
  afterStepCallbacks.current.delete(wrapped);
@@ -1430,7 +1576,7 @@ function MujocoSimProvider({
1430
1576
  useEffect(() => {
1431
1577
  if (status === "ready") {
1432
1578
  const api2 = apiRef.current;
1433
- if (onReady) onReady(api2);
1579
+ if (onReady) onReady({ api: api2 });
1434
1580
  if (externalApiRef) {
1435
1581
  if (typeof externalApiRef === "function") {
1436
1582
  externalApiRef(api2);
@@ -1450,7 +1596,7 @@ function MujocoSimProvider({
1450
1596
  data.qfrc_applied[i] = 0;
1451
1597
  }
1452
1598
  for (const cb of beforeStepCallbacks.current) {
1453
- cb(model, data);
1599
+ cb({ model, data });
1454
1600
  }
1455
1601
  const numSubsteps = substepsRef.current;
1456
1602
  if (!interpolateRef.current) {
@@ -1501,14 +1647,14 @@ function MujocoSimProvider({
1501
1647
  interpolationStateRef.current.alpha = Math.min(Math.max(physicsAccumulatorRef.current / stepDt, 0), 1);
1502
1648
  interpolationStateRef.current.valid = true;
1503
1649
  if (!stepped) {
1504
- onStepRef.current?.(data.time);
1650
+ onStepRef.current?.({ time: data.time, model, data });
1505
1651
  return;
1506
1652
  }
1507
1653
  }
1508
1654
  for (const cb of afterStepCallbacks.current) {
1509
- cb(model, data);
1655
+ cb({ model, data });
1510
1656
  }
1511
- onStepRef.current?.(data.time);
1657
+ onStepRef.current?.({ time: data.time, model, data });
1512
1658
  }, -1);
1513
1659
  function ensureInterpolationBuffers(model) {
1514
1660
  const state = interpolationStateRef.current;
@@ -1539,7 +1685,7 @@ function MujocoSimProvider({
1539
1685
  }
1540
1686
  }
1541
1687
  }
1542
- configRef.current.onReset?.(model, data);
1688
+ configRef.current.onReset?.({ model, data });
1543
1689
  mujoco.mj_forward(model, data);
1544
1690
  for (const cb of resetCallbacks.current) {
1545
1691
  cb();
@@ -2209,6 +2355,7 @@ var MujocoCanvas = forwardRef(
2209
2355
  paused,
2210
2356
  speed,
2211
2357
  interpolate,
2358
+ loadingFallback,
2212
2359
  children,
2213
2360
  ...canvasProps
2214
2361
  }, ref) {
@@ -2218,7 +2365,10 @@ var MujocoCanvas = forwardRef(
2218
2365
  onError(new Error(wasmError ?? "WASM load failed"));
2219
2366
  }
2220
2367
  }, [wasmStatus, wasmError, onError]);
2221
- if (wasmStatus === "error" || wasmStatus === "loading" || !mujoco) {
2368
+ if (wasmStatus === "loading" || !mujoco) {
2369
+ return loadingFallback ? /* @__PURE__ */ jsx(Canvas, { ...canvasProps, children: loadingFallback }) : null;
2370
+ }
2371
+ if (wasmStatus === "error") {
2222
2372
  return null;
2223
2373
  }
2224
2374
  return /* @__PURE__ */ jsx(Canvas, { ...canvasProps, children: /* @__PURE__ */ jsx(
@@ -2691,9 +2841,9 @@ var useIkController = createControllerHook(
2691
2841
  }
2692
2842
  }, [config?.siteName, config?.numJoints, config?.joints, config?.actuators, status, mjModelRef, mjDataRef, config]);
2693
2843
  const ikSolveFn = useCallback(
2694
- (pos, quat, currentQ) => {
2844
+ ({ position, quaternion, currentQ, context }) => {
2695
2845
  if (!config) return null;
2696
- if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
2846
+ if (config.ikSolveFn) return config.ikSolveFn({ position, quaternion, currentQ, context });
2697
2847
  const model = mjModelRef.current;
2698
2848
  const data = mjDataRef.current;
2699
2849
  const controlGroup = controlGroupRef.current;
@@ -2703,8 +2853,8 @@ var useIkController = createControllerHook(
2703
2853
  data,
2704
2854
  siteIdRef.current,
2705
2855
  controlGroup.qposAdr,
2706
- pos,
2707
- quat,
2856
+ position,
2857
+ quaternion,
2708
2858
  currentQ,
2709
2859
  { damping: config.damping, maxIterations: config.maxIterations }
2710
2860
  );
@@ -2733,7 +2883,7 @@ var useIkController = createControllerHook(
2733
2883
  target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
2734
2884
  if (t >= 1) ga.active = false;
2735
2885
  });
2736
- useBeforePhysicsStep((model, data) => {
2886
+ useBeforePhysicsStep(({ model, data }) => {
2737
2887
  if (!config || !ikEnabledRef.current) {
2738
2888
  ikCalculatingRef.current = false;
2739
2889
  return;
@@ -2744,12 +2894,21 @@ var useIkController = createControllerHook(
2744
2894
  const controlGroup = controlGroupRef.current;
2745
2895
  if (!controlGroup) return;
2746
2896
  const currentQ = Array.from(controlGroup.readQpos(data));
2747
- const solution = config.ikSolveFn ? config.ikSolveFn(target.position, target.quaternion, currentQ, {
2748
- model,
2749
- data,
2750
- siteId: siteIdRef.current,
2751
- controlGroup
2752
- }) : ikSolveFnRef.current(target.position, target.quaternion, currentQ);
2897
+ const solution = config.ikSolveFn ? config.ikSolveFn({
2898
+ position: target.position,
2899
+ quaternion: target.quaternion,
2900
+ currentQ,
2901
+ context: {
2902
+ model,
2903
+ data,
2904
+ siteId: siteIdRef.current,
2905
+ controlGroup
2906
+ }
2907
+ }) : ikSolveFnRef.current({
2908
+ position: target.position,
2909
+ quaternion: target.quaternion,
2910
+ currentQ
2911
+ });
2753
2912
  if (solution) {
2754
2913
  controlGroup.writeCtrl(data, solution);
2755
2914
  }
@@ -2788,8 +2947,8 @@ var useIkController = createControllerHook(
2788
2947
  if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
2789
2948
  }, [mjDataRef]);
2790
2949
  const solveIK = useCallback(
2791
- (pos, quat, currentQ) => {
2792
- return ikSolveFnRef.current(pos, quat, currentQ);
2950
+ (input) => {
2951
+ return ikSolveFnRef.current(input);
2793
2952
  },
2794
2953
  []
2795
2954
  );
@@ -2860,6 +3019,7 @@ function Body({
2860
3019
  solref,
2861
3020
  solimp,
2862
3021
  condim,
3022
+ group,
2863
3023
  children
2864
3024
  }) {
2865
3025
  const { bodyRegistryRef, hiddenBodiesRef, requestBodyReload, mjDataRef, mjModelRef, status } = useMujocoContext();
@@ -2879,7 +3039,8 @@ function Body({
2879
3039
  friction,
2880
3040
  solref,
2881
3041
  solimp,
2882
- condim
3042
+ condim,
3043
+ group
2883
3044
  };
2884
3045
  bodyRegistryRef.current.set(name, { definition, hasCustomChildren: hasChildren });
2885
3046
  if (hasChildren) {
@@ -2892,7 +3053,7 @@ function Body({
2892
3053
  requestBodyReload();
2893
3054
  }
2894
3055
  };
2895
- }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
3056
+ }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, group, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
2896
3057
  useEffect(() => {
2897
3058
  if (status !== "ready") return;
2898
3059
  const model = mjModelRef.current;
@@ -2904,12 +3065,12 @@ function Body({
2904
3065
  if (!hasChildren) return;
2905
3066
  const data = mjDataRef.current;
2906
3067
  const id = bodyIdRef.current;
2907
- const group = groupRef.current;
2908
- if (!data || id < 0 || !group) return;
3068
+ const group2 = groupRef.current;
3069
+ if (!data || id < 0 || !group2) return;
2909
3070
  const i3 = id * 3;
2910
3071
  const i4 = id * 4;
2911
- group.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
2912
- group.quaternion.set(
3072
+ group2.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
3073
+ group2.quaternion.set(
2913
3074
  data.xquat[i4 + 1],
2914
3075
  data.xquat[i4 + 2],
2915
3076
  data.xquat[i4 + 3],
@@ -3001,7 +3162,7 @@ function IkGizmo({ controller, siteName, scale = 0.18, onDrag }) {
3001
3162
  onDrag: (_l, _dl, world) => {
3002
3163
  world.decompose(_pos, _quat, _scale);
3003
3164
  if (onDrag) {
3004
- onDrag(_pos.clone(), _quat.clone());
3165
+ onDrag({ position: _pos.clone(), quaternion: _quat.clone() });
3005
3166
  } else {
3006
3167
  const target = ikTargetRef.current;
3007
3168
  if (target) {
@@ -3175,7 +3336,7 @@ function DragInteraction({
3175
3336
  window.removeEventListener("pointercancel", onPointerUp);
3176
3337
  };
3177
3338
  }, [gl, camera, scene, controls, mjDataRef]);
3178
- useBeforePhysicsStep((model, data) => {
3339
+ useBeforePhysicsStep(({ model, data }) => {
3179
3340
  if (!draggingRef.current || bodyIdRef.current <= 0) return;
3180
3341
  const bid = bodyIdRef.current;
3181
3342
  const mujoco = mujocoRef.current;
@@ -3966,7 +4127,7 @@ function useContacts(bodyName, callback) {
3966
4127
  bodyIdRef.current = findBodyByName(model, bodyName);
3967
4128
  bodyResolvedRef.current = true;
3968
4129
  }, [bodyName, status, mjModelRef]);
3969
- useAfterPhysicsStep((model, data) => {
4130
+ useAfterPhysicsStep(({ model, data }) => {
3970
4131
  if (bodyName && !bodyResolvedRef.current) {
3971
4132
  bodyIdRef.current = findBodyByName(model, bodyName);
3972
4133
  bodyResolvedRef.current = true;
@@ -4072,7 +4233,7 @@ function useTrajectoryPlayer(trajectory, options = {}) {
4072
4233
  const setState = useCallback((next) => {
4073
4234
  if (stateRef.current === next) return;
4074
4235
  stateRef.current = next;
4075
- optionsRef.current.onStateChange?.(next);
4236
+ optionsRef.current.onStateChange?.({ state: next });
4076
4237
  }, []);
4077
4238
  const play = useCallback(() => {
4078
4239
  const traj = trajectoryRef.current;
@@ -4154,7 +4315,7 @@ function useTrajectoryPlayer(trajectory, options = {}) {
4154
4315
  }
4155
4316
  }
4156
4317
  });
4157
- useBeforePhysicsStep((model, data) => {
4318
+ useBeforePhysicsStep(({ model, data }) => {
4158
4319
  if (stateRef.current !== "playing") return;
4159
4320
  if ((optionsRef.current.mode ?? "kinematic") !== "physics") return;
4160
4321
  const traj = trajectoryRef.current;
@@ -4239,7 +4400,10 @@ function TrajectoryPlayer({
4239
4400
  const currentFrame = player.frame;
4240
4401
  if (currentFrame !== lastReportedFrameRef.current && player.playing) {
4241
4402
  lastReportedFrameRef.current = currentFrame;
4242
- onFrameRef.current(currentFrame);
4403
+ onFrameRef.current({
4404
+ frameIndex: currentFrame,
4405
+ frame: trajectory[currentFrame]
4406
+ });
4243
4407
  }
4244
4408
  });
4245
4409
  return null;
@@ -4313,7 +4477,7 @@ function useSitePosition(siteName) {
4313
4477
 
4314
4478
  // src/hooks/useGravityCompensation.ts
4315
4479
  function useGravityCompensation(enabled = true) {
4316
- useBeforePhysicsStep((model, data) => {
4480
+ useBeforePhysicsStep(({ model, data }) => {
4317
4481
  if (!enabled) return;
4318
4482
  for (let i = 0; i < model.nv; i++) {
4319
4483
  data.qfrc_applied[i] += data.qfrc_bias[i];
@@ -4340,7 +4504,7 @@ function useSensor(name) {
4340
4504
  }
4341
4505
  sensorIdRef.current = -1;
4342
4506
  }, [name, status, mjModelRef]);
4343
- useAfterPhysicsStep((_model, data) => {
4507
+ useAfterPhysicsStep(({ data }) => {
4344
4508
  if (sensorIdRef.current < 0) return;
4345
4509
  const adr = sensorAdrRef.current;
4346
4510
  const dim = sensorDimRef.current;
@@ -4437,7 +4601,7 @@ function useJointState(name) {
4437
4601
  }
4438
4602
  jointIdRef.current = -1;
4439
4603
  }, [name, status, mjModelRef]);
4440
- useAfterPhysicsStep((_model, data) => {
4604
+ useAfterPhysicsStep(({ data }) => {
4441
4605
  if (jointIdRef.current < 0) return;
4442
4606
  const qa = qposAdrRef.current;
4443
4607
  const da = dofAdrRef.current;
@@ -4467,7 +4631,7 @@ function useBodyState(name) {
4467
4631
  if (!model || status !== "ready") return;
4468
4632
  bodyIdRef.current = findBodyByName(model, name);
4469
4633
  }, [name, status, mjModelRef]);
4470
- useAfterPhysicsStep((_model, data) => {
4634
+ useAfterPhysicsStep(({ data }) => {
4471
4635
  const bid = bodyIdRef.current;
4472
4636
  if (bid < 0) return;
4473
4637
  const i3 = bid * 3;
@@ -4563,7 +4727,7 @@ function useKeyboardTeleop(config) {
4563
4727
  window.removeEventListener("keyup", onKeyUp);
4564
4728
  };
4565
4729
  }, []);
4566
- useBeforePhysicsStep((_model, data) => {
4730
+ useBeforePhysicsStep(({ data }) => {
4567
4731
  if (!enabledRef.current) return;
4568
4732
  const bindings = bindingsRef.current;
4569
4733
  const cache = actuatorCacheRef.current;
@@ -4584,22 +4748,25 @@ function useKeyboardTeleop(config) {
4584
4748
  });
4585
4749
  }
4586
4750
  function usePolicy(config) {
4587
- const { mjModelRef } = useMujocoContext();
4588
4751
  const lastActionTimeRef = useRef(0);
4752
+ const lastObservationRef = useRef(null);
4589
4753
  const lastActionRef = useRef(null);
4590
- const isRunningRef = useRef(true);
4754
+ const isRunningRef = useRef(config.enabled ?? true);
4591
4755
  const configRef = useRef(config);
4592
4756
  configRef.current = config;
4593
- useBeforePhysicsStep((model, data) => {
4757
+ isRunningRef.current = config.enabled ?? isRunningRef.current;
4758
+ useBeforePhysicsStep(({ model, data }) => {
4594
4759
  if (!isRunningRef.current) return;
4595
4760
  const cfg = configRef.current;
4596
4761
  model.opt?.timestep ?? 2e-3;
4597
4762
  const interval = 1 / cfg.frequency;
4598
4763
  if (data.time - lastActionTimeRef.current >= interval) {
4599
- const obs = cfg.onObservation(model, data);
4600
- cfg.onAction(obs, model, data);
4764
+ const observation = cfg.onObservation({ model, data });
4765
+ const action = cfg.infer ? cfg.infer({ observation, model, data }) : observation;
4766
+ cfg.onAction({ action, observation, model, data });
4601
4767
  lastActionTimeRef.current = data.time;
4602
- lastActionRef.current = obs;
4768
+ lastObservationRef.current = observation;
4769
+ lastActionRef.current = action;
4603
4770
  }
4604
4771
  });
4605
4772
  return {
@@ -4613,6 +4780,9 @@ function usePolicy(config) {
4613
4780
  isRunningRef.current = false;
4614
4781
  },
4615
4782
  get lastObservation() {
4783
+ return lastObservationRef.current;
4784
+ },
4785
+ get lastAction() {
4616
4786
  return lastActionRef.current;
4617
4787
  }
4618
4788
  };
@@ -4642,7 +4812,7 @@ function useTrajectoryRecorder(options = {}) {
4642
4812
  const recordingRef = useRef(false);
4643
4813
  const framesRef = useRef([]);
4644
4814
  const fields = options.fields ?? ["qpos"];
4645
- useAfterPhysicsStep((_model, data) => {
4815
+ useAfterPhysicsStep(({ data }) => {
4646
4816
  if (!recordingRef.current) return;
4647
4817
  const frame = {
4648
4818
  time: data.time,
@@ -4731,7 +4901,7 @@ function useGamepad(config) {
4731
4901
  buttonCacheRef.current.set(Number(idx), findActuatorByName(model, name));
4732
4902
  }
4733
4903
  }, [config.axes, config.buttons, status, mjModelRef]);
4734
- useBeforePhysicsStep((_model, data) => {
4904
+ useBeforePhysicsStep(({ data }) => {
4735
4905
  const cfg = configRef.current;
4736
4906
  if (cfg.enabled === false) return;
4737
4907
  const gamepads = navigator.getGamepads?.();
@@ -4809,12 +4979,120 @@ function useVideoRecorder(options = {}) {
4809
4979
  }
4810
4980
  };
4811
4981
  }
4982
+ function isTargetRef(target) {
4983
+ return Boolean(target && typeof target === "object" && "current" in target);
4984
+ }
4985
+ function resolveCanvasTarget(target) {
4986
+ const resolvedTarget = isTargetRef(target) ? target.current : target;
4987
+ if (!resolvedTarget) {
4988
+ throw new Error("No frame capture target is available.");
4989
+ }
4990
+ if (resolvedTarget instanceof HTMLCanvasElement) {
4991
+ return resolvedTarget;
4992
+ }
4993
+ const canvas = resolvedTarget.querySelector("canvas");
4994
+ if (!canvas) {
4995
+ throw new Error("Frame capture target does not contain a canvas.");
4996
+ }
4997
+ return canvas;
4998
+ }
4999
+ function waitForNextAnimationFrame() {
5000
+ return new Promise((resolve) => {
5001
+ requestAnimationFrame(() => resolve());
5002
+ });
5003
+ }
5004
+ async function captureFrame(options) {
5005
+ const type = options.type ?? "image/png";
5006
+ const canvas = resolveCanvasTarget(options.target);
5007
+ if (options.waitForAnimationFrame ?? true) {
5008
+ await waitForNextAnimationFrame();
5009
+ }
5010
+ return {
5011
+ canvas,
5012
+ dataUrl: canvas.toDataURL(type, options.quality),
5013
+ type
5014
+ };
5015
+ }
5016
+ async function captureFrameBlob(options) {
5017
+ const type = options.type ?? "image/png";
5018
+ const canvas = resolveCanvasTarget(options.target);
5019
+ if (options.waitForAnimationFrame ?? true) {
5020
+ await waitForNextAnimationFrame();
5021
+ }
5022
+ const blob = await new Promise((resolve, reject) => {
5023
+ canvas.toBlob(
5024
+ (nextBlob) => {
5025
+ if (nextBlob) {
5026
+ resolve(nextBlob);
5027
+ } else {
5028
+ reject(new Error("Canvas frame capture did not produce a Blob."));
5029
+ }
5030
+ },
5031
+ type,
5032
+ options.quality
5033
+ );
5034
+ });
5035
+ return { canvas, blob, type };
5036
+ }
5037
+ function useFrameCapture(defaultOptions = {}) {
5038
+ const [status, setStatus] = useState("idle");
5039
+ const [error, setError] = useState(null);
5040
+ const reset = useCallback(() => {
5041
+ setStatus("idle");
5042
+ setError(null);
5043
+ }, []);
5044
+ const capture = useCallback(
5045
+ async (options = {}) => {
5046
+ setStatus("capturing");
5047
+ setError(null);
5048
+ try {
5049
+ const result = await captureFrame({ ...defaultOptions, ...options });
5050
+ setStatus("captured");
5051
+ return result;
5052
+ } catch (nextError) {
5053
+ const error2 = nextError instanceof Error ? nextError : new Error("Unable to capture the current canvas frame.");
5054
+ setError(error2);
5055
+ setStatus("error");
5056
+ throw error2;
5057
+ }
5058
+ },
5059
+ [defaultOptions]
5060
+ );
5061
+ const captureBlob = useCallback(
5062
+ async (options = {}) => {
5063
+ setStatus("capturing");
5064
+ setError(null);
5065
+ try {
5066
+ const result = await captureFrameBlob({
5067
+ ...defaultOptions,
5068
+ ...options
5069
+ });
5070
+ setStatus("captured");
5071
+ return result;
5072
+ } catch (nextError) {
5073
+ const error2 = nextError instanceof Error ? nextError : new Error("Unable to capture the current canvas frame.");
5074
+ setError(error2);
5075
+ setStatus("error");
5076
+ throw error2;
5077
+ }
5078
+ },
5079
+ [defaultOptions]
5080
+ );
5081
+ return {
5082
+ status,
5083
+ error,
5084
+ isCapturing: status === "capturing",
5085
+ capture,
5086
+ captureBlob,
5087
+ reset
5088
+ };
5089
+ }
4812
5090
  function useCtrlNoise(config = {}) {
4813
5091
  const { mjModelRef } = useMujocoContext();
4814
5092
  const configRef = useRef(config);
4815
5093
  configRef.current = config;
4816
5094
  const noiseRef = useRef(null);
4817
- useBeforePhysicsStep((_model, data) => {
5095
+ useBeforePhysicsStep(({ data }) => {
4818
5096
  const cfg = configRef.current;
4819
5097
  if (cfg.enabled === false) return;
4820
5098
  const rate = cfg.rate ?? 0.01;
@@ -5094,6 +5372,12 @@ function useCameraAnimation() {
5094
5372
  *
5095
5373
  * useVideoRecorder — canvas video recording hook (spec 13.3)
5096
5374
  */
5375
+ /**
5376
+ * @license
5377
+ * SPDX-License-Identifier: Apache-2.0
5378
+ *
5379
+ * useFrameCapture — still-frame capture for canvas-backed MuJoCo/R3F scenes.
5380
+ */
5097
5381
  /**
5098
5382
  * @license
5099
5383
  * SPDX-License-Identifier: Apache-2.0
@@ -5124,6 +5408,6 @@ function useCameraAnimation() {
5124
5408
  * useCameraAnimation — composable camera animation hook.
5125
5409
  */
5126
5410
 
5127
- export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, InstancedGeomRenderer, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, RobotActuators, RobotBodies, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, registerRobotResources, 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 };
5411
+ export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, InstancedGeomRenderer, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, RobotActuators, RobotBodies, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, captureFrame, captureFrameBlob, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, registerRobotResources, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useFrameCapture, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
5128
5412
  //# sourceMappingURL=index.js.map
5129
5413
  //# sourceMappingURL=index.js.map