mujoco-react 8.11.0 → 9.1.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/spark.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as _sparkjsdev_spark from '@sparkjsdev/spark';
3
- import { l as SplatEnvironmentProps } from './types-BmneHLBM.js';
3
+ import { n as SplatEnvironmentProps } from './types-C5gTvR7b.js';
4
4
  import 'react';
5
5
  import '@react-three/fiber';
6
6
  import 'three';
package/dist/spark.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useSplatEnvironment, SplatEnvironment } from './chunk-SEWQULWO.js';
1
+ import { useSplatEnvironment, SplatEnvironment } from './chunk-33CV6HSV.js';
2
2
  import { useThree } from '@react-three/fiber';
3
3
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
4
4
  import * as THREE from 'three';
@@ -327,7 +327,7 @@ interface LoadFromFilesOptions {
327
327
  homeJoints?: number[];
328
328
  xmlPatches?: XmlPatch[];
329
329
  sceneObjects?: SceneObject[];
330
- onReset?: (model: MujocoModel, data: MujocoData) => void;
330
+ onReset?: (input: ResetCallbackInput) => void;
331
331
  }
332
332
  interface SceneConfig {
333
333
  /** Base URL for fetching model files. The loader fetches `src + sceneFile` and follows dependencies. */
@@ -347,7 +347,7 @@ interface SceneConfig {
347
347
  sceneObjects?: SceneObject[];
348
348
  homeJoints?: number[];
349
349
  xmlPatches?: XmlPatch[];
350
- onReset?: (model: MujocoModel, data: MujocoData) => void;
350
+ onReset?: (input: ResetCallbackInput) => void;
351
351
  }
352
352
  type ResourceSelector<TInfo, TName extends string = string> = TName | readonly TName[] | RegExp | ((info: TInfo) => boolean);
353
353
  interface IkConfig {
@@ -380,7 +380,7 @@ interface IkContextValue {
380
380
  setIkEnabled: (enabled: boolean) => void;
381
381
  moveTarget: (pos: THREE.Vector3, duration?: number) => void;
382
382
  syncTargetToSite: () => void;
383
- solveIK: (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]) => number[] | null;
383
+ solveIK: (input: IkSolveInput) => number[] | null;
384
384
  getGizmoStats: () => {
385
385
  pos: THREE.Vector3;
386
386
  rot: THREE.Euler;
@@ -398,14 +398,38 @@ interface PhysicsConfig {
398
398
  paused?: boolean;
399
399
  speed?: number;
400
400
  }
401
- type IKSolveFn = (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[], context?: IKSolveContext) => number[] | null;
401
+ type IKSolveFn = (input: IkSolveInput) => number[] | null;
402
+ interface IkSolveInput {
403
+ position: THREE.Vector3;
404
+ quaternion: THREE.Quaternion;
405
+ currentQ: number[];
406
+ context?: IKSolveContext;
407
+ }
402
408
  interface IKSolveContext {
403
409
  model: MujocoModel;
404
410
  data: MujocoData;
405
411
  siteId: number;
406
412
  controlGroup: ControlGroupInfo;
407
413
  }
408
- type PhysicsStepCallback = (model: MujocoModel, data: MujocoData) => void;
414
+ interface PhysicsStepInput {
415
+ model: MujocoModel;
416
+ data: MujocoData;
417
+ }
418
+ interface ResetCallbackInput extends PhysicsStepInput {
419
+ }
420
+ interface ReadyCallbackInput {
421
+ api: MujocoSimAPI;
422
+ }
423
+ interface StepCallbackInput {
424
+ time: number;
425
+ model: MujocoModel;
426
+ data: MujocoData;
427
+ }
428
+ interface SelectionCallbackInput {
429
+ bodyId: number;
430
+ name: string;
431
+ }
432
+ type PhysicsStepCallback = (input: PhysicsStepInput) => void;
409
433
  interface StateSnapshot {
410
434
  time: number;
411
435
  qpos: Float64Array;
@@ -606,7 +630,11 @@ interface IkGizmoProps {
606
630
  controller: IkContextValue;
607
631
  siteName?: string;
608
632
  scale?: number;
609
- onDrag?: (position: THREE.Vector3, quaternion: THREE.Quaternion) => void;
633
+ onDrag?: (input: IkGizmoDragInput) => void;
634
+ }
635
+ interface IkGizmoDragInput {
636
+ position: THREE.Vector3;
637
+ quaternion: THREE.Quaternion;
610
638
  }
611
639
  interface DragInteractionProps {
612
640
  stiffness?: number;
@@ -718,7 +746,11 @@ interface VisualScenarioEffectsProps {
718
746
  background?: THREE.ColorRepresentation;
719
747
  fogNear?: number;
720
748
  fogFar?: number;
721
- materialFilter?: (object: THREE.Object3D, material: THREE.Material) => boolean;
749
+ materialFilter?: (input: VisualScenarioMaterialFilterInput) => boolean;
750
+ }
751
+ interface VisualScenarioMaterialFilterInput {
752
+ object: THREE.Object3D;
753
+ material: THREE.Material;
722
754
  }
723
755
  type TrajectoryInput = TrajectoryFrame[] | number[][];
724
756
  interface TrajectoryPlayerProps {
@@ -728,9 +760,16 @@ interface TrajectoryPlayerProps {
728
760
  loop?: boolean;
729
761
  playing?: boolean;
730
762
  mode?: 'kinematic' | 'physics';
731
- onFrame?: (frameIdx: number) => void;
763
+ onFrame?: (input: TrajectoryFrameCallbackInput) => void;
732
764
  onComplete?: () => void;
733
- onStateChange?: (state: PlaybackState) => void;
765
+ onStateChange?: (input: TrajectoryStateChangeInput) => void;
766
+ }
767
+ interface TrajectoryFrameCallbackInput {
768
+ frameIndex: number;
769
+ frame: TrajectoryFrame | number[] | undefined;
770
+ }
771
+ interface TrajectoryStateChangeInput {
772
+ state: PlaybackState;
734
773
  }
735
774
  interface ContactListenerProps {
736
775
  body: Bodies;
@@ -798,7 +837,10 @@ interface MujocoSimAPI {
798
837
  addBody(body: SceneObject): Promise<void>;
799
838
  removeBody(name: Bodies): Promise<void>;
800
839
  recompile(patches?: XmlPatch[]): Promise<void>;
840
+ getCanvas(): HTMLCanvasElement | null;
801
841
  getCanvasSnapshot(width?: number, height?: number, mimeType?: string): string;
842
+ captureFrame(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureResult>;
843
+ captureFrameBlob(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureBlobResult>;
802
844
  project2DTo3D(x: number, y: number, cameraPos: THREE.Vector3, lookAt: THREE.Vector3): {
803
845
  point: THREE.Vector3;
804
846
  bodyId: number;
@@ -810,14 +852,42 @@ interface MujocoSimAPI {
810
852
  readonly mjModelRef: React__default.RefObject<MujocoModel | null>;
811
853
  readonly mjDataRef: React__default.RefObject<MujocoData | null>;
812
854
  }
855
+ type FrameCaptureStatus = 'idle' | 'capturing' | 'captured' | 'error';
856
+ type FrameCaptureTarget = HTMLCanvasElement | HTMLElement | null | undefined;
857
+ type FrameCaptureTargetRef = React__default.RefObject<HTMLCanvasElement | HTMLElement | null>;
858
+ interface FrameCaptureOptions {
859
+ target?: FrameCaptureTarget | FrameCaptureTargetRef;
860
+ type?: string;
861
+ quality?: number;
862
+ waitForAnimationFrame?: boolean;
863
+ }
864
+ type MujocoFrameCaptureOptions = Omit<FrameCaptureOptions, 'target'>;
865
+ interface FrameCaptureResult {
866
+ canvas: HTMLCanvasElement;
867
+ dataUrl: string;
868
+ type: string;
869
+ }
870
+ interface FrameCaptureBlobResult {
871
+ canvas: HTMLCanvasElement;
872
+ blob: Blob;
873
+ type: string;
874
+ }
875
+ interface FrameCaptureAPI {
876
+ status: FrameCaptureStatus;
877
+ error: Error | null;
878
+ isCapturing: boolean;
879
+ capture: (options?: FrameCaptureOptions) => Promise<FrameCaptureResult>;
880
+ captureBlob: (options?: FrameCaptureOptions) => Promise<FrameCaptureBlobResult>;
881
+ reset: () => void;
882
+ }
813
883
  type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
814
884
  config: SceneConfig;
815
885
  /** R3F content rendered while the MuJoCo WASM module is still loading. */
816
886
  loadingFallback?: ReactNode;
817
- onReady?: (api: MujocoSimAPI) => void;
887
+ onReady?: (input: ReadyCallbackInput) => void;
818
888
  onError?: (error: Error) => void;
819
- onStep?: (time: number) => void;
820
- onSelection?: (bodyId: number, name: string) => void;
889
+ onStep?: (input: StepCallbackInput) => void;
890
+ onSelection?: (input: SelectionCallbackInput) => void;
821
891
  gravity?: [number, number, number];
822
892
  timestep?: number;
823
893
  substeps?: number;
@@ -868,4 +938,4 @@ interface JointStateResult {
868
938
  velocity: React__default.RefObject<number | Float64Array>;
869
939
  }
870
940
 
871
- export { type TrajectoryInput as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type SitePositionResult as E, type Sensors as F, type GeomInfo as G, type SensorHandle as H, type IkConfig as I, type SensorInfo as J, type Joints as K, type JointStateResult as L, type MujocoContextValue as M, type Bodies as N, type ObservationConfig as O, type PhysicsStepCallback as P, type BodyStateResult as Q, type Actuators as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type CtrlHandle as U, type VisualScenarioEffectsProps as V, type ContactInfo as W, type KeyboardTeleopConfig as X, type PolicyConfig as Y, type PolicyVector as Z, type ObservationHandle as _, type MujocoCanvasProps as a, type PlaybackState as a0, type TrajectoryFrame as a1, type BodyInfo as a2, type ControlJointInfo as a3, type Geoms as a4, type IKSolveFn as a5, type JointInfo as a6, type KeyBinding as a7, type Keyframes as a8, type ModelOptions as a9, type SensorResult as aA, type SiteInfo as aB, type SplatAssetConfig as aC, type SplatScenarioConfig as aD, type StateSnapshot as aE, type TrajectoryData as aF, type XmlPatch as aG, getContact as aH, registerRobotResources as aI, type MujocoContact as aa, type MujocoContactArray as ab, type ObservationLayoutItem as ac, type ObservationOutput as ad, type PhysicsConfig as ae, type PolicyActionInput as af, type PolicyInferenceInput as ag, type PolicyObservationInput as ah, type RayHit as ai, type Register as aj, type RegisteredRobotMap as ak, type ResourceSelector as al, RobotActuators as am, RobotBodies as an, RobotGeoms as ao, RobotJoints as ap, RobotKeyframes as aq, type RobotResource as ar, RobotResources as as, RobotSensors as at, RobotSites as au, type Robots as av, type ScenarioCameraConfig as aw, type ScenarioMaterialConfig as ax, type SceneMarker as ay, type SceneObject as az, type MujocoSimAPI as b, type MujocoModule as c, type MujocoModel as d, type MujocoData as e, type ControlGroupSelector as f, type ObservationResult as g, type IkContextValue as h, type IkGizmoProps as i, type SceneLightsProps as j, type ScenarioLightingProps as k, type SplatEnvironmentProps as l, type VisualScenarioConfig as m, type SplatRendererKind as n, type PairedSplatEnvironmentConfig as o, type SplatFormat as p, type SplatCollisionProxyConfig as q, type SplatCollisionPrimitive as r, type ScenarioLightingPreset as s, type SplatEnvironmentMetadataInput as t, type SplatEnvironmentMetadata as u, type SplatSceneInput as v, type DebugProps as w, type ContactListenerProps as x, type ActuatorInfo as y, type Sites as z };
941
+ export { type PolicyConfig as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type ActuatorInfo as E, type Sites as F, type GeomInfo as G, type SitePositionResult as H, type IkConfig as I, type Sensors as J, type SensorHandle as K, type SensorInfo as L, type MujocoContextValue as M, type Joints as N, type ObservationConfig as O, type PhysicsStepCallback as P, type JointStateResult as Q, type ReadyCallbackInput as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type Bodies as U, type VisualScenarioEffectsProps as V, type BodyStateResult as W, type Actuators as X, type CtrlHandle as Y, type ContactInfo as Z, type KeyboardTeleopConfig as _, type MujocoCanvasProps as a, type PolicyVector as a0, type ObservationHandle as a1, type TrajectoryInput as a2, type TrajectoryStateChangeInput as a3, type PlaybackState as a4, type TrajectoryFrame as a5, type FrameCaptureOptions as a6, type FrameCaptureResult as a7, type FrameCaptureBlobResult as a8, type FrameCaptureAPI as a9, type ResetCallbackInput as aA, type ResourceSelector as aB, RobotActuators as aC, RobotBodies as aD, RobotGeoms as aE, RobotJoints as aF, RobotKeyframes as aG, type RobotResource as aH, RobotResources as aI, RobotSensors as aJ, RobotSites as aK, type Robots as aL, type ScenarioCameraConfig as aM, type ScenarioMaterialConfig as aN, type SceneMarker as aO, type SceneObject as aP, type SensorResult as aQ, type SiteInfo as aR, type SplatAssetConfig as aS, type SplatScenarioConfig as aT, type StateSnapshot as aU, type TrajectoryData as aV, type TrajectoryFrameCallbackInput as aW, type VisualScenarioMaterialFilterInput as aX, type XmlPatch as aY, getContact as aZ, registerRobotResources as a_, type BodyInfo as aa, type ControlJointInfo as ab, type FrameCaptureStatus as ac, type FrameCaptureTarget as ad, type FrameCaptureTargetRef as ae, type Geoms as af, type IKSolveFn as ag, type IkGizmoDragInput as ah, type IkSolveInput as ai, type JointInfo as aj, type KeyBinding as ak, type Keyframes as al, type ModelOptions as am, type MujocoContact as an, type MujocoContactArray as ao, type MujocoFrameCaptureOptions as ap, type ObservationLayoutItem as aq, type ObservationOutput as ar, type PhysicsConfig as as, type PhysicsStepInput as at, type PolicyActionInput as au, type PolicyInferenceInput as av, type PolicyObservationInput as aw, type RayHit as ax, type Register as ay, type RegisteredRobotMap as az, type MujocoSimAPI as b, type StepCallbackInput as c, type SelectionCallbackInput as d, type MujocoModule as e, type MujocoModel as f, type MujocoData as g, type ControlGroupSelector as h, type ObservationResult as i, type IkContextValue as j, type IkGizmoProps as k, type SceneLightsProps as l, type ScenarioLightingProps as m, type SplatEnvironmentProps as n, type VisualScenarioConfig as o, type SplatRendererKind as p, type PairedSplatEnvironmentConfig as q, type SplatFormat as r, type SplatCollisionProxyConfig as s, type SplatCollisionPrimitive as t, type ScenarioLightingPreset as u, type SplatEnvironmentMetadataInput as v, type SplatEnvironmentMetadata as w, type SplatSceneInput as x, type DebugProps as y, type ContactListenerProps as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "8.11.0",
3
+ "version": "9.1.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -168,7 +168,7 @@ export function DragInteraction({
168
168
  }, [gl, camera, scene, controls, mjDataRef]);
169
169
 
170
170
  // Apply spring force each physics frame
171
- useBeforePhysicsStep((model, data) => {
171
+ useBeforePhysicsStep(({ model, data }) => {
172
172
  if (!draggingRef.current || bodyIdRef.current <= 0) return;
173
173
 
174
174
  const bid = bodyIdRef.current;
@@ -26,7 +26,7 @@ const _scale = new THREE.Vector3(1, 1, 1);
26
26
  * - `controller` — IkContextValue from `useIkController()`.
27
27
  * - `siteName` — MuJoCo site to track. Defaults to the controller's configured site.
28
28
  * - `scale` — Gizmo handle scale. Default: 0.18.
29
- * - `onDrag` — Custom drag callback `(pos, quat) => void`.
29
+ * - `onDrag` — Custom drag callback `({ position, quaternion }) => void`.
30
30
  * When omitted, dragging enables IK and writes to the IK target.
31
31
  * When provided, the consumer handles what happens during drag.
32
32
  */
@@ -112,7 +112,7 @@ export function IkGizmo({ controller, siteName, scale = 0.18, onDrag }: IkGizmoP
112
112
  world.decompose(_pos, _quat, _scale);
113
113
  if (onDrag) {
114
114
  // Custom: consumer handles the drag
115
- onDrag(_pos.clone(), _quat.clone());
115
+ onDrag({ position: _pos.clone(), quaternion: _quat.clone() });
116
116
  } else {
117
117
  // Default: write to IK target
118
118
  const target = ikTargetRef.current;
@@ -139,7 +139,7 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
139
139
  const model = mjModelRef.current;
140
140
  if (model && bodyID < model.nbody && onSelectionRef.current) {
141
141
  const name = getName(model, model.name_bodyadr[bodyID]);
142
- onSelectionRef.current(bodyID, name);
142
+ onSelectionRef.current({ bodyId: bodyID, name });
143
143
  }
144
144
  }
145
145
  }}
@@ -54,7 +54,10 @@ export function TrajectoryPlayer({
54
54
  const currentFrame = player.frame;
55
55
  if (currentFrame !== lastReportedFrameRef.current && player.playing) {
56
56
  lastReportedFrameRef.current = currentFrame;
57
- onFrameRef.current(currentFrame);
57
+ onFrameRef.current({
58
+ frameIndex: currentFrame,
59
+ frame: trajectory[currentFrame],
60
+ });
58
61
  }
59
62
  });
60
63
 
@@ -490,7 +490,7 @@ function applyScenarioMaterials(
490
490
  for (const material of normalizeMaterials(object.material)) {
491
491
  const mutable = getMutableScenarioMaterial(material);
492
492
  if (!mutable) continue;
493
- if (materialFilter && !materialFilter(object, material)) continue;
493
+ if (materialFilter && !materialFilter({ object, material })) continue;
494
494
 
495
495
  if (!snapshots.has(material)) {
496
496
  snapshots.set(material, {
@@ -6,19 +6,25 @@
6
6
  import { forwardRef, useEffect } from 'react';
7
7
  import { useMujocoWasm } from './MujocoProvider';
8
8
  import { MujocoSimProvider } from './MujocoSimProvider';
9
- import type { MujocoSimAPI, SceneConfig } from '../types';
9
+ import type {
10
+ MujocoSimAPI,
11
+ ReadyCallbackInput,
12
+ SceneConfig,
13
+ SelectionCallbackInput,
14
+ StepCallbackInput,
15
+ } from '../types';
10
16
 
11
17
  export interface MujocoPhysicsProps {
12
18
  /** Scene/robot configuration. */
13
19
  config: SceneConfig;
14
20
  /** Fires when model is loaded and API is ready. */
15
- onReady?: (api: MujocoSimAPI) => void;
21
+ onReady?: (input: ReadyCallbackInput) => void;
16
22
  /** Fires on scene load failure. */
17
23
  onError?: (error: Error) => void;
18
24
  /** Called each physics step. */
19
- onStep?: (time: number) => void;
25
+ onStep?: (input: StepCallbackInput) => void;
20
26
  /** Called on body double-click selection. */
21
- onSelection?: (bodyId: number, name: string) => void;
27
+ onSelection?: (input: SelectionCallbackInput) => void;
22
28
  /** Override model gravity. */
23
29
  gravity?: [number, number, number];
24
30
  /** Override model.opt.timestep. */
@@ -31,13 +31,20 @@ import {
31
31
  MujocoSimAPI,
32
32
  PhysicsStepCallback,
33
33
  RayHit,
34
+ ReadyCallbackInput,
34
35
  SceneConfig,
35
36
  SceneObject,
37
+ SelectionCallbackInput,
36
38
  SensorInfo,
37
39
  SiteInfo,
38
40
  StateSnapshot,
41
+ StepCallbackInput,
39
42
  XmlPatch,
40
43
  } from '../types';
44
+ import {
45
+ captureFrame as captureCanvasFrame,
46
+ captureFrameBlob as captureCanvasFrameBlob,
47
+ } from '../hooks/useFrameCapture';
41
48
  import {
42
49
  loadScene,
43
50
  createSceneConfigFromFiles,
@@ -117,7 +124,7 @@ export interface MujocoSimContextValue {
117
124
  interpolateRef: React.RefObject<boolean>;
118
125
  interpolationStateRef: React.RefObject<BodyInterpolationState>;
119
126
  onSelectionRef: React.RefObject<
120
- ((bodyId: number, name: string) => void) | undefined
127
+ ((input: SelectionCallbackInput) => void) | undefined
121
128
  >;
122
129
  beforeStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
123
130
  afterStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
@@ -197,7 +204,7 @@ export function useBeforePhysicsStep(callback: PhysicsStepCallback) {
197
204
  callbackRef.current = callback;
198
205
 
199
206
  useEffect(() => {
200
- const wrapped: PhysicsStepCallback = (model, data) => callbackRef.current(model, data);
207
+ const wrapped: PhysicsStepCallback = (input) => callbackRef.current(input);
201
208
  beforeStepCallbacks.current.add(wrapped);
202
209
  return () => { beforeStepCallbacks.current.delete(wrapped); };
203
210
  }, [beforeStepCallbacks]);
@@ -209,7 +216,7 @@ export function useAfterPhysicsStep(callback: PhysicsStepCallback) {
209
216
  callbackRef.current = callback;
210
217
 
211
218
  useEffect(() => {
212
- const wrapped: PhysicsStepCallback = (model, data) => callbackRef.current(model, data);
219
+ const wrapped: PhysicsStepCallback = (input) => callbackRef.current(input);
213
220
  afterStepCallbacks.current.add(wrapped);
214
221
  return () => { afterStepCallbacks.current.delete(wrapped); };
215
222
  }, [afterStepCallbacks]);
@@ -219,10 +226,10 @@ interface MujocoSimProviderProps {
219
226
  mujoco: MujocoModule;
220
227
  config: SceneConfig;
221
228
  apiRef?: React.ForwardedRef<MujocoSimAPI>;
222
- onReady?: (api: MujocoSimAPI) => void;
229
+ onReady?: (input: ReadyCallbackInput) => void;
223
230
  onError?: (error: Error) => void;
224
- onStep?: (time: number) => void;
225
- onSelection?: (bodyId: number, name: string) => void;
231
+ onStep?: (input: StepCallbackInput) => void;
232
+ onSelection?: (input: SelectionCallbackInput) => void;
226
233
  // Declarative physics config props
227
234
  gravity?: [number, number, number];
228
235
  timestep?: number;
@@ -380,7 +387,7 @@ export function MujocoSimProvider({
380
387
  useEffect(() => {
381
388
  if (status === 'ready') {
382
389
  const api = apiRef.current;
383
- if (onReady) onReady(api);
390
+ if (onReady) onReady({ api });
384
391
  // Assign the forwarded ref
385
392
  if (externalApiRef) {
386
393
  if (typeof externalApiRef === 'function') {
@@ -409,7 +416,7 @@ export function MujocoSimProvider({
409
416
 
410
417
  // Before-step callbacks
411
418
  for (const cb of beforeStepCallbacks.current) {
412
- cb(model, data);
419
+ cb({ model, data });
413
420
  }
414
421
 
415
422
  const numSubsteps = substepsRef.current;
@@ -466,17 +473,17 @@ export function MujocoSimProvider({
466
473
  interpolationStateRef.current.valid = true;
467
474
 
468
475
  if (!stepped) {
469
- onStepRef.current?.(data.time);
476
+ onStepRef.current?.({ time: data.time, model, data });
470
477
  return;
471
478
  }
472
479
  }
473
480
 
474
481
  // After-step callbacks
475
482
  for (const cb of afterStepCallbacks.current) {
476
- cb(model, data);
483
+ cb({ model, data });
477
484
  }
478
485
 
479
- onStepRef.current?.(data.time);
486
+ onStepRef.current?.({ time: data.time, model, data });
480
487
  }, -1);
481
488
 
482
489
  function ensureInterpolationBuffers(model: MujocoModel) {
@@ -515,7 +522,7 @@ export function MujocoSimProvider({
515
522
  }
516
523
  }
517
524
 
518
- configRef.current.onReset?.(model, data);
525
+ configRef.current.onReset?.({ model, data });
519
526
  mujoco.mj_forward(model, data);
520
527
 
521
528
  // Notify composable plugins (e.g. IkController)
@@ -1016,6 +1023,24 @@ export function MujocoSimProvider({
1016
1023
  [gl]
1017
1024
  );
1018
1025
 
1026
+ const getCanvas = useCallback((): HTMLCanvasElement | null => {
1027
+ return gl.domElement ?? null;
1028
+ }, [gl]);
1029
+
1030
+ const captureFrameApi = useCallback(
1031
+ (options = {}) => {
1032
+ return captureCanvasFrame({ ...options, target: gl.domElement });
1033
+ },
1034
+ [gl]
1035
+ );
1036
+
1037
+ const captureFrameBlobApi = useCallback(
1038
+ (options = {}) => {
1039
+ return captureCanvasFrameBlob({ ...options, target: gl.domElement });
1040
+ },
1041
+ [gl]
1042
+ );
1043
+
1019
1044
  const project2DTo3D = useCallback(
1020
1045
  (x: number, y: number, cameraPos: THREE.Vector3, lookAt: THREE.Vector3): { point: THREE.Vector3; bodyId: number; geomId: number } | null => {
1021
1046
  const virtCam = (camera as THREE.PerspectiveCamera).clone();
@@ -1125,7 +1150,10 @@ export function MujocoSimProvider({
1125
1150
  addBody: addBodyApi,
1126
1151
  removeBody: removeBodyApi,
1127
1152
  recompile: recompileApi,
1153
+ getCanvas,
1128
1154
  getCanvasSnapshot,
1155
+ captureFrame: captureFrameApi,
1156
+ captureFrameBlob: captureFrameBlobApi,
1129
1157
  project2DTo3D,
1130
1158
  setBodyMass,
1131
1159
  setGeomFriction,
@@ -1143,7 +1171,8 @@ export function MujocoSimProvider({
1143
1171
  getActuatorsApi, getSensors, getModelOption, setGravity, setTimestepApi,
1144
1172
  raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
1145
1173
  loadFromFilesApi, addBodyApi, removeBodyApi, recompileApi,
1146
- getCanvasSnapshot, project2DTo3D,
1174
+ getCanvas, getCanvasSnapshot, captureFrameApi, captureFrameBlobApi,
1175
+ project2DTo3D,
1147
1176
  setBodyMass, setGeomFriction, setGeomSize,
1148
1177
  ]
1149
1178
  );
@@ -43,7 +43,7 @@ export type ControllerComponent<TConfig> = React.FC<{
43
43
  * const MyController = createController<{ speed: number }>(
44
44
  * { name: 'my-controller', defaultConfig: { speed: 1.0 } },
45
45
  * function MyControllerImpl({ config }) {
46
- * useBeforePhysicsStep((_model, data) => {
46
+ * useBeforePhysicsStep(({ data }) => {
47
47
  * data.ctrl[0] = config.speed;
48
48
  * });
49
49
  * return null;
@@ -100,7 +100,7 @@ export function createController<TConfig>(
100
100
  * { name: 'useMyController', defaultConfig: { gain: 1.0 } },
101
101
  * function useMyControllerImpl(config) {
102
102
  * // config is MyConfig | null — hooks must be called unconditionally
103
- * useBeforePhysicsStep((_model, data) => {
103
+ * useBeforePhysicsStep(({ data }) => {
104
104
  * if (!config) return;
105
105
  * data.ctrl[0] = config.gain * Math.sin(data.time);
106
106
  * });
@@ -29,7 +29,7 @@ export function useBodyState(name: Bodies): BodyStateResult {
29
29
  bodyIdRef.current = findBodyByName(model, name);
30
30
  }, [name, status, mjModelRef]);
31
31
 
32
- useAfterPhysicsStep((_model, data) => {
32
+ useAfterPhysicsStep(({ data }) => {
33
33
  const bid = bodyIdRef.current;
34
34
  if (bid < 0) return;
35
35
 
@@ -60,7 +60,7 @@ export function useContacts(
60
60
  bodyResolvedRef.current = true;
61
61
  }, [bodyName, status, mjModelRef]);
62
62
 
63
- useAfterPhysicsStep((model, data) => {
63
+ useAfterPhysicsStep(({ model, data }) => {
64
64
  // Resolve body id lazily once model exists, to avoid missing the first ready frame.
65
65
  if (bodyName && !bodyResolvedRef.current) {
66
66
  bodyIdRef.current = findBodyByName(model, bodyName);
@@ -30,7 +30,7 @@ export function useCtrlNoise(config: CtrlNoiseConfig = {}) {
30
30
  configRef.current = config;
31
31
  const noiseRef = useRef<Float64Array | null>(null);
32
32
 
33
- useBeforePhysicsStep((_model, data) => {
33
+ useBeforePhysicsStep(({ data }) => {
34
34
  const cfg = configRef.current;
35
35
  if (cfg.enabled === false) return;
36
36
 
@@ -6,48 +6,14 @@
6
6
  */
7
7
 
8
8
  import { useCallback, useState } from 'react';
9
- import type React from 'react';
10
-
11
- export type FrameCaptureStatus = 'idle' | 'capturing' | 'captured' | 'error';
12
-
13
- export type FrameCaptureTarget =
14
- | HTMLCanvasElement
15
- | HTMLElement
16
- | null
17
- | undefined;
18
-
19
- export type FrameCaptureTargetRef =
20
- React.RefObject<HTMLCanvasElement | HTMLElement | null>;
21
-
22
- export interface FrameCaptureOptions {
23
- target?: FrameCaptureTarget | FrameCaptureTargetRef;
24
- type?: string;
25
- quality?: number;
26
- waitForAnimationFrame?: boolean;
27
- }
28
-
29
- export interface FrameCaptureResult {
30
- canvas: HTMLCanvasElement;
31
- dataUrl: string;
32
- type: string;
33
- }
34
-
35
- export interface FrameCaptureBlobResult {
36
- canvas: HTMLCanvasElement;
37
- blob: Blob;
38
- type: string;
39
- }
40
-
41
- export interface FrameCaptureAPI {
42
- status: FrameCaptureStatus;
43
- error: Error | null;
44
- isCapturing: boolean;
45
- capture: (options?: FrameCaptureOptions) => Promise<FrameCaptureResult>;
46
- captureBlob: (
47
- options?: FrameCaptureOptions
48
- ) => Promise<FrameCaptureBlobResult>;
49
- reset: () => void;
50
- }
9
+ import type {
10
+ FrameCaptureAPI,
11
+ FrameCaptureBlobResult,
12
+ FrameCaptureOptions,
13
+ FrameCaptureResult,
14
+ FrameCaptureStatus,
15
+ FrameCaptureTargetRef,
16
+ } from '../types';
51
17
 
52
18
  function isTargetRef(
53
19
  target: FrameCaptureOptions['target']
@@ -50,7 +50,7 @@ export function useGamepad(config: GamepadConfig) {
50
50
  }
51
51
  }, [config.axes, config.buttons, status, mjModelRef]);
52
52
 
53
- useBeforePhysicsStep((_model, data) => {
53
+ useBeforePhysicsStep(({ data }) => {
54
54
  const cfg = configRef.current;
55
55
  if (cfg.enabled === false) return;
56
56
 
@@ -13,7 +13,7 @@ import { useBeforePhysicsStep } from '../core/MujocoSimProvider';
13
13
  * hook (and DragInteraction) compose correctly — both add to a clean slate.
14
14
  */
15
15
  export function useGravityCompensation(enabled = true): void {
16
- useBeforePhysicsStep((model, data) => {
16
+ useBeforePhysicsStep(({ model, data }) => {
17
17
  if (!enabled) return;
18
18
  for (let i = 0; i < model.nv; i++) {
19
19
  data.qfrc_applied[i] += data.qfrc_bias[i];
@@ -10,7 +10,7 @@ import { createControllerHook } from '../core/createController';
10
10
  import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
11
11
  import { GenericIK } from '../core/GenericIK';
12
12
  import { createContiguousControlGroup, findSiteByName, resolveControlGroup } from '../core/SceneLoader';
13
- import type { ControlGroupInfo, IkConfig, IkContextValue, IKSolveFn, MujocoData } from '../types';
13
+ import type { ControlGroupInfo, IkConfig, IkContextValue, IKSolveFn, IkSolveInput, MujocoData } from '../types';
14
14
 
15
15
  // Preallocated temp for syncGizmoToSite
16
16
  const _syncMat4 = new THREE.Matrix4();
@@ -84,16 +84,16 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
84
84
 
85
85
  // IK solve function
86
86
  const ikSolveFn = useCallback(
87
- (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
87
+ ({ position, quaternion, currentQ, context }: IkSolveInput): number[] | null => {
88
88
  if (!config) return null;
89
- if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
89
+ if (config.ikSolveFn) return config.ikSolveFn({ position, quaternion, currentQ, context });
90
90
  const model = mjModelRef.current;
91
91
  const data = mjDataRef.current;
92
92
  const controlGroup = controlGroupRef.current;
93
93
  if (!model || !data || !controlGroup || siteIdRef.current === -1) return null;
94
94
  return genericIkRef.current.solve(
95
95
  model, data, siteIdRef.current, controlGroup.qposAdr,
96
- pos, quat, currentQ,
96
+ position, quaternion, currentQ,
97
97
  { damping: config.damping, maxIterations: config.maxIterations },
98
98
  );
99
99
  },
@@ -128,7 +128,7 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
128
128
  });
129
129
 
130
130
  // IK solve in physics loop
131
- useBeforePhysicsStep((model, data) => {
131
+ useBeforePhysicsStep(({ model, data }) => {
132
132
  if (!config || !ikEnabledRef.current) {
133
133
  ikCalculatingRef.current = false;
134
134
  return;
@@ -142,13 +142,22 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
142
142
 
143
143
  const currentQ = Array.from(controlGroup.readQpos(data));
144
144
  const solution = config.ikSolveFn
145
- ? config.ikSolveFn(target.position, target.quaternion, currentQ, {
146
- model,
147
- data,
148
- siteId: siteIdRef.current,
149
- controlGroup,
145
+ ? config.ikSolveFn({
146
+ position: target.position,
147
+ quaternion: target.quaternion,
148
+ currentQ,
149
+ context: {
150
+ model,
151
+ data,
152
+ siteId: siteIdRef.current,
153
+ controlGroup,
154
+ },
150
155
  })
151
- : ikSolveFnRef.current(target.position, target.quaternion, currentQ);
156
+ : ikSolveFnRef.current({
157
+ position: target.position,
158
+ quaternion: target.quaternion,
159
+ currentQ,
160
+ });
152
161
  if (solution) {
153
162
  controlGroup.writeCtrl(data, solution);
154
163
  }
@@ -192,8 +201,8 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
192
201
  }, [mjDataRef]);
193
202
 
194
203
  const solveIK = useCallback(
195
- (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
196
- return ikSolveFnRef.current(pos, quat, currentQ);
204
+ (input: IkSolveInput): number[] | null => {
205
+ return ikSolveFnRef.current(input);
197
206
  },
198
207
  [],
199
208
  );