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.
@@ -8,20 +8,26 @@ import type { ThreeElements } from '@react-three/fiber';
8
8
  import type { ReactNode } from 'react';
9
9
  import { useEffect, useMemo } from 'react';
10
10
  import * as THREE from 'three';
11
+ import { SplatEnvironmentReadinessStatus } from '../types';
11
12
  import type {
12
13
  PairedSplatEnvironmentConfig,
13
14
  ScenarioMaterialConfig,
14
15
  SceneConfig,
15
16
  SplatCollisionProxyConfig,
17
+ SplatEnvironmentReadiness,
16
18
  SplatEnvironmentMetadata,
17
19
  SplatEnvironmentMetadataInput,
18
20
  SplatFormat,
19
21
  SplatRendererKind,
22
+ SplatSceneConfigInput,
23
+ SplatSceneConfigState,
20
24
  SplatSceneInput,
21
25
  ScenarioLightingPreset,
22
26
  ScenarioLightingProps,
23
27
  SplatEnvironmentProps,
24
28
  VisualScenarioConfig,
29
+ VisualScenarioExecutionContext,
30
+ VisualScenarioExecutionContextInput,
25
31
  VisualScenarioEffectsProps,
26
32
  } from '../types';
27
33
 
@@ -110,6 +116,84 @@ export function getScenarioCameraPosition(
110
116
  ];
111
117
  }
112
118
 
