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.
package/src/spark.tsx CHANGED
@@ -15,9 +15,14 @@ import * as THREE from 'three';
15
15
  import {
16
16
  SplatEnvironment,
17
17
  useSplatEnvironment,
18
+ useSplatSceneConfig,
18
19
  } from './components/VisualScenario';
19
20
  import type {
21
+ PairedSplatEnvironmentConfig,
22
+ SceneConfig,
20
23
  SplatEnvironmentProps,
24
+ SplatEnvironmentReadiness,
25
+ VisualScenarioConfig,
21
26
  } from './types';
22
27
 
23
28
  type SparkModule = typeof import('@sparkjsdev/spark');
@@ -26,9 +31,22 @@ type SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;
26
31
  type SparkDisposable = {
27
32
  dispose?: () => unknown;
28
33
  };
34
+ type SparkWorkerMessage = {
35
+ reject?: (error: unknown) => void;
36
+ };
37
+ type SparkWorkerLike = {
38
+ messages?: Record<string, SparkWorkerMessage>;
39
+ };
40
+ type SparkResourceWithWorkers = SparkDisposable & {
41
+ worker?: SparkWorkerLike;
42
+ sortWorker?: SparkWorkerLike;
43
+ lodWorker?: SparkWorkerLike;
44
+ };
29
45
 
30
46
  export type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';
31
47
 
48
+ let sparkDisposeRejectionHandlerRegistered = false;
49
+
32
50
  export interface SparkSplatLifecycle {
33
51
  status: SparkSplatStatus;
34
52
  error: Error | null;
@@ -39,6 +57,18 @@ export interface SparkSplatLifecycle {
39
57
  reset: () => void;
40
58
  }
41
59
 
60
+ export interface SparkSplatEnvironmentState {
61
+ environment: PairedSplatEnvironmentConfig | undefined;
62
+ sceneConfig: SceneConfig;
63
+ readiness: SplatEnvironmentReadiness;
64
+ lifecycle: SparkSplatLifecycle;
65
+ props: Pick<
66
+ SparkSplatEnvironmentProps,
67
+ 'environment' | 'scenario' | 'src' | 'format' | 'onStatusChange' | 'onError'
68
+ >;
69
+ enabled: boolean;
70
+ }
71
+
42
72
  export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
43
73
  /** Enable Spark LoD handling for large splat assets. Default: true. */
44
74
  lod?: boolean | 'quality';
@@ -53,6 +83,67 @@ export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
53
83
  onError?: (error: Error) => void;
54
84
  }
55
85
 
