mujoco-react 9.2.0 → 9.3.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,107 @@
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
+ createMountedCameraFrameSequencePlanFromApi,
12
+ recordMountedCameraFrameSequence,
13
+ type MountedCameraFrameSequencePlan,
14
+ type MountedCameraFrameSequencePlanOptions,
15
+ type MountedCameraFrameSequenceRecordOptions,
16
+ type MountedCameraFrameSequenceRecordResult,
17
+ } from '../rendering/cameraFrameSource';
18
+ import type {
19
+ CameraFrameSequenceRecorderAPI,
20
+ FrameCaptureStatus,
21
+ } from '../types';
22
+
23
+ export type MountedCameraSequencePlanOptions =
24
+ MountedCameraFrameSequencePlanOptions;
25
+ export type MountedCameraSequenceRecordOptions =
26
+ MountedCameraFrameSequenceRecordOptions;
27
+ export type MountedCameraSequenceRecordResult =
28
+ MountedCameraFrameSequenceRecordResult;
29
+
30
+ export interface MountedCameraSequenceRecorderAPI
31
+ extends Omit<CameraFrameSequenceRecorderAPI, 'record'> {
32
+ createPlan: (
33
+ cameraKeys: readonly string[],
34
+ options?: MountedCameraSequencePlanOptions
35
+ ) => MountedCameraFrameSequencePlan;
36
+ record: (
37
+ options: MountedCameraSequenceRecordOptions
38
+ ) => Promise<MountedCameraSequenceRecordResult>;
39
+ }
40
+
41
+ export function useMountedCameraSequenceRecorder(
42
+ defaultOptions: MountedCameraSequencePlanOptions = {}
43
+ ): MountedCameraSequenceRecorderAPI {
44
+ const mujoco = useMujoco();
45
+ const [status, setStatus] = useState<FrameCaptureStatus>('idle');
46
+ const [error, setError] = useState<Error | null>(null);
47
+
48
+ const reset = useCallback(() => {
49
+ setStatus('idle');
50
+ setError(null);
51
+ }, []);
52
+
53
+ const createPlan = useCallback(
54
+ (
55
+ cameraKeys: readonly string[],
56
+ options: MountedCameraSequencePlanOptions = {}
57
+ ) => {
58
+ if (!mujoco.api) {
59
+ throw new Error('MuJoCo scene is not ready for mounted camera sequence planning.');
60
+ }
61
+
62
+ return createMountedCameraFrameSequencePlanFromApi(mujoco.api, cameraKeys, {
63
+ ...defaultOptions,
64
+ ...options,
65
+ });
66
+ },
67
+ [defaultOptions, mujoco.api]
68
+ );
69
+
70
+ const record = useCallback(
71
+ async (options: MountedCameraSequenceRecordOptions) => {
72
+ if (!mujoco.api) {
73
+ throw new Error('MuJoCo scene is not ready for mounted camera sequence recording.');
74
+ }
75
+
76
+ setStatus('capturing');
77
+ setError(null);
78
+
79
+ try {
80
+ const result = await recordMountedCameraFrameSequence(mujoco.api, {
81
+ ...defaultOptions,
82
+ ...options,
83
+ });
84
+ setStatus('captured');
85
+ return result;
86
+ } catch (nextError) {
87
+ const error =
88
+ nextError instanceof Error
89
+ ? nextError
90
+ : new Error('Unable to record the requested mounted camera sequence.');
91
+ setError(error);
92
+ setStatus('error');
93
+ throw error;
94
+ }
95
+ },
96
+ [defaultOptions, mujoco.api]
97
+ );
98
+
99
+ return {
100
+ status,
101
+ error,
102
+ isRecording: status === 'capturing',
103
+ createPlan,
104
+ record,
105
+ reset,
106
+ };
107
+ }
package/src/index.ts CHANGED
@@ -48,9 +48,11 @@ export {
48
48
  createPairedSplatEnvironment,
49
49
  createSparkSplatViewerUrl,
50
50
  createSplatEnvironmentUserData,
51
+ getSplatEnvironmentReadiness,
51
52
  getScenarioBackground,
52
53
  getScenarioCameraPosition,
53
54
  useSplatEnvironment,
55
+ useSplatSceneConfig,
54
56
  useVisualScenarioEffects,
55
57
  withSplatEnvironment,
56
58
  } from './components/VisualScenario';
@@ -84,11 +86,47 @@ export {
84
86
  } from './hooks/useFrameCapture';
85
87
  export { useCameraFrameCapture } from './hooks/useCameraFrameCapture';
86
88
  export { useCameraSequenceRecorder } from './hooks/useCameraSequenceRecorder';
89
+ export { useMountedCameraSequenceRecorder } from './hooks/useMountedCameraSequenceRecorder';
90
+ export type {
91
+ MountedCameraSequencePlanOptions,
92
+ MountedCameraSequenceRecorderAPI,
93
+ MountedCameraSequenceRecordOptions,
94
+ MountedCameraSequenceRecordResult,
95
+ } from './hooks/useMountedCameraSequenceRecorder';
87
96
  export {
88
97
  captureCameraFrame,
89
98
  captureCameraFrameBlob,
99
+ createCameraFrameCaptureSession,
90
100
  renderCameraFrameToCanvas,
91
101
  } from './rendering/cameraFrameCapture';
102
+ export {
103
+ createMountedCameraFrameSequenceReadiness,
104
+ MountedCameraFrameSequenceReadinessStatus,
105
+ createMountedCameraFrameSequencePlanFromApi,
106
+ createMountedCameraFrameSequencePlan,
107
+ getCameraFrameCaptureSourceTarget,
108
+ getMountedCameraFrameCaptureSource,
109
+ isMountedCameraFrameCaptureSource,
110
+ recordMountedCameraFrameSequence,
111
+ resolveMountedCameraFrameSource,
112
+ } from './rendering/cameraFrameSource';
113
+ export type {
114
+ CameraFrameMountSelector,
115
+ CreateMountedCameraFrameSequencePlanOptions,
116
+ MountedCameraFrameCaptureSource,
117
+ MountedCameraFrameSequencePlanOptions,
118
+ MountedCameraFrameSequenceRecorderTarget,
119
+ MountedCameraFrameSequenceCameraOptions,
120
+ MountedCameraFrameSequenceDefaults,
121
+ MountedCameraFrameSequencePlan,
122
+ MountedCameraFrameSequenceReadiness,
123
+ MountedCameraFrameSequenceRecordOptions,
124
+ MountedCameraFrameSequenceRecordResult,
125
+ MountedCameraFrameSequenceSourceReadiness,
126
+ NamedCameraFrameResource,
127
+ ResolveMountedCameraFrameSourceOptions,
128
+ ResolvedMountedCameraFrameSource,
129
+ } from './rendering/cameraFrameSource';
92
130
  export { useCtrlNoise } from './hooks/useCtrlNoise';
93
131
  export { useBodyMeshes } from './hooks/useBodyMeshes';
94
132
  export { useSelectionHighlight } from './hooks/useSelectionHighlight';
@@ -130,6 +168,7 @@ export type {
130
168
  SiteInfo,
131
169
  ActuatorInfo,
132
170
  SensorInfo,
171
+ CameraInfo,
133
172
  // Contacts
134
173
  ContactInfo,
135
174
  // Raycast
@@ -172,8 +211,11 @@ export type {
172
211
  SplatScenarioConfig,
173
212
  SplatCollisionProxyConfig,
174
213
  PairedSplatEnvironmentConfig,
214
+ SplatEnvironmentReadiness,
175
215
  SplatEnvironmentMetadataInput,
176
216
  SplatEnvironmentMetadata,
217
+ SplatSceneConfigInput,
218
+ SplatSceneConfigState,
177
219
  SplatSceneInput,
178
220
  VisualScenarioConfig,
179
221
  ScenarioLightingProps,
@@ -193,12 +235,16 @@ export type {
193
235
  CameraFrameCaptureOptions,
194
236
  CameraFrameCaptureQuaternion,
195
237
  CameraFrameCaptureResult,
238
+ CameraFrameCaptureSource,
196
239
  CameraFrameCaptureVector3,
197
240
  CameraFrameSequenceCamera,
241
+ CameraFrameSequenceCameraSummary,
198
242
  CameraFrameSequenceFrame,
199
243
  CameraFrameSequenceOptions,
200
244
  CameraFrameSequenceRecorderAPI,
201
245
  CameraFrameSequenceResult,
246
+ CameraFrameSequenceSampleInput,
247
+ CameraFrameSequenceStepInput,
202
248
  MujocoCanvasProps,
203
249
  MujocoContextValue,
204
250
  // Hook return types
@@ -227,6 +273,7 @@ export type {
227
273
  Sites,
228
274
  Geoms,
229
275
  Keyframes,
276
+ Cameras,
230
277
  } from './types';
231
278
 
232
279
  export {
@@ -239,6 +286,8 @@ export {
239
286
  RobotSites,
240
287
  RobotGeoms,
241
288
  RobotKeyframes,
289
+ RobotCameras,
290
+ SplatEnvironmentReadinessStatus,
242
291
  } from './types';
243
292
 
244
293
  // 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