mujoco-react 9.2.0 → 9.4.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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * React state wrapper for named MuJoCo camera/site/body sequence recording.
6
+ */
7
+
8
+ import { useCallback, useState } from 'react';
9
+ import { useMujoco } from '../core/MujocoSimProvider';
10
+ import {
11
+ createMountedCameraFrameSequenceReadiness,
12
+ createMountedCameraFrameSequencePlanFromApi,
13
+ recordMountedCameraFrameSequence,
14
+ type MountedCameraFrameSequencePlan,
15
+ type MountedCameraFrameSequencePlanOptions,
16
+ type MountedCameraFrameSequenceReadiness,
17
+ type MountedCameraFrameSequenceRecordOptions,
18
+ type MountedCameraFrameSequenceRecordResult,
19
+ } from '../rendering/cameraFrameSource';
20
+ import type {
21
+ CameraFrameSequenceRecorderAPI,
22
+ FrameCaptureStatus,
23
+ } from '../types';
24
+
25
+ export type MountedCameraSequencePlanOptions =
26
+ MountedCameraFrameSequencePlanOptions;
27
+ export type MountedCameraSequenceRecordOptions =
28
+ MountedCameraFrameSequenceRecordOptions;
29
+ export type MountedCameraSequenceRecordResult =
30
+ MountedCameraFrameSequenceRecordResult;
31
+ export type MountedCameraSequenceReadiness =
32
+ MountedCameraFrameSequenceReadiness;
33
+
34
+ export interface MountedCameraSequenceRecorderAPI
35
+ extends Omit<CameraFrameSequenceRecorderAPI, 'record'> {
36
+ plan: MountedCameraFrameSequencePlan | null;
37
+ readiness: MountedCameraSequenceReadiness | null;
38
+ result: MountedCameraSequenceRecordResult | null;
39
+ createPlan: (
40
+ cameraKeys: readonly string[],
41
+ options?: MountedCameraSequencePlanOptions
42
+ ) => MountedCameraFrameSequencePlan;
43
+ checkReadiness: (
44
+ cameraKeys: readonly string[],
45
+ options?: MountedCameraSequencePlanOptions
46
+ ) => MountedCameraSequenceReadiness;
47
+ record: (
48
+ options: MountedCameraSequenceRecordOptions
49
+ ) => Promise<MountedCameraSequenceRecordResult>;
50
+ }
51
+
52
+ export function useMountedCameraSequenceRecorder(
53
+ defaultOptions: MountedCameraSequencePlanOptions = {}
54
+ ): MountedCameraSequenceRecorderAPI {
55
+ const mujoco = useMujoco();
56
+ const [status, setStatus] = useState<FrameCaptureStatus>('idle');
57
+ const [error, setError] = useState<Error | null>(null);
58
+ const [plan, setPlan] = useState<MountedCameraFrameSequencePlan | null>(null);
59
+ const [readiness, setReadiness] =
60
+ useState<MountedCameraSequenceReadiness | null>(null);
61
+ const [result, setResult] = useState<MountedCameraSequenceRecordResult | null>(
62
+ null
63
+ );
64
+
65
+ const reset = useCallback(() => {
66
+ setStatus('idle');
67
+ setError(null);
68
+ setPlan(null);
69
+ setReadiness(null);
70
+ setResult(null);
71
+ }, []);
72
+
73
+ const createPlan = useCallback(
74
+ (
75
+ cameraKeys: readonly string[],
76
+ options: MountedCameraSequencePlanOptions = {}
77
+ ) => {
78
+ if (!mujoco.api) {
79
+ throw new Error('MuJoCo scene is not ready for mounted camera sequence planning.');
80
+ }
81
+
82
+ const nextPlan = createMountedCameraFrameSequencePlanFromApi(
83
+ mujoco.api,
84
+ cameraKeys,
85
+ {
86
+ ...defaultOptions,
87
+ ...options,
88
+ }
89
+ );
90
+ setPlan(nextPlan);
91
+ setReadiness(null);
92
+ return nextPlan;
93
+ },
94
+ [defaultOptions, mujoco.api]
95
+ );
96
+
97
+ const checkReadiness = useCallback(
98
+ (
99
+ cameraKeys: readonly string[],
100
+ options: MountedCameraSequencePlanOptions = {}
101
+ ) => {
102
+ const nextPlan = createPlan(cameraKeys, options);
103
+ const nextReadiness = createMountedCameraFrameSequenceReadiness(nextPlan);
104
+ setReadiness(nextReadiness);
105
+ return nextReadiness;
106
+ },
107
+ [createPlan]
108
+ );
109
+
110
+ const record = useCallback(
111
+ async (options: MountedCameraSequenceRecordOptions) => {
112
+ if (!mujoco.api) {
113
+ throw new Error('MuJoCo scene is not ready for mounted camera sequence recording.');
114
+ }
115
+
116
+ setStatus('capturing');
117
+ setError(null);
118
+ setResult(null);
119
+
120
+ try {
121
+ const nextResult = await recordMountedCameraFrameSequence(mujoco.api, {
122
+ ...defaultOptions,
123
+ ...options,
124
+ });
125
+ setPlan(nextResult.plan);
126
+ setReadiness(nextResult.readiness);
127
+ setResult(nextResult);
128
+ setStatus('captured');
129
+ return nextResult;
130
+ } catch (nextError) {
131
+ const error =
132
+ nextError instanceof Error
133
+ ? nextError
134
+ : new Error('Unable to record the requested mounted camera sequence.');
135
+ setError(error);
136
+ setStatus('error');
137
+ throw error;
138
+ }
139
+ },
140
+ [defaultOptions, mujoco.api]
141
+ );
142
+
143
+ return {
144
+ status,
145
+ error,
146
+ plan,
147
+ readiness,
148
+ result,
149
+ isRecording: status === 'capturing',
150
+ createPlan,
151
+ checkReadiness,
152
+ record,
153
+ reset,
154
+ };
155
+ }
package/src/index.ts CHANGED
@@ -48,12 +48,32 @@ export {
48
48
  createPairedSplatEnvironment,
49
49
  createSparkSplatViewerUrl,
50
50
  createSplatEnvironmentUserData,
51
+ createSplatSceneConfig,
52
+ createVisualScenarioExecutionContext,
53
+ getSplatEnvironmentReadiness,
51
54
  getScenarioBackground,
52
55
  getScenarioCameraPosition,
53
56
  useSplatEnvironment,
57
+ useSplatSceneConfig,
58
+ useVisualScenarioExecutionContext,
54
59
  useVisualScenarioEffects,
55
60
  withSplatEnvironment,
56
61
  } from './components/VisualScenario';