86
+ /**
87
+ * Resolve a visual scenario's paired splat environment, compose its MJCF
88
+ * collision proxy into the MuJoCo scene config, and expose Spark lifecycle
89
+ * props for `<SparkSplatEnvironment />`.
90
+ */
91
+ export function useSparkSplatEnvironment({
92
+ sceneConfig,
93
+ scenario,
94
+ environment,
95
+ enabled = true,
96
+ renderer = 'spark',
97
+ onError,
98
+ onStatusChange,
99
+ }: {
100
+ sceneConfig: SceneConfig;
101
+ scenario?: VisualScenarioConfig;
102
+ environment?: PairedSplatEnvironmentConfig;
103
+ enabled?: boolean;
104
+ renderer?: 'spark';
105
+ onError?: (error: Error) => void;
106
+ onStatusChange?: (status: SparkSplatStatus) => void;
107
+ }): SparkSplatEnvironmentState {
108
+ const splatScene = useSplatSceneConfig({
109
+ sceneConfig,
110
+ scenario,
111
+ environment,
112
+ enabled,
113
+ renderer,
114
+ });
115
+ const metadata = useSplatEnvironment({
116
+ scenario,
117
+ environment: splatScene.environment,
118
+ renderer,
119
+ });
120
+ const renderEnabled = enabled && Boolean(metadata.src);
121
+ const readiness = enabled ? metadata.readiness : splatScene.readiness;
122
+ const lifecycle = useSparkSplatLifecycle({
123
+ enabled: renderEnabled,
124
+ onError,
125
+ onStatusChange,
126
+ });
127
+
128
+ return useMemo(
129
+ () => ({
130
+ environment: splatScene.environment,
131
+ sceneConfig: splatScene.sceneConfig,
132
+ readiness,
133
+ lifecycle,
134
+ props: {
135
+ environment: splatScene.environment,
136
+ scenario: enabled ? scenario : undefined,
137
+ src: enabled ? metadata.src : undefined,
138
+ format: metadata.format,
139
+ ...lifecycle.props,
140
+ },
141
+ enabled: renderEnabled,
142
+ }),
143
+ [enabled, lifecycle, metadata, readiness, renderEnabled, scenario, splatScene]
144
+ );
145
+ }
146
+
56
147
  /**
57
148
  * Tracks Spark 3DGS loading state for UI that wraps `SparkSplatEnvironment`.
58
149
  *
@@ -177,6 +268,7 @@ export function SparkSplatEnvironment({
177
268
 
178
269
  useEffect(() => {
179
270
  let disposed = false;
271
+ ensureSparkDisposeRejectionHandler();
180
272
 
181
273
  function setLifecycleStatus(nextStatus: SparkSplatStatus) {
182
274
  setStatus(nextStatus);
@@ -306,6 +398,7 @@ export function SparkSplatEnvironment({
306
398
 
307
399
  function safelyDisposeSparkResource(resource: SparkDisposable) {
308
400
  try {
401
+ silenceSparkWorkerTerminateRejections(resource);
309
402
  const result = resource.dispose?.();
310
403
  if (isPromiseLike(result)) {
311
404
  void Promise.resolve(result).catch(handleSparkDisposeError);
@@ -324,6 +417,33 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
324
417
  );
325
418
  }
326
419
 
420
+ function silenceSparkWorkerTerminateRejections(resource: SparkDisposable) {
421
+ const workers = getSparkWorkers(resource);
422
+ for (const worker of workers) {
423
+ if (!worker.messages) continue;
424
+
425
+ for (const message of Object.values(worker.messages)) {
426
+ const reject = message.reject;
427
+ if (!reject) continue;
428
+
429
+ message.reject = (error: unknown) => {
430
+ if (!isSparkWorkerTerminateError(error)) {
431
+ reject(error);
432
+ }
433
+ };
434
+ }
435
+ }
436
+ }
437
+
438
+ function getSparkWorkers(resource: SparkDisposable): SparkWorkerLike[] {
439
+ const sparkResource = resource as SparkResourceWithWorkers;
440
+ return [
441
+ sparkResource.worker,
442
+ sparkResource.sortWorker,
443
+ sparkResource.lodWorker,
444
+ ].filter((worker): worker is SparkWorkerLike => Boolean(worker));
445
+ }
446
+
327
447
  function handleSparkDisposeError(error: unknown) {
328
448
  if (
329
449
  error instanceof Error &&
@@ -334,3 +454,27 @@ function handleSparkDisposeError(error: unknown) {
334
454
 
335
455
  console.warn('[mujoco-react] Spark resource disposal failed.', error);
336
456
  }
457
+
458
+ function ensureSparkDisposeRejectionHandler() {
459
+ if (
460
+ sparkDisposeRejectionHandlerRegistered ||
461
+ typeof window === 'undefined' ||
462
+ typeof window.addEventListener !== 'function'
463
+ ) {
464
+ return;
465
+ }
466
+
467
+ sparkDisposeRejectionHandlerRegistered = true;
468
+ window.addEventListener('unhandledrejection', (event) => {
469
+ if (isSparkWorkerTerminateError(event.reason)) {
470
+ event.preventDefault();
471
+ }
472
+ });
473
+ }
474
+
475
+ function isSparkWorkerTerminateError(reason: unknown) {
476
+ return (
477
+ reason instanceof Error &&
478
+ reason.message.toLowerCase().includes('worker terminate')
479
+ );
480
+ }
package/src/types.ts CHANGED
@@ -54,8 +54,9 @@ export type RobotJoints<TRobot extends string> = RobotResource<TRobot, 'joints'>
54
54
  export type RobotSites<TRobot extends string> = RobotResource<TRobot, 'sites'>;
55
55
  export type RobotGeoms<TRobot extends string> = RobotResource<TRobot, 'geoms'>;
56
56
  export type RobotKeyframes<TRobot extends string> = RobotResource<TRobot, 'keyframes'>;
57
+ export type RobotCameras<TRobot extends string> = RobotResource<TRobot, 'cameras'>;
57
58
 
58
- export type RegisterResourceKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes';
59
+ export type RegisterResourceKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes' | 'cameras';
59
60
  export type RobotResourceObject<TRobot extends string, TKey extends RegisterResourceKey> =
60
61
  string extends RobotResource<TRobot, TKey>
61
62
  ? Record<string, string>
@@ -73,7 +74,7 @@ type RuntimeRobotResources = Record<string, Record<RegisterResourceKey, Record<s
73
74
  type RuntimeRobotResourceRegistration = Readonly<Record<string, Readonly<Record<RegisterResourceKey, Readonly<Record<string, string>>>>>>;
74
75
 
75
76
  const runtimeRobotResources: RuntimeRobotResources = {};
76
- const REGISTER_RESOURCE_KEYS: RegisterResourceKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes'];
77
+ const REGISTER_RESOURCE_KEYS: RegisterResourceKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes', 'cameras'];
77
78
 
78
79
  function createEmptyRuntimeResources(): Record<RegisterResourceKey, Record<string, string>> {
79
80
  return {
@@ -84,6 +85,7 @@ function createEmptyRuntimeResources(): Record<RegisterResourceKey, Record<strin
84
85
  sites: {},
85
86
  geoms: {},
86
87
  keyframes: {},
88
+ cameras: {},
87
89
  };
88
90
  }
89
91
 
@@ -134,6 +136,7 @@ export const RobotJoints: RobotResourceCategory<'joints'> = createResourceCatego
134
136
  export const RobotSites: RobotResourceCategory<'sites'> = createResourceCategory('sites');
135
137
  export const RobotGeoms: RobotResourceCategory<'geoms'> = createResourceCategory('geoms');
136
138
  export const RobotKeyframes: RobotResourceCategory<'keyframes'> = createResourceCategory('keyframes');
139
+ export const RobotCameras: RobotResourceCategory<'cameras'> = createResourceCategory('cameras');
137
140
 
138
141
  export type Actuators = Register extends { actuators: infer T extends string } ? T : string;
139
142
  export type Sensors = Register extends { sensors: infer T extends string } ? T : string;
@@ -142,6 +145,7 @@ export type Joints = Register extends { joints: infer T extends string } ? T : s
142
145
  export type Sites = Register extends { sites: infer T extends string } ? T : string;
143
146
  export type Geoms = Register extends { geoms: infer T extends string } ? T : string;
144
147
  export type Keyframes = Register extends { keyframes: infer T extends string } ? T : string;
148
+ export type Cameras = Register extends { cameras: infer T extends string } ? T : string;
145
149
 
146
150
  // ---- MuJoCo WASM Types ----
147
151
 
@@ -209,6 +213,7 @@ export interface MujocoModel {
209
213
  nflex: number;
210
214
  nmesh: number;
211
215
  nmat: number;
216
+ ncam?: number;
212
217
 
213
218
  // Name tables
214
219
  names: Int8Array;
@@ -220,6 +225,7 @@ export interface MujocoModel {
220
225
  name_keyadr: Int32Array;
221
226
  name_sensoradr: Int32Array;
222
227
  name_tendonadr: Int32Array;
228
+ name_camadr?: Int32Array;
223
229
 
224
230
  // Body
225
231
  body_mass: Float64Array;
@@ -307,6 +313,12 @@ export interface MujocoModel {
307
313
  light_exponent: Float32Array;
308
314
  light_intensity: Float32Array;
309
315
 
316
+ // Camera
317
+ cam_bodyid?: Int32Array;
318
+ cam_pos?: Float64Array;
319
+ cam_quat?: Float64Array;
320
+ cam_fovy?: Float64Array;
321
+
310
322
  // Tendon
311
323
  ten_wrapadr: Int32Array;
312
324
  ten_wrapnum: Int32Array;
@@ -350,6 +362,9 @@ export interface MujocoData {
350
362
  qfrc_bias: Float64Array;
351
363
  site_xpos: Float64Array;
352
364
  site_xmat: Float64Array;
365
+ cam_xpos?: Float64Array;
366
+ cam_xmat?: Float64Array;
367
+ xmat?: Float64Array;
353
368
  sensordata: Float64Array;
354
369
  ncon: number;
355
370
  contact: MujocoContactArray;
@@ -683,6 +698,15 @@ export interface SensorInfo {
683
698
  adr: number;
684
699
  }
685
700
 
701
+ export interface CameraInfo {
702
+ id: number;
703
+ name: string;
704
+ bodyId: number;
705
+ fov: number | null;
706
+ position: [number, number, number] | null;
707
+ quaternion: [number, number, number, number] | null;
708
+ }
709
+
686
710
  // ---- Contacts (spec 2.4, 2.5) ----
687
711
 
688
712
  export interface ContactInfo {
@@ -904,8 +928,29 @@ export interface PairedSplatEnvironmentConfig {
904
928
  description?: string;
905
929
  /** Visual-only Gaussian splat asset. */
906
930
  splat: SplatAssetConfig;
907
- /** MJCF/XML contact geometry paired with the visual splat. */
908
- collisionProxy: SplatCollisionProxyConfig & { xmlPath: string };
931
+ /** Optional MJCF/XML contact geometry paired with the visual splat. */
932
+ collisionProxy?: SplatCollisionProxyConfig & { xmlPath: string };
933
+ }
934
+
935
+ export const SplatEnvironmentReadinessStatus = {
936
+ Disabled: 'disabled',
937
+ MissingSplat: 'missing-splat',
938
+ MissingCollisionProxy: 'missing-collision-proxy',
939
+ UnsupportedFormat: 'unsupported-format',
940
+ Ready: 'ready',
941
+ } as const;
942
+
943
+ export type SplatEnvironmentReadinessStatus =
944
+ (typeof SplatEnvironmentReadinessStatus)[keyof typeof SplatEnvironmentReadinessStatus];
945
+
946
+ export interface SplatEnvironmentReadiness {
947
+ status: SplatEnvironmentReadinessStatus;
948
+ ready: boolean;
949
+ requiresCollisionProxy: boolean;
950
+ missing: Array<'splat' | 'collisionProxy'>;
951
+ format?: SplatFormat;
952
+ renderer?: SplatRendererKind;
953
+ message: string;
909
954
  }
910
955
 
911
956
  export interface SplatEnvironmentMetadataInput {
@@ -921,15 +966,73 @@ export interface SplatEnvironmentMetadata {
921
966
  src?: string;
922
967
  format: SplatFormat;
923
968
  collisionProxy?: SplatCollisionProxyConfig;
969
+ readiness: SplatEnvironmentReadiness;
924
970
  userData: Record<string, unknown>;
925
971
  }
926
972
 
973
+ export interface ResolvedScenarioCameraConfig {
974
+ jitter: number;
975
+ exposure: number;
976
+ noise: number;
977
+ blur: number;
978
+ }
979
+
980
+ export interface ResolvedScenarioMaterialConfig {
981
+ randomizeObjectColors: boolean;
982
+ randomizeTableMaterial: boolean;
983
+ roughness?: number;
984
+ metalness?: number;
985
+ }
986
+
987
+ export interface VisualScenarioExecutionContext {
988
+ scenarioId: string;
989
+ scenarioLabel: string;
990
+ variantId?: string;
991
+ seed: number;
992
+ lighting: ScenarioLightingPreset;
993
+ environment?: string;
994
+ camera: ResolvedScenarioCameraConfig;
995
+ materials: ResolvedScenarioMaterialConfig;
996
+ splatEnabled: boolean;
997
+ splatSrc?: string;
998
+ splatFormat: SplatFormat;
999
+ splatRenderer?: SplatRendererKind;
1000
+ collisionProxyXmlPath?: string;
1001
+ collisionProxyStatus?: SplatCollisionProxyConfig['status'];
1002
+ collisionProxyPrimitives: SplatCollisionPrimitive[];
1003
+ readiness: SplatEnvironmentReadiness;
1004
+ transformSource: 'visualScenario.camera';
1005
+ }
1006
+
1007
+ export interface VisualScenarioExecutionContextInput {
1008
+ scenario?: VisualScenarioConfig;
1009
+ environment?: PairedSplatEnvironmentConfig;
1010
+ renderer?: SplatRendererKind;
1011
+ variantId?: string;
1012
+ enabled?: boolean;
1013
+ }
1014
+
927
1015
  export type SplatSceneInput =
928
1016
  | PairedSplatEnvironmentConfig
929
1017
  | VisualScenarioConfig
930
1018
  | undefined
931
1019
  | null;
932
1020
 
1021
+ export interface SplatSceneConfigInput {
1022
+ sceneConfig: SceneConfig;
1023
+ scenario?: VisualScenarioConfig;
1024
+ environment?: PairedSplatEnvironmentConfig;
1025
+ enabled?: boolean;
1026
+ renderer?: SplatRendererKind;
1027
+ }
1028
+
1029
+ export interface SplatSceneConfigState {
1030
+ environment: PairedSplatEnvironmentConfig | undefined;
1031
+ sceneConfig: SceneConfig;
1032
+ enabled: boolean;
1033
+ readiness: SplatEnvironmentReadiness;
1034
+ }
1035
+
933
1036
  export interface VisualScenarioConfig {
934
1037
  id?: string;
935
1038
  label?: string;
@@ -1079,6 +1182,7 @@ export interface MujocoSimAPI {
1079
1182
  getSites(): SiteInfo[];
1080
1183
  getActuators(): ActuatorInfo[];
1081
1184
  getSensors(): SensorInfo[];
1185
+ getCameras(): CameraInfo[];
1082
1186
 
1083
1187
  // Model parameters (spec 5.3)
1084
1188
  getModelOption(): ModelOptions;
@@ -1176,7 +1280,14 @@ export type CameraFrameCaptureQuaternion =
1176
1280
  | readonly [number, number, number, number];
1177
1281
 
1178
1282
  export interface CameraFrameCaptureOptions {
1283
+ /** Existing Three camera to clone before applying pose overrides. */
1179
1284
  camera?: THREE.Camera;
1285
+ /** Named MuJoCo `<camera>` to render from when available in the loaded model. */
1286
+ cameraName?: Cameras;
1287
+ /** Named MuJoCo site to use as the rendered camera pose. Useful for robot-mounted optical frames. */
1288
+ siteName?: Sites;
1289
+ /** Named MuJoCo body to use as the rendered camera pose. */
1290
+ bodyName?: Bodies;
1180
1291
  position?: CameraFrameCaptureVector3;
1181
1292
  lookAt?: CameraFrameCaptureVector3;
1182
1293
  quaternion?: CameraFrameCaptureQuaternion;
@@ -1188,8 +1299,18 @@ export interface CameraFrameCaptureOptions {
1188
1299
  fov?: number;
1189
1300
  near?: number;
1190
1301
  far?: number;
1302
+ /** Provenance for the camera pose used by the capture. Usually set by the MuJoCo provider. */
1303
+ source?: CameraFrameCaptureSource;
1191
1304
  }
1192
1305
 
1306
+ export type CameraFrameCaptureSource =
1307
+ | { kind: 'mujoco-camera'; cameraName: Cameras }
1308
+ | { kind: 'mujoco-site'; siteName: Sites }
1309
+ | { kind: 'mujoco-body'; bodyName: Bodies }
1310
+ | { kind: 'custom-camera' }
1311
+ | { kind: 'explicit-pose' }
1312
+ | { kind: 'fallback-camera' };
1313
+
1193
1314
  export interface CameraFrameCaptureResult {
1194
1315
  canvas: HTMLCanvasElement;
1195
1316
  camera: THREE.Camera;
@@ -1197,6 +1318,7 @@ export interface CameraFrameCaptureResult {
1197
1318
  type: string;
1198
1319
  width: number;
1199
1320
  height: number;
1321
+ source: CameraFrameCaptureSource;
1200
1322
  }
1201
1323
 
1202
1324
  export interface CameraFrameCaptureBlobResult {
@@ -1206,6 +1328,7 @@ export interface CameraFrameCaptureBlobResult {
1206
1328
  type: string;
1207
1329
  width: number;
1208
1330
  height: number;
1331
+ source: CameraFrameCaptureSource;
1209
1332
  }
1210
1333
 
1211
1334
  export interface CameraFrameCaptureAPI {
@@ -1231,18 +1354,57 @@ export interface CameraFrameSequenceFrame {
1231
1354
  cameras: Record<string, CameraFrameCaptureResult>;
1232
1355
  }
1233
1356
 
1357
+ export interface CameraFrameSequenceCameraSummary {
1358
+ key: string;
1359
+ width: number;
1360
+ height: number;
1361
+ source: CameraFrameCaptureSource;
1362
+ frameCount: number;
1363
+ firstFrameIndex: number | null;
1364
+ lastFrameIndex: number | null;
1365
+ firstTimestamp: number | null;
1366
+ lastTimestamp: number | null;
1367
+ }
1368
+
1369
+ export interface CameraFrameSequenceSampleInput extends PhysicsStepInput {
1370
+ frameIndex: number;
1371
+ time: number;
1372
+ }
1373
+
1374
+ export interface CameraFrameSequenceStepInput extends PhysicsStepInput {
1375
+ frameIndex: number;
1376
+ stepIndex: number;
1377
+ time: number;
1378
+ }
1379
+
1234
1380
  export interface CameraFrameSequenceOptions {
1235
1381
  cameras: readonly CameraFrameSequenceCamera[];
1236
1382
  frames: number;
1383
+ /** Number of MuJoCo steps between captured frames. Use 0 for static camera provenance captures. */
1237
1384
  stepsPerFrame?: number;
1238
1385
  reset?: boolean;
1239
1386
  captureInitialFrame?: boolean;
1387
+ retainFrames?: boolean;
1388
+ /**
1389
+ * Require each recorded stream to resolve from exactly one mounted MuJoCo
1390
+ * camera/site/body selector. Defaults to true because sequence recording is
1391
+ * intended for dataset/policy camera streams.
1392
+ */
1393
+ requireMountedSources?: boolean;
1394
+ signal?: AbortSignal;
1395
+ /** Called after stepping and before image capture for this frame. Use this to record synchronized state/action rows. */
1396
+ onSample?: (input: CameraFrameSequenceSampleInput) => void | Promise<void>;
1397
+ /** Called before each MuJoCo step inside sequence recording. Use this to apply policy/control actions. */
1398
+ onBeforeStep?: (input: CameraFrameSequenceStepInput) => void | Promise<void>;
1399
+ /** Called after each MuJoCo step inside sequence recording. Use this for step-level telemetry. */
1400
+ onAfterStep?: (input: CameraFrameSequenceStepInput) => void | Promise<void>;
1240
1401
  onFrame?: (frame: CameraFrameSequenceFrame) => void | Promise<void>;
1241
1402
  }
1242
1403
 
1243
1404
  export interface CameraFrameSequenceResult {
1244
1405
  frames: CameraFrameSequenceFrame[];
1245
1406
  cameraKeys: string[];
1407
+ cameraSummaries: Record<string, CameraFrameSequenceCameraSummary>;
1246
1408
  frameCount: number;
1247
1409
  }
1248
1410
 
package/src/vite.ts CHANGED
@@ -38,7 +38,7 @@ export interface MujocoRegisterCodegenResult {
38
38
  counts: Record<RegisterKey, number>;
39
39
  }
40
40
 
41
- type RegisterKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes';
41
+ type RegisterKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes' | 'cameras';
42
42
  export type ModelInput = string | readonly string[] | Record<string, string>;
43
43
 
44
44
  interface ModelEntry {
@@ -47,7 +47,7 @@ interface ModelEntry {
47
47
  names: Record<RegisterKey, Set<string>>;
48
48
  }
49
49
 
50
- const REGISTER_KEYS: RegisterKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes'];
50
+ const REGISTER_KEYS: RegisterKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes', 'cameras'];
51
51
  const MODEL_EXTENSIONS = new Set(['.xml', '.mjcf', '.urdf']);
52
52
 
53
53
  function createEmptyNames(): Record<RegisterKey, Set<string>> {
@@ -59,6 +59,7 @@ function createEmptyNames(): Record<RegisterKey, Set<string>> {
59
59
  sites: new Set(),
60
60
  geoms: new Set(),
61
61
  keyframes: new Set(),
62
+ cameras: new Set(),
62
63
  };
63
64
  }
64
65
 
@@ -157,7 +158,7 @@ async function scanModel(
157
158
  seen: Set<string>,
158
159
  names: Record<RegisterKey, Set<string>>
159
160
  ) {
160
- const normalized = path.normalize(filePath);
161
+ const normalized = path.resolve(filePath);
161
162
  if (seen.has(normalized)) return;
162
163
  seen.add(normalized);
163
164
 
@@ -166,13 +167,14 @@ async function scanModel(
166
167
  collectSimpleTagNames(xml, 'joint', names.joints);
167
168
  collectSimpleTagNames(xml, 'site', names.sites);
168
169
  collectSimpleTagNames(xml, 'geom', names.geoms);
170
+ collectSimpleTagNames(xml, 'camera', names.cameras);
169
171
  collectSimpleTagNames(xml, 'key', names.keyframes);
170
172
  collectSectionNames(xml, 'actuator', names.actuators);
171
173
  collectSectionNames(xml, 'sensor', names.sensors);
172
174
 
173
175
  for (const includePath of collectIncludePaths(xml)) {
174
176
  const next = path.resolve(path.dirname(normalized), includePath);
175
- if (next.startsWith(root)) await scanModel(next, root, seen, names);
177
+ if (isPathInsideRoot(next, root)) await scanModel(next, root, seen, names);
176
178
  }
177
179
  }
178
180
 
@@ -300,6 +302,7 @@ function renderNamespaceAliases(models: readonly ModelEntry[]): string {
300
302
  sites: 'RobotSites',
301
303
  geoms: 'RobotGeoms',
302
304
  keyframes: 'RobotKeyframes',
305
+ cameras: 'RobotCameras',
303
306
  };
304
307
 
305
308
  const blocks = REGISTER_KEYS
@@ -343,7 +346,7 @@ function shouldInjectRegisterImport(id: string, root: string, generatedRegister:
343
346
  if (file.includes(`${path.sep}node_modules${path.sep}`)) return false;
344
347
  const absolute = path.resolve(file);
345
348
  if (absolute === generatedRegister) return false;
346
- return absolute.startsWith(root);
349
+ return isPathInsideRoot(absolute, root);
347
350
  }
348
351
 
349
352
  function renderGeneratedImport(id: string, generatedRegister: string): string {
@@ -392,5 +395,10 @@ function shouldRegenerate(file: string, watchedFiles: string[], models: readonly
392
395
  if (watchedFiles.includes(absolute)) return true;
393
396
  if (!MODEL_EXTENSIONS.has(path.extname(absolute).toLowerCase())) return false;
394
397
  const modelDirs = models.map((model) => path.dirname(path.resolve(root, model.file)));
395
- return modelDirs.some((dir) => absolute.startsWith(dir));
398
+ return modelDirs.some((dir) => isPathInsideRoot(absolute, dir));
399
+ }
400
+
401
+ function isPathInsideRoot(filePath: string, root: string): boolean {
402
+ const relative = path.relative(path.resolve(root), path.resolve(filePath));
403
+ return relative === '' || (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative));
396
404
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/VisualScenario.tsx"],"names":[],"mappings":"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","file":"chunk-33CV6HSV.js","sourcesContent":["/**\n * @license\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { useThree } from '@react-three/fiber';\nimport type { ThreeElements } from '@react-three/fiber';\nimport type { ReactNode } from 'react';\nimport { useEffect, useMemo } from 'react';\nimport * as THREE from 'three';\nimport type {\n PairedSplatEnvironmentConfig,\n ScenarioMaterialConfig,\n SceneConfig,\n SplatCollisionProxyConfig,\n SplatEnvironmentMetadata,\n SplatEnvironmentMetadataInput,\n SplatFormat,\n SplatRendererKind,\n SplatSceneInput,\n ScenarioLightingPreset,\n ScenarioLightingProps,\n SplatEnvironmentProps,\n VisualScenarioConfig,\n VisualScenarioEffectsProps,\n} from '../types';\n\nconst DEFAULT_BACKGROUND = '#181a1f';\n\nexport function ScenarioLighting({\n preset = 'studio',\n castShadow = true,\n intensity = 1,\n}: ScenarioLightingProps) {\n if (preset === 'warehouse') {\n return (\n <>\n <ambientLight intensity={0.18 * intensity} />\n <directionalLight\n position={[3.5, -2, 5]}\n intensity={2.2 * intensity}\n castShadow={castShadow}\n />\n <directionalLight position={[-2, 1.5, 2.5]} intensity={0.25 * intensity} />\n </>\n );\n }\n\n if (preset === 'low-light') {\n return (\n <>\n <ambientLight intensity={0.08 * intensity} />\n <directionalLight\n position={[2, -2, 3]}\n intensity={0.75 * intensity}\n castShadow={castShadow}\n />\n <pointLight position={[-0.5, -0.8, 1.3]} intensity={0.6 * intensity} />\n </>\n );\n }\n\n if (preset === 'splat') {\n return (\n <>\n <ambientLight intensity={0.42 * intensity} />\n <directionalLight\n position={[1.8, -2.4, 3.5]}\n intensity={1.2 * intensity}\n castShadow={castShadow}\n />\n <pointLight position={[0.4, 0.2, 1.4]} intensity={0.35 * intensity} />\n </>\n );\n }\n\n return (\n <>\n <ambientLight intensity={0.35 * intensity} />\n <directionalLight\n position={[2.5, -3, 4]}\n intensity={1.6 * intensity}\n castShadow={castShadow}\n />\n </>\n );\n}\n\nexport function getScenarioBackground(\n preset: ScenarioLightingPreset | undefined,\n fallback = DEFAULT_BACKGROUND\n) {\n if (preset === 'warehouse') return '#20242b';\n if (preset === 'low-light') return '#0f1115';\n if (preset === 'splat') return '#1b1f24';\n return fallback;\n}\n\nexport function getScenarioCameraPosition(\n basePosition: readonly [number, number, number],\n scenario?: Pick<VisualScenarioConfig, 'camera'>\n): [number, number, number] {\n const [x, y, z] = basePosition;\n const jitter = scenario?.camera?.jitter ?? 0;\n\n return [\n Number((x + jitter * 0.6).toFixed(3)),\n Number((y - jitter * 0.4).toFixed(3)),\n Number((z + jitter * 0.25).toFixed(3)),\n ];\n}\n\nexport function VisualScenarioEffects(props: VisualScenarioEffectsProps) {\n useVisualScenarioEffects(props);\n return null;\n}\n\nexport function useVisualScenarioEffects({\n scenario,\n enabled = true,\n applyBackground = true,\n applyFog = true,\n applyRenderer = true,\n applyMaterials = true,\n background,\n fogNear,\n fogFar,\n materialFilter,\n}: VisualScenarioEffectsProps) {\n const { gl, scene, invalidate } = useThree();\n\n useEffect(() => {\n if (!enabled || !scenario) {\n return undefined;\n }\n\n const previousExposure = gl.toneMappingExposure;\n const previousBackground = scene.background;\n const previousFog = scene.fog;\n const materialSnapshots = new Map<\n THREE.Material,\n {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n }\n >();\n\n if (applyRenderer) {\n gl.toneMappingExposure = scenario.camera?.exposure ?? 1;\n }\n\n if (applyBackground) {\n scene.background = new THREE.Color(\n background ?? getScenarioBackground(scenario.lighting)\n );\n }\n\n if (applyFog) {\n scene.fog = createScenarioFog(scenario, background, fogNear, fogFar);\n }\n\n if (applyMaterials && scenario.materials) {\n applyScenarioMaterials(scene, scenario, materialSnapshots, materialFilter);\n }\n\n invalidate();\n\n return () => {\n gl.toneMappingExposure = previousExposure;\n scene.background = previousBackground;\n scene.fog = previousFog;\n\n for (const [material, snapshot] of materialSnapshots) {\n const mutable = getMutableScenarioMaterial(material);\n if (!mutable) continue;\n if (snapshot.color) mutable.color.copy(snapshot.color);\n if (typeof snapshot.roughness === 'number') {\n mutable.roughness = snapshot.roughness;\n }\n if (typeof snapshot.metalness === 'number') {\n mutable.metalness = snapshot.metalness;\n }\n mutable.needsUpdate = true;\n }\n\n invalidate();\n };\n }, [\n applyBackground,\n applyFog,\n applyMaterials,\n applyRenderer,\n background,\n enabled,\n fogFar,\n fogNear,\n gl,\n invalidate,\n materialFilter,\n scenario,\n scene,\n ]);\n}\n\n/**\n * Renderer-agnostic Gaussian splat environment boundary.\n *\n * This component intentionally does not import a specific 3DGS renderer. Pass a\n * Spark/GaussianSplats3D object as `children` once the app chooses a renderer,\n * and pass MuJoCo/MJCF collision proxy visuals via `collisionProxy`.\n */\nexport function SplatEnvironment({\n environment,\n scenario,\n renderer,\n src,\n format,\n collisionProxy,\n collisionProxyMetadata,\n children,\n showPlaceholder = true,\n ...groupProps\n}: SplatEnvironmentProps) {\n const metadata = useSplatEnvironment({\n environment,\n scenario,\n renderer,\n src,\n format,\n collisionProxy: collisionProxyMetadata,\n });\n const existingUserData =\n typeof groupProps.userData === 'object' && groupProps.userData !== null\n ? groupProps.userData\n : {};\n\n return (\n <group\n {...groupProps}\n userData={{\n ...existingUserData,\n ...metadata.userData,\n }}\n >\n {children}\n {children || !showPlaceholder ? null : <SplatPlaceholder />}\n {collisionProxy}\n </group>\n );\n}\n\nexport function useSplatEnvironment({\n environment,\n scenario,\n renderer,\n src,\n format,\n collisionProxy,\n}: SplatEnvironmentMetadataInput): SplatEnvironmentMetadata {\n const scenarioEnvironment = useMemo(\n () =>\n environment ??\n (scenario\n ? createPairedSplatEnvironment(scenario, { renderer })\n : undefined),\n [environment, renderer, scenario]\n );\n const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;\n const resolvedFormat =\n format ??\n scenarioEnvironment?.splat.format ??\n scenario?.splat?.format ??\n 'spz';\n const resolvedCollisionProxy =\n collisionProxy ??\n scenarioEnvironment?.collisionProxy ??\n scenario?.splat?.collisionProxy ??\n undefined;\n\n return useMemo(\n () => ({\n src: resolvedSrc,\n format: resolvedFormat,\n collisionProxy: resolvedCollisionProxy,\n userData: createSplatEnvironmentUserData({\n environment: scenarioEnvironment,\n src: resolvedSrc,\n format: resolvedFormat,\n collisionProxy: resolvedCollisionProxy,\n }),\n }),\n [scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]\n );\n}\n\n/**\n * Convert a generic visual scenario splat block into a paired visual/physics\n * environment config. Returns undefined until both the splat asset and MJCF\n * collision proxy are present.\n */\nexport function createPairedSplatEnvironment(\n scenario: Pick<VisualScenarioConfig, 'id' | 'label' | 'environment' | 'splat'>,\n options: {\n id?: string;\n label?: string;\n description?: string;\n renderer?: SplatRendererKind;\n } = {}\n): PairedSplatEnvironmentConfig | undefined {\n const splat = scenario.splat;\n const collisionProxy = splat?.collisionProxy;\n\n if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {\n return undefined;\n }\n\n return {\n id: options.id ?? scenario.id ?? 'splat-environment',\n label: options.label ?? scenario.label ?? 'Gaussian splat environment',\n description:\n options.description ??\n (scenario.environment\n ? `Visual ${scenario.environment} splat paired with MJCF collision proxy.`\n : undefined),\n splat: {\n src: splat.src,\n format: splat.format ?? 'spz',\n renderer: options.renderer,\n },\n collisionProxy: {\n ...collisionProxy,\n xmlPath: collisionProxy.xmlPath,\n },\n };\n}\n\nfunction isPairedSplatEnvironment(input: SplatSceneInput): input is PairedSplatEnvironmentConfig {\n return !!input && 'collisionProxy' in input && 'splat' in input;\n}\n\nfunction sceneRelativePath(sceneConfig: SceneConfig, path: string): string {\n const src = sceneConfig.src;\n if (!src) return path;\n\n const base = src.endsWith('/') ? src : src + '/';\n if (path.startsWith(base)) return path.slice(base.length);\n return path;\n}\n\nfunction uniquePaths(paths: readonly string[]): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const path of paths) {\n if (seen.has(path)) continue;\n seen.add(path);\n result.push(path);\n }\n return result;\n}\n\n/**\n * Compose a MuJoCo scene config with a paired splat collision proxy.\n *\n * This keeps the common hybrid setup declarative:\n * robot XML remains `sceneFile`, the `.spz` remains a visual-only layer, and\n * the paired MJCF collision proxy is added to `environmentFiles`.\n */\nexport function withSplatEnvironment(\n sceneConfig: SceneConfig,\n input: SplatSceneInput,\n options: { renderer?: SplatRendererKind } = {}\n): SceneConfig {\n const environment = isPairedSplatEnvironment(input)\n ? input\n : input\n ? createPairedSplatEnvironment(input, options)\n : undefined;\n const xmlPath = environment?.collisionProxy.xmlPath;\n if (!xmlPath) return sceneConfig;\n\n return {\n ...sceneConfig,\n environmentFiles: uniquePaths([\n ...(sceneConfig.environmentFiles ?? []),\n sceneRelativePath(sceneConfig, xmlPath),\n ]),\n };\n}\n\nexport function createSplatEnvironmentUserData({\n environment,\n src,\n format = 'spz',\n collisionProxy,\n}: {\n environment?: PairedSplatEnvironmentConfig;\n src?: string;\n format?: SplatFormat;\n collisionProxy?: SplatCollisionProxyConfig;\n}) {\n return {\n role: 'splat-environment',\n environmentId: environment?.id,\n environmentLabel: environment?.label,\n splatSrc: src,\n splatFormat: format,\n splatRenderer: environment?.splat.renderer,\n collisionProxyStatus: collisionProxy?.status ?? 'missing',\n collisionProxyXmlPath: collisionProxy?.xmlPath,\n collisionProxyPrimitives: collisionProxy?.primitives ?? [],\n };\n}\n\nexport function createSparkSplatViewerUrl({\n viewerUrl,\n splatSrc,\n}: {\n viewerUrl: string;\n splatSrc: string;\n}) {\n const url = new URL(viewerUrl, 'http://mujoco-react.local');\n url.searchParams.set('splat', splatSrc);\n return viewerUrl.startsWith('http') ? url.toString() : `${url.pathname}${url.search}`;\n}\n\nfunction SplatPlaceholder() {\n return (\n <group>\n <mesh position={[0, 0, 1.2]}>\n <boxGeometry args={[2.4, 2.4, 2.4]} />\n <meshBasicMaterial\n color=\"#8b8b8b\"\n transparent\n opacity={0.06}\n wireframe\n side={THREE.DoubleSide}\n />\n </mesh>\n </group>\n );\n}\n\nfunction createScenarioFog(\n scenario: VisualScenarioConfig,\n background: THREE.ColorRepresentation | undefined,\n fogNear: number | undefined,\n fogFar: number | undefined\n) {\n if (scenario.lighting === 'low-light') {\n return new THREE.Fog(\n background ?? getScenarioBackground(scenario.lighting),\n fogNear ?? 2.5,\n fogFar ?? 9\n );\n }\n\n if (scenario.lighting === 'warehouse') {\n return new THREE.Fog(\n background ?? getScenarioBackground(scenario.lighting),\n fogNear ?? 5,\n fogFar ?? 16\n );\n }\n\n return null;\n}\n\nfunction applyScenarioMaterials(\n scene: THREE.Scene,\n scenario: VisualScenarioConfig,\n snapshots: Map<\n THREE.Material,\n {\n color?: THREE.Color;\n roughness?: number;\n metalness?: number;\n }\n >,\n materialFilter: VisualScenarioEffectsProps['materialFilter']\n) {\n const materials = scenario.materials;\n if (!materials) return;\n\n scene.traverse((object) => {\n if (!(object instanceof THREE.Mesh)) {\n return;\n }\n\n for (const material of normalizeMaterials(object.material)) {\n const mutable = getMutableScenarioMaterial(material);\n if (!mutable) continue;\n if (materialFilter && !materialFilter({ object, material })) continue;\n\n if (!snapshots.has(material)) {\n snapshots.set(material, {\n color: mutable.color.clone(),\n roughness: mutable.roughness,\n metalness: mutable.metalness,\n });\n }\n\n applyScenarioMaterial(mutable, object, scenario, materials);\n }\n });\n}\n\nfunction applyScenarioMaterial(\n material: THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial,\n object: THREE.Object3D,\n scenario: VisualScenarioConfig,\n materials: ScenarioMaterialConfig\n) {\n const seed = scenario.seed ?? 0;\n const objectKey = `${scenario.id ?? 'scenario'}:${object.name}:${material.name}:${seed}`;\n const variation = hashToUnitInterval(objectKey);\n\n if (materials.randomizeObjectColors) {\n material.color.setHSL(variation, 0.38, 0.42);\n }\n\n if (materials.randomizeTableMaterial) {\n material.roughness = clamp01(\n materials.roughness ?? 0.35 + variation * 0.45\n );\n material.metalness = clamp01(\n materials.metalness ?? variation * 0.12\n );\n }\n\n material.needsUpdate = true;\n}\n\nfunction normalizeMaterials(\n material: THREE.Material | THREE.Material[]\n): THREE.Material[] {\n return Array.isArray(material) ? material : [material];\n}\n\nfunction getMutableScenarioMaterial(\n material: THREE.Material\n): THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial | null {\n if (\n material instanceof THREE.MeshStandardMaterial ||\n material instanceof THREE.MeshPhysicalMaterial\n ) {\n return material;\n }\n\n return null;\n}\n\nfunction hashToUnitInterval(value: string) {\n let hash = 2166136261;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n return (hash >>> 0) / 4294967295;\n}\n\nfunction clamp01(value: number) {\n return Math.max(0, Math.min(1, value));\n}\n\nexport type SplatCollisionProxy = ReactNode | ThreeElements['group'];\n"]}