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
@@ -8,7 +8,7 @@
8
8
  import { useCallback, useRef } from 'react';
9
9
  import { useFrame } from '@react-three/fiber';
10
10
  import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
11
- import type { PlaybackState, TrajectoryFrame, TrajectoryInput } from '../types';
11
+ import type { PlaybackState, TrajectoryStateChangeInput, TrajectoryFrame, TrajectoryInput } from '../types';
12
12
 
13
13
  export interface TrajectoryPlayerOptions {
14
14
  fps?: number;
@@ -16,7 +16,7 @@ export interface TrajectoryPlayerOptions {
16
16
  loop?: boolean;
17
17
  mode?: 'kinematic' | 'physics';
18
18
  onComplete?: () => void;
19
- onStateChange?: (state: PlaybackState) => void;
19
+ onStateChange?: (input: TrajectoryStateChangeInput) => void;
20
20
  }
21
21
 
22
22
  /** Check if input is TrajectoryFrame[] (vs number[][]) */
@@ -74,7 +74,7 @@ export function useTrajectoryPlayer(
74
74
  const setState = useCallback((next: PlaybackState) => {
75
75
  if (stateRef.current === next) return;
76
76
  stateRef.current = next;
77
- optionsRef.current.onStateChange?.(next);
77
+ optionsRef.current.onStateChange?.({ state: next });
78
78
  }, []);
79
79
 
80
80
  const play = useCallback(() => {
@@ -181,7 +181,7 @@ export function useTrajectoryPlayer(
181
181
  });
182
182
 
183
183
  // --- Physics mode: set ctrl values each physics step ---
184
- useBeforePhysicsStep((model, data) => {
184
+ useBeforePhysicsStep(({ model, data }) => {
185
185
  if (stateRef.current !== 'playing') return;
186
186
  if ((optionsRef.current.mode ?? 'kinematic') !== 'physics') return;
187
187
 
@@ -22,7 +22,7 @@ export function useTrajectoryRecorder(options: RecorderOptions = {}) {
22
22
  const framesRef = useRef<TrajectoryFrame[]>([]);
23
23
  const fields = options.fields ?? ['qpos'];
24
24
 
25
- useAfterPhysicsStep((_model, data) => {
25
+ useAfterPhysicsStep(({ data }) => {
26
26
  if (!recordingRef.current) return;
27
27
 
28
28
  const frame: TrajectoryFrame = {
package/src/index.ts CHANGED
@@ -44,11 +44,15 @@ export { SceneLights } from './components/SceneLights';
44
44
  export {
45
45
  ScenarioLighting,
46
46
  SplatEnvironment,
47
+ VisualScenarioEffects,
48
+ createPairedSplatEnvironment,
47
49
  createSparkSplatViewerUrl,
48
50
  createSplatEnvironmentUserData,
49
51
  getScenarioBackground,
50
52
  getScenarioCameraPosition,
51
53
  useSplatEnvironment,
54
+ useVisualScenarioEffects,
55
+ withSplatEnvironment,
52
56
  } from './components/VisualScenario';
53
57
  export { Debug } from './components/Debug';
54
58
  export { TendonRenderer } from './components/TendonRenderer';
@@ -73,6 +77,20 @@ export { useTrajectoryPlayer } from './hooks/useTrajectoryPlayer';
73
77
  export { useTrajectoryRecorder } from './hooks/useTrajectoryRecorder';
74
78
  export { useGamepad } from './hooks/useGamepad';
75
79
  export { useVideoRecorder } from './hooks/useVideoRecorder';
80
+ export {
81
+ captureFrame,
82
+ captureFrameBlob,
83
+ useFrameCapture,
84
+ } from './hooks/useFrameCapture';
85
+ export type {
86
+ FrameCaptureAPI,
87
+ FrameCaptureBlobResult,
88
+ FrameCaptureOptions,
89
+ FrameCaptureResult,
90
+ FrameCaptureStatus,
91
+ FrameCaptureTarget,
92
+ FrameCaptureTargetRef,
93
+ } from './hooks/useFrameCapture';
76
94
  export { useCtrlNoise } from './hooks/useCtrlNoise';
77
95
  export { useBodyMeshes } from './hooks/useBodyMeshes';
78
96
  export { useSelectionHighlight } from './hooks/useSelectionHighlight';
@@ -92,8 +110,14 @@ export type {
92
110
  IkConfig,
93
111
  IkContextValue,
94
112
  IKSolveFn,
113
+ IkSolveInput,
95
114
  // Callbacks
96
115
  PhysicsStepCallback,
116
+ PhysicsStepInput,
117
+ ResetCallbackInput,
118
+ ReadyCallbackInput,
119
+ StepCallbackInput,
120
+ SelectionCallbackInput,
97
121
  // State management
98
122
  StateSnapshot,
99
123
  // Model introspection
@@ -124,6 +148,10 @@ export type {
124
148
  KeyboardTeleopConfig,
125
149
  // Policy
126
150
  PolicyConfig,
151
+ PolicyVector,
152
+ PolicyObservationInput,
153
+ PolicyInferenceInput,
154
+ PolicyActionInput,
127
155
  // Observations
128
156
  ObservationConfig,
129
157
  ObservationHandle,
@@ -133,6 +161,7 @@ export type {
133
161
  // Component props
134
162
  BodyProps,
135
163
  IkGizmoProps,
164
+ IkGizmoDragInput,
136
165
  DragInteractionProps,
137
166
  DebugProps,
138
167
  SceneLightsProps,
@@ -147,10 +176,16 @@ export type {
147
176
  PairedSplatEnvironmentConfig,
148
177
  SplatEnvironmentMetadataInput,
149
178
  SplatEnvironmentMetadata,
179
+ SplatSceneInput,
150
180
  VisualScenarioConfig,
151
181
  ScenarioLightingProps,
182
+ ScenarioMaterialConfig,
152
183
  SplatEnvironmentProps,
184
+ VisualScenarioEffectsProps,
185
+ VisualScenarioMaterialFilterInput,
153
186
  TrajectoryPlayerProps,
187
+ TrajectoryFrameCallbackInput,
188
+ TrajectoryStateChangeInput,
154
189
  ContactListenerProps,
155
190
  // API
156
191
  MujocoSimAPI,
package/src/spark.tsx CHANGED
@@ -4,7 +4,13 @@
4
4
  */
5
5
 
6
6
  import { useThree } from '@react-three/fiber';
7
- import { useEffect, useRef, useState } from 'react';
7
+ import {
8
+ useCallback,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
8
14
  import * as THREE from 'three';
9
15
  import {
10
16
  SplatEnvironment,
@@ -17,9 +23,22 @@ import type {
17
23
  type SparkModule = typeof import('@sparkjsdev/spark');
18
24
  type SparkRendererInstance = InstanceType<SparkModule['SparkRenderer']>;
19
25
  type SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;
26
+ type SparkDisposable = {
27
+ dispose?: () => unknown;
28
+ };
20
29
 
21
30
  export type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';
22
31
 
32
+ export interface SparkSplatLifecycle {
33
+ status: SparkSplatStatus;
34
+ error: Error | null;
35
+ isLoading: boolean;
36
+ isReady: boolean;
37
+ isError: boolean;
38
+ props: Pick<SparkSplatEnvironmentProps, 'onStatusChange' | 'onError'>;
39
+ reset: () => void;
40
+ }
41
+
23
42
  export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
24
43
  /** Enable Spark LoD handling for large splat assets. Default: true. */
25
44
  lod?: boolean | 'quality';
@@ -34,6 +53,75 @@ export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
34
53
  onError?: (error: Error) => void;
35
54
  }
36
55
 
56
+ /**
57
+ * Tracks Spark 3DGS loading state for UI that wraps `SparkSplatEnvironment`.
58
+ *
59
+ * Use the returned `props` with `<SparkSplatEnvironment {...lifecycle.props} />`
60
+ * to avoid repeating status/error state in app code.
61
+ */
62
+ export function useSparkSplatLifecycle({
63
+ enabled = true,
64
+ initialStatus,
65
+ onError,
66
+ onStatusChange,
67
+ }: {
68
+ enabled?: boolean;
69
+ initialStatus?: SparkSplatStatus;
70
+ onError?: (error: Error) => void;
71
+ onStatusChange?: (status: SparkSplatStatus) => void;
72
+ } = {}): SparkSplatLifecycle {
73
+ const [status, setStatus] = useState<SparkSplatStatus>(
74
+ initialStatus ?? (enabled ? 'loading' : 'idle')
75
+ );
76
+ const [error, setError] = useState<Error | null>(null);
77
+
78
+ useEffect(() => {
79
+ setStatus(enabled ? initialStatus ?? 'loading' : 'idle');
80
+ setError(null);
81
+ }, [enabled, initialStatus]);
82
+
83
+ const handleStatusChange = useCallback(
84
+ (nextStatus: SparkSplatStatus) => {
85
+ setStatus(nextStatus);
86
+ if (nextStatus !== 'error') {
87
+ setError(null);
88
+ }
89
+ onStatusChange?.(nextStatus);
90
+ },
91
+ [onStatusChange]
92
+ );
93
+
94
+ const handleError = useCallback(
95
+ (nextError: Error) => {
96
+ setError(nextError);
97
+ setStatus('error');
98
+ onError?.(nextError);
99
+ },
100
+ [onError]
101
+ );
102
+
103
+ const reset = useCallback(() => {
104
+ setStatus(enabled ? initialStatus ?? 'loading' : 'idle');
105
+ setError(null);
106
+ }, [enabled, initialStatus]);
107
+
108
+ return useMemo(
109
+ () => ({
110
+ status,
111
+ error,
112
+ isLoading: status === 'loading',
113
+ isReady: status === 'ready',
114
+ isError: status === 'error',
115
+ props: {
116
+ onStatusChange: handleStatusChange,
117
+ onError: handleError,
118
+ },
119
+ reset,
120
+ }),
121
+ [error, handleError, handleStatusChange, reset, status]
122
+ );
123
+ }
124
+
37
125
  /**
38
126
  * Optional SparkJS-backed Gaussian splat renderer for React Three Fiber scenes.
39
127
  *
@@ -42,6 +130,8 @@ export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
42
130
  */
43
131
  export function SparkSplatEnvironment({
44
132
  environment,
133
+ scenario,
134
+ renderer = 'spark',
45
135
  src,
46
136
  format,
47
137
  collisionProxy,
@@ -66,6 +156,8 @@ export function SparkSplatEnvironment({
66
156
  const { gl, invalidate } = useThree();
67
157
  const metadata = useSplatEnvironment({
68
158
  environment,
159
+ scenario,
160
+ renderer,
69
161
  src,
70
162
  format,
71
163
  collisionProxy: collisionProxyMetadata,
@@ -99,11 +191,20 @@ export function SparkSplatEnvironment({
99
191
  }
100
192
 
101
193
  async function loadSplat() {
102
- if (!metadata.src || metadata.format !== 'spz') {
194
+ if (!metadata.src) {
103
195
  setLifecycleStatus('idle');
104
196
  return;
105
197
  }
106
198
 
199
+ if (metadata.format !== 'spz') {
200
+ const unsupportedFormatError = new Error(
201
+ `SparkSplatEnvironment only supports .spz assets; received "${metadata.format}".`
202
+ );
203
+ setLifecycleStatus('error');
204
+ onErrorRef.current?.(unsupportedFormatError);
205
+ return;
206
+ }
207
+
107
208
  setLifecycleStatus('loading');
108
209
 
109
210
  try {
@@ -166,13 +267,13 @@ export function SparkSplatEnvironment({
166
267
 
167
268
  if (meshRef.current) {
168
269
  groupRef.current?.remove(meshRef.current);
169
- meshRef.current.dispose?.();
270
+ safelyDisposeSparkResource(meshRef.current);
170
271
  meshRef.current = null;
171
272
  }
172
273
 
173
274
  if (sparkRef.current) {
174
275
  groupRef.current?.remove(sparkRef.current);
175
- sparkRef.current.dispose?.();
276
+ safelyDisposeSparkResource(sparkRef.current);
176
277
  sparkRef.current = null;
177
278
  }
178
279
  };
@@ -189,6 +290,8 @@ export function SparkSplatEnvironment({
189
290
  <SplatEnvironment
190
291
  {...groupProps}
191
292
  environment={environment}
293
+ scenario={scenario}
294
+ renderer={renderer}
192
295
  src={metadata.src}
193
296
  format={metadata.format}
194
297
  collisionProxyMetadata={metadata.collisionProxy}
@@ -200,3 +303,34 @@ export function SparkSplatEnvironment({
200
303
  </SplatEnvironment>
201
304
  );
202
305
  }
306
+
307
+ function safelyDisposeSparkResource(resource: SparkDisposable) {
308
+ try {
309
+ const result = resource.dispose?.();
310
+ if (isPromiseLike(result)) {
311
+ void Promise.resolve(result).catch(handleSparkDisposeError);
312
+ }
313
+ } catch (error) {
314
+ handleSparkDisposeError(error);
315
+ }
316
+ }
317
+
318
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
319
+ return (
320
+ typeof value === 'object' &&
321
+ value !== null &&
322
+ 'then' in value &&
323
+ typeof (value as { then?: unknown }).then === 'function'
324
+ );
325
+ }
326
+
327
+ function handleSparkDisposeError(error: unknown) {
328
+ if (
329
+ error instanceof Error &&
330
+ error.message.toLowerCase().includes('worker terminate')
331
+ ) {
332
+ return;
333
+ }
334
+
335
+ console.warn('[mujoco-react] Spark resource disposal failed.', error);
336
+ }
package/src/types.ts CHANGED
@@ -429,6 +429,8 @@ export interface SceneObject {
429
429
  solref?: string;
430
430
  solimp?: string;
431
431
  condim?: number;
432
+ /** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
433
+ group?: number;
432
434
  }
433
435
 
434
436
  export interface XmlPatch {
@@ -443,10 +445,12 @@ export type LocalMujocoFile = File;
443
445
  export interface LoadFromFilesOptions {
444
446
  /** Entry MJCF/URDF file. Inferred from scene.xml, model.xml, robot.xml, or the first XML/URDF file when omitted. */
445
447
  sceneFile?: string;
448
+ /** Additional MJCF environment XML files merged into the entry scene before MuJoCo compilation. */
449
+ environmentFiles?: string[];
446
450
  homeJoints?: number[];
447
451
  xmlPatches?: XmlPatch[];
448
452
  sceneObjects?: SceneObject[];
449
- onReset?: (model: MujocoModel, data: MujocoData) => void;
453
+ onReset?: (input: ResetCallbackInput) => void;
450
454
  }
451
455
 
452
456
  export interface SceneConfig {
@@ -456,10 +460,18 @@ export interface SceneConfig {
456
460
  sceneFile: string;
457
461
  /** Browser-selected files for local MJCF/URDF loading. Preserves webkitRelativePath when available. */
458
462
  files?: readonly LocalMujocoFile[];
463
+ /**
464
+ * Additional MJCF environment XML files merged into the entry scene before compilation.
465
+ *
466
+ * Use this for static collision/physics layers such as a Gaussian-splat
467
+ * environment's proxy `scene.xml`; render the splat itself as a separate
468
+ * visual layer.
469
+ */
470
+ environmentFiles?: string[];
459
471
  sceneObjects?: SceneObject[];
460
472
  homeJoints?: number[];
461
473
  xmlPatches?: XmlPatch[];
462
- onReset?: (model: MujocoModel, data: MujocoData) => void;
474
+ onReset?: (input: ResetCallbackInput) => void;
463
475
  }
464
476
 
465
477
  // ---- IK Controller Config ----
@@ -501,7 +513,7 @@ export interface IkContextValue {
501
513
  setIkEnabled: (enabled: boolean) => void;
502
514
  moveTarget: (pos: THREE.Vector3, duration?: number) => void;
503
515
  syncTargetToSite: () => void;
504
- solveIK: (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]) => number[] | null;
516
+ solveIK: (input: IkSolveInput) => number[] | null;
505
517
  getGizmoStats: () => { pos: THREE.Vector3; rot: THREE.Euler } | null;
506
518
  }
507
519
 
@@ -524,12 +536,16 @@ export interface PhysicsConfig {
524
536
  // ---- IK ----
525
537
 
526
538
  export type IKSolveFn = (
527
- pos: THREE.Vector3,
528
- quat: THREE.Quaternion,
529
- currentQ: number[],
530
- context?: IKSolveContext
539
+ input: IkSolveInput
531
540
  ) => number[] | null;
532
541
 
542
+ export interface IkSolveInput {
543
+ position: THREE.Vector3;
544
+ quaternion: THREE.Quaternion;
545
+ currentQ: number[];
546
+ context?: IKSolveContext;
547
+ }
548
+
533
549
  export interface IKSolveContext {
534
550
  model: MujocoModel;
535
551
  data: MujocoData;
@@ -539,10 +555,29 @@ export interface IKSolveContext {
539
555
 
540
556
  // ---- Callbacks ----
541
557
 
542
- export type PhysicsStepCallback = (
543
- model: MujocoModel,
544
- data: MujocoData
545
- ) => void;
558
+ export interface PhysicsStepInput {
559
+ model: MujocoModel;
560
+ data: MujocoData;
561
+ }
562
+
563
+ export interface ResetCallbackInput extends PhysicsStepInput {}
564
+
565
+ export interface ReadyCallbackInput {
566
+ api: MujocoSimAPI;
567
+ }
568
+
569
+ export interface StepCallbackInput {
570
+ time: number;
571
+ model: MujocoModel;
572
+ data: MujocoData;
573
+ }
574
+
575
+ export interface SelectionCallbackInput {
576
+ bodyId: number;
577
+ name: string;
578
+ }
579
+
580
+ export type PhysicsStepCallback = (input: PhysicsStepInput) => void;
546
581
 
547
582
  // ---- State Management (spec 4.1) ----
548
583
 
@@ -709,10 +744,28 @@ export interface KeyboardTeleopConfig {
709
744
 
710
745
  // ---- Policy (spec 10.1) ----
711
746
 
747
+ export type PolicyVector = Float32Array | Float64Array | number[];
748
+
749
+ export interface PolicyObservationInput {
750
+ model: MujocoModel;
751
+ data: MujocoData;
752
+ }
753
+
754
+ export interface PolicyInferenceInput extends PolicyObservationInput {
755
+ observation: PolicyVector;
756
+ }
757
+
758
+ export interface PolicyActionInput extends PolicyInferenceInput {
759
+ action: PolicyVector;
760
+ }
761
+
712
762
  export interface PolicyConfig {
713
763
  frequency: number;
714
- onObservation: (model: MujocoModel, data: MujocoData) => Float32Array | Float64Array | number[];
715
- onAction: (action: Float32Array | Float64Array | number[], model: MujocoModel, data: MujocoData) => void;
764
+ enabled?: boolean;
765
+ onObservation: (input: PolicyObservationInput) => PolicyVector;
766
+ /** Run policy inference. Omit to pass observations directly to `onAction` for custom inline controllers. */
767
+ infer?: (input: PolicyInferenceInput) => PolicyVector;
768
+ onAction: (input: PolicyActionInput) => void;
716
769
  }
717
770
 
718
771
  // ---- Observation Builder ----
@@ -778,7 +831,12 @@ export interface IkGizmoProps {
778
831
  controller: IkContextValue;
779
832
  siteName?: string;
780
833
  scale?: number;
781
- onDrag?: (position: THREE.Vector3, quaternion: THREE.Quaternion) => void;
834
+ onDrag?: (input: IkGizmoDragInput) => void;
835
+ }
836
+
837
+ export interface IkGizmoDragInput {
838
+ position: THREE.Vector3;
839
+ quaternion: THREE.Quaternion;
782
840
  }
783
841
 
784
842
  export interface DragInteractionProps {
@@ -805,6 +863,13 @@ export interface ScenarioCameraConfig {
805
863
  blur?: number;
806
864
  }
807
865
 
866
+ export interface ScenarioMaterialConfig {
867
+ randomizeObjectColors?: boolean;
868
+ randomizeTableMaterial?: boolean;
869
+ roughness?: number;
870
+ metalness?: number;
871
+ }
872
+
808
873
  export interface SplatAssetConfig {
809
874
  src: string;
810
875
  /** Common browser-friendly splat format. Renderer-specific loaders may accept more. */
@@ -819,7 +884,7 @@ export interface SplatScenarioConfig {
819
884
  format?: SplatFormat;
820
885
  src?: string;
821
886
  requiresCollisionProxy?: boolean;
822
- collisionProxy?: SplatCollisionProxyConfig;
887
+ collisionProxy?: SplatCollisionProxyConfig | null;
823
888
  }
824
889
 
825
890
  export interface SplatCollisionProxyConfig {
@@ -845,6 +910,8 @@ export interface PairedSplatEnvironmentConfig {
845
910
 
846
911
  export interface SplatEnvironmentMetadataInput {
847
912
  environment?: PairedSplatEnvironmentConfig;
913
+ scenario?: VisualScenarioConfig;
914
+ renderer?: SplatRendererKind;
848
915
  src?: string;
849
916
  format?: SplatFormat;
850
917
  collisionProxy?: SplatCollisionProxyConfig;
@@ -857,6 +924,12 @@ export interface SplatEnvironmentMetadata {
857
924
  userData: Record<string, unknown>;
858
925
  }
859
926
 
927
+ export type SplatSceneInput =
928
+ | PairedSplatEnvironmentConfig
929
+ | VisualScenarioConfig
930
+ | undefined
931
+ | null;
932
+
860
933
  export interface VisualScenarioConfig {
861
934
  id?: string;
862
935
  label?: string;
@@ -864,7 +937,8 @@ export interface VisualScenarioConfig {
864
937
  lighting?: ScenarioLightingPreset;
865
938
  environment?: string;
866
939
  camera?: ScenarioCameraConfig;
867
- splat?: SplatScenarioConfig;
940
+ materials?: ScenarioMaterialConfig;
941
+ splat?: SplatScenarioConfig | null;
868
942
  }
869
943
 
870
944
  export interface ScenarioLightingProps {
@@ -875,6 +949,8 @@ export interface ScenarioLightingProps {
875
949
 
876
950
  export interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref'> {
877
951
  environment?: PairedSplatEnvironmentConfig;
952
+ scenario?: VisualScenarioConfig;
953
+ renderer?: SplatRendererKind;
878
954
  src?: string;
879
955
  format?: SplatFormat;
880
956
  collisionProxy?: ReactNode;
@@ -882,6 +958,24 @@ export interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref
882
958
  showPlaceholder?: boolean;
883
959
  }
884
960
 
961
+ export interface VisualScenarioEffectsProps {
962
+ scenario?: VisualScenarioConfig;
963
+ enabled?: boolean;
964
+ applyBackground?: boolean;
965
+ applyFog?: boolean;
966
+ applyRenderer?: boolean;
967
+ applyMaterials?: boolean;
968
+ background?: THREE.ColorRepresentation;
969
+ fogNear?: number;
970
+ fogFar?: number;
971
+ materialFilter?: (input: VisualScenarioMaterialFilterInput) => boolean;
972
+ }
973
+
974
+ export interface VisualScenarioMaterialFilterInput {
975
+ object: THREE.Object3D;
976
+ material: THREE.Material;
977
+ }
978
+
885
979
  export type TrajectoryInput = TrajectoryFrame[] | number[][];
886
980
 
887
981
  export interface TrajectoryPlayerProps {
@@ -891,9 +985,18 @@ export interface TrajectoryPlayerProps {
891
985
  loop?: boolean;
892
986
  playing?: boolean;
893
987
  mode?: 'kinematic' | 'physics';
894
- onFrame?: (frameIdx: number) => void;
988
+ onFrame?: (input: TrajectoryFrameCallbackInput) => void;
895
989
  onComplete?: () => void;
896
- onStateChange?: (state: PlaybackState) => void;
990
+ onStateChange?: (input: TrajectoryStateChangeInput) => void;
991
+ }
992
+
993
+ export interface TrajectoryFrameCallbackInput {
994
+ frameIndex: number;
995
+ frame: TrajectoryFrame | number[] | undefined;
996
+ }
997
+
998
+ export interface TrajectoryStateChangeInput {
999
+ state: PlaybackState;
897
1000
  }
898
1001
 
899
1002
  export interface SelectionHighlightProps {
@@ -920,6 +1023,8 @@ export interface BodyProps {
920
1023
  solref?: string;
921
1024
  solimp?: string;
922
1025
  condim?: number;
1026
+ /** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
1027
+ group?: number;
923
1028
  children?: ReactNode;
924
1029
  }
925
1030
 
@@ -1017,10 +1122,12 @@ export interface MujocoSimAPI {
1017
1122
 
1018
1123
  export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
1019
1124
  config: SceneConfig;
1020
- onReady?: (api: MujocoSimAPI) => void;
1125
+ /** R3F content rendered while the MuJoCo WASM module is still loading. */
1126
+ loadingFallback?: ReactNode;
1127
+ onReady?: (input: ReadyCallbackInput) => void;
1021
1128
  onError?: (error: Error) => void;
1022
- onStep?: (time: number) => void;
1023
- onSelection?: (bodyId: number, name: string) => void;
1129
+ onStep?: (input: StepCallbackInput) => void;
1130
+ onSelection?: (input: SelectionCallbackInput) => void;
1024
1131
  // Declarative physics config (spec 1.1)
1025
1132
  gravity?: [number, number, number];
1026
1133
  timestep?: number;