62
+ export {
63
+ canFetchSplatCollisionProxyXml,
64
+ fetchSplatCollisionProxyXml,
65
+ parseSplatCollisionProxyGeoms,
66
+ SplatCollisionProxyPreview,
67
+ useSplatCollisionProxyGeoms,
68
+ } from './components/SplatCollisionProxyPreview';
69
+ export type {
70
+ SplatCollisionProxyGeomPreview,
71
+ SplatCollisionProxyGeomsState,
72
+ SplatCollisionProxyPreviewProps,
73
+ SplatCollisionProxyPreviewStatus,
74
+ SplatCollisionProxyPreviewVector3,
75
+ UseSplatCollisionProxyGeomsOptions,
76
+ } from './components/SplatCollisionProxyPreview';
57
77
  export { Debug } from './components/Debug';
58
78
  export { TendonRenderer } from './components/TendonRenderer';
59
79
  export { FlexRenderer } from './components/FlexRenderer';
@@ -84,11 +104,56 @@ export {
84
104
  } from './hooks/useFrameCapture';
85
105
  export { useCameraFrameCapture } from './hooks/useCameraFrameCapture';
86
106
  export { useCameraSequenceRecorder } from './hooks/useCameraSequenceRecorder';
107
+ export { useMountedCameraSequenceRecorder } from './hooks/useMountedCameraSequenceRecorder';
108
+ export type {
109
+ MountedCameraSequencePlanOptions,
110
+ MountedCameraSequenceRecorderAPI,
111
+ MountedCameraSequenceReadiness,
112
+ MountedCameraSequenceRecordOptions,
113
+ MountedCameraSequenceRecordResult,
114
+ } from './hooks/useMountedCameraSequenceRecorder';
87
115
  export {
88
116
  captureCameraFrame,
89
117
  captureCameraFrameBlob,
118
+ createCameraFrameCaptureSession,
90
119
  renderCameraFrameToCanvas,
91
120
  } from './rendering/cameraFrameCapture';
121
+ export {
122
+ createMountedCameraFrameSequenceManifest,
123
+ createMountedCameraFrameSequenceReadiness,
124
+ createMountedCameraFrameSourceSuggestions,
125
+ MountedCameraFrameSequenceManifestStatus,
126
+ MountedCameraFrameSequenceReadinessStatus,
127
+ MountedCameraFrameSourceSuggestionMatch,
128
+ createMountedCameraFrameSequencePlanFromApi,
129
+ createMountedCameraFrameSequencePlan,
130
+ getCameraFrameCaptureSourceTarget,
131
+ getMountedCameraFrameCaptureSource,
132
+ isMountedCameraFrameCaptureSource,
133
+ recordMountedCameraFrameSequence,
134
+ resolveMountedCameraFrameSource,
135
+ } from './rendering/cameraFrameSource';
136
+ export type {
137
+ CameraFrameMountSelector,
138
+ CreateMountedCameraFrameSequenceManifestOptions,
139
+ CreateMountedCameraFrameSequencePlanOptions,
140
+ MountedCameraFrameCaptureSource,
141
+ MountedCameraFrameSequenceManifest,
142
+ MountedCameraFrameSequencePlanOptions,
143
+ MountedCameraFrameSequenceRecorderTarget,
144
+ MountedCameraFrameSequenceCameraOptions,
145
+ MountedCameraFrameSequenceDefaults,
146
+ MountedCameraFrameSequencePlan,
147
+ MountedCameraFrameSequenceReadiness,
148
+ MountedCameraFrameSequenceRecordOptions,
149
+ MountedCameraFrameSequenceRecordResult,
150
+ MountedCameraFrameSequenceSourceReadiness,
151
+ MountedCameraFrameSequenceStreamSummary,
152
+ MountedCameraFrameSourceSuggestion,
153
+ NamedCameraFrameResource,
154
+ ResolveMountedCameraFrameSourceOptions,
155
+ ResolvedMountedCameraFrameSource,
156
+ } from './rendering/cameraFrameSource';
92
157
  export { useCtrlNoise } from './hooks/useCtrlNoise';
93
158
  export { useBodyMeshes } from './hooks/useBodyMeshes';
94
159
  export { useSelectionHighlight } from './hooks/useSelectionHighlight';
@@ -130,6 +195,7 @@ export type {
130
195
  SiteInfo,
131
196
  ActuatorInfo,
132
197
  SensorInfo,
198
+ CameraInfo,
133
199
  // Contacts
134
200
  ContactInfo,
135
201
  // Raycast
@@ -172,8 +238,15 @@ export type {
172
238
  SplatScenarioConfig,
173
239
  SplatCollisionProxyConfig,
174
240
  PairedSplatEnvironmentConfig,
241
+ SplatEnvironmentReadiness,
175
242
  SplatEnvironmentMetadataInput,
176
243
  SplatEnvironmentMetadata,
244
+ VisualScenarioExecutionContext,
245
+ VisualScenarioExecutionContextInput,
246
+ ResolvedScenarioCameraConfig,
247
+ ResolvedScenarioMaterialConfig,
248
+ SplatSceneConfigInput,
249
+ SplatSceneConfigState,
177
250
  SplatSceneInput,
178
251
  VisualScenarioConfig,
179
252
  ScenarioLightingProps,
@@ -193,12 +266,16 @@ export type {
193
266
  CameraFrameCaptureOptions,
194
267
  CameraFrameCaptureQuaternion,
195
268
  CameraFrameCaptureResult,
269
+ CameraFrameCaptureSource,
196
270
  CameraFrameCaptureVector3,
197
271
  CameraFrameSequenceCamera,
272
+ CameraFrameSequenceCameraSummary,
198
273
  CameraFrameSequenceFrame,
199
274
  CameraFrameSequenceOptions,
200
275
  CameraFrameSequenceRecorderAPI,
201
276
  CameraFrameSequenceResult,
277
+ CameraFrameSequenceSampleInput,
278
+ CameraFrameSequenceStepInput,
202
279
  MujocoCanvasProps,
203
280
  MujocoContextValue,
204
281
  // Hook return types
@@ -227,6 +304,7 @@ export type {
227
304
  Sites,
228
305
  Geoms,
229
306
  Keyframes,
307
+ Cameras,
230
308
  } from './types';
231
309
 
232
310
  export {
@@ -239,6 +317,8 @@ export {
239
317
  RobotSites,
240
318
  RobotGeoms,
241
319
  RobotKeyframes,
320
+ RobotCameras,
321
+ SplatEnvironmentReadinessStatus,
242
322
  } from './types';
243
323
 
244
324
  // Re-export MuJoCo types for convenience
@@ -10,9 +10,25 @@ import type {
10
10
  CameraFrameCaptureBlobResult,
11
11
  CameraFrameCaptureOptions,
12
12
  CameraFrameCaptureResult,
13
+ CameraFrameCaptureSource,
13
14
  CameraFrameCaptureVector3,
14
15
  } from '../types';
15
16
 
17
+ export interface CameraFrameCaptureSession {
18
+ readonly width: number;
19
+ readonly height: number;
20
+ capture(options?: CameraFrameCaptureOptions): {
21
+ canvas: HTMLCanvasElement;
22
+ camera: THREE.Camera;
23
+ width: number;
24
+ height: number;
25
+ source: CameraFrameCaptureSource;
26
+ };
27
+ captureDataUrl(options?: CameraFrameCaptureOptions): CameraFrameCaptureResult;
28
+ captureBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
29
+ dispose(): void;
30
+ }
31
+
16
32
  function toVector3(
17
33
  value: CameraFrameCaptureVector3 | undefined,
18
34
  fallback: THREE.Vector3
@@ -75,24 +91,55 @@ function createCaptureCamera(
75
91
  return camera;
76
92
  }
77
93
 
94
+ function getCaptureDimensions(
95
+ renderer: THREE.WebGLRenderer,
96
+ options: CameraFrameCaptureOptions
97
+ ) {
98
+ const width = Math.max(
99
+ 1,
100
+ Math.floor(options.width ?? renderer.domElement.width)
101
+ );
102
+ const height = Math.max(
103
+ 1,
104
+ Math.floor(options.height ?? renderer.domElement.height)
105
+ );
106
+ return { width, height };
107
+ }
108
+
109
+ function prepareCaptureCamera(
110
+ camera: THREE.Camera,
111
+ options: CameraFrameCaptureOptions,
112
+ fallbackCamera: THREE.Camera,
113
+ width: number,
114
+ height: number
115
+ ) {
116
+ if (options.camera) {
117
+ camera.copy(options.camera);
118
+ }
119
+
120
+ if (camera instanceof THREE.PerspectiveCamera) {
121
+ camera.aspect = width / height;
122
+ camera.fov = options.fov ?? camera.fov;
123
+ camera.near = options.near ?? camera.near;
124
+ camera.far = options.far ?? camera.far;
125
+ camera.updateProjectionMatrix();
126
+ }
127
+
128
+ applyCameraPose(camera, options, fallbackCamera);
129
+ }
130
+
78
131
  function readRenderTargetToCanvas(
79
132
  renderer: THREE.WebGLRenderer,
80
133
  target: THREE.WebGLRenderTarget,
134
+ canvas: HTMLCanvasElement,
135
+ context: CanvasRenderingContext2D,
136
+ pixels: Uint8Array,
137
+ imageData: ImageData,
81
138
  width: number,
82
139
  height: number
83
140
  ) {
84
- const pixels = new Uint8Array(width * height * 4);
85
141
  renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
86
142
 
87
- const canvas = document.createElement('canvas');
88
- canvas.width = width;
89
- canvas.height = height;
90
- const context = canvas.getContext('2d');
91
- if (!context) {
92
- throw new Error('Unable to create a 2D canvas for camera frame capture.');
93
- }
94
-
95
- const imageData = context.createImageData(width, height);
96
143
  const rowBytes = width * 4;
97
144
  for (let y = 0; y < height; y += 1) {
98
145
  const sourceStart = (height - y - 1) * rowBytes;
@@ -106,34 +153,156 @@ function readRenderTargetToCanvas(
106
153
  return canvas;
107
154
  }
108
155
 
109
- export function renderCameraFrameToCanvas(
156
+ function getCameraFrameCaptureSource(
157
+ options: CameraFrameCaptureOptions
158
+ ): CameraFrameCaptureSource {
159
+ if (options.source) return options.source;
160
+ if (options.cameraName) {
161
+ return { kind: 'mujoco-camera', cameraName: options.cameraName };
162
+ }
163
+ if (options.siteName) {
164
+ return { kind: 'mujoco-site', siteName: options.siteName };
165
+ }
166
+ if (options.bodyName) {
167
+ return { kind: 'mujoco-body', bodyName: options.bodyName };
168
+ }
169
+ if (options.camera) return { kind: 'custom-camera' };
170
+ if (options.position || options.lookAt || options.quaternion) {
171
+ return { kind: 'explicit-pose' };
172
+ }
173
+ return { kind: 'fallback-camera' };
174
+ }
175
+
176
+ export function createCameraFrameCaptureSession(
110
177
  renderer: THREE.WebGLRenderer,
111
178
  scene: THREE.Scene,
112
179
  fallbackCamera: THREE.Camera,
113
180
  options: CameraFrameCaptureOptions = {}
114
- ) {
115
- const width = Math.max(1, Math.floor(options.width ?? renderer.domElement.width));
116
- const height = Math.max(1, Math.floor(options.height ?? renderer.domElement.height));
181
+ ): CameraFrameCaptureSession {
182
+ const { width, height } = getCaptureDimensions(renderer, options);
117
183
  const camera = createCaptureCamera(options, fallbackCamera, width, height);
118
184
  const target = new THREE.WebGLRenderTarget(width, height, {
119
185
  format: THREE.RGBAFormat,
120
186
  type: THREE.UnsignedByteType,
121
187
  });
122
- const previousTarget = renderer.getRenderTarget();
123
- const previousXrEnabled = renderer.xr.enabled;
188
+ const canvas = document.createElement('canvas');
189
+ canvas.width = width;
190
+ canvas.height = height;
191
+ const context = canvas.getContext('2d');
192
+ if (!context) {
193
+ target.dispose();
194
+ throw new Error('Unable to create a 2D canvas for camera frame capture.');
195
+ }
196
+ const drawContext = context;
197
+
198
+ const pixels = new Uint8Array(width * height * 4);
199
+ const imageData = drawContext.createImageData(width, height);
200
+
201
+ function capture(nextOptions: CameraFrameCaptureOptions = {}) {
202
+ const captureOptions = { ...options, ...nextOptions };
203
+ const nextDimensions = getCaptureDimensions(renderer, captureOptions);
204
+ if (
205
+ nextDimensions.width !== width ||
206
+ nextDimensions.height !== height
207
+ ) {
208
+ throw new Error(
209
+ 'Camera frame capture sessions require stable width and height.'
210
+ );
211
+ }
212
+
213
+ prepareCaptureCamera(
214
+ camera,
215
+ captureOptions,
216
+ fallbackCamera,
217
+ width,
218
+ height
219
+ );
220
+
221
+ const previousTarget = renderer.getRenderTarget();
222
+ const previousXrEnabled = renderer.xr.enabled;
124
223
 
125
- scene.updateMatrixWorld(true);
224
+ scene.updateMatrixWorld(true);
225
+ try {
226
+ renderer.xr.enabled = false;
227
+ renderer.setRenderTarget(target);
228
+ renderer.clear();
229
+ renderer.render(scene, camera);
230
+ readRenderTargetToCanvas(
231
+ renderer,
232
+ target,
233
+ canvas,
234
+ drawContext,
235
+ pixels,
236
+ imageData,
237
+ width,
238
+ height
239
+ );
240
+ return {
241
+ canvas,
242
+ camera,
243
+ width,
244
+ height,
245
+ source: getCameraFrameCaptureSource(captureOptions),
246
+ };
247
+ } finally {
248
+ renderer.setRenderTarget(previousTarget);
249
+ renderer.xr.enabled = previousXrEnabled;
250
+ }
251
+ }
252
+
253
+ return {
254
+ width,
255
+ height,
256
+ capture,
257
+ captureDataUrl(nextOptions = {}) {
258
+ const type = nextOptions.type ?? options.type ?? 'image/png';
259
+ const result = capture(nextOptions);
260
+ return {
261
+ ...result,
262
+ dataUrl: result.canvas.toDataURL(
263
+ type,
264
+ nextOptions.quality ?? options.quality
265
+ ),
266
+ type,
267
+ };
268
+ },
269
+ async captureBlob(nextOptions = {}) {
270
+ const type = nextOptions.type ?? options.type ?? 'image/png';
271
+ const result = capture(nextOptions);
272
+ const blob = await new Promise<Blob>((resolve, reject) => {
273
+ result.canvas.toBlob(
274
+ (nextBlob) => {
275
+ if (nextBlob) resolve(nextBlob);
276
+ else reject(new Error('Camera frame capture did not produce a Blob.'));
277
+ },
278
+ type,
279
+ nextOptions.quality ?? options.quality
280
+ );
281
+ });
282
+ return { ...result, blob, type };
283
+ },
284
+ dispose() {
285
+ target.dispose();
286
+ },
287
+ };
288
+ }
289
+
290
+ export function renderCameraFrameToCanvas(
291
+ renderer: THREE.WebGLRenderer,
292
+ scene: THREE.Scene,
293
+ fallbackCamera: THREE.Camera,
294
+ options: CameraFrameCaptureOptions = {}
295
+ ) {
296
+ const session = createCameraFrameCaptureSession(
297
+ renderer,
298
+ scene,
299
+ fallbackCamera,
300
+ options
301
+ );
126
302
  try {
127
- renderer.xr.enabled = false;
128
- renderer.setRenderTarget(target);
129
- renderer.clear();
130
- renderer.render(scene, camera);
131
- const canvas = readRenderTargetToCanvas(renderer, target, width, height);
132
- return { canvas, camera, width, height };
303
+ return session.capture();
133
304
  } finally {
134
- renderer.setRenderTarget(previousTarget);
135
- renderer.xr.enabled = previousXrEnabled;
136
- target.dispose();
305
+ session.dispose();
137
306
  }
138
307
  }
139
308