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.
- package/README.md +81 -44
- package/dist/chunk-33CV6HSV.js +400 -0
- package/dist/chunk-33CV6HSV.js.map +1 -0
- package/dist/index.d.ts +92 -24
- package/dist/index.js +338 -54
- 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-izZlUweI.d.ts} +109 -16
- package/package.json +1 -1
- package/src/components/Body.tsx +3 -1
- package/src/components/DragInteraction.tsx +1 -1
- package/src/components/IkGizmo.tsx +2 -2
- package/src/components/SceneRenderer.tsx +1 -1
- package/src/components/TrajectoryPlayer.tsx +4 -1
- package/src/components/VisualScenario.tsx +343 -6
- package/src/core/MujocoCanvas.tsx +8 -1
- package/src/core/MujocoPhysics.tsx +10 -4
- package/src/core/MujocoSimProvider.tsx +15 -12
- package/src/core/SceneLoader.ts +182 -3
- package/src/core/createController.tsx +2 -2
- package/src/hooks/useBodyState.ts +1 -1
- package/src/hooks/useContacts.ts +1 -1
- package/src/hooks/useCtrlNoise.ts +1 -1
- package/src/hooks/useFrameCapture.ts +206 -0
- package/src/hooks/useGamepad.ts +1 -1
- package/src/hooks/useGravityCompensation.ts +1 -1
- package/src/hooks/useIkController.ts +22 -13
- package/src/hooks/useJointState.ts +1 -1
- package/src/hooks/useKeyboardTeleop.ts +1 -1
- package/src/hooks/usePolicy.ts +13 -9
- package/src/hooks/useSensor.ts +1 -1
- package/src/hooks/useTrajectoryPlayer.ts +4 -4
- package/src/hooks/useTrajectoryRecorder.ts +1 -1
- package/src/index.ts +35 -0
- package/src/spark.tsx +138 -4
- package/src/types.ts +128 -21
- package/dist/chunk-KGFRKPLS.js +0 -186
- package/dist/chunk-KGFRKPLS.js.map +0 -1
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { useThree } from '@react-three/fiber';
|
|
6
7
|
import type { ThreeElements } from '@react-three/fiber';
|
|
7
8
|
import type { ReactNode } from 'react';
|
|
8
|
-
import { useMemo } from 'react';
|
|
9
|
+
import { useEffect, useMemo } from 'react';
|
|
9
10
|
import * as THREE from 'three';
|
|
10
11
|
import type {
|
|
11
12
|
PairedSplatEnvironmentConfig,
|
|
13
|
+
ScenarioMaterialConfig,
|
|
14
|
+
SceneConfig,
|
|
12
15
|
SplatCollisionProxyConfig,
|
|
13
16
|
SplatEnvironmentMetadata,
|
|
14
17
|
SplatEnvironmentMetadataInput,
|
|
15
18
|
SplatFormat,
|
|
19
|
+
SplatRendererKind,
|
|
20
|
+
SplatSceneInput,
|
|
16
21
|
ScenarioLightingPreset,
|
|
17
22
|
ScenarioLightingProps,
|
|
18
23
|
SplatEnvironmentProps,
|
|
19
24
|
VisualScenarioConfig,
|
|
25
|
+
VisualScenarioEffectsProps,
|
|
20
26
|
} from '../types';
|
|
21
27
|
|
|
22
28
|
const DEFAULT_BACKGROUND = '#181a1f';
|
|
@@ -104,6 +110,99 @@ export function getScenarioCameraPosition(
|
|
|
104
110
|
];
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
export function VisualScenarioEffects(props: VisualScenarioEffectsProps) {
|
|
114
|
+
useVisualScenarioEffects(props);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useVisualScenarioEffects({
|
|
119
|
+
scenario,
|
|
120
|
+
enabled = true,
|
|
121
|
+
applyBackground = true,
|
|
122
|
+
applyFog = true,
|
|
123
|
+
applyRenderer = true,
|
|
124
|
+
applyMaterials = true,
|
|
125
|
+
background,
|
|
126
|
+
fogNear,
|
|
127
|
+
fogFar,
|
|
128
|
+
materialFilter,
|
|
129
|
+
}: VisualScenarioEffectsProps) {
|
|
130
|
+
const { gl, scene, invalidate } = useThree();
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!enabled || !scenario) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const previousExposure = gl.toneMappingExposure;
|
|
138
|
+
const previousBackground = scene.background;
|
|
139
|
+
const previousFog = scene.fog;
|
|
140
|
+
const materialSnapshots = new Map<
|
|
141
|
+
THREE.Material,
|
|
142
|
+
{
|
|
143
|
+
color?: THREE.Color;
|
|
144
|
+
roughness?: number;
|
|
145
|
+
metalness?: number;
|
|
146
|
+
}
|
|
147
|
+
>();
|
|
148
|
+
|
|
149
|
+
if (applyRenderer) {
|
|
150
|
+
gl.toneMappingExposure = scenario.camera?.exposure ?? 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (applyBackground) {
|
|
154
|
+
scene.background = new THREE.Color(
|
|
155
|
+
background ?? getScenarioBackground(scenario.lighting)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (applyFog) {
|
|
160
|
+
scene.fog = createScenarioFog(scenario, background, fogNear, fogFar);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (applyMaterials && scenario.materials) {
|
|
164
|
+
applyScenarioMaterials(scene, scenario, materialSnapshots, materialFilter);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
invalidate();
|
|
168
|
+
|
|
169
|
+
return () => {
|
|
170
|
+
gl.toneMappingExposure = previousExposure;
|
|
171
|
+
scene.background = previousBackground;
|
|
172
|
+
scene.fog = previousFog;
|
|
173
|
+
|
|
174
|
+
for (const [material, snapshot] of materialSnapshots) {
|
|
175
|
+
const mutable = getMutableScenarioMaterial(material);
|
|
176
|
+
if (!mutable) continue;
|
|
177
|
+
if (snapshot.color) mutable.color.copy(snapshot.color);
|
|
178
|
+
if (typeof snapshot.roughness === 'number') {
|
|
179
|
+
mutable.roughness = snapshot.roughness;
|
|
180
|
+
}
|
|
181
|
+
if (typeof snapshot.metalness === 'number') {
|
|
182
|
+
mutable.metalness = snapshot.metalness;
|
|
183
|
+
}
|
|
184
|
+
mutable.needsUpdate = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
invalidate();
|
|
188
|
+
};
|
|
189
|
+
}, [
|
|
190
|
+
applyBackground,
|
|
191
|
+
applyFog,
|
|
192
|
+
applyMaterials,
|
|
193
|
+
applyRenderer,
|
|
194
|
+
background,
|
|
195
|
+
enabled,
|
|
196
|
+
fogFar,
|
|
197
|
+
fogNear,
|
|
198
|
+
gl,
|
|
199
|
+
invalidate,
|
|
200
|
+
materialFilter,
|
|
201
|
+
scenario,
|
|
202
|
+
scene,
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
|
|
107
206
|
/**
|
|
108
207
|
* Renderer-agnostic Gaussian splat environment boundary.
|
|
109
208
|
*
|
|
@@ -113,6 +212,8 @@ export function getScenarioCameraPosition(
|
|
|
113
212
|
*/
|
|
114
213
|
export function SplatEnvironment({
|
|
115
214
|
environment,
|
|
215
|
+
scenario,
|
|
216
|
+
renderer,
|
|
116
217
|
src,
|
|
117
218
|
format,
|
|
118
219
|
collisionProxy,
|
|
@@ -123,6 +224,8 @@ export function SplatEnvironment({
|
|
|
123
224
|
}: SplatEnvironmentProps) {
|
|
124
225
|
const metadata = useSplatEnvironment({
|
|
125
226
|
environment,
|
|
227
|
+
scenario,
|
|
228
|
+
renderer,
|
|
126
229
|
src,
|
|
127
230
|
format,
|
|
128
231
|
collisionProxy: collisionProxyMetadata,
|
|
@@ -149,13 +252,31 @@ export function SplatEnvironment({
|
|
|
149
252
|
|
|
150
253
|
export function useSplatEnvironment({
|
|
151
254
|
environment,
|
|
255
|
+
scenario,
|
|
256
|
+
renderer,
|
|
152
257
|
src,
|
|
153
258
|
format,
|
|
154
259
|
collisionProxy,
|
|
155
260
|
}: SplatEnvironmentMetadataInput): SplatEnvironmentMetadata {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
261
|
+
const scenarioEnvironment = useMemo(
|
|
262
|
+
() =>
|
|
263
|
+
environment ??
|
|
264
|
+
(scenario
|
|
265
|
+
? createPairedSplatEnvironment(scenario, { renderer })
|
|
266
|
+
: undefined),
|
|
267
|
+
[environment, renderer, scenario]
|
|
268
|
+
);
|
|
269
|
+
const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;
|
|
270
|
+
const resolvedFormat =
|
|
271
|
+
format ??
|
|
272
|
+
scenarioEnvironment?.splat.format ??
|
|
273
|
+
scenario?.splat?.format ??
|
|
274
|
+
'spz';
|
|
275
|
+
const resolvedCollisionProxy =
|
|
276
|
+
collisionProxy ??
|
|
277
|
+
scenarioEnvironment?.collisionProxy ??
|
|
278
|
+
scenario?.splat?.collisionProxy ??
|
|
279
|
+
undefined;
|
|
159
280
|
|
|
160
281
|
return useMemo(
|
|
161
282
|
() => ({
|
|
@@ -163,16 +284,110 @@ export function useSplatEnvironment({
|
|
|
163
284
|
format: resolvedFormat,
|
|
164
285
|
collisionProxy: resolvedCollisionProxy,
|
|
165
286
|
userData: createSplatEnvironmentUserData({
|
|
166
|
-
environment,
|
|
287
|
+
environment: scenarioEnvironment,
|
|
167
288
|
src: resolvedSrc,
|
|
168
289
|
format: resolvedFormat,
|
|
169
290
|
collisionProxy: resolvedCollisionProxy,
|
|
170
291
|
}),
|
|
171
292
|
}),
|
|
172
|
-
[
|
|
293
|
+
[scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
|
|
173
294
|
);
|
|
174
295
|
}
|
|
175
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Convert a generic visual scenario splat block into a paired visual/physics
|
|
299
|
+
* environment config. Returns undefined until both the splat asset and MJCF
|
|
300
|
+
* collision proxy are present.
|
|
301
|
+
*/
|
|
302
|
+
export function createPairedSplatEnvironment(
|
|
303
|
+
scenario: Pick<VisualScenarioConfig, 'id' | 'label' | 'environment' | 'splat'>,
|
|
304
|
+
options: {
|
|
305
|
+
id?: string;
|
|
306
|
+
label?: string;
|
|
307
|
+
description?: string;
|
|
308
|
+
renderer?: SplatRendererKind;
|
|
309
|
+
} = {}
|
|
310
|
+
): PairedSplatEnvironmentConfig | undefined {
|
|
311
|
+
const splat = scenario.splat;
|
|
312
|
+
const collisionProxy = splat?.collisionProxy;
|
|
313
|
+
|
|
314
|
+
if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
id: options.id ?? scenario.id ?? 'splat-environment',
|
|
320
|
+
label: options.label ?? scenario.label ?? 'Gaussian splat environment',
|
|
321
|
+
description:
|
|
322
|
+
options.description ??
|
|
323
|
+
(scenario.environment
|
|
324
|
+
? `Visual ${scenario.environment} splat paired with MJCF collision proxy.`
|
|
325
|
+
: undefined),
|
|
326
|
+
splat: {
|
|
327
|
+
src: splat.src,
|
|
328
|
+
format: splat.format ?? 'spz',
|
|
329
|
+
renderer: options.renderer,
|
|
330
|
+
},
|
|
331
|
+
collisionProxy: {
|
|
332
|
+
...collisionProxy,
|
|
333
|
+
xmlPath: collisionProxy.xmlPath,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isPairedSplatEnvironment(input: SplatSceneInput): input is PairedSplatEnvironmentConfig {
|
|
339
|
+
return !!input && 'collisionProxy' in input && 'splat' in input;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function sceneRelativePath(sceneConfig: SceneConfig, path: string): string {
|
|
343
|
+
const src = sceneConfig.src;
|
|
344
|
+
if (!src) return path;
|
|
345
|
+
|
|
346
|
+
const base = src.endsWith('/') ? src : src + '/';
|
|
347
|
+
if (path.startsWith(base)) return path.slice(base.length);
|
|
348
|
+
return path;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function uniquePaths(paths: readonly string[]): string[] {
|
|
352
|
+
const seen = new Set<string>();
|
|
353
|
+
const result: string[] = [];
|
|
354
|
+
for (const path of paths) {
|
|
355
|
+
if (seen.has(path)) continue;
|
|
356
|
+
seen.add(path);
|
|
357
|
+
result.push(path);
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Compose a MuJoCo scene config with a paired splat collision proxy.
|
|
364
|
+
*
|
|
365
|
+
* This keeps the common hybrid setup declarative:
|
|
366
|
+
* robot XML remains `sceneFile`, the `.spz` remains a visual-only layer, and
|
|
367
|
+
* the paired MJCF collision proxy is added to `environmentFiles`.
|
|
368
|
+
*/
|
|
369
|
+
export function withSplatEnvironment(
|
|
370
|
+
sceneConfig: SceneConfig,
|
|
371
|
+
input: SplatSceneInput,
|
|
372
|
+
options: { renderer?: SplatRendererKind } = {}
|
|
373
|
+
): SceneConfig {
|
|
374
|
+
const environment = isPairedSplatEnvironment(input)
|
|
375
|
+
? input
|
|
376
|
+
: input
|
|
377
|
+
? createPairedSplatEnvironment(input, options)
|
|
378
|
+
: undefined;
|
|
379
|
+
const xmlPath = environment?.collisionProxy.xmlPath;
|
|
380
|
+
if (!xmlPath) return sceneConfig;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
...sceneConfig,
|
|
384
|
+
environmentFiles: uniquePaths([
|
|
385
|
+
...(sceneConfig.environmentFiles ?? []),
|
|
386
|
+
sceneRelativePath(sceneConfig, xmlPath),
|
|
387
|
+
]),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
176
391
|
export function createSplatEnvironmentUserData({
|
|
177
392
|
environment,
|
|
178
393
|
src,
|
|
@@ -226,4 +441,126 @@ function SplatPlaceholder() {
|
|
|
226
441
|
);
|
|
227
442
|
}
|
|
228
443
|
|
|
444
|
+
function createScenarioFog(
|
|
445
|
+
scenario: VisualScenarioConfig,
|
|
446
|
+
background: THREE.ColorRepresentation | undefined,
|
|
447
|
+
fogNear: number | undefined,
|
|
448
|
+
fogFar: number | undefined
|
|
449
|
+
) {
|
|
450
|
+
if (scenario.lighting === 'low-light') {
|
|
451
|
+
return new THREE.Fog(
|
|
452
|
+
background ?? getScenarioBackground(scenario.lighting),
|
|
453
|
+
fogNear ?? 2.5,
|
|
454
|
+
fogFar ?? 9
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (scenario.lighting === 'warehouse') {
|
|
459
|
+
return new THREE.Fog(
|
|
460
|
+
background ?? getScenarioBackground(scenario.lighting),
|
|
461
|
+
fogNear ?? 5,
|
|
462
|
+
fogFar ?? 16
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function applyScenarioMaterials(
|
|
470
|
+
scene: THREE.Scene,
|
|
471
|
+
scenario: VisualScenarioConfig,
|
|
472
|
+
snapshots: Map<
|
|
473
|
+
THREE.Material,
|
|
474
|
+
{
|
|
475
|
+
color?: THREE.Color;
|
|
476
|
+
roughness?: number;
|
|
477
|
+
metalness?: number;
|
|
478
|
+
}
|
|
479
|
+
>,
|
|
480
|
+
materialFilter: VisualScenarioEffectsProps['materialFilter']
|
|
481
|
+
) {
|
|
482
|
+
const materials = scenario.materials;
|
|
483
|
+
if (!materials) return;
|
|
484
|
+
|
|
485
|
+
scene.traverse((object) => {
|
|
486
|
+
if (!(object instanceof THREE.Mesh)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const material of normalizeMaterials(object.material)) {
|
|
491
|
+
const mutable = getMutableScenarioMaterial(material);
|
|
492
|
+
if (!mutable) continue;
|
|
493
|
+
if (materialFilter && !materialFilter({ object, material })) continue;
|
|
494
|
+
|
|
495
|
+
if (!snapshots.has(material)) {
|
|
496
|
+
snapshots.set(material, {
|
|
497
|
+
color: mutable.color.clone(),
|
|
498
|
+
roughness: mutable.roughness,
|
|
499
|
+
metalness: mutable.metalness,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
applyScenarioMaterial(mutable, object, scenario, materials);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function applyScenarioMaterial(
|
|
509
|
+
material: THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial,
|
|
510
|
+
object: THREE.Object3D,
|
|
511
|
+
scenario: VisualScenarioConfig,
|
|
512
|
+
materials: ScenarioMaterialConfig
|
|
513
|
+
) {
|
|
514
|
+
const seed = scenario.seed ?? 0;
|
|
515
|
+
const objectKey = `${scenario.id ?? 'scenario'}:${object.name}:${material.name}:${seed}`;
|
|
516
|
+
const variation = hashToUnitInterval(objectKey);
|
|
517
|
+
|
|
518
|
+
if (materials.randomizeObjectColors) {
|
|
519
|
+
material.color.setHSL(variation, 0.38, 0.42);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (materials.randomizeTableMaterial) {
|
|
523
|
+
material.roughness = clamp01(
|
|
524
|
+
materials.roughness ?? 0.35 + variation * 0.45
|
|
525
|
+
);
|
|
526
|
+
material.metalness = clamp01(
|
|
527
|
+
materials.metalness ?? variation * 0.12
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
material.needsUpdate = true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeMaterials(
|
|
535
|
+
material: THREE.Material | THREE.Material[]
|
|
536
|
+
): THREE.Material[] {
|
|
537
|
+
return Array.isArray(material) ? material : [material];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getMutableScenarioMaterial(
|
|
541
|
+
material: THREE.Material
|
|
542
|
+
): THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial | null {
|
|
543
|
+
if (
|
|
544
|
+
material instanceof THREE.MeshStandardMaterial ||
|
|
545
|
+
material instanceof THREE.MeshPhysicalMaterial
|
|
546
|
+
) {
|
|
547
|
+
return material;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function hashToUnitInterval(value: string) {
|
|
554
|
+
let hash = 2166136261;
|
|
555
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
556
|
+
hash ^= value.charCodeAt(index);
|
|
557
|
+
hash = Math.imul(hash, 16777619);
|
|
558
|
+
}
|
|
559
|
+
return (hash >>> 0) / 4294967295;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function clamp01(value: number) {
|
|
563
|
+
return Math.max(0, Math.min(1, value));
|
|
564
|
+
}
|
|
565
|
+
|
|
229
566
|
export type SplatCollisionProxy = ReactNode | ThreeElements['group'];
|
|
@@ -31,6 +31,7 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
31
31
|
paused,
|
|
32
32
|
speed,
|
|
33
33
|
interpolate,
|
|
34
|
+
loadingFallback,
|
|
34
35
|
children,
|
|
35
36
|
...canvasProps
|
|
36
37
|
},
|
|
@@ -44,7 +45,13 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
44
45
|
}
|
|
45
46
|
}, [wasmStatus, wasmError, onError]);
|
|
46
47
|
|
|
47
|
-
if (wasmStatus === '
|
|
48
|
+
if (wasmStatus === 'loading' || !mujoco) {
|
|
49
|
+
return loadingFallback ? (
|
|
50
|
+
<Canvas {...canvasProps}>{loadingFallback}</Canvas>
|
|
51
|
+
) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (wasmStatus === 'error') {
|
|
48
55
|
return null;
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -6,19 +6,25 @@
|
|
|
6
6
|
import { forwardRef, useEffect } from 'react';
|
|
7
7
|
import { useMujocoWasm } from './MujocoProvider';
|
|
8
8
|
import { MujocoSimProvider } from './MujocoSimProvider';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
MujocoSimAPI,
|
|
11
|
+
ReadyCallbackInput,
|
|
12
|
+
SceneConfig,
|
|
13
|
+
SelectionCallbackInput,
|
|
14
|
+
StepCallbackInput,
|
|
15
|
+
} from '../types';
|
|
10
16
|
|
|
11
17
|
export interface MujocoPhysicsProps {
|
|
12
18
|
/** Scene/robot configuration. */
|
|
13
19
|
config: SceneConfig;
|
|
14
20
|
/** Fires when model is loaded and API is ready. */
|
|
15
|
-
onReady?: (
|
|
21
|
+
onReady?: (input: ReadyCallbackInput) => void;
|
|
16
22
|
/** Fires on scene load failure. */
|
|
17
23
|
onError?: (error: Error) => void;
|
|
18
24
|
/** Called each physics step. */
|
|
19
|
-
onStep?: (
|
|
25
|
+
onStep?: (input: StepCallbackInput) => void;
|
|
20
26
|
/** Called on body double-click selection. */
|
|
21
|
-
onSelection?: (
|
|
27
|
+
onSelection?: (input: SelectionCallbackInput) => void;
|
|
22
28
|
/** Override model gravity. */
|
|
23
29
|
gravity?: [number, number, number];
|
|
24
30
|
/** Override model.opt.timestep. */
|
|
@@ -31,11 +31,14 @@ import {
|
|
|
31
31
|
MujocoSimAPI,
|
|
32
32
|
PhysicsStepCallback,
|
|
33
33
|
RayHit,
|
|
34
|
+
ReadyCallbackInput,
|
|
34
35
|
SceneConfig,
|
|
35
36
|
SceneObject,
|
|
37
|
+
SelectionCallbackInput,
|
|
36
38
|
SensorInfo,
|
|
37
39
|
SiteInfo,
|
|
38
40
|
StateSnapshot,
|
|
41
|
+
StepCallbackInput,
|
|
39
42
|
XmlPatch,
|
|
40
43
|
} from '../types';
|
|
41
44
|
import {
|
|
@@ -117,7 +120,7 @@ export interface MujocoSimContextValue {
|
|
|
117
120
|
interpolateRef: React.RefObject<boolean>;
|
|
118
121
|
interpolationStateRef: React.RefObject<BodyInterpolationState>;
|
|
119
122
|
onSelectionRef: React.RefObject<
|
|
120
|
-
((
|
|
123
|
+
((input: SelectionCallbackInput) => void) | undefined
|
|
121
124
|
>;
|
|
122
125
|
beforeStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
123
126
|
afterStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
@@ -197,7 +200,7 @@ export function useBeforePhysicsStep(callback: PhysicsStepCallback) {
|
|
|
197
200
|
callbackRef.current = callback;
|
|
198
201
|
|
|
199
202
|
useEffect(() => {
|
|
200
|
-
const wrapped: PhysicsStepCallback = (
|
|
203
|
+
const wrapped: PhysicsStepCallback = (input) => callbackRef.current(input);
|
|
201
204
|
beforeStepCallbacks.current.add(wrapped);
|
|
202
205
|
return () => { beforeStepCallbacks.current.delete(wrapped); };
|
|
203
206
|
}, [beforeStepCallbacks]);
|
|
@@ -209,7 +212,7 @@ export function useAfterPhysicsStep(callback: PhysicsStepCallback) {
|
|
|
209
212
|
callbackRef.current = callback;
|
|
210
213
|
|
|
211
214
|
useEffect(() => {
|
|
212
|
-
const wrapped: PhysicsStepCallback = (
|
|
215
|
+
const wrapped: PhysicsStepCallback = (input) => callbackRef.current(input);
|
|
213
216
|
afterStepCallbacks.current.add(wrapped);
|
|
214
217
|
return () => { afterStepCallbacks.current.delete(wrapped); };
|
|
215
218
|
}, [afterStepCallbacks]);
|
|
@@ -219,10 +222,10 @@ interface MujocoSimProviderProps {
|
|
|
219
222
|
mujoco: MujocoModule;
|
|
220
223
|
config: SceneConfig;
|
|
221
224
|
apiRef?: React.ForwardedRef<MujocoSimAPI>;
|
|
222
|
-
onReady?: (
|
|
225
|
+
onReady?: (input: ReadyCallbackInput) => void;
|
|
223
226
|
onError?: (error: Error) => void;
|
|
224
|
-
onStep?: (
|
|
225
|
-
onSelection?: (
|
|
227
|
+
onStep?: (input: StepCallbackInput) => void;
|
|
228
|
+
onSelection?: (input: SelectionCallbackInput) => void;
|
|
226
229
|
// Declarative physics config props
|
|
227
230
|
gravity?: [number, number, number];
|
|
228
231
|
timestep?: number;
|
|
@@ -380,7 +383,7 @@ export function MujocoSimProvider({
|
|
|
380
383
|
useEffect(() => {
|
|
381
384
|
if (status === 'ready') {
|
|
382
385
|
const api = apiRef.current;
|
|
383
|
-
if (onReady) onReady(api);
|
|
386
|
+
if (onReady) onReady({ api });
|
|
384
387
|
// Assign the forwarded ref
|
|
385
388
|
if (externalApiRef) {
|
|
386
389
|
if (typeof externalApiRef === 'function') {
|
|
@@ -409,7 +412,7 @@ export function MujocoSimProvider({
|
|
|
409
412
|
|
|
410
413
|
// Before-step callbacks
|
|
411
414
|
for (const cb of beforeStepCallbacks.current) {
|
|
412
|
-
cb(model, data);
|
|
415
|
+
cb({ model, data });
|
|
413
416
|
}
|
|
414
417
|
|
|
415
418
|
const numSubsteps = substepsRef.current;
|
|
@@ -466,17 +469,17 @@ export function MujocoSimProvider({
|
|
|
466
469
|
interpolationStateRef.current.valid = true;
|
|
467
470
|
|
|
468
471
|
if (!stepped) {
|
|
469
|
-
onStepRef.current?.(data.time);
|
|
472
|
+
onStepRef.current?.({ time: data.time, model, data });
|
|
470
473
|
return;
|
|
471
474
|
}
|
|
472
475
|
}
|
|
473
476
|
|
|
474
477
|
// After-step callbacks
|
|
475
478
|
for (const cb of afterStepCallbacks.current) {
|
|
476
|
-
cb(model, data);
|
|
479
|
+
cb({ model, data });
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
onStepRef.current?.(data.time);
|
|
482
|
+
onStepRef.current?.({ time: data.time, model, data });
|
|
480
483
|
}, -1);
|
|
481
484
|
|
|
482
485
|
function ensureInterpolationBuffers(model: MujocoModel) {
|
|
@@ -515,7 +518,7 @@ export function MujocoSimProvider({
|
|
|
515
518
|
}
|
|
516
519
|
}
|
|
517
520
|
|
|
518
|
-
configRef.current.onReset?.(model, data);
|
|
521
|
+
configRef.current.onReset?.({ model, data });
|
|
519
522
|
mujoco.mj_forward(model, data);
|
|
520
523
|
|
|
521
524
|
// Notify composable plugins (e.g. IkController)
|