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/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-FFW7ykBu.js';
3
+ import { n as SplatEnvironmentProps } from './types-izZlUweI.js';
4
4
  import 'react';
5
5
  import '@react-three/fiber';
6
6
  import 'three';
@@ -8,6 +8,15 @@ import 'three';
8
8
  type SparkModule = typeof _sparkjsdev_spark;
9
9
  type SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;
10
10
  type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';
11
+ interface SparkSplatLifecycle {
12
+ status: SparkSplatStatus;
13
+ error: Error | null;
14
+ isLoading: boolean;
15
+ isReady: boolean;
16
+ isError: boolean;
17
+ props: Pick<SparkSplatEnvironmentProps, 'onStatusChange' | 'onError'>;
18
+ reset: () => void;
19
+ }
11
20
  interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
12
21
  /** Enable Spark LoD handling for large splat assets. Default: true. */
13
22
  lod?: boolean | 'quality';
@@ -21,12 +30,24 @@ interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
21
30
  onLoad?: (mesh: SparkSplatMeshInstance) => void;
22
31
  onError?: (error: Error) => void;
23
32
  }
33
+ /**
34
+ * Tracks Spark 3DGS loading state for UI that wraps `SparkSplatEnvironment`.
35
+ *
36
+ * Use the returned `props` with `<SparkSplatEnvironment {...lifecycle.props} />`
37
+ * to avoid repeating status/error state in app code.
38
+ */
39
+ declare function useSparkSplatLifecycle({ enabled, initialStatus, onError, onStatusChange, }?: {
40
+ enabled?: boolean;
41
+ initialStatus?: SparkSplatStatus;
42
+ onError?: (error: Error) => void;
43
+ onStatusChange?: (status: SparkSplatStatus) => void;
44
+ }): SparkSplatLifecycle;
24
45
  /**
25
46
  * Optional SparkJS-backed Gaussian splat renderer for React Three Fiber scenes.
26
47
  *
27
48
  * Import from `mujoco-react/spark` and install `@sparkjsdev/spark` in the app
28
49
  * that uses it. The core `mujoco-react` entrypoint does not depend on Spark.
29
50
  */
30
- declare function SparkSplatEnvironment({ environment, src, format, collisionProxy, collisionProxyMetadata, showPlaceholder, children, lod, hideGroundMeshes, onStatusChange, onLoad, onError, ...groupProps }: SparkSplatEnvironmentProps): react_jsx_runtime.JSX.Element;
51
+ declare function SparkSplatEnvironment({ environment, scenario, renderer, src, format, collisionProxy, collisionProxyMetadata, showPlaceholder, children, lod, hideGroundMeshes, onStatusChange, onLoad, onError, ...groupProps }: SparkSplatEnvironmentProps): react_jsx_runtime.JSX.Element;
31
52
 
32
- export { SparkSplatEnvironment, type SparkSplatEnvironmentProps, type SparkSplatStatus };
53
+ export { SparkSplatEnvironment, type SparkSplatEnvironmentProps, type SparkSplatLifecycle, type SparkSplatStatus, useSparkSplatLifecycle };
package/dist/spark.js CHANGED
@@ -1,11 +1,65 @@
1
- import { useSplatEnvironment, SplatEnvironment } from './chunk-KGFRKPLS.js';
1
+ import { useSplatEnvironment, SplatEnvironment } from './chunk-33CV6HSV.js';
2
2
  import { useThree } from '@react-three/fiber';
3
- import { useRef, useState, useEffect } from 'react';
3
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
4
4
  import * as THREE from 'three';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
6
 
7
+ function useSparkSplatLifecycle({
8
+ enabled = true,
9
+ initialStatus,
10
+ onError,
11
+ onStatusChange
12
+ } = {}) {
13
+ const [status, setStatus] = useState(
14
+ initialStatus ?? (enabled ? "loading" : "idle")
15
+ );
16
+ const [error, setError] = useState(null);
17
+ useEffect(() => {
18
+ setStatus(enabled ? initialStatus ?? "loading" : "idle");
19
+ setError(null);
20
+ }, [enabled, initialStatus]);
21
+ const handleStatusChange = useCallback(
22
+ (nextStatus) => {
23
+ setStatus(nextStatus);
24
+ if (nextStatus !== "error") {
25
+ setError(null);
26
+ }
27
+ onStatusChange?.(nextStatus);
28
+ },
29
+ [onStatusChange]
30
+ );
31
+ const handleError = useCallback(
32
+ (nextError) => {
33
+ setError(nextError);
34
+ setStatus("error");
35
+ onError?.(nextError);
36
+ },
37
+ [onError]
38
+ );
39
+ const reset = useCallback(() => {
40
+ setStatus(enabled ? initialStatus ?? "loading" : "idle");
41
+ setError(null);
42
+ }, [enabled, initialStatus]);
43
+ return useMemo(
44
+ () => ({
45
+ status,
46
+ error,
47
+ isLoading: status === "loading",
48
+ isReady: status === "ready",
49
+ isError: status === "error",
50
+ props: {
51
+ onStatusChange: handleStatusChange,
52
+ onError: handleError
53
+ },
54
+ reset
55
+ }),
56
+ [error, handleError, handleStatusChange, reset, status]
57
+ );
58
+ }
7
59
  function SparkSplatEnvironment({
8
60
  environment,
61
+ scenario,
62
+ renderer = "spark",
9
63
  src,
10
64
  format,
11
65
  collisionProxy,
@@ -30,6 +84,8 @@ function SparkSplatEnvironment({
30
84
  const { gl, invalidate } = useThree();
31
85
  const metadata = useSplatEnvironment({
32
86
  environment,
87
+ scenario,
88
+ renderer,
33
89
  src,
34
90
  format,
35
91
  collisionProxy: collisionProxyMetadata
@@ -56,10 +112,18 @@ function SparkSplatEnvironment({
56
112
  hiddenMeshesRef.current = [];
57
113
  }
58
114
  async function loadSplat() {
59
- if (!metadata.src || metadata.format !== "spz") {
115
+ if (!metadata.src) {
60
116
  setLifecycleStatus("idle");
61
117
  return;
62
118
  }
119
+ if (metadata.format !== "spz") {
120
+ const unsupportedFormatError = new Error(
121
+ `SparkSplatEnvironment only supports .spz assets; received "${metadata.format}".`
122
+ );
123
+ setLifecycleStatus("error");
124
+ onErrorRef.current?.(unsupportedFormatError);
125
+ return;
126
+ }
63
127
  setLifecycleStatus("loading");
64
128
  try {
65
129
  const sparkModule = await import('@sparkjsdev/spark');
@@ -106,12 +170,12 @@ function SparkSplatEnvironment({
106
170
  restoreHiddenMeshes();
107
171
  if (meshRef.current) {
108
172
  groupRef.current?.remove(meshRef.current);
109
- meshRef.current.dispose?.();
173
+ safelyDisposeSparkResource(meshRef.current);
110
174
  meshRef.current = null;
111
175
  }
112
176
  if (sparkRef.current) {
113
177
  groupRef.current?.remove(sparkRef.current);
114
- sparkRef.current.dispose?.();
178
+ safelyDisposeSparkResource(sparkRef.current);
115
179
  sparkRef.current = null;
116
180
  }
117
181
  };
@@ -128,6 +192,8 @@ function SparkSplatEnvironment({
128
192
  {
129
193
  ...groupProps,
130
194
  environment,
195
+ scenario,
196
+ renderer,
131
197
  src: metadata.src,
132
198
  format: metadata.format,
133
199
  collisionProxyMetadata: metadata.collisionProxy,
@@ -140,11 +206,30 @@ function SparkSplatEnvironment({
140
206
  }
141
207
  );
142
208
  }
209
+ function safelyDisposeSparkResource(resource) {
210
+ try {
211
+ const result = resource.dispose?.();
212
+ if (isPromiseLike(result)) {
213
+ void Promise.resolve(result).catch(handleSparkDisposeError);
214
+ }
215
+ } catch (error) {
216
+ handleSparkDisposeError(error);
217
+ }
218
+ }
219
+ function isPromiseLike(value) {
220
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
221
+ }
222
+ function handleSparkDisposeError(error) {
223
+ if (error instanceof Error && error.message.toLowerCase().includes("worker terminate")) {
224
+ return;
225
+ }
226
+ console.warn("[mujoco-react] Spark resource disposal failed.", error);
227
+ }
143
228
  /**
144
229
  * @license
145
230
  * SPDX-License-Identifier: Apache-2.0
146
231
  */
147
232
 
148
- export { SparkSplatEnvironment };
233
+ export { SparkSplatEnvironment, useSparkSplatLifecycle };
149
234
  //# sourceMappingURL=spark.js.map
150
235
  //# sourceMappingURL=spark.js.map
package/dist/spark.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/spark.tsx"],"names":[],"mappings":"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","file":"spark.js","sourcesContent":["/**\n * @license\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { useThree } from '@react-three/fiber';\nimport { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport {\n SplatEnvironment,\n useSplatEnvironment,\n} from './components/VisualScenario';\nimport type {\n SplatEnvironmentProps,\n} from './types';\n\ntype SparkModule = typeof import('@sparkjsdev/spark');\ntype SparkRendererInstance = InstanceType<SparkModule['SparkRenderer']>;\ntype SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;\n\nexport type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';\n\nexport interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {\n /** Enable Spark LoD handling for large splat assets. Default: true. */\n lod?: boolean | 'quality';\n /**\n * Hide meshes whose names include floor, ground, or plane while the splat is\n * active. This mirrors the common hybrid-rendering setup where MJCF keeps\n * collision geometry but the splat owns the visual environment.\n */\n hideGroundMeshes?: boolean;\n onStatusChange?: (status: SparkSplatStatus) => void;\n onLoad?: (mesh: SparkSplatMeshInstance) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Optional SparkJS-backed Gaussian splat renderer for React Three Fiber scenes.\n *\n * Import from `mujoco-react/spark` and install `@sparkjsdev/spark` in the app\n * that uses it. The core `mujoco-react` entrypoint does not depend on Spark.\n */\nexport function SparkSplatEnvironment({\n environment,\n src,\n format,\n collisionProxy,\n collisionProxyMetadata,\n showPlaceholder,\n children,\n lod = true,\n hideGroundMeshes = false,\n onStatusChange,\n onLoad,\n onError,\n ...groupProps\n}: SparkSplatEnvironmentProps) {\n const groupRef = useRef<THREE.Group>(null);\n const sparkRef = useRef<SparkRendererInstance | null>(null);\n const meshRef = useRef<SparkSplatMeshInstance | null>(null);\n const hiddenMeshesRef = useRef<THREE.Mesh[]>([]);\n const onStatusChangeRef = useRef(onStatusChange);\n const onLoadRef = useRef(onLoad);\n const onErrorRef = useRef(onError);\n const [status, setStatus] = useState<SparkSplatStatus>('idle');\n const { gl, invalidate } = useThree();\n const metadata = useSplatEnvironment({\n environment,\n src,\n format,\n collisionProxy: collisionProxyMetadata,\n });\n\n useEffect(() => {\n onStatusChangeRef.current = onStatusChange;\n }, [onStatusChange]);\n\n useEffect(() => {\n onLoadRef.current = onLoad;\n }, [onLoad]);\n\n useEffect(() => {\n onErrorRef.current = onError;\n }, [onError]);\n\n useEffect(() => {\n let disposed = false;\n\n function setLifecycleStatus(nextStatus: SparkSplatStatus) {\n setStatus(nextStatus);\n onStatusChangeRef.current?.(nextStatus);\n }\n\n function restoreHiddenMeshes() {\n for (const mesh of hiddenMeshesRef.current) {\n mesh.visible = true;\n }\n hiddenMeshesRef.current = [];\n }\n\n async function loadSplat() {\n if (!metadata.src || metadata.format !== 'spz') {\n setLifecycleStatus('idle');\n return;\n }\n\n setLifecycleStatus('loading');\n\n try {\n const sparkModule = await import('@sparkjsdev/spark');\n if (disposed || !groupRef.current) return;\n\n const spark = new sparkModule.SparkRenderer({\n renderer: gl,\n onDirty: invalidate,\n });\n const mesh = new sparkModule.SplatMesh({\n url: metadata.src,\n lod,\n });\n mesh.name = 'GaussianSplatMesh';\n\n groupRef.current.add(spark);\n groupRef.current.add(mesh);\n sparkRef.current = spark;\n meshRef.current = mesh;\n\n if (hideGroundMeshes && groupRef.current.parent) {\n groupRef.current.parent.traverse((object) => {\n if (\n !(object instanceof THREE.Mesh) ||\n object === (mesh as unknown as THREE.Object3D)\n ) {\n return;\n }\n const name = object.name.toLowerCase();\n if (\n name.includes('floor') ||\n name.includes('ground') ||\n name.includes('plane')\n ) {\n object.visible = false;\n hiddenMeshesRef.current.push(object);\n }\n });\n }\n\n await mesh.initialized;\n if (disposed) return;\n setLifecycleStatus('ready');\n onLoadRef.current?.(mesh);\n invalidate();\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error));\n setLifecycleStatus('error');\n onErrorRef.current?.(normalizedError);\n }\n }\n\n void loadSplat();\n\n return () => {\n disposed = true;\n restoreHiddenMeshes();\n\n if (meshRef.current) {\n groupRef.current?.remove(meshRef.current);\n meshRef.current.dispose?.();\n meshRef.current = null;\n }\n\n if (sparkRef.current) {\n groupRef.current?.remove(sparkRef.current);\n sparkRef.current.dispose?.();\n sparkRef.current = null;\n }\n };\n }, [\n gl,\n hideGroundMeshes,\n invalidate,\n lod,\n metadata.format,\n metadata.src,\n ]);\n\n return (\n <SplatEnvironment\n {...groupProps}\n environment={environment}\n src={metadata.src}\n format={metadata.format}\n collisionProxyMetadata={metadata.collisionProxy}\n collisionProxy={collisionProxy}\n showPlaceholder={showPlaceholder ?? status !== 'ready'}\n >\n <group ref={groupRef} />\n {children}\n </SplatEnvironment>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/spark.tsx"],"names":[],"mappings":"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","file":"spark.js","sourcesContent":["/**\n * @license\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { useThree } from '@react-three/fiber';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport * as THREE from 'three';\nimport {\n SplatEnvironment,\n useSplatEnvironment,\n} from './components/VisualScenario';\nimport type {\n SplatEnvironmentProps,\n} from './types';\n\ntype SparkModule = typeof import('@sparkjsdev/spark');\ntype SparkRendererInstance = InstanceType<SparkModule['SparkRenderer']>;\ntype SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;\ntype SparkDisposable = {\n dispose?: () => unknown;\n};\n\nexport type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';\n\nexport interface SparkSplatLifecycle {\n status: SparkSplatStatus;\n error: Error | null;\n isLoading: boolean;\n isReady: boolean;\n isError: boolean;\n props: Pick<SparkSplatEnvironmentProps, 'onStatusChange' | 'onError'>;\n reset: () => void;\n}\n\nexport interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {\n /** Enable Spark LoD handling for large splat assets. Default: true. */\n lod?: boolean | 'quality';\n /**\n * Hide meshes whose names include floor, ground, or plane while the splat is\n * active. This mirrors the common hybrid-rendering setup where MJCF keeps\n * collision geometry but the splat owns the visual environment.\n */\n hideGroundMeshes?: boolean;\n onStatusChange?: (status: SparkSplatStatus) => void;\n onLoad?: (mesh: SparkSplatMeshInstance) => void;\n onError?: (error: Error) => void;\n}\n\n/**\n * Tracks Spark 3DGS loading state for UI that wraps `SparkSplatEnvironment`.\n *\n * Use the returned `props` with `<SparkSplatEnvironment {...lifecycle.props} />`\n * to avoid repeating status/error state in app code.\n */\nexport function useSparkSplatLifecycle({\n enabled = true,\n initialStatus,\n onError,\n onStatusChange,\n}: {\n enabled?: boolean;\n initialStatus?: SparkSplatStatus;\n onError?: (error: Error) => void;\n onStatusChange?: (status: SparkSplatStatus) => void;\n} = {}): SparkSplatLifecycle {\n const [status, setStatus] = useState<SparkSplatStatus>(\n initialStatus ?? (enabled ? 'loading' : 'idle')\n );\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n setStatus(enabled ? initialStatus ?? 'loading' : 'idle');\n setError(null);\n }, [enabled, initialStatus]);\n\n const handleStatusChange = useCallback(\n (nextStatus: SparkSplatStatus) => {\n setStatus(nextStatus);\n if (nextStatus !== 'error') {\n setError(null);\n }\n onStatusChange?.(nextStatus);\n },\n [onStatusChange]\n );\n\n const handleError = useCallback(\n (nextError: Error) => {\n setError(nextError);\n setStatus('error');\n onError?.(nextError);\n },\n [onError]\n );\n\n const reset = useCallback(() => {\n setStatus(enabled ? initialStatus ?? 'loading' : 'idle');\n setError(null);\n }, [enabled, initialStatus]);\n\n return useMemo(\n () => ({\n status,\n error,\n isLoading: status === 'loading',\n isReady: status === 'ready',\n isError: status === 'error',\n props: {\n onStatusChange: handleStatusChange,\n onError: handleError,\n },\n reset,\n }),\n [error, handleError, handleStatusChange, reset, status]\n );\n}\n\n/**\n * Optional SparkJS-backed Gaussian splat renderer for React Three Fiber scenes.\n *\n * Import from `mujoco-react/spark` and install `@sparkjsdev/spark` in the app\n * that uses it. The core `mujoco-react` entrypoint does not depend on Spark.\n */\nexport function SparkSplatEnvironment({\n environment,\n scenario,\n renderer = 'spark',\n src,\n format,\n collisionProxy,\n collisionProxyMetadata,\n showPlaceholder,\n children,\n lod = true,\n hideGroundMeshes = false,\n onStatusChange,\n onLoad,\n onError,\n ...groupProps\n}: SparkSplatEnvironmentProps) {\n const groupRef = useRef<THREE.Group>(null);\n const sparkRef = useRef<SparkRendererInstance | null>(null);\n const meshRef = useRef<SparkSplatMeshInstance | null>(null);\n const hiddenMeshesRef = useRef<THREE.Mesh[]>([]);\n const onStatusChangeRef = useRef(onStatusChange);\n const onLoadRef = useRef(onLoad);\n const onErrorRef = useRef(onError);\n const [status, setStatus] = useState<SparkSplatStatus>('idle');\n const { gl, invalidate } = useThree();\n const metadata = useSplatEnvironment({\n environment,\n scenario,\n renderer,\n src,\n format,\n collisionProxy: collisionProxyMetadata,\n });\n\n useEffect(() => {\n onStatusChangeRef.current = onStatusChange;\n }, [onStatusChange]);\n\n useEffect(() => {\n onLoadRef.current = onLoad;\n }, [onLoad]);\n\n useEffect(() => {\n onErrorRef.current = onError;\n }, [onError]);\n\n useEffect(() => {\n let disposed = false;\n\n function setLifecycleStatus(nextStatus: SparkSplatStatus) {\n setStatus(nextStatus);\n onStatusChangeRef.current?.(nextStatus);\n }\n\n function restoreHiddenMeshes() {\n for (const mesh of hiddenMeshesRef.current) {\n mesh.visible = true;\n }\n hiddenMeshesRef.current = [];\n }\n\n async function loadSplat() {\n if (!metadata.src) {\n setLifecycleStatus('idle');\n return;\n }\n\n if (metadata.format !== 'spz') {\n const unsupportedFormatError = new Error(\n `SparkSplatEnvironment only supports .spz assets; received \"${metadata.format}\".`\n );\n setLifecycleStatus('error');\n onErrorRef.current?.(unsupportedFormatError);\n return;\n }\n\n setLifecycleStatus('loading');\n\n try {\n const sparkModule = await import('@sparkjsdev/spark');\n if (disposed || !groupRef.current) return;\n\n const spark = new sparkModule.SparkRenderer({\n renderer: gl,\n onDirty: invalidate,\n });\n const mesh = new sparkModule.SplatMesh({\n url: metadata.src,\n lod,\n });\n mesh.name = 'GaussianSplatMesh';\n\n groupRef.current.add(spark);\n groupRef.current.add(mesh);\n sparkRef.current = spark;\n meshRef.current = mesh;\n\n if (hideGroundMeshes && groupRef.current.parent) {\n groupRef.current.parent.traverse((object) => {\n if (\n !(object instanceof THREE.Mesh) ||\n object === (mesh as unknown as THREE.Object3D)\n ) {\n return;\n }\n const name = object.name.toLowerCase();\n if (\n name.includes('floor') ||\n name.includes('ground') ||\n name.includes('plane')\n ) {\n object.visible = false;\n hiddenMeshesRef.current.push(object);\n }\n });\n }\n\n await mesh.initialized;\n if (disposed) return;\n setLifecycleStatus('ready');\n onLoadRef.current?.(mesh);\n invalidate();\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error));\n setLifecycleStatus('error');\n onErrorRef.current?.(normalizedError);\n }\n }\n\n void loadSplat();\n\n return () => {\n disposed = true;\n restoreHiddenMeshes();\n\n if (meshRef.current) {\n groupRef.current?.remove(meshRef.current);\n safelyDisposeSparkResource(meshRef.current);\n meshRef.current = null;\n }\n\n if (sparkRef.current) {\n groupRef.current?.remove(sparkRef.current);\n safelyDisposeSparkResource(sparkRef.current);\n sparkRef.current = null;\n }\n };\n }, [\n gl,\n hideGroundMeshes,\n invalidate,\n lod,\n metadata.format,\n metadata.src,\n ]);\n\n return (\n <SplatEnvironment\n {...groupProps}\n environment={environment}\n scenario={scenario}\n renderer={renderer}\n src={metadata.src}\n format={metadata.format}\n collisionProxyMetadata={metadata.collisionProxy}\n collisionProxy={collisionProxy}\n showPlaceholder={showPlaceholder ?? status !== 'ready'}\n >\n <group ref={groupRef} />\n {children}\n </SplatEnvironment>\n );\n}\n\nfunction safelyDisposeSparkResource(resource: SparkDisposable) {\n try {\n const result = resource.dispose?.();\n if (isPromiseLike(result)) {\n void Promise.resolve(result).catch(handleSparkDisposeError);\n }\n } catch (error) {\n handleSparkDisposeError(error);\n }\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'then' in value &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n\nfunction handleSparkDisposeError(error: unknown) {\n if (\n error instanceof Error &&\n error.message.toLowerCase().includes('worker terminate')\n ) {\n return;\n }\n\n console.warn('[mujoco-react] Spark resource disposal failed.', error);\n}\n"]}
@@ -309,6 +309,8 @@ interface SceneObject {
309
309
  solref?: string;
310
310
  solimp?: string;
311
311
  condim?: number;
312
+ /** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
313
+ group?: number;
312
314
  }
313
315
  interface XmlPatch {
314
316
  target: string;
@@ -320,10 +322,12 @@ type LocalMujocoFile = File;
320
322
  interface LoadFromFilesOptions {
321
323
  /** Entry MJCF/URDF file. Inferred from scene.xml, model.xml, robot.xml, or the first XML/URDF file when omitted. */
322
324
  sceneFile?: string;
325
+ /** Additional MJCF environment XML files merged into the entry scene before MuJoCo compilation. */
326
+ environmentFiles?: string[];
323
327
  homeJoints?: number[];
324
328
  xmlPatches?: XmlPatch[];
325
329
  sceneObjects?: SceneObject[];
326
- onReset?: (model: MujocoModel, data: MujocoData) => void;
330
+ onReset?: (input: ResetCallbackInput) => void;
327
331
  }
328
332
  interface SceneConfig {
329
333
  /** Base URL for fetching model files. The loader fetches `src + sceneFile` and follows dependencies. */
@@ -332,10 +336,18 @@ interface SceneConfig {
332
336
  sceneFile: string;
333
337
  /** Browser-selected files for local MJCF/URDF loading. Preserves webkitRelativePath when available. */
334
338
  files?: readonly LocalMujocoFile[];
339
+ /**
340
+ * Additional MJCF environment XML files merged into the entry scene before compilation.
341
+ *
342
+ * Use this for static collision/physics layers such as a Gaussian-splat
343
+ * environment's proxy `scene.xml`; render the splat itself as a separate
344
+ * visual layer.
345
+ */
346
+ environmentFiles?: string[];
335
347
  sceneObjects?: SceneObject[];
336
348
  homeJoints?: number[];
337
349
  xmlPatches?: XmlPatch[];
338
- onReset?: (model: MujocoModel, data: MujocoData) => void;
350
+ onReset?: (input: ResetCallbackInput) => void;
339
351
  }
340
352
  type ResourceSelector<TInfo, TName extends string = string> = TName | readonly TName[] | RegExp | ((info: TInfo) => boolean);
341
353
  interface IkConfig {
@@ -368,7 +380,7 @@ interface IkContextValue {
368
380
  setIkEnabled: (enabled: boolean) => void;
369
381
  moveTarget: (pos: THREE.Vector3, duration?: number) => void;
370
382
  syncTargetToSite: () => void;
371
- solveIK: (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]) => number[] | null;
383
+ solveIK: (input: IkSolveInput) => number[] | null;
372
384
  getGizmoStats: () => {
373
385
  pos: THREE.Vector3;
374
386
  rot: THREE.Euler;
@@ -386,14 +398,38 @@ interface PhysicsConfig {
386
398
  paused?: boolean;
387
399
  speed?: number;
388
400
  }
389
- 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
+ }
390
408
  interface IKSolveContext {
391
409
  model: MujocoModel;
392
410
  data: MujocoData;
393
411
  siteId: number;
394
412
  controlGroup: ControlGroupInfo;
395
413
  }
396
- 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;
397
433
  interface StateSnapshot {
398
434
  time: number;
399
435
  qpos: Float64Array;
@@ -524,10 +560,24 @@ interface KeyboardTeleopConfig {
524
560
  bindings: Record<string, KeyBinding>;
525
561
  enabled?: boolean;
526
562
  }
563
+ type PolicyVector = Float32Array | Float64Array | number[];
564
+ interface PolicyObservationInput {
565
+ model: MujocoModel;
566
+ data: MujocoData;
567
+ }
568
+ interface PolicyInferenceInput extends PolicyObservationInput {
569
+ observation: PolicyVector;
570
+ }
571
+ interface PolicyActionInput extends PolicyInferenceInput {
572
+ action: PolicyVector;
573
+ }
527
574
  interface PolicyConfig {
528
575
  frequency: number;
529
- onObservation: (model: MujocoModel, data: MujocoData) => Float32Array | Float64Array | number[];
530
- onAction: (action: Float32Array | Float64Array | number[], model: MujocoModel, data: MujocoData) => void;
576
+ enabled?: boolean;
577
+ onObservation: (input: PolicyObservationInput) => PolicyVector;
578
+ /** Run policy inference. Omit to pass observations directly to `onAction` for custom inline controllers. */
579
+ infer?: (input: PolicyInferenceInput) => PolicyVector;
580
+ onAction: (input: PolicyActionInput) => void;
531
581
  }
532
582
  type ObservationOutput = 'float32' | 'float64';
533
583
  interface ObservationConfig {
@@ -580,7 +630,11 @@ interface IkGizmoProps {
580
630
  controller: IkContextValue;
581
631
  siteName?: string;
582
632
  scale?: number;
583
- 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;
584
638
  }
585
639
  interface DragInteractionProps {
586
640
  stiffness?: number;
@@ -600,6 +654,12 @@ interface ScenarioCameraConfig {
600
654
  noise?: number;
601
655
  blur?: number;
602
656
  }
657
+ interface ScenarioMaterialConfig {
658
+ randomizeObjectColors?: boolean;
659
+ randomizeTableMaterial?: boolean;
660
+ roughness?: number;
661
+ metalness?: number;
662
+ }
603
663
  interface SplatAssetConfig {
604
664
  src: string;
605
665
  /** Common browser-friendly splat format. Renderer-specific loaders may accept more. */
@@ -613,7 +673,7 @@ interface SplatScenarioConfig {
613
673
  format?: SplatFormat;
614
674
  src?: string;
615
675
  requiresCollisionProxy?: boolean;
616
- collisionProxy?: SplatCollisionProxyConfig;
676
+ collisionProxy?: SplatCollisionProxyConfig | null;
617
677
  }
618
678
  interface SplatCollisionProxyConfig {
619
679
  /** MJCF/XML file or artifact path that provides physics collision for the visual splat. */
@@ -638,6 +698,8 @@ interface PairedSplatEnvironmentConfig {
638
698
  }
639
699
  interface SplatEnvironmentMetadataInput {
640
700
  environment?: PairedSplatEnvironmentConfig;
701
+ scenario?: VisualScenarioConfig;
702
+ renderer?: SplatRendererKind;
641
703
  src?: string;
642
704
  format?: SplatFormat;
643
705
  collisionProxy?: SplatCollisionProxyConfig;
@@ -648,6 +710,7 @@ interface SplatEnvironmentMetadata {
648
710
  collisionProxy?: SplatCollisionProxyConfig;
649
711
  userData: Record<string, unknown>;
650
712
  }
713
+ type SplatSceneInput = PairedSplatEnvironmentConfig | VisualScenarioConfig | undefined | null;
651
714
  interface VisualScenarioConfig {
652
715
  id?: string;
653
716
  label?: string;
@@ -655,7 +718,8 @@ interface VisualScenarioConfig {
655
718
  lighting?: ScenarioLightingPreset;
656
719
  environment?: string;
657
720
  camera?: ScenarioCameraConfig;
658
- splat?: SplatScenarioConfig;
721
+ materials?: ScenarioMaterialConfig;
722
+ splat?: SplatScenarioConfig | null;
659
723
  }
660
724
  interface ScenarioLightingProps {
661
725
  preset?: ScenarioLightingPreset;
@@ -664,12 +728,30 @@ interface ScenarioLightingProps {
664
728
  }
665
729
  interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref'> {
666
730
  environment?: PairedSplatEnvironmentConfig;
731
+ scenario?: VisualScenarioConfig;
732
+ renderer?: SplatRendererKind;
667
733
  src?: string;
668
734
  format?: SplatFormat;
669
735
  collisionProxy?: ReactNode;
670
736
  collisionProxyMetadata?: SplatCollisionProxyConfig;
671
737
  showPlaceholder?: boolean;
672
738
  }
739
+ interface VisualScenarioEffectsProps {
740
+ scenario?: VisualScenarioConfig;
741
+ enabled?: boolean;
742
+ applyBackground?: boolean;
743
+ applyFog?: boolean;
744
+ applyRenderer?: boolean;
745
+ applyMaterials?: boolean;
746
+ background?: THREE.ColorRepresentation;
747
+ fogNear?: number;
748
+ fogFar?: number;
749
+ materialFilter?: (input: VisualScenarioMaterialFilterInput) => boolean;
750
+ }
751
+ interface VisualScenarioMaterialFilterInput {
752
+ object: THREE.Object3D;
753
+ material: THREE.Material;
754
+ }
673
755
  type TrajectoryInput = TrajectoryFrame[] | number[][];
674
756
  interface TrajectoryPlayerProps {
675
757
  trajectory: TrajectoryInput;
@@ -678,9 +760,16 @@ interface TrajectoryPlayerProps {
678
760
  loop?: boolean;
679
761
  playing?: boolean;
680
762
  mode?: 'kinematic' | 'physics';
681
- onFrame?: (frameIdx: number) => void;
763
+ onFrame?: (input: TrajectoryFrameCallbackInput) => void;
682
764
  onComplete?: () => void;
683
- 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;
684
773
  }
685
774
  interface ContactListenerProps {
686
775
  body: Bodies;
@@ -699,6 +788,8 @@ interface BodyProps {
699
788
  solref?: string;
700
789
  solimp?: string;
701
790
  condim?: number;
791
+ /** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
792
+ group?: number;
702
793
  children?: ReactNode;
703
794
  }
704
795
  interface MujocoSimAPI {
@@ -760,10 +851,12 @@ interface MujocoSimAPI {
760
851
  }
761
852
  type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
762
853
  config: SceneConfig;
763
- onReady?: (api: MujocoSimAPI) => void;
854
+ /** R3F content rendered while the MuJoCo WASM module is still loading. */
855
+ loadingFallback?: ReactNode;
856
+ onReady?: (input: ReadyCallbackInput) => void;
764
857
  onError?: (error: Error) => void;
765
- onStep?: (time: number) => void;
766
- onSelection?: (bodyId: number, name: string) => void;
858
+ onStep?: (input: StepCallbackInput) => void;
859
+ onSelection?: (input: SelectionCallbackInput) => void;
767
860
  gravity?: [number, number, number];
768
861
  timestep?: number;
769
862
  substeps?: number;
@@ -814,4 +907,4 @@ interface JointStateResult {
814
907
  velocity: React__default.RefObject<number | Float64Array>;
815
908
  }
816
909
 
817
- export { type BodyInfo as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type SensorHandle as E, type SensorInfo as F, type GeomInfo as G, type JointStateResult as H, type IkConfig as I, type Joints as J, type Bodies as K, type BodyStateResult as L, type MujocoContextValue as M, type Actuators as N, type ObservationConfig as O, type PhysicsStepCallback as P, type CtrlHandle as Q, type ContactInfo as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type KeyboardTeleopConfig as U, type VisualScenarioConfig as V, type PolicyConfig as W, type ObservationHandle as X, type TrajectoryInput as Y, type PlaybackState as Z, type TrajectoryFrame as _, type MujocoCanvasProps as a, type ControlJointInfo as a0, type Geoms as a1, type IKSolveFn as a2, type JointInfo as a3, type KeyBinding as a4, type Keyframes as a5, type ModelOptions as a6, type MujocoContact as a7, type MujocoContactArray as a8, type ObservationLayoutItem as a9, getContact as aA, registerRobotResources as aB, type ObservationOutput as aa, type PhysicsConfig as ab, type RayHit as ac, type Register as ad, type RegisteredRobotMap as ae, type ResourceSelector as af, RobotActuators as ag, RobotBodies as ah, RobotGeoms as ai, RobotJoints as aj, RobotKeyframes as ak, type RobotResource as al, RobotResources as am, RobotSensors as an, RobotSites as ao, type Robots as ap, type ScenarioCameraConfig as aq, type SceneMarker as ar, type SceneObject as as, type SensorResult as at, type SiteInfo as au, type SplatAssetConfig as av, type SplatScenarioConfig as aw, type StateSnapshot as ax, type TrajectoryData as ay, type XmlPatch 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 PairedSplatEnvironmentConfig as m, type SplatFormat as n, type SplatCollisionProxyConfig as o, type SplatRendererKind as p, type SplatCollisionPrimitive as q, type ScenarioLightingPreset as r, type SplatEnvironmentMetadataInput as s, type SplatEnvironmentMetadata as t, type DebugProps as u, type ContactListenerProps as v, type ActuatorInfo as w, type Sites as x, type SitePositionResult as y, type Sensors as z };
910
+ 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 BodyInfo as a6, type ControlJointInfo as a7, type Geoms as a8, type IKSolveFn as a9, RobotResources as aA, RobotSensors as aB, RobotSites as aC, type Robots as aD, type ScenarioCameraConfig as aE, type ScenarioMaterialConfig as aF, type SceneMarker as aG, type SceneObject as aH, type SensorResult as aI, type SiteInfo as aJ, type SplatAssetConfig as aK, type SplatScenarioConfig as aL, type StateSnapshot as aM, type TrajectoryData as aN, type TrajectoryFrameCallbackInput as aO, type VisualScenarioMaterialFilterInput as aP, type XmlPatch as aQ, getContact as aR, registerRobotResources as aS, type IkGizmoDragInput as aa, type IkSolveInput as ab, type JointInfo as ac, type KeyBinding as ad, type Keyframes as ae, type ModelOptions as af, type MujocoContact as ag, type MujocoContactArray as ah, type ObservationLayoutItem as ai, type ObservationOutput as aj, type PhysicsConfig as ak, type PhysicsStepInput as al, type PolicyActionInput as am, type PolicyInferenceInput as an, type PolicyObservationInput as ao, type RayHit as ap, type Register as aq, type RegisteredRobotMap as ar, type ResetCallbackInput as as, type ResourceSelector as at, RobotActuators as au, RobotBodies as av, RobotGeoms as aw, RobotJoints as ax, RobotKeyframes as ay, type RobotResource 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.10.0",
3
+ "version": "9.0.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,6 +30,7 @@ export function Body({
30
30
  solref,
31
31
  solimp,
32
32
  condim,
33
+ group,
33
34
  children,
34
35
  }: BodyProps) {
35
36
  const { bodyRegistryRef, hiddenBodiesRef, requestBodyReload, mjDataRef, mjModelRef, status } =
@@ -53,6 +54,7 @@ export function Body({
53
54
  solref,
54
55
  solimp,
55
56
  condim,
57
+ group,
56
58
  };
57
59
  bodyRegistryRef.current.set(name, { definition, hasCustomChildren: hasChildren });
58
60
  if (hasChildren) {
@@ -66,7 +68,7 @@ export function Body({
66
68
  requestBodyReload();
67
69
  }
68
70
  };
69
- }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
71
+ }, [name, type, size, position, rgba, mass, freejoint, friction, solref, solimp, condim, group, hasChildren, bodyRegistryRef, hiddenBodiesRef, requestBodyReload]);
70
72
 
71
73
  // Resolve body ID once the scene is ready
72
74
  useEffect(() => {
@@ -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