mujoco-react 9.4.0 → 9.6.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 +22 -6
- package/dist/{chunk-VDSEPZYQ.js → chunk-4JHALVB2.js} +397 -4
- package/dist/chunk-4JHALVB2.js.map +1 -0
- package/dist/index.d.ts +26 -4
- package/dist/index.js +497 -431
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +27 -3
- package/dist/spark.js +156 -3
- package/dist/spark.js.map +1 -1
- package/dist/{types-BuJ4boaq.d.ts → types-C1rwH74Y.d.ts} +37 -1
- package/package.json +1 -1
- package/src/components/ContactMarkers.tsx +8 -1
- package/src/components/Debug.tsx +154 -3
- package/src/components/DragInteraction.tsx +2 -0
- package/src/components/IkGizmo.tsx +5 -1
- package/src/core/MujocoSimProvider.tsx +5 -5
- package/src/hooks/useIkController.ts +8 -1
- package/src/hooks/useKeyboardIkTarget.ts +170 -0
- package/src/index.ts +5 -0
- package/src/rendering/cameraFrameCapture.ts +259 -28
- package/src/rendering/cameraFrameSource.ts +10 -2
- package/src/spark.tsx +241 -1
- package/src/types.ts +51 -0
- package/dist/chunk-VDSEPZYQ.js.map +0 -1
|
@@ -24,11 +24,66 @@ export interface CameraFrameCaptureSession {
|
|
|
24
24
|
height: number;
|
|
25
25
|
source: CameraFrameCaptureSource;
|
|
26
26
|
};
|
|
27
|
+
captureAsync(options?: CameraFrameCaptureOptions): Promise<{
|
|
28
|
+
canvas: HTMLCanvasElement;
|
|
29
|
+
camera: THREE.Camera;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
source: CameraFrameCaptureSource;
|
|
33
|
+
}>;
|
|
27
34
|
captureDataUrl(options?: CameraFrameCaptureOptions): CameraFrameCaptureResult;
|
|
35
|
+
captureDataUrlAsync(
|
|
36
|
+
options?: CameraFrameCaptureOptions
|
|
37
|
+
): Promise<CameraFrameCaptureResult>;
|
|
28
38
|
captureBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
|
|
29
39
|
dispose(): void;
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
export const CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY =
|
|
43
|
+
'mujocoReactCameraFrameCaptureRender';
|
|
44
|
+
export const CAPTURE_EXCLUDE_KEY =
|
|
45
|
+
'mujoco.capture.exclude';
|
|
46
|
+
|
|
47
|
+
export type CameraFrameCaptureRenderInput = {
|
|
48
|
+
renderer: THREE.WebGLRenderer;
|
|
49
|
+
scene: THREE.Scene;
|
|
50
|
+
camera: THREE.Camera;
|
|
51
|
+
target: THREE.WebGLRenderTarget;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type CameraFrameCaptureRenderResult = {
|
|
57
|
+
pixels: Uint8Array;
|
|
58
|
+
width?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
flipY?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type CameraFrameCaptureRender = (
|
|
64
|
+
input: CameraFrameCaptureRenderInput
|
|
65
|
+
) =>
|
|
66
|
+
| CameraFrameCaptureRenderResult
|
|
67
|
+
| null
|
|
68
|
+
| undefined
|
|
69
|
+
| Promise<CameraFrameCaptureRenderResult | null | undefined>;
|
|
70
|
+
|
|
71
|
+
type RendererState = {
|
|
72
|
+
target: THREE.WebGLRenderTarget | null;
|
|
73
|
+
xrEnabled: boolean;
|
|
74
|
+
viewport: THREE.Vector4;
|
|
75
|
+
scissor: THREE.Vector4;
|
|
76
|
+
scissorTest: boolean;
|
|
77
|
+
clearColor: THREE.Color;
|
|
78
|
+
clearAlpha: number;
|
|
79
|
+
autoClear: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type VisibilityState = {
|
|
83
|
+
object: THREE.Object3D;
|
|
84
|
+
visible: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
32
87
|
function toVector3(
|
|
33
88
|
value: CameraFrameCaptureVector3 | undefined,
|
|
34
89
|
fallback: THREE.Vector3
|
|
@@ -136,21 +191,79 @@ function readRenderTargetToCanvas(
|
|
|
136
191
|
pixels: Uint8Array,
|
|
137
192
|
imageData: ImageData,
|
|
138
193
|
width: number,
|
|
139
|
-
height: number
|
|
194
|
+
height: number,
|
|
195
|
+
outputColorSpace: string
|
|
140
196
|
) {
|
|
141
197
|
renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
|
|
142
198
|
|
|
143
199
|
const rowBytes = width * 4;
|
|
200
|
+
const encodeSrgb = outputColorSpace === THREE.SRGBColorSpace;
|
|
144
201
|
for (let y = 0; y < height; y += 1) {
|
|
145
202
|
const sourceStart = (height - y - 1) * rowBytes;
|
|
146
203
|
const targetStart = y * rowBytes;
|
|
204
|
+
const row = pixels.subarray(sourceStart, sourceStart + rowBytes);
|
|
205
|
+
if (!encodeSrgb) {
|
|
206
|
+
imageData.data.set(row, targetStart);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (let x = 0; x < rowBytes; x += 4) {
|
|
211
|
+
const pixelOffset = targetStart + x;
|
|
212
|
+
imageData.data[pixelOffset] = linearByteToSrgbByte(row[x]);
|
|
213
|
+
imageData.data[pixelOffset + 1] = linearByteToSrgbByte(row[x + 1]);
|
|
214
|
+
imageData.data[pixelOffset + 2] = linearByteToSrgbByte(row[x + 2]);
|
|
215
|
+
imageData.data[pixelOffset + 3] = row[x + 3];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
context.putImageData(imageData, 0, 0);
|
|
219
|
+
return canvas;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function linearByteToSrgbByte(value: number) {
|
|
223
|
+
const normalized = value / 255;
|
|
224
|
+
const encoded =
|
|
225
|
+
normalized <= 0.0031308
|
|
226
|
+
? normalized * 12.92
|
|
227
|
+
: 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
|
|
228
|
+
return Math.min(255, Math.max(0, Math.round(encoded * 255)));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readPixelsToCanvas(
|
|
232
|
+
pixels: Uint8Array,
|
|
233
|
+
context: CanvasRenderingContext2D,
|
|
234
|
+
imageData: ImageData,
|
|
235
|
+
width: number,
|
|
236
|
+
height: number,
|
|
237
|
+
flipY = true
|
|
238
|
+
) {
|
|
239
|
+
const rowBytes = width * 4;
|
|
240
|
+
for (let y = 0; y < height; y += 1) {
|
|
241
|
+
const sourceY = flipY ? height - y - 1 : y;
|
|
242
|
+
const sourceStart = sourceY * rowBytes;
|
|
243
|
+
const targetStart = y * rowBytes;
|
|
147
244
|
imageData.data.set(
|
|
148
245
|
pixels.subarray(sourceStart, sourceStart + rowBytes),
|
|
149
246
|
targetStart
|
|
150
247
|
);
|
|
151
248
|
}
|
|
152
249
|
context.putImageData(imageData, 0, 0);
|
|
153
|
-
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function hideExcludedCaptureObjects(scene: THREE.Scene): VisibilityState[] {
|
|
253
|
+
const hidden: VisibilityState[] = [];
|
|
254
|
+
scene.traverse((object) => {
|
|
255
|
+
if (!object.visible) return;
|
|
256
|
+
if (!object.userData[CAPTURE_EXCLUDE_KEY]) return;
|
|
257
|
+
hidden.push({ object, visible: object.visible });
|
|
258
|
+
object.visible = false;
|
|
259
|
+
});
|
|
260
|
+
return hidden;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function restoreObjectVisibility(hidden: VisibilityState[]) {
|
|
264
|
+
for (const { object, visible } of hidden) {
|
|
265
|
+
object.visible = visible;
|
|
266
|
+
}
|
|
154
267
|
}
|
|
155
268
|
|
|
156
269
|
function getCameraFrameCaptureSource(
|
|
@@ -173,6 +286,52 @@ function getCameraFrameCaptureSource(
|
|
|
173
286
|
return { kind: 'fallback-camera' };
|
|
174
287
|
}
|
|
175
288
|
|
|
289
|
+
function saveRendererState(renderer: THREE.WebGLRenderer): RendererState {
|
|
290
|
+
const viewport = new THREE.Vector4();
|
|
291
|
+
const scissor = new THREE.Vector4();
|
|
292
|
+
const clearColor = new THREE.Color();
|
|
293
|
+
renderer.getViewport(viewport);
|
|
294
|
+
renderer.getScissor(scissor);
|
|
295
|
+
renderer.getClearColor(clearColor);
|
|
296
|
+
return {
|
|
297
|
+
target: renderer.getRenderTarget(),
|
|
298
|
+
xrEnabled: renderer.xr.enabled,
|
|
299
|
+
viewport,
|
|
300
|
+
scissor,
|
|
301
|
+
scissorTest: renderer.getScissorTest(),
|
|
302
|
+
clearColor,
|
|
303
|
+
clearAlpha: renderer.getClearAlpha(),
|
|
304
|
+
autoClear: renderer.autoClear,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function restoreRendererState(
|
|
309
|
+
renderer: THREE.WebGLRenderer,
|
|
310
|
+
state: RendererState
|
|
311
|
+
) {
|
|
312
|
+
renderer.setRenderTarget(state.target);
|
|
313
|
+
renderer.xr.enabled = state.xrEnabled;
|
|
314
|
+
renderer.setViewport(state.viewport);
|
|
315
|
+
renderer.setScissor(state.scissor);
|
|
316
|
+
renderer.setScissorTest(state.scissorTest);
|
|
317
|
+
renderer.setClearColor(state.clearColor, state.clearAlpha);
|
|
318
|
+
renderer.autoClear = state.autoClear;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getCaptureRenderer(
|
|
322
|
+
scene: THREE.Scene
|
|
323
|
+
): CameraFrameCaptureRender | null {
|
|
324
|
+
const renderers: CameraFrameCaptureRender[] = [];
|
|
325
|
+
scene.traverse((object) => {
|
|
326
|
+
if (renderers.length) return;
|
|
327
|
+
const render = object.userData[
|
|
328
|
+
CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY
|
|
329
|
+
] as CameraFrameCaptureRender | undefined;
|
|
330
|
+
if (typeof render === 'function') renderers.push(render);
|
|
331
|
+
});
|
|
332
|
+
return renderers[0] ?? null;
|
|
333
|
+
}
|
|
334
|
+
|
|
176
335
|
export function createCameraFrameCaptureSession(
|
|
177
336
|
renderer: THREE.WebGLRenderer,
|
|
178
337
|
scene: THREE.Scene,
|
|
@@ -198,7 +357,7 @@ export function createCameraFrameCaptureSession(
|
|
|
198
357
|
const pixels = new Uint8Array(width * height * 4);
|
|
199
358
|
const imageData = drawContext.createImageData(width, height);
|
|
200
359
|
|
|
201
|
-
function
|
|
360
|
+
function resolveCaptureOptions(nextOptions: CameraFrameCaptureOptions = {}) {
|
|
202
361
|
const captureOptions = { ...options, ...nextOptions };
|
|
203
362
|
const nextDimensions = getCaptureDimensions(renderer, captureOptions);
|
|
204
363
|
if (
|
|
@@ -218,13 +377,20 @@ export function createCameraFrameCaptureSession(
|
|
|
218
377
|
height
|
|
219
378
|
);
|
|
220
379
|
|
|
221
|
-
|
|
222
|
-
|
|
380
|
+
return captureOptions;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
|
|
384
|
+
const previousState = saveRendererState(renderer);
|
|
385
|
+
const hidden = hideExcludedCaptureObjects(scene);
|
|
223
386
|
|
|
224
387
|
scene.updateMatrixWorld(true);
|
|
225
388
|
try {
|
|
226
389
|
renderer.xr.enabled = false;
|
|
227
390
|
renderer.setRenderTarget(target);
|
|
391
|
+
renderer.setViewport(0, 0, width, height);
|
|
392
|
+
renderer.setScissor(0, 0, width, height);
|
|
393
|
+
renderer.setScissorTest(false);
|
|
228
394
|
renderer.clear();
|
|
229
395
|
renderer.render(scene, camera);
|
|
230
396
|
readRenderTargetToCanvas(
|
|
@@ -235,7 +401,8 @@ export function createCameraFrameCaptureSession(
|
|
|
235
401
|
pixels,
|
|
236
402
|
imageData,
|
|
237
403
|
width,
|
|
238
|
-
height
|
|
404
|
+
height,
|
|
405
|
+
renderer.outputColorSpace
|
|
239
406
|
);
|
|
240
407
|
return {
|
|
241
408
|
canvas,
|
|
@@ -245,15 +412,69 @@ export function createCameraFrameCaptureSession(
|
|
|
245
412
|
source: getCameraFrameCaptureSource(captureOptions),
|
|
246
413
|
};
|
|
247
414
|
} finally {
|
|
248
|
-
|
|
249
|
-
renderer
|
|
415
|
+
restoreObjectVisibility(hidden);
|
|
416
|
+
restoreRendererState(renderer, previousState);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function capture(nextOptions: CameraFrameCaptureOptions = {}) {
|
|
421
|
+
return renderPreparedCapture(resolveCaptureOptions(nextOptions));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function captureAsync(nextOptions: CameraFrameCaptureOptions = {}) {
|
|
425
|
+
const captureOptions = resolveCaptureOptions(nextOptions);
|
|
426
|
+
scene.updateMatrixWorld(true);
|
|
427
|
+
const captureRenderer = getCaptureRenderer(scene);
|
|
428
|
+
if (captureRenderer) {
|
|
429
|
+
const previousState = saveRendererState(renderer);
|
|
430
|
+
const hidden = hideExcludedCaptureObjects(scene);
|
|
431
|
+
try {
|
|
432
|
+
renderer.xr.enabled = false;
|
|
433
|
+
const captureResult = await captureRenderer({
|
|
434
|
+
renderer,
|
|
435
|
+
scene,
|
|
436
|
+
camera,
|
|
437
|
+
target,
|
|
438
|
+
width,
|
|
439
|
+
height,
|
|
440
|
+
});
|
|
441
|
+
if (captureResult) {
|
|
442
|
+
const captureWidth = captureResult.width ?? width;
|
|
443
|
+
const captureHeight = captureResult.height ?? height;
|
|
444
|
+
if (captureWidth !== width || captureHeight !== height) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
'Camera frame capture renderer returned unexpected dimensions.'
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
readPixelsToCanvas(
|
|
450
|
+
captureResult.pixels,
|
|
451
|
+
drawContext,
|
|
452
|
+
imageData,
|
|
453
|
+
width,
|
|
454
|
+
height,
|
|
455
|
+
captureResult.flipY ?? true
|
|
456
|
+
);
|
|
457
|
+
return {
|
|
458
|
+
canvas,
|
|
459
|
+
camera,
|
|
460
|
+
width,
|
|
461
|
+
height,
|
|
462
|
+
source: getCameraFrameCaptureSource(captureOptions),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
restoreObjectVisibility(hidden);
|
|
467
|
+
restoreRendererState(renderer, previousState);
|
|
468
|
+
}
|
|
250
469
|
}
|
|
470
|
+
return renderPreparedCapture(captureOptions);
|
|
251
471
|
}
|
|
252
472
|
|
|
253
473
|
return {
|
|
254
474
|
width,
|
|
255
475
|
height,
|
|
256
476
|
capture,
|
|
477
|
+
captureAsync,
|
|
257
478
|
captureDataUrl(nextOptions = {}) {
|
|
258
479
|
const type = nextOptions.type ?? options.type ?? 'image/png';
|
|
259
480
|
const result = capture(nextOptions);
|
|
@@ -266,9 +487,21 @@ export function createCameraFrameCaptureSession(
|
|
|
266
487
|
type,
|
|
267
488
|
};
|
|
268
489
|
},
|
|
490
|
+
async captureDataUrlAsync(nextOptions = {}) {
|
|
491
|
+
const type = nextOptions.type ?? options.type ?? 'image/png';
|
|
492
|
+
const result = await captureAsync(nextOptions);
|
|
493
|
+
return {
|
|
494
|
+
...result,
|
|
495
|
+
dataUrl: result.canvas.toDataURL(
|
|
496
|
+
type,
|
|
497
|
+
nextOptions.quality ?? options.quality
|
|
498
|
+
),
|
|
499
|
+
type,
|
|
500
|
+
};
|
|
501
|
+
},
|
|
269
502
|
async captureBlob(nextOptions = {}) {
|
|
270
503
|
const type = nextOptions.type ?? options.type ?? 'image/png';
|
|
271
|
-
const result =
|
|
504
|
+
const result = await captureAsync(nextOptions);
|
|
272
505
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
273
506
|
result.canvas.toBlob(
|
|
274
507
|
(nextBlob) => {
|
|
@@ -313,17 +546,22 @@ export async function captureCameraFrame(
|
|
|
313
546
|
options: CameraFrameCaptureOptions = {}
|
|
314
547
|
): Promise<CameraFrameCaptureResult> {
|
|
315
548
|
const type = options.type ?? 'image/png';
|
|
316
|
-
const
|
|
549
|
+
const session = createCameraFrameCaptureSession(
|
|
317
550
|
renderer,
|
|
318
551
|
scene,
|
|
319
552
|
fallbackCamera,
|
|
320
553
|
options
|
|
321
554
|
);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
555
|
+
try {
|
|
556
|
+
const result = await session.captureAsync();
|
|
557
|
+
return {
|
|
558
|
+
...result,
|
|
559
|
+
dataUrl: result.canvas.toDataURL(type, options.quality),
|
|
560
|
+
type,
|
|
561
|
+
};
|
|
562
|
+
} finally {
|
|
563
|
+
session.dispose();
|
|
564
|
+
}
|
|
327
565
|
}
|
|
328
566
|
|
|
329
567
|
export async function captureCameraFrameBlob(
|
|
@@ -332,22 +570,15 @@ export async function captureCameraFrameBlob(
|
|
|
332
570
|
fallbackCamera: THREE.Camera,
|
|
333
571
|
options: CameraFrameCaptureOptions = {}
|
|
334
572
|
): Promise<CameraFrameCaptureBlobResult> {
|
|
335
|
-
const
|
|
336
|
-
const result = renderCameraFrameToCanvas(
|
|
573
|
+
const session = createCameraFrameCaptureSession(
|
|
337
574
|
renderer,
|
|
338
575
|
scene,
|
|
339
576
|
fallbackCamera,
|
|
340
577
|
options
|
|
341
578
|
);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
type,
|
|
349
|
-
options.quality
|
|
350
|
-
);
|
|
351
|
-
});
|
|
352
|
-
return { ...result, blob, type };
|
|
579
|
+
try {
|
|
580
|
+
return await session.captureBlob();
|
|
581
|
+
} finally {
|
|
582
|
+
session.dispose();
|
|
583
|
+
}
|
|
353
584
|
}
|
|
@@ -471,9 +471,8 @@ export function resolveMountedCameraFrameSource(
|
|
|
471
471
|
{ bodyName: key },
|
|
472
472
|
];
|
|
473
473
|
const aliasCandidates = normalizeAliasCandidates(options.aliases?.[key]);
|
|
474
|
-
const candidates = [...directCandidates, ...aliasCandidates];
|
|
475
474
|
|
|
476
|
-
for (const selector of
|
|
475
|
+
for (const selector of aliasCandidates) {
|
|
477
476
|
if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
|
|
478
477
|
continue;
|
|
479
478
|
}
|
|
@@ -499,6 +498,15 @@ export function resolveMountedCameraFrameSource(
|
|
|
499
498
|
}
|
|
500
499
|
}
|
|
501
500
|
|
|
501
|
+
for (const selector of directCandidates) {
|
|
502
|
+
if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const source = getMountedCameraFrameCaptureSource(selector);
|
|
506
|
+
if (!source) continue;
|
|
507
|
+
return { key, selector, source };
|
|
508
|
+
}
|
|
509
|
+
|
|
502
510
|
return null;
|
|
503
511
|
}
|
|
504
512
|
|