mujoco-react 8.9.2 → 8.11.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
@@ -1,3 +1,4 @@
1
+ export { ScenarioLighting, SplatEnvironment, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getScenarioBackground, getScenarioCameraPosition, useSplatEnvironment, useVisualScenarioEffects, withSplatEnvironment } from './chunk-SEWQULWO.js';
1
2
  import loadMujoco from '@mujoco/mujoco';
2
3
  import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
3
4
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
@@ -6,7 +7,6 @@ import { Canvas, useThree, useFrame } from '@react-three/fiber';
6
7
  import * as THREE11 from 'three';
7
8
  import { PivotControls } from '@react-three/drei';
8
9
 
9
- // src/core/MujocoProvider.tsx
10
10
  var MujocoContext = createContext({
11
11
  mujoco: null,
12
12
  status: "loading",
@@ -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);
@@ -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(
@@ -2860,6 +3010,7 @@ function Body({
2860
3010
  solref,
2861
3011
  solimp,
2862
3012
  condim,
3013
+ group,
2863
3014
  children
2864
3015
  }) {
2865
3016
  const { bodyRegistryRef, hiddenBodiesRef, requestBodyReload, mjDataRef, mjModelRef, status } = useMujocoContext();
@@ -2879,7 +3030,8 @@ function Body({
2879
3030
  friction,
2880
3031
  solref,
2881
3032
  solimp,
2882
- condim
3033
+ condim,
3034
+ group
2883
3035
  };
2884
3036
  bodyRegistryRef.current.set(name, { definition, hasCustomChildren: hasChildren });
2885
3037
  if (hasChildren) {
@@ -2892,7 +3044,7 @@ function Body({
2892
3044
  requestBodyReload();
2893
3045
  }
2894
3046
  };
2895
- }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
3047
+ }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, group, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
2896
3048
  useEffect(() => {
2897
3049
  if (status !== "ready") return;
2898
3050
  const model = mjModelRef.current;
@@ -2904,12 +3056,12 @@ function Body({
2904
3056
  if (!hasChildren) return;
2905
3057
  const data = mjDataRef.current;
2906
3058
  const id = bodyIdRef.current;
2907
- const group = groupRef.current;
2908
- if (!data || id < 0 || !group) return;
3059
+ const group2 = groupRef.current;
3060
+ if (!data || id < 0 || !group2) return;
2909
3061
  const i3 = id * 3;
2910
3062
  const i4 = id * 4;
2911
- group.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
2912
- group.quaternion.set(
3063
+ group2.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
3064
+ group2.quaternion.set(
2913
3065
  data.xquat[i4 + 1],
2914
3066
  data.xquat[i4 + 2],
2915
3067
  data.xquat[i4 + 3],
@@ -3242,24 +3394,35 @@ function useSceneLights(intensity = 1) {
3242
3394
  targetsRef.current = [];
3243
3395
  const nlight = model.nlight ?? 0;
3244
3396
  if (nlight === 0) return;
3397
+ const lightActive = getModelArray(model, "light_active");
3398
+ const lightTypeArray = getModelArray(model, "light_type");
3399
+ const lightCastShadow = getModelArray(model, "light_castshadow");
3400
+ const lightIntensity = getModelArray(model, "light_intensity");
3401
+ const lightDiffuse = getModelArray(model, "light_diffuse");
3402
+ const lightPos = getModelArray(model, "light_pos");
3403
+ const lightDir = getModelArray(model, "light_dir");
3404
+ const lightCutoff = getModelArray(model, "light_cutoff");
3405
+ const lightExponent = getModelArray(model, "light_exponent");
3406
+ const lightAttenuation = getModelArray(model, "light_attenuation");
3407
+ if (!lightPos || !lightDir) return;
3245
3408
  for (let i = 0; i < nlight; i++) {
3246
- const active = model.light_active ? model.light_active[i] : 1;
3409
+ const active = lightActive ? lightActive[i] : 1;
3247
3410
  if (!active) continue;
3248
- const lightType = model.light_type ? model.light_type[i] : 0;
3411
+ const lightType = lightTypeArray ? lightTypeArray[i] : 0;
3249
3412
  const isDirectional = lightType === 0;
3250
- const castShadow = model.light_castshadow ? model.light_castshadow[i] !== 0 : false;
3251
- const mjIntensity = model.light_intensity ? model.light_intensity[i] : 1;
3413
+ const castShadow = lightCastShadow ? lightCastShadow[i] !== 0 : false;
3414
+ const mjIntensity = lightIntensity ? lightIntensity[i] : 1;
3252
3415
  const finalIntensity = intensity * mjIntensity;
3253
- const dr = model.light_diffuse ? model.light_diffuse[3 * i] : 1;
3254
- const dg = model.light_diffuse ? model.light_diffuse[3 * i + 1] : 1;
3255
- const db = model.light_diffuse ? model.light_diffuse[3 * i + 2] : 1;
3416
+ const dr = lightDiffuse ? lightDiffuse[3 * i] : 1;
3417
+ const dg = lightDiffuse ? lightDiffuse[3 * i + 1] : 1;
3418
+ const db = lightDiffuse ? lightDiffuse[3 * i + 2] : 1;
3256
3419
  const color = new THREE11.Color(dr, dg, db);
3257
- const px = model.light_pos[3 * i];
3258
- const py = model.light_pos[3 * i + 1];
3259
- const pz = model.light_pos[3 * i + 2];
3260
- const dx = model.light_dir[3 * i];
3261
- const dy = model.light_dir[3 * i + 1];
3262
- const dz = model.light_dir[3 * i + 2];
3420
+ const px = lightPos[3 * i];
3421
+ const py = lightPos[3 * i + 1];
3422
+ const pz = lightPos[3 * i + 2];
3423
+ const dx = lightDir[3 * i];
3424
+ const dy = lightDir[3 * i + 1];
3425
+ const dz = lightDir[3 * i + 2];
3263
3426
  if (isDirectional) {
3264
3427
  const light = new THREE11.DirectionalLight(color, finalIntensity);
3265
3428
  light.position.set(px, py, pz);
@@ -3281,16 +3444,16 @@ function useSceneLights(intensity = 1) {
3281
3444
  lightsRef.current.push(light);
3282
3445
  targetsRef.current.push(light.target);
3283
3446
  } else {
3284
- const cutoff = model.light_cutoff ? model.light_cutoff[i] : 45;
3285
- const exponent = model.light_exponent ? model.light_exponent[i] : 10;
3447
+ const cutoff = lightCutoff ? lightCutoff[i] : 45;
3448
+ const exponent = lightExponent ? lightExponent[i] : 10;
3286
3449
  const angle = cutoff * Math.PI / 180;
3287
3450
  const light = new THREE11.SpotLight(color, finalIntensity, 0, angle, exponent / 128);
3288
3451
  light.position.set(px, py, pz);
3289
3452
  light.target.position.set(px + dx, py + dy, pz + dz);
3290
3453
  light.castShadow = castShadow;
3291
- if (model.light_attenuation) {
3292
- const att1 = model.light_attenuation[3 * i + 1];
3293
- const att2 = model.light_attenuation[3 * i + 2];
3454
+ if (lightAttenuation) {
3455
+ const att1 = lightAttenuation[3 * i + 1];
3456
+ const att2 = lightAttenuation[3 * i + 2];
3294
3457
  light.decay = att2 > 0 ? 2 : att1 > 0 ? 1 : 0;
3295
3458
  light.distance = att1 > 0 ? 1 / att1 : 0;
3296
3459
  }
@@ -3315,6 +3478,17 @@ function useSceneLights(intensity = 1) {
3315
3478
  };
3316
3479
  }, [status, mjModelRef, scene, intensity]);
3317
3480
  }
3481
+ function getModelArray(model, key) {
3482
+ try {
3483
+ const value = model[key];
3484
+ return isArrayLikeNumber(value) ? value : void 0;
3485
+ } catch {
3486
+ return void 0;
3487
+ }
3488
+ }
3489
+ function isArrayLikeNumber(value) {
3490
+ return typeof value === "object" && value !== null && "length" in value && typeof value.length === "number";
3491
+ }
3318
3492
 
3319
3493
  // src/components/SceneLights.tsx
3320
3494
  function SceneLights({ intensity = 1 }) {
@@ -4562,22 +4736,25 @@ function useKeyboardTeleop(config) {
4562
4736
  });
4563
4737
  }
4564
4738
  function usePolicy(config) {
4565
- const { mjModelRef } = useMujocoContext();
4566
4739
  const lastActionTimeRef = useRef(0);
4740
+ const lastObservationRef = useRef(null);
4567
4741
  const lastActionRef = useRef(null);
4568
- const isRunningRef = useRef(true);
4742
+ const isRunningRef = useRef(config.enabled ?? true);
4569
4743
  const configRef = useRef(config);
4570
4744
  configRef.current = config;
4745
+ isRunningRef.current = config.enabled ?? isRunningRef.current;
4571
4746
  useBeforePhysicsStep((model, data) => {
4572
4747
  if (!isRunningRef.current) return;
4573
4748
  const cfg = configRef.current;
4574
4749
  model.opt?.timestep ?? 2e-3;
4575
4750
  const interval = 1 / cfg.frequency;
4576
4751
  if (data.time - lastActionTimeRef.current >= interval) {
4577
- const obs = cfg.onObservation(model, data);
4578
- cfg.onAction(obs, model, data);
4752
+ const observation = cfg.onObservation({ model, data });
4753
+ const action = cfg.infer ? cfg.infer({ observation, model, data }) : observation;
4754
+ cfg.onAction({ action, observation, model, data });
4579
4755
  lastActionTimeRef.current = data.time;
4580
- lastActionRef.current = obs;
4756
+ lastObservationRef.current = observation;
4757
+ lastActionRef.current = action;
4581
4758
  }
4582
4759
  });
4583
4760
  return {
@@ -4591,6 +4768,9 @@ function usePolicy(config) {
4591
4768
  isRunningRef.current = false;
4592
4769
  },
4593
4770
  get lastObservation() {
4771
+ return lastObservationRef.current;
4772
+ },
4773
+ get lastAction() {
4594
4774
  return lastActionRef.current;
4595
4775
  }
4596
4776
  };
@@ -4787,6 +4967,114 @@ function useVideoRecorder(options = {}) {
4787
4967
  }
4788
4968
  };
4789
4969
  }
4970
+ function isTargetRef(target) {
4971
+ return Boolean(target && typeof target === "object" && "current" in target);
4972
+ }
4973
+ function resolveCanvasTarget(target) {
4974
+ const resolvedTarget = isTargetRef(target) ? target.current : target;
4975
+ if (!resolvedTarget) {
4976
+ throw new Error("No frame capture target is available.");
4977
+ }
4978
+ if (resolvedTarget instanceof HTMLCanvasElement) {
4979
+ return resolvedTarget;
4980
+ }
4981
+ const canvas = resolvedTarget.querySelector("canvas");
4982
+ if (!canvas) {
4983
+ throw new Error("Frame capture target does not contain a canvas.");
4984
+ }
4985
+ return canvas;
4986
+ }
4987
+ function waitForNextAnimationFrame() {
4988
+ return new Promise((resolve) => {
4989
+ requestAnimationFrame(() => resolve());
4990
+ });
4991
+ }
4992
+ async function captureFrame(options) {
4993
+ const type = options.type ?? "image/png";
4994
+ const canvas = resolveCanvasTarget(options.target);
4995
+ if (options.waitForAnimationFrame ?? true) {
4996
+ await waitForNextAnimationFrame();
4997
+ }
4998
+ return {
4999
+ canvas,
5000
+ dataUrl: canvas.toDataURL(type, options.quality),
5001
+ type
5002
+ };
5003
+ }
5004
+ async function captureFrameBlob(options) {
5005
+ const type = options.type ?? "image/png";
5006
+ const canvas = resolveCanvasTarget(options.target);
5007
+ if (options.waitForAnimationFrame ?? true) {
5008
+ await waitForNextAnimationFrame();
5009
+ }
5010
+ const blob = await new Promise((resolve, reject) => {
5011
+ canvas.toBlob(
5012
+ (nextBlob) => {
5013
+ if (nextBlob) {
5014
+ resolve(nextBlob);
5015
+ } else {
5016
+ reject(new Error("Canvas frame capture did not produce a Blob."));
5017
+ }
5018
+ },
5019
+ type,
5020
+ options.quality
5021
+ );
5022
+ });
5023
+ return { canvas, blob, type };
5024
+ }
5025
+ function useFrameCapture(defaultOptions = {}) {
5026
+ const [status, setStatus] = useState("idle");
5027
+ const [error, setError] = useState(null);
5028
+ const reset = useCallback(() => {
5029
+ setStatus("idle");
5030
+ setError(null);
5031
+ }, []);
5032
+ const capture = useCallback(
5033
+ async (options = {}) => {
5034
+ setStatus("capturing");
5035
+ setError(null);
5036
+ try {
5037
+ const result = await captureFrame({ ...defaultOptions, ...options });
5038
+ setStatus("captured");
5039
+ return result;
5040
+ } catch (nextError) {
5041
+ const error2 = nextError instanceof Error ? nextError : new Error("Unable to capture the current canvas frame.");
5042
+ setError(error2);
5043
+ setStatus("error");
5044
+ throw error2;
5045
+ }
5046
+ },
5047
+ [defaultOptions]
5048
+ );
5049
+ const captureBlob = useCallback(
5050
+ async (options = {}) => {
5051
+ setStatus("capturing");
5052
+ setError(null);
5053
+ try {
5054
+ const result = await captureFrameBlob({
5055
+ ...defaultOptions,
5056
+ ...options
5057
+ });
5058
+ setStatus("captured");
5059
+ return result;
5060
+ } catch (nextError) {
5061
+ const error2 = nextError instanceof Error ? nextError : new Error("Unable to capture the current canvas frame.");
5062
+ setError(error2);
5063
+ setStatus("error");
5064
+ throw error2;
5065
+ }
5066
+ },
5067
+ [defaultOptions]
5068
+ );
5069
+ return {
5070
+ status,
5071
+ error,
5072
+ isCapturing: status === "capturing",
5073
+ capture,
5074
+ captureBlob,
5075
+ reset
5076
+ };
5077
+ }
4790
5078
  function useCtrlNoise(config = {}) {
4791
5079
  const { mjModelRef } = useMujocoContext();
4792
5080
  const configRef = useRef(config);
@@ -5072,6 +5360,12 @@ function useCameraAnimation() {
5072
5360
  *
5073
5361
  * useVideoRecorder — canvas video recording hook (spec 13.3)
5074
5362
  */
5363
+ /**
5364
+ * @license
5365
+ * SPDX-License-Identifier: Apache-2.0
5366
+ *
5367
+ * useFrameCapture — still-frame capture for canvas-backed MuJoCo/R3F scenes.
5368
+ */
5075
5369
  /**
5076
5370
  * @license
5077
5371
  * SPDX-License-Identifier: Apache-2.0
@@ -5102,6 +5396,6 @@ function useCameraAnimation() {
5102
5396
  * useCameraAnimation — composable camera animation hook.
5103
5397
  */
5104
5398
 
5105
- 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 };
5399
+ 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 };
5106
5400
  //# sourceMappingURL=index.js.map
5107
5401
  //# sourceMappingURL=index.js.map