119
+ export function useVisualScenarioExecutionContext({
120
+ scenario,
121
+ environment,
122
+ renderer,
123
+ variantId,
124
+ enabled,
125
+ }: VisualScenarioExecutionContextInput): VisualScenarioExecutionContext {
126
+ return useMemo(
127
+ () =>
128
+ createVisualScenarioExecutionContext({
129
+ scenario,
130
+ environment,
131
+ renderer,
132
+ variantId,
133
+ enabled,
134
+ }),
135
+ [enabled, environment, renderer, scenario, variantId]
136
+ );
137
+ }
138
+
139
+ export function createVisualScenarioExecutionContext({
140
+ scenario,
141
+ environment,
142
+ renderer,
143
+ variantId,
144
+ enabled = true,
145
+ }: VisualScenarioExecutionContextInput): VisualScenarioExecutionContext {
146
+ const pairedEnvironment =
147
+ environment ??
148
+ (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : undefined);
149
+ const splat = scenario?.splat;
150
+ const collisionProxy =
151
+ pairedEnvironment?.collisionProxy ?? splat?.collisionProxy ?? undefined;
152
+ const readiness = getSplatEnvironmentReadiness({
153
+ environment: pairedEnvironment,
154
+ scenario,
155
+ renderer,
156
+ enabled,
157
+ });
158
+ const format =
159
+ pairedEnvironment?.splat.format ?? splat?.format ?? readiness.format ?? 'spz';
160
+
161
+ return {
162
+ scenarioId: scenario?.id ?? pairedEnvironment?.id ?? 'visual-scenario',
163
+ scenarioLabel:
164
+ scenario?.label ?? pairedEnvironment?.label ?? 'Visual scenario',
165
+ variantId,
166
+ seed: scenario?.seed ?? 0,
167
+ lighting: scenario?.lighting ?? 'studio',
168
+ environment: scenario?.environment,
169
+ camera: {
170
+ jitter: scenario?.camera?.jitter ?? 0,
171
+ exposure: scenario?.camera?.exposure ?? 1,
172
+ noise: scenario?.camera?.noise ?? 0,
173
+ blur: scenario?.camera?.blur ?? 0,
174
+ },
175
+ materials: {
176
+ randomizeObjectColors: Boolean(
177
+ scenario?.materials?.randomizeObjectColors
178
+ ),
179
+ randomizeTableMaterial: Boolean(
180
+ scenario?.materials?.randomizeTableMaterial
181
+ ),
182
+ roughness: scenario?.materials?.roughness,
183
+ metalness: scenario?.materials?.metalness,
184
+ },
185
+ splatEnabled: Boolean(splat?.enabled || pairedEnvironment),
186
+ splatSrc: pairedEnvironment?.splat.src ?? splat?.src,
187
+ splatFormat: format,
188
+ splatRenderer: renderer ?? pairedEnvironment?.splat.renderer,
189
+ collisionProxyXmlPath: collisionProxy?.xmlPath,
190
+ collisionProxyStatus: collisionProxy?.status,
191
+ collisionProxyPrimitives: collisionProxy?.primitives ?? [],
192
+ readiness,
193
+ transformSource: 'visualScenario.camera',
194
+ };
195
+ }
196
+
113
197
  export function VisualScenarioEffects(props: VisualScenarioEffectsProps) {
114
198
  useVisualScenarioEffects(props);
115
199
  return null;
@@ -277,27 +361,208 @@ export function useSplatEnvironment({
277
361
  scenarioEnvironment?.collisionProxy ??
278
362
  scenario?.splat?.collisionProxy ??
279
363
  undefined;
364
+ const readiness = useMemo(
365
+ () =>
366
+ getSplatEnvironmentReadiness({
367
+ environment: scenarioEnvironment,
368
+ scenario,
369
+ renderer,
370
+ src: resolvedSrc,
371
+ format: resolvedFormat,
372
+ collisionProxy: resolvedCollisionProxy,
373
+ }),
374
+ [
375
+ collisionProxy,
376
+ renderer,
377
+ resolvedCollisionProxy,
378
+ resolvedFormat,
379
+ resolvedSrc,
380
+ scenario,
381
+ scenarioEnvironment,
382
+ ]
383
+ );
280
384
 
281
385
  return useMemo(
282
386
  () => ({
283
387
  src: resolvedSrc,
284
388
  format: resolvedFormat,
285
389
  collisionProxy: resolvedCollisionProxy,
390
+ readiness,
286
391
  userData: createSplatEnvironmentUserData({
287
392
  environment: scenarioEnvironment,
288
393
  src: resolvedSrc,
289
394
  format: resolvedFormat,
290
395
  collisionProxy: resolvedCollisionProxy,
396
+ readiness,
291
397
  }),
292
398
  }),
293
- [scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
399
+ [
400
+ scenarioEnvironment,
401
+ resolvedSrc,
402
+ resolvedFormat,
403
+ resolvedCollisionProxy,
404
+ readiness,
405
+ ]
406
+ );
407
+ }
408
+
409
+ /**
410
+ * Resolve a visual scenario's paired splat environment and compose its MJCF
411
+ * collision proxy into a MuJoCo scene config.
412
+ *
413
+ * This hook is renderer-agnostic: apps can use it with Spark, another 3DGS
414
+ * renderer, or their own Three scene objects while keeping physics collision
415
+ * files paired with the visual splat metadata.
416
+ */
417
+ export function useSplatSceneConfig({
418
+ sceneConfig,
419
+ scenario,
420
+ environment,
421
+ enabled = true,
422
+ renderer,
423
+ }: SplatSceneConfigInput): SplatSceneConfigState {
424
+ return useMemo(
425
+ () =>
426
+ createSplatSceneConfig({
427
+ sceneConfig,
428
+ scenario,
429
+ environment,
430
+ enabled,
431
+ renderer,
432
+ }),
433
+ [enabled, environment, renderer, scenario, sceneConfig]
294
434
  );
295
435
  }
296
436
 
297
437
  /**
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.
438
+ * Resolve a visual scenario's paired splat environment without requiring React.
439
+ *
440
+ * Use this in codegen, import validators, backend handoff metadata, or app code
441
+ * that needs the same behavior as `useSplatSceneConfig` outside a component.
442
+ */
443
+ export function createSplatSceneConfig({
444
+ sceneConfig,
445
+ scenario,
446
+ environment,
447
+ enabled = true,
448
+ renderer,
449
+ }: SplatSceneConfigInput): SplatSceneConfigState {
450
+ const resolvedEnvironment = enabled
451
+ ? environment ??
452
+ (scenario
453
+ ? createPairedSplatEnvironment(scenario, { renderer })
454
+ : undefined)
455
+ : undefined;
456
+ const readiness = getSplatEnvironmentReadiness({
457
+ environment: resolvedEnvironment,
458
+ scenario,
459
+ renderer,
460
+ enabled,
461
+ });
462
+ const resolvedSceneConfig = resolvedEnvironment
463
+ ? withSplatEnvironment(sceneConfig, resolvedEnvironment, { renderer })
464
+ : sceneConfig;
465
+
466
+ return {
467
+ environment: resolvedEnvironment,
468
+ sceneConfig: resolvedSceneConfig,
469
+ enabled:
470
+ enabled && readiness.status !== SplatEnvironmentReadinessStatus.Disabled,
471
+ readiness,
472
+ };
473
+ }
474
+
475
+ export function getSplatEnvironmentReadiness({
476
+ environment,
477
+ scenario,
478
+ renderer,
479
+ src,
480
+ format,
481
+ collisionProxy,
482
+ enabled = true,
483
+ }: {
484
+ environment?: PairedSplatEnvironmentConfig;
485
+ scenario?: Pick<VisualScenarioConfig, 'splat'>;
486
+ renderer?: SplatRendererKind;
487
+ src?: string;
488
+ format?: SplatFormat;
489
+ collisionProxy?: SplatCollisionProxyConfig;
490
+ enabled?: boolean;
491
+ }): SplatEnvironmentReadiness {
492
+ const splat = scenario?.splat;
493
+ const resolvedSrc = src ?? environment?.splat.src ?? splat?.src;
494
+ const resolvedFormat =
495
+ format ?? environment?.splat.format ?? splat?.format ?? 'spz';
496
+ const resolvedRenderer = renderer ?? environment?.splat.renderer;
497
+ const resolvedCollisionProxy =
498
+ collisionProxy ?? environment?.collisionProxy ?? splat?.collisionProxy ?? undefined;
499
+ const requiresCollisionProxy = splat?.requiresCollisionProxy ?? true;
500
+
501
+ if (!enabled || (splat && splat.enabled === false && !environment)) {
502
+ return {
503
+ status: SplatEnvironmentReadinessStatus.Disabled,
504
+ ready: false,
505
+ requiresCollisionProxy,
506
+ missing: [],
507
+ format: resolvedFormat,
508
+ renderer: resolvedRenderer,
509
+ message: 'Splat environment is disabled.',
510
+ };
511
+ }
512
+
513
+ if (!resolvedSrc) {
514
+ return {
515
+ status: SplatEnvironmentReadinessStatus.MissingSplat,
516
+ ready: false,
517
+ requiresCollisionProxy,
518
+ missing: ['splat'],
519
+ format: resolvedFormat,
520
+ renderer: resolvedRenderer,
521
+ message: 'Splat environment is missing a visual asset source.',
522
+ };
523
+ }
524
+
525
+ if (resolvedRenderer === 'spark' && resolvedFormat !== 'spz') {
526
+ return {
527
+ status: SplatEnvironmentReadinessStatus.UnsupportedFormat,
528
+ ready: false,
529
+ requiresCollisionProxy,
530
+ missing: [],
531
+ format: resolvedFormat,
532
+ renderer: resolvedRenderer,
533
+ message: `Spark splat rendering requires .spz assets; received ${resolvedFormat}.`,
534
+ };
535
+ }
536
+
537
+ if (requiresCollisionProxy && !resolvedCollisionProxy?.xmlPath) {
538
+ return {
539
+ status: SplatEnvironmentReadinessStatus.MissingCollisionProxy,
540
+ ready: false,
541
+ requiresCollisionProxy,
542
+ missing: ['collisionProxy'],
543
+ format: resolvedFormat,
544
+ renderer: resolvedRenderer,
545
+ message: 'Splat environment is missing paired MJCF collision proxy XML.',
546
+ };
547
+ }
548
+
549
+ return {
550
+ status: SplatEnvironmentReadinessStatus.Ready,
551
+ ready: true,
552
+ requiresCollisionProxy,
553
+ missing: [],
554
+ format: resolvedFormat,
555
+ renderer: resolvedRenderer,
556
+ message: requiresCollisionProxy
557
+ ? 'Splat environment has visual asset and collision proxy metadata.'
558
+ : 'Splat environment has a visual asset and does not require collision proxy metadata.',
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Convert a generic visual scenario splat block into a composable splat
564
+ * environment config. Visual-only splats are valid; readiness reports whether
565
+ * a paired MJCF collision proxy is required before training/physics handoff.
301
566
  */
302
567
  export function createPairedSplatEnvironment(
303
568
  scenario: Pick<VisualScenarioConfig, 'id' | 'label' | 'environment' | 'splat'>,
@@ -311,7 +576,7 @@ export function createPairedSplatEnvironment(
311
576
  const splat = scenario.splat;
312
577
  const collisionProxy = splat?.collisionProxy;
313
578
 
314
- if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {
579
+ if (!splat?.enabled || !splat.src) {
315
580
  return undefined;
316
581
  }
317
582
 
@@ -328,15 +593,22 @@ export function createPairedSplatEnvironment(
328
593
  format: splat.format ?? 'spz',
329
594
  renderer: options.renderer,
330
595
  },
331
- collisionProxy: {
332
- ...collisionProxy,
333
- xmlPath: collisionProxy.xmlPath,
334
- },
596
+ collisionProxy: collisionProxy?.xmlPath
597
+ ? {
598
+ ...collisionProxy,
599
+ xmlPath: collisionProxy.xmlPath,
600
+ }
601
+ : undefined,
335
602
  };
336
603
  }
337
604
 
338
605
  function isPairedSplatEnvironment(input: SplatSceneInput): input is PairedSplatEnvironmentConfig {
339
- return !!input && 'collisionProxy' in input && 'splat' in input;
606
+ return (
607
+ !!input &&
608
+ 'splat' in input &&
609
+ !!input.splat &&
610
+ !('enabled' in input.splat)
611
+ );
340
612
  }
341
613
 
342
614
  function sceneRelativePath(sceneConfig: SceneConfig, path: string): string {
@@ -376,7 +648,7 @@ export function withSplatEnvironment(
376
648
  : input
377
649
  ? createPairedSplatEnvironment(input, options)
378
650
  : undefined;
379
- const xmlPath = environment?.collisionProxy.xmlPath;
651
+ const xmlPath = environment?.collisionProxy?.xmlPath;
380
652
  if (!xmlPath) return sceneConfig;
381
653
 
382
654
  return {
@@ -393,11 +665,13 @@ export function createSplatEnvironmentUserData({
393
665
  src,
394
666
  format = 'spz',
395
667
  collisionProxy,
668
+ readiness,
396
669
  }: {
397
670
  environment?: PairedSplatEnvironmentConfig;
398
671
  src?: string;
399
672
  format?: SplatFormat;
400
673
  collisionProxy?: SplatCollisionProxyConfig;
674
+ readiness?: SplatEnvironmentReadiness;
401
675
  }) {
402
676
  return {
403
677
  role: 'splat-environment',
@@ -409,6 +683,8 @@ export function createSplatEnvironmentUserData({
409
683
  collisionProxyStatus: collisionProxy?.status ?? 'missing',
410
684
  collisionProxyXmlPath: collisionProxy?.xmlPath,
411
685
  collisionProxyPrimitives: collisionProxy?.primitives ?? [],
686
+ readinessStatus: readiness?.status,
687
+ readinessMessage: readiness?.message,
412
688
  };
413
689
  }
414
690