mujoco-react 9.3.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.
@@ -56,6 +56,27 @@ export interface ResolvedMountedCameraFrameSource {
56
56
  source: MountedCameraFrameCaptureSource;
57
57
  }
58
58
 
59
+ export const MountedCameraFrameSourceSuggestionMatch = {
60
+ Direct: 'direct',
61
+ Alias: 'alias',
62
+ Normalized: 'normalized',
63
+ Prefix: 'prefix',
64
+ Suffix: 'suffix',
65
+ Contains: 'contains',
66
+ } as const;
67
+
68
+ export type MountedCameraFrameSourceSuggestionMatch =
69
+ (typeof MountedCameraFrameSourceSuggestionMatch)[keyof typeof MountedCameraFrameSourceSuggestionMatch];
70
+
71
+ export interface MountedCameraFrameSourceSuggestion {
72
+ key: string;
73
+ selector: CameraFrameMountSelector;
74
+ source: MountedCameraFrameCaptureSource;
75
+ resourceName: string;
76
+ resourceKind: MountedCameraFrameCaptureSource['kind'];
77
+ match: MountedCameraFrameSourceSuggestionMatch;
78
+ }
79
+
59
80
  export type MountedCameraFrameSequenceDefaults = Omit<
60
81
  CameraFrameSequenceCamera,
61
82
  'key' | 'cameraName' | 'siteName' | 'bodyName' | 'source'
@@ -123,6 +144,57 @@ export interface MountedCameraFrameSequenceRecordResult
123
144
  readiness: MountedCameraFrameSequenceReadiness;
124
145
  }
125
146
 
147
+ export const MountedCameraFrameSequenceManifestStatus = {
148
+ Complete: 'complete',
149
+ Partial: 'partial',
150
+ Missing: 'missing',
151
+ } as const;
152
+
153
+ export type MountedCameraFrameSequenceManifestStatus =
154
+ (typeof MountedCameraFrameSequenceManifestStatus)[keyof typeof MountedCameraFrameSequenceManifestStatus];
155
+
156
+ export interface MountedCameraFrameSequenceStreamSummary {
157
+ key: string;
158
+ ready: boolean;
159
+ complete: boolean;
160
+ status: MountedCameraFrameSequenceManifestStatus;
161
+ source?: CameraFrameCaptureSource;
162
+ selector?: CameraFrameMountSelector;
163
+ target?: string;
164
+ width?: number;
165
+ height?: number;
166
+ expectedFrameCount: number;
167
+ recordedFrameCount: number;
168
+ missingFrameCount: number;
169
+ firstFrameIndex: number | null;
170
+ lastFrameIndex: number | null;
171
+ firstTimestamp: number | null;
172
+ lastTimestamp: number | null;
173
+ message: string;
174
+ }
175
+
176
+ export interface CreateMountedCameraFrameSequenceManifestOptions {
177
+ expectedFrameCount?: number;
178
+ cameraKeys?: readonly string[];
179
+ }
180
+
181
+ export interface MountedCameraFrameSequenceManifest {
182
+ schema: 'mujoco-react/mounted-camera-frame-sequence-manifest@1';
183
+ ready: boolean;
184
+ complete: boolean;
185
+ status: MountedCameraFrameSequenceManifestStatus;
186
+ cameraKeys: string[];
187
+ resolvedKeys: string[];
188
+ missingKeys: string[];
189
+ expectedFrameCount: number;
190
+ recordedFrameCount: number;
191
+ missingFrameCount: number;
192
+ streamSummaries: Record<string, MountedCameraFrameSequenceStreamSummary>;
193
+ streams: MountedCameraFrameSequenceStreamSummary[];
194
+ readiness: MountedCameraFrameSequenceReadiness;
195
+ message: string;
196
+ }
197
+
126
198
  export type MountedCameraFrameSequenceRecorderTarget = Pick<
127
199
  MujocoSimAPI,
128
200
  'getCameras' | 'getSites' | 'getBodies' | 'recordCameraSequence'
@@ -147,6 +219,16 @@ function createNameSet(
147
219
  );
148
220
  }
149
221
 
222
+ function createResourceNames(
223
+ resources:
224
+ | readonly (string | NamedCameraFrameResource | null | undefined)[]
225
+ | undefined
226
+ ) {
227
+ return (resources ?? [])
228
+ .map((resource) => getResourceName(resource))
229
+ .filter((name): name is string => Boolean(name));
230
+ }
231
+
150
232
  function normalizeAliasCandidates(
151
233
  value: CameraFrameMountSelector | readonly CameraFrameMountSelector[] | undefined
152
234
  ) {
@@ -160,6 +242,35 @@ function countMountedSelectors(selector: CameraFrameMountSelector) {
160
242
  Number(Boolean(selector.bodyName));
161
243
  }
162
244
 
245
+ function normalizeCameraSourceName(value: string) {
246
+ return value
247
+ .trim()
248
+ .toLowerCase()
249
+ .replace(/[^a-z0-9]+/g, '_')
250
+ .replace(/^_+|_+$/g, '');
251
+ }
252
+
253
+ function createCameraSourceKeyVariants(key: string) {
254
+ const candidates = [
255
+ key,
256
+ key.startsWith('observation.images.')
257
+ ? key.slice('observation.images.'.length)
258
+ : '',
259
+ key.includes('.') ? key.split('.').at(-1) ?? '' : '',
260
+ key.includes('/') ? key.split('/').at(-1) ?? '' : '',
261
+ ];
262
+ return candidates
263
+ .map((candidate) => candidate.trim())
264
+ .filter((candidate, index, items) => candidate && items.indexOf(candidate) === index);
265
+ }
266
+
267
+ function getSelectorKey(selector: CameraFrameMountSelector) {
268
+ if (selector.cameraName) return `camera:${selector.cameraName}`;
269
+ if (selector.siteName) return `site:${selector.siteName}`;
270
+ if (selector.bodyName) return `body:${selector.bodyName}`;
271
+ return null;
272
+ }
273
+
163
274
  export function getMountedCameraFrameCaptureSource(
164
275
  selector: CameraFrameMountSelector
165
276
  ): MountedCameraFrameCaptureSource | null {
@@ -197,6 +308,66 @@ export function getCameraFrameCaptureSourceTarget(
197
308
  return 'fallback camera';
198
309
  }
199
310
 
311
+ function createMountedCameraFrameSourceSuggestion(
312
+ key: string,
313
+ selector: CameraFrameMountSelector,
314
+ resourceName: string,
315
+ match: MountedCameraFrameSourceSuggestionMatch
316
+ ): MountedCameraFrameSourceSuggestion | null {
317
+ const source = getMountedCameraFrameCaptureSource(selector);
318
+ if (!source) return null;
319
+ return {
320
+ key,
321
+ selector,
322
+ source,
323
+ resourceName,
324
+ resourceKind: source.kind,
325
+ match,
326
+ };
327
+ }
328
+
329
+ function addMountedCameraFrameSourceSuggestion(
330
+ suggestions: MountedCameraFrameSourceSuggestion[],
331
+ seen: Set<string>,
332
+ suggestion: MountedCameraFrameSourceSuggestion | null
333
+ ) {
334
+ if (!suggestion) return;
335
+ const selectorKey = getSelectorKey(suggestion.selector);
336
+ if (!selectorKey || seen.has(selectorKey)) return;
337
+ seen.add(selectorKey);
338
+ suggestions.push(suggestion);
339
+ }
340
+
341
+ function getCameraFrameResourceMatch(
342
+ key: string,
343
+ resourceName: string
344
+ ): MountedCameraFrameSourceSuggestionMatch | null {
345
+ if (resourceName === key) return MountedCameraFrameSourceSuggestionMatch.Direct;
346
+
347
+ const normalizedResource = normalizeCameraSourceName(resourceName);
348
+ if (!normalizedResource) return null;
349
+
350
+ for (const variant of createCameraSourceKeyVariants(key)) {
351
+ if (resourceName === variant) return MountedCameraFrameSourceSuggestionMatch.Direct;
352
+
353
+ const normalizedKey = normalizeCameraSourceName(variant);
354
+ if (!normalizedKey) continue;
355
+ if (normalizedResource === normalizedKey) {
356
+ return MountedCameraFrameSourceSuggestionMatch.Normalized;
357
+ }
358
+ if (normalizedResource.startsWith(`${normalizedKey}_`)) {
359
+ return MountedCameraFrameSourceSuggestionMatch.Prefix;
360
+ }
361
+ if (normalizedResource.endsWith(`_${normalizedKey}`)) {
362
+ return MountedCameraFrameSourceSuggestionMatch.Suffix;
363
+ }
364
+ if (normalizedResource.includes(`_${normalizedKey}_`)) {
365
+ return MountedCameraFrameSourceSuggestionMatch.Contains;
366
+ }
367
+ }
368
+ return null;
369
+ }
370
+
200
371
  function isSelectorMounted(
201
372
  selector: CameraFrameMountSelector,
202
373
  cameraNames: Set<string>,
@@ -211,6 +382,82 @@ function isSelectorMounted(
211
382
  );
212
383
  }
213
384
 
385
+ export function createMountedCameraFrameSourceSuggestions(
386
+ key: string,
387
+ options: ResolveMountedCameraFrameSourceOptions
388
+ ): MountedCameraFrameSourceSuggestion[] {
389
+ const cameraNames = createNameSet(options.cameras);
390
+ const siteNames = createNameSet(options.sites);
391
+ const bodyNames = createNameSet(options.bodies);
392
+ const suggestions: MountedCameraFrameSourceSuggestion[] = [];
393
+ const seen = new Set<string>();
394
+
395
+ for (const selector of normalizeAliasCandidates(options.aliases?.[key])) {
396
+ if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
397
+ continue;
398
+ }
399
+ const source = getMountedCameraFrameCaptureSource(selector);
400
+ if (!source) continue;
401
+ addMountedCameraFrameSourceSuggestion(
402
+ suggestions,
403
+ seen,
404
+ createMountedCameraFrameSourceSuggestion(
405
+ key,
406
+ selector,
407
+ getCameraFrameCaptureSourceTarget(source),
408
+ MountedCameraFrameSourceSuggestionMatch.Alias
409
+ )
410
+ );
411
+ }
412
+
413
+ for (const cameraName of createResourceNames(options.cameras)) {
414
+ const match = getCameraFrameResourceMatch(key, cameraName);
415
+ if (!match) continue;
416
+ addMountedCameraFrameSourceSuggestion(
417
+ suggestions,
418
+ seen,
419
+ createMountedCameraFrameSourceSuggestion(
420
+ key,
421
+ { cameraName },
422
+ cameraName,
423
+ match
424
+ )
425
+ );
426
+ }
427
+
428
+ for (const siteName of createResourceNames(options.sites)) {
429
+ const match = getCameraFrameResourceMatch(key, siteName);
430
+ if (!match) continue;
431
+ addMountedCameraFrameSourceSuggestion(
432
+ suggestions,
433
+ seen,
434
+ createMountedCameraFrameSourceSuggestion(
435
+ key,
436
+ { siteName },
437
+ siteName,
438
+ match
439
+ )
440
+ );
441
+ }
442
+
443
+ for (const bodyName of createResourceNames(options.bodies)) {
444
+ const match = getCameraFrameResourceMatch(key, bodyName);
445
+ if (!match) continue;
446
+ addMountedCameraFrameSourceSuggestion(
447
+ suggestions,
448
+ seen,
449
+ createMountedCameraFrameSourceSuggestion(
450
+ key,
451
+ { bodyName },
452
+ bodyName,
453
+ match
454
+ )
455
+ );
456
+ }
457
+
458
+ return suggestions;
459
+ }
460
+
214
461
  export function resolveMountedCameraFrameSource(
215
462
  key: string,
216
463
  options: ResolveMountedCameraFrameSourceOptions
@@ -235,6 +482,15 @@ export function resolveMountedCameraFrameSource(
235
482
  return { key, selector, source };
236
483
  }
237
484
 
485
+ const [suggestion] = createMountedCameraFrameSourceSuggestions(key, options);
486
+ if (suggestion) {
487
+ return {
488
+ key,
489
+ selector: suggestion.selector,
490
+ source: suggestion.source,
491
+ };
492
+ }
493
+
238
494
  if (options.allowAliasFallback) {
239
495
  for (const selector of aliasCandidates) {
240
496
  const source = getMountedCameraFrameCaptureSource(selector);
@@ -335,6 +591,122 @@ export function createMountedCameraFrameSequenceReadiness(
335
591
  };
336
592
  }
337
593
 
594
+ function normalizeFrameCount(frameCount: number | undefined) {
595
+ return Number.isFinite(frameCount) && frameCount !== undefined
596
+ ? Math.max(0, Math.floor(frameCount))
597
+ : 0;
598
+ }
599
+
600
+ export function createMountedCameraFrameSequenceManifest(
601
+ result: MountedCameraFrameSequenceRecordResult,
602
+ options: CreateMountedCameraFrameSequenceManifestOptions = {}
603
+ ): MountedCameraFrameSequenceManifest {
604
+ const cameraKeys = [
605
+ ...(options.cameraKeys ??
606
+ result.readiness.cameraKeys ??
607
+ result.plan.cameraKeys ??
608
+ result.cameraKeys),
609
+ ];
610
+ const expectedFrameCount = normalizeFrameCount(
611
+ options.expectedFrameCount ?? result.frameCount
612
+ );
613
+ const recordedFrameCount = normalizeFrameCount(result.frameCount);
614
+ const streamSummaries: Record<string, MountedCameraFrameSequenceStreamSummary> =
615
+ {};
616
+ const streams: MountedCameraFrameSequenceStreamSummary[] = [];
617
+ let missingFrameCount = 0;
618
+ let completeStreamCount = 0;
619
+ let resolvedOrRecordedStreamCount = 0;
620
+
621
+ for (const key of cameraKeys) {
622
+ const summary = result.cameraSummaries[key];
623
+ const readiness = result.readiness.cameras[key];
624
+ const source = summary?.source ?? readiness?.source;
625
+ const ready = readiness?.ready ?? Boolean(summary);
626
+ const recorded = normalizeFrameCount(summary?.frameCount);
627
+ const missing = Math.max(expectedFrameCount - recorded, 0);
628
+ const complete = ready && missing === 0;
629
+ const status = complete
630
+ ? MountedCameraFrameSequenceManifestStatus.Complete
631
+ : ready || recorded > 0
632
+ ? MountedCameraFrameSequenceManifestStatus.Partial
633
+ : MountedCameraFrameSequenceManifestStatus.Missing;
634
+ const target = source
635
+ ? getCameraFrameCaptureSourceTarget(source)
636
+ : readiness?.message
637
+ ? undefined
638
+ : 'missing MuJoCo camera';
639
+ const message = complete
640
+ ? `Camera stream "${key}" recorded ${recorded} of ${expectedFrameCount} frame${
641
+ expectedFrameCount === 1 ? '' : 's'
642
+ }.`
643
+ : ready || recorded > 0
644
+ ? `Camera stream "${key}" recorded ${recorded} of ${expectedFrameCount} frame${
645
+ expectedFrameCount === 1 ? '' : 's'
646
+ }.`
647
+ : readiness?.message ??
648
+ `Camera stream "${key}" did not record any frames.`;
649
+ const stream = {
650
+ key,
651
+ ready,
652
+ complete,
653
+ status,
654
+ source,
655
+ selector: readiness?.selector,
656
+ target,
657
+ width: summary?.width,
658
+ height: summary?.height,
659
+ expectedFrameCount,
660
+ recordedFrameCount: recorded,
661
+ missingFrameCount: missing,
662
+ firstFrameIndex: summary?.firstFrameIndex ?? null,
663
+ lastFrameIndex: summary?.lastFrameIndex ?? null,
664
+ firstTimestamp: summary?.firstTimestamp ?? null,
665
+ lastTimestamp: summary?.lastTimestamp ?? null,
666
+ message,
667
+ };
668
+
669
+ streamSummaries[key] = stream;
670
+ streams.push(stream);
671
+ missingFrameCount += missing;
672
+ if (complete) completeStreamCount += 1;
673
+ if (ready || recorded > 0) resolvedOrRecordedStreamCount += 1;
674
+ }
675
+
676
+ const complete =
677
+ result.readiness.ready &&
678
+ streams.length === completeStreamCount &&
679
+ missingFrameCount === 0;
680
+ const status = complete
681
+ ? MountedCameraFrameSequenceManifestStatus.Complete
682
+ : resolvedOrRecordedStreamCount > 0
683
+ ? MountedCameraFrameSequenceManifestStatus.Partial
684
+ : MountedCameraFrameSequenceManifestStatus.Missing;
685
+
686
+ return {
687
+ schema: 'mujoco-react/mounted-camera-frame-sequence-manifest@1',
688
+ ready: result.readiness.ready,
689
+ complete,
690
+ status,
691
+ cameraKeys,
692
+ resolvedKeys: [...result.readiness.resolvedKeys],
693
+ missingKeys: [...result.readiness.missingKeys],
694
+ expectedFrameCount,
695
+ recordedFrameCount,
696
+ missingFrameCount,
697
+ streamSummaries,
698
+ streams,
699
+ readiness: result.readiness,
700
+ message: complete
701
+ ? `All ${cameraKeys.length} camera stream${
702
+ cameraKeys.length === 1 ? '' : 's'
703
+ } recorded ${expectedFrameCount} frame${
704
+ expectedFrameCount === 1 ? '' : 's'
705
+ }.`
706
+ : `Mounted camera sequence coverage is ${status}.`,
707
+ };
708
+ }
709
+
338
710
  export function createMountedCameraFrameSequencePlanFromApi(
339
711
  api: MountedCameraFrameSequenceRecorderTarget,
340
712
  cameraKeys: readonly string[],
package/src/types.ts CHANGED
@@ -928,8 +928,8 @@ export interface PairedSplatEnvironmentConfig {
928
928
  description?: string;
929
929
  /** Visual-only Gaussian splat asset. */
930
930
  splat: SplatAssetConfig;
931
- /** MJCF/XML contact geometry paired with the visual splat. */
932
- collisionProxy: SplatCollisionProxyConfig & { xmlPath: string };
931
+ /** Optional MJCF/XML contact geometry paired with the visual splat. */
932
+ collisionProxy?: SplatCollisionProxyConfig & { xmlPath: string };
933
933
  }
934
934
 
935
935
  export const SplatEnvironmentReadinessStatus = {
@@ -970,6 +970,48 @@ export interface SplatEnvironmentMetadata {
970
970
  userData: Record<string, unknown>;
971
971
  }
972
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
+
973
1015
  export type SplatSceneInput =
974
1016
  | PairedSplatEnvironmentConfig
975
1017
  | VisualScenarioConfig
package/src/vite.ts CHANGED
@@ -158,7 +158,7 @@ async function scanModel(
158
158
  seen: Set<string>,
159
159
  names: Record<RegisterKey, Set<string>>
160
160
  ) {
161
- const normalized = path.normalize(filePath);
161
+ const normalized = path.resolve(filePath);
162
162
  if (seen.has(normalized)) return;
163
163
  seen.add(normalized);
164
164
 
@@ -174,7 +174,7 @@ async function scanModel(
174
174
 
175
175
  for (const includePath of collectIncludePaths(xml)) {
176
176
  const next = path.resolve(path.dirname(normalized), includePath);
177
- if (next.startsWith(root)) await scanModel(next, root, seen, names);
177
+ if (isPathInsideRoot(next, root)) await scanModel(next, root, seen, names);
178
178
  }
179
179
  }
180
180
 
@@ -346,7 +346,7 @@ function shouldInjectRegisterImport(id: string, root: string, generatedRegister:
346
346
  if (file.includes(`${path.sep}node_modules${path.sep}`)) return false;
347
347
  const absolute = path.resolve(file);
348
348
  if (absolute === generatedRegister) return false;
349
- return absolute.startsWith(root);
349
+ return isPathInsideRoot(absolute, root);
350
350
  }
351
351
 
352
352
  function renderGeneratedImport(id: string, generatedRegister: string): string {
@@ -395,5 +395,10 @@ function shouldRegenerate(file: string, watchedFiles: string[], models: readonly
395
395
  if (watchedFiles.includes(absolute)) return true;
396
396
  if (!MODEL_EXTENSIONS.has(path.extname(absolute).toLowerCase())) return false;
397
397
  const modelDirs = models.map((model) => path.dirname(path.resolve(root, model.file)));
398
- 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));
399
404
  }