mujoco-react 8.10.0 → 8.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -28
- package/dist/chunk-SEWQULWO.js +400 -0
- package/dist/chunk-SEWQULWO.js.map +1 -0
- package/dist/index.d.ts +82 -14
- package/dist/index.js +289 -17
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +24 -3
- package/dist/spark.js +91 -6
- package/dist/spark.js.map +1 -1
- package/dist/{types-FFW7ykBu.d.ts → types-BmneHLBM.d.ts} +59 -5
- package/package.json +1 -1
- package/src/components/Body.tsx +3 -1
- package/src/components/VisualScenario.tsx +343 -6
- package/src/core/MujocoCanvas.tsx +8 -1
- package/src/core/SceneLoader.ts +182 -3
- package/src/hooks/useFrameCapture.ts +206 -0
- package/src/hooks/usePolicy.ts +12 -8
- package/src/index.ts +25 -0
- package/src/spark.tsx +138 -4
- package/src/types.ts +69 -4
- package/dist/chunk-KGFRKPLS.js +0 -186
- package/dist/chunk-KGFRKPLS.js.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* useFrameCapture — still-frame capture for canvas-backed MuJoCo/R3F scenes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
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
|
+
}
|
|
51
|
+
|
|
52
|
+
function isTargetRef(
|
|
53
|
+
target: FrameCaptureOptions['target']
|
|
54
|
+
): target is FrameCaptureTargetRef {
|
|
55
|
+
return Boolean(target && typeof target === 'object' && 'current' in target);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCanvasTarget(
|
|
59
|
+
target: FrameCaptureOptions['target']
|
|
60
|
+
): HTMLCanvasElement {
|
|
61
|
+
const resolvedTarget = isTargetRef(target) ? target.current : target;
|
|
62
|
+
|
|
63
|
+
if (!resolvedTarget) {
|
|
64
|
+
throw new Error('No frame capture target is available.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (resolvedTarget instanceof HTMLCanvasElement) {
|
|
68
|
+
return resolvedTarget;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const canvas = resolvedTarget.querySelector('canvas');
|
|
72
|
+
if (!canvas) {
|
|
73
|
+
throw new Error('Frame capture target does not contain a canvas.');
|
|
74
|
+
}
|
|
75
|
+
return canvas;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function waitForNextAnimationFrame() {
|
|
79
|
+
return new Promise<void>((resolve) => {
|
|
80
|
+
requestAnimationFrame(() => resolve());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Capture the current canvas frame as a data URL.
|
|
86
|
+
*
|
|
87
|
+
* For WebGL scenes, create the renderer with `preserveDrawingBuffer: true`
|
|
88
|
+
* when you need deterministic captures after the frame has presented.
|
|
89
|
+
*/
|
|
90
|
+
export async function captureFrame(
|
|
91
|
+
options: FrameCaptureOptions
|
|
92
|
+
): Promise<FrameCaptureResult> {
|
|
93
|
+
const type = options.type ?? 'image/png';
|
|
94
|
+
const canvas = resolveCanvasTarget(options.target);
|
|
95
|
+
|
|
96
|
+
if (options.waitForAnimationFrame ?? true) {
|
|
97
|
+
await waitForNextAnimationFrame();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
canvas,
|
|
102
|
+
dataUrl: canvas.toDataURL(type, options.quality),
|
|
103
|
+
type,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Capture the current canvas frame as a Blob.
|
|
109
|
+
*/
|
|
110
|
+
export async function captureFrameBlob(
|
|
111
|
+
options: FrameCaptureOptions
|
|
112
|
+
): Promise<FrameCaptureBlobResult> {
|
|
113
|
+
const type = options.type ?? 'image/png';
|
|
114
|
+
const canvas = resolveCanvasTarget(options.target);
|
|
115
|
+
|
|
116
|
+
if (options.waitForAnimationFrame ?? true) {
|
|
117
|
+
await waitForNextAnimationFrame();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
121
|
+
canvas.toBlob(
|
|
122
|
+
(nextBlob) => {
|
|
123
|
+
if (nextBlob) {
|
|
124
|
+
resolve(nextBlob);
|
|
125
|
+
} else {
|
|
126
|
+
reject(new Error('Canvas frame capture did not produce a Blob.'));
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
type,
|
|
130
|
+
options.quality
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return { canvas, blob, type };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* React state wrapper around `captureFrame` and `captureFrameBlob`.
|
|
139
|
+
*/
|
|
140
|
+
export function useFrameCapture(
|
|
141
|
+
defaultOptions: FrameCaptureOptions = {}
|
|
142
|
+
): FrameCaptureAPI {
|
|
143
|
+
const [status, setStatus] = useState<FrameCaptureStatus>('idle');
|
|
144
|
+
const [error, setError] = useState<Error | null>(null);
|
|
145
|
+
|
|
146
|
+
const reset = useCallback(() => {
|
|
147
|
+
setStatus('idle');
|
|
148
|
+
setError(null);
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
const capture = useCallback(
|
|
152
|
+
async (options: FrameCaptureOptions = {}) => {
|
|
153
|
+
setStatus('capturing');
|
|
154
|
+
setError(null);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await captureFrame({ ...defaultOptions, ...options });
|
|
158
|
+
setStatus('captured');
|
|
159
|
+
return result;
|
|
160
|
+
} catch (nextError) {
|
|
161
|
+
const error =
|
|
162
|
+
nextError instanceof Error
|
|
163
|
+
? nextError
|
|
164
|
+
: new Error('Unable to capture the current canvas frame.');
|
|
165
|
+
setError(error);
|
|
166
|
+
setStatus('error');
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
[defaultOptions]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const captureBlob = useCallback(
|
|
174
|
+
async (options: FrameCaptureOptions = {}) => {
|
|
175
|
+
setStatus('capturing');
|
|
176
|
+
setError(null);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const result = await captureFrameBlob({
|
|
180
|
+
...defaultOptions,
|
|
181
|
+
...options,
|
|
182
|
+
});
|
|
183
|
+
setStatus('captured');
|
|
184
|
+
return result;
|
|
185
|
+
} catch (nextError) {
|
|
186
|
+
const error =
|
|
187
|
+
nextError instanceof Error
|
|
188
|
+
? nextError
|
|
189
|
+
: new Error('Unable to capture the current canvas frame.');
|
|
190
|
+
setError(error);
|
|
191
|
+
setStatus('error');
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
[defaultOptions]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
status,
|
|
200
|
+
error,
|
|
201
|
+
isCapturing: status === 'capturing',
|
|
202
|
+
capture,
|
|
203
|
+
captureBlob,
|
|
204
|
+
reset,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/src/hooks/usePolicy.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useRef } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
10
10
|
import type { PolicyConfig } from '../types';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -20,12 +20,13 @@ import type { PolicyConfig } from '../types';
|
|
|
20
20
|
* @returns { step, isRunning } control handles
|
|
21
21
|
*/
|
|
22
22
|
export function usePolicy(config: PolicyConfig) {
|
|
23
|
-
const { mjModelRef } = useMujocoContext();
|
|
24
23
|
const lastActionTimeRef = useRef(0);
|
|
24
|
+
const lastObservationRef = useRef<ReturnType<PolicyConfig['onObservation']> | null>(null);
|
|
25
25
|
const lastActionRef = useRef<Float32Array | Float64Array | number[] | null>(null);
|
|
26
|
-
const isRunningRef = useRef(true);
|
|
26
|
+
const isRunningRef = useRef(config.enabled ?? true);
|
|
27
27
|
const configRef = useRef(config);
|
|
28
28
|
configRef.current = config;
|
|
29
|
+
isRunningRef.current = config.enabled ?? isRunningRef.current;
|
|
29
30
|
|
|
30
31
|
useBeforePhysicsStep((model, data) => {
|
|
31
32
|
if (!isRunningRef.current) return;
|
|
@@ -37,13 +38,15 @@ export function usePolicy(config: PolicyConfig) {
|
|
|
37
38
|
// Check if it's time for a new action
|
|
38
39
|
if (data.time - lastActionTimeRef.current >= interval) {
|
|
39
40
|
// Build observation
|
|
40
|
-
const
|
|
41
|
+
const observation = cfg.onObservation({ model, data });
|
|
42
|
+
const action = cfg.infer ? cfg.infer({ observation, model, data }) : observation;
|
|
41
43
|
|
|
42
|
-
// Apply action
|
|
43
|
-
cfg.onAction(
|
|
44
|
+
// Apply action. If `infer` is omitted, this preserves the legacy inline-controller path.
|
|
45
|
+
cfg.onAction({ action, observation, model, data });
|
|
44
46
|
|
|
45
47
|
lastActionTimeRef.current = data.time;
|
|
46
|
-
|
|
48
|
+
lastObservationRef.current = observation;
|
|
49
|
+
lastActionRef.current = action;
|
|
47
50
|
}
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -51,6 +54,7 @@ export function usePolicy(config: PolicyConfig) {
|
|
|
51
54
|
get isRunning() { return isRunningRef.current; },
|
|
52
55
|
start: () => { isRunningRef.current = true; },
|
|
53
56
|
stop: () => { isRunningRef.current = false; },
|
|
54
|
-
get lastObservation() { return
|
|
57
|
+
get lastObservation() { return lastObservationRef.current; },
|
|
58
|
+
get lastAction() { return lastActionRef.current; },
|
|
55
59
|
};
|
|
56
60
|
}
|
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';
|
|
@@ -124,6 +142,10 @@ export type {
|
|
|
124
142
|
KeyboardTeleopConfig,
|
|
125
143
|
// Policy
|
|
126
144
|
PolicyConfig,
|
|
145
|
+
PolicyVector,
|
|
146
|
+
PolicyObservationInput,
|
|
147
|
+
PolicyInferenceInput,
|
|
148
|
+
PolicyActionInput,
|
|
127
149
|
// Observations
|
|
128
150
|
ObservationConfig,
|
|
129
151
|
ObservationHandle,
|
|
@@ -147,9 +169,12 @@ export type {
|
|
|
147
169
|
PairedSplatEnvironmentConfig,
|
|
148
170
|
SplatEnvironmentMetadataInput,
|
|
149
171
|
SplatEnvironmentMetadata,
|
|
172
|
+
SplatSceneInput,
|
|
150
173
|
VisualScenarioConfig,
|
|
151
174
|
ScenarioLightingProps,
|
|
175
|
+
ScenarioMaterialConfig,
|
|
152
176
|
SplatEnvironmentProps,
|
|
177
|
+
VisualScenarioEffectsProps,
|
|
153
178
|
TrajectoryPlayerProps,
|
|
154
179
|
ContactListenerProps,
|
|
155
180
|
// API
|
package/src/spark.tsx
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useThree } from '@react-three/fiber';
|
|
7
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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,6 +445,8 @@ 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[];
|
|
@@ -456,6 +460,14 @@ 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[];
|
|
@@ -709,10 +721,28 @@ export interface KeyboardTeleopConfig {
|
|
|
709
721
|
|
|
710
722
|
// ---- Policy (spec 10.1) ----
|
|
711
723
|
|
|
724
|
+
export type PolicyVector = Float32Array | Float64Array | number[];
|
|
725
|
+
|
|
726
|
+
export interface PolicyObservationInput {
|
|
727
|
+
model: MujocoModel;
|
|
728
|
+
data: MujocoData;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export interface PolicyInferenceInput extends PolicyObservationInput {
|
|
732
|
+
observation: PolicyVector;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface PolicyActionInput extends PolicyInferenceInput {
|
|
736
|
+
action: PolicyVector;
|
|
737
|
+
}
|
|
738
|
+
|
|
712
739
|
export interface PolicyConfig {
|
|
713
740
|
frequency: number;
|
|
714
|
-
|
|
715
|
-
|
|
741
|
+
enabled?: boolean;
|
|
742
|
+
onObservation: (input: PolicyObservationInput) => PolicyVector;
|
|
743
|
+
/** Run policy inference. Omit to pass observations directly to `onAction` for custom inline controllers. */
|
|
744
|
+
infer?: (input: PolicyInferenceInput) => PolicyVector;
|
|
745
|
+
onAction: (input: PolicyActionInput) => void;
|
|
716
746
|
}
|
|
717
747
|
|
|
718
748
|
// ---- Observation Builder ----
|
|
@@ -805,6 +835,13 @@ export interface ScenarioCameraConfig {
|
|
|
805
835
|
blur?: number;
|
|
806
836
|
}
|
|
807
837
|
|
|
838
|
+
export interface ScenarioMaterialConfig {
|
|
839
|
+
randomizeObjectColors?: boolean;
|
|
840
|
+
randomizeTableMaterial?: boolean;
|
|
841
|
+
roughness?: number;
|
|
842
|
+
metalness?: number;
|
|
843
|
+
}
|
|
844
|
+
|
|
808
845
|
export interface SplatAssetConfig {
|
|
809
846
|
src: string;
|
|
810
847
|
/** Common browser-friendly splat format. Renderer-specific loaders may accept more. */
|
|
@@ -819,7 +856,7 @@ export interface SplatScenarioConfig {
|
|
|
819
856
|
format?: SplatFormat;
|
|
820
857
|
src?: string;
|
|
821
858
|
requiresCollisionProxy?: boolean;
|
|
822
|
-
collisionProxy?: SplatCollisionProxyConfig;
|
|
859
|
+
collisionProxy?: SplatCollisionProxyConfig | null;
|
|
823
860
|
}
|
|
824
861
|
|
|
825
862
|
export interface SplatCollisionProxyConfig {
|
|
@@ -845,6 +882,8 @@ export interface PairedSplatEnvironmentConfig {
|
|
|
845
882
|
|
|
846
883
|
export interface SplatEnvironmentMetadataInput {
|
|
847
884
|
environment?: PairedSplatEnvironmentConfig;
|
|
885
|
+
scenario?: VisualScenarioConfig;
|
|
886
|
+
renderer?: SplatRendererKind;
|
|
848
887
|
src?: string;
|
|
849
888
|
format?: SplatFormat;
|
|
850
889
|
collisionProxy?: SplatCollisionProxyConfig;
|
|
@@ -857,6 +896,12 @@ export interface SplatEnvironmentMetadata {
|
|
|
857
896
|
userData: Record<string, unknown>;
|
|
858
897
|
}
|
|
859
898
|
|
|
899
|
+
export type SplatSceneInput =
|
|
900
|
+
| PairedSplatEnvironmentConfig
|
|
901
|
+
| VisualScenarioConfig
|
|
902
|
+
| undefined
|
|
903
|
+
| null;
|
|
904
|
+
|
|
860
905
|
export interface VisualScenarioConfig {
|
|
861
906
|
id?: string;
|
|
862
907
|
label?: string;
|
|
@@ -864,7 +909,8 @@ export interface VisualScenarioConfig {
|
|
|
864
909
|
lighting?: ScenarioLightingPreset;
|
|
865
910
|
environment?: string;
|
|
866
911
|
camera?: ScenarioCameraConfig;
|
|
867
|
-
|
|
912
|
+
materials?: ScenarioMaterialConfig;
|
|
913
|
+
splat?: SplatScenarioConfig | null;
|
|
868
914
|
}
|
|
869
915
|
|
|
870
916
|
export interface ScenarioLightingProps {
|
|
@@ -875,6 +921,8 @@ export interface ScenarioLightingProps {
|
|
|
875
921
|
|
|
876
922
|
export interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
877
923
|
environment?: PairedSplatEnvironmentConfig;
|
|
924
|
+
scenario?: VisualScenarioConfig;
|
|
925
|
+
renderer?: SplatRendererKind;
|
|
878
926
|
src?: string;
|
|
879
927
|
format?: SplatFormat;
|
|
880
928
|
collisionProxy?: ReactNode;
|
|
@@ -882,6 +930,19 @@ export interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref
|
|
|
882
930
|
showPlaceholder?: boolean;
|
|
883
931
|
}
|
|
884
932
|
|
|
933
|
+
export interface VisualScenarioEffectsProps {
|
|
934
|
+
scenario?: VisualScenarioConfig;
|
|
935
|
+
enabled?: boolean;
|
|
936
|
+
applyBackground?: boolean;
|
|
937
|
+
applyFog?: boolean;
|
|
938
|
+
applyRenderer?: boolean;
|
|
939
|
+
applyMaterials?: boolean;
|
|
940
|
+
background?: THREE.ColorRepresentation;
|
|
941
|
+
fogNear?: number;
|
|
942
|
+
fogFar?: number;
|
|
943
|
+
materialFilter?: (object: THREE.Object3D, material: THREE.Material) => boolean;
|
|
944
|
+
}
|
|
945
|
+
|
|
885
946
|
export type TrajectoryInput = TrajectoryFrame[] | number[][];
|
|
886
947
|
|
|
887
948
|
export interface TrajectoryPlayerProps {
|
|
@@ -920,6 +981,8 @@ export interface BodyProps {
|
|
|
920
981
|
solref?: string;
|
|
921
982
|
solimp?: string;
|
|
922
983
|
condim?: number;
|
|
984
|
+
/** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
|
|
985
|
+
group?: number;
|
|
923
986
|
children?: ReactNode;
|
|
924
987
|
}
|
|
925
988
|
|
|
@@ -1017,6 +1080,8 @@ export interface MujocoSimAPI {
|
|
|
1017
1080
|
|
|
1018
1081
|
export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
|
|
1019
1082
|
config: SceneConfig;
|
|
1083
|
+
/** R3F content rendered while the MuJoCo WASM module is still loading. */
|
|
1084
|
+
loadingFallback?: ReactNode;
|
|
1020
1085
|
onReady?: (api: MujocoSimAPI) => void;
|
|
1021
1086
|
onError?: (error: Error) => void;
|
|
1022
1087
|
onStep?: (time: number) => void;
|