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/README.md +225 -15
- package/dist/{chunk-33CV6HSV.js → chunk-VDSEPZYQ.js} +303 -14
- package/dist/chunk-VDSEPZYQ.js.map +1 -0
- package/dist/index.d.ts +274 -7
- package/dist/index.js +1172 -131
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +24 -2
- package/dist/spark.js +89 -3
- package/dist/spark.js.map +1 -1
- package/dist/{types-S8ggQY2n.d.ts → types-BuJ4boaq.d.ts} +160 -5
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +14 -7
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SplatCollisionProxyPreview.tsx +350 -0
- package/src/components/VisualScenario.tsx +287 -11
- package/src/core/MujocoSimProvider.tsx +374 -30
- package/src/core/SceneLoader.ts +13 -0
- package/src/hooks/useMountedCameraSequenceRecorder.ts +155 -0
- package/src/index.ts +80 -0
- package/src/rendering/cameraFrameCapture.ts +195 -26
- package/src/rendering/cameraFrameSource.ts +747 -0
- package/src/spark.tsx +144 -0
- package/src/types.ts +166 -4
- package/src/vite.ts +14 -6
- package/dist/chunk-33CV6HSV.js.map +0 -1
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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":";;;;;;AA2BA,IAAM,kBAAA,GAAqB,SAAA;AAEpB,SAAS,gBAAA,CAAiB;AAAA,EAC/B,MAAA,GAAS,QAAA;AAAA,EACT,UAAA,GAAa,IAAA;AAAA,EACb,SAAA,GAAY;AACd,CAAA,EAA0B;AACxB,EAAA,IAAI,WAAW,WAAA,EAAa;AAC1B,IAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,cAAA,EAAA,EAAa,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW,CAAA;AAAA,sBAC3C,GAAA;AAAA,QAAC,kBAAA;AAAA,QAAA;AAAA,UACC,QAAA,EAAU,CAAC,GAAA,EAAK,EAAA,EAAI,CAAC,CAAA;AAAA,UACrB,WAAW,GAAA,GAAM,SAAA;AAAA,UACjB;AAAA;AAAA,OACF;AAAA,sBACA,GAAA,CAAC,kBAAA,EAAA,EAAiB,QAAA,EAAU,CAAC,EAAA,EAAI,KAAK,GAAG,CAAA,EAAG,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW;AAAA,KAAA,EAC3E,CAAA;AAAA,EAEJ;AAEA,EAAA,IAAI,WAAW,WAAA,EAAa;AAC1B,IAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,cAAA,EAAA,EAAa,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW,CAAA;AAAA,sBAC3C,GAAA;AAAA,QAAC,kBAAA;AAAA,QAAA;AAAA,UACC,QAAA,EAAU,CAAC,CAAA,EAAG,EAAA,EAAI,CAAC,CAAA;AAAA,UACnB,WAAW,IAAA,GAAO,SAAA;AAAA,UAClB;AAAA;AAAA,OACF;AAAA,sBACA,GAAA,CAAC,YAAA,EAAA,EAAW,QAAA,EAAU,CAAC,IAAA,EAAM,MAAM,GAAG,CAAA,EAAG,SAAA,EAAW,GAAA,GAAM,SAAA,EAAW;AAAA,KAAA,EACvE,CAAA;AAAA,EAEJ;AAEA,EAAA,IAAI,WAAW,OAAA,EAAS;AACtB,IAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,cAAA,EAAA,EAAa,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW,CAAA;AAAA,sBAC3C,GAAA;AAAA,QAAC,kBAAA;AAAA,QAAA;AAAA,UACC,QAAA,EAAU,CAAC,GAAA,EAAK,IAAA,EAAM,GAAG,CAAA;AAAA,UACzB,WAAW,GAAA,GAAM,SAAA;AAAA,UACjB;AAAA;AAAA,OACF;AAAA,sBACA,GAAA,CAAC,YAAA,EAAA,EAAW,QAAA,EAAU,CAAC,GAAA,EAAK,KAAK,GAAG,CAAA,EAAG,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW;AAAA,KAAA,EACtE,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,cAAA,EAAA,EAAa,SAAA,EAAW,IAAA,GAAO,SAAA,EAAW,CAAA;AAAA,oBAC3C,GAAA;AAAA,MAAC,kBAAA;AAAA,MAAA;AAAA,QACC,QAAA,EAAU,CAAC,GAAA,EAAK,EAAA,EAAI,CAAC,CAAA;AAAA,QACrB,WAAW,GAAA,GAAM,SAAA;AAAA,QACjB;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ;AAEO,SAAS,qBAAA,CACd,MAAA,EACA,QAAA,GAAW,kBAAA,EACX;AACA,EAAA,IAAI,MAAA,KAAW,aAAa,OAAO,SAAA;AACnC,EAAA,IAAI,MAAA,KAAW,aAAa,OAAO,SAAA;AACnC,EAAA,IAAI,MAAA,KAAW,SAAS,OAAO,SAAA;AAC/B,EAAA,OAAO,QAAA;AACT;AAEO,SAAS,yBAAA,CACd,cACA,QAAA,EAC0B;AAC1B,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,YAAA;AAClB,EAAA,MAAM,MAAA,GAAS,QAAA,EAAU,MAAA,EAAQ,MAAA,IAAU,CAAA;AAE3C,EAAA,OAAO;AAAA,IACL,QAAQ,CAAA,GAAI,MAAA,GAAS,GAAA,EAAK,OAAA,CAAQ,CAAC,CAAC,CAAA;AAAA,IACpC,QAAQ,CAAA,GAAI,MAAA,GAAS,GAAA,EAAK,OAAA,CAAQ,CAAC,CAAC,CAAA;AAAA,IACpC,QAAQ,CAAA,GAAI,MAAA,GAAS,IAAA,EAAM,OAAA,CAAQ,CAAC,CAAC;AAAA,GACvC;AACF;AAEO,SAAS,sBAAsB,KAAA,EAAmC;AACvE,EAAA,wBAAA,CAAyB,KAAK,CAAA;AAC9B,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,wBAAA,CAAyB;AAAA,EACvC,QAAA;AAAA,EACA,OAAA,GAAU,IAAA;AAAA,EACV,eAAA,GAAkB,IAAA;AAAA,EAClB,QAAA,GAAW,IAAA;AAAA,EACX,aAAA,GAAgB,IAAA;AAAA,EAChB,cAAA,GAAiB,IAAA;AAAA,EACjB,UAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAA+B;AAC7B,EAAA,MAAM,EAAE,EAAA,EAAI,KAAA,EAAO,UAAA,KAAe,QAAA,EAAS;AAE3C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,QAAA,EAAU;AACzB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,mBAAmB,EAAA,CAAG,mBAAA;AAC5B,IAAA,MAAM,qBAAqB,KAAA,CAAM,UAAA;AACjC,IAAA,MAAM,cAAc,KAAA,CAAM,GAAA;AAC1B,IAAA,MAAM,iBAAA,uBAAwB,GAAA,EAO5B;AAEF,IAAA,IAAI,aAAA,EAAe;AACjB,MAAA,EAAA,CAAG,mBAAA,GAAsB,QAAA,CAAS,MAAA,EAAQ,QAAA,IAAY,CAAA;AAAA,IACxD;AAEA,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,KAAA,CAAM,aAAa,IAAU,KAAA,CAAA,KAAA;AAAA,QAC3B,UAAA,IAAc,qBAAA,CAAsB,QAAA,CAAS,QAAQ;AAAA,OACvD;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,KAAA,CAAM,GAAA,GAAM,iBAAA,CAAkB,QAAA,EAAU,UAAA,EAAY,SAAS,MAAM,CAAA;AAAA,IACrE;AAEA,IAAA,IAAI,cAAA,IAAkB,SAAS,SAAA,EAAW;AACxC,MAAA,sBAAA,CAAuB,KAAA,EAAO,QAAA,EAAU,iBAAA,EAAmB,cAAc,CAAA;AAAA,IAC3E;AAEA,IAAA,UAAA,EAAW;AAEX,IAAA,OAAO,MAAM;AACX,MAAA,EAAA,CAAG,mBAAA,GAAsB,gBAAA;AACzB,MAAA,KAAA,CAAM,UAAA,GAAa,kBAAA;AACnB,MAAA,KAAA,CAAM,GAAA,GAAM,WAAA;AAEZ,MAAA,KAAA,MAAW,CAAC,QAAA,EAAU,QAAQ,CAAA,IAAK,iBAAA,EAAmB;AACpD,QAAA,MAAM,OAAA,GAAU,2BAA2B,QAAQ,CAAA;AACnD,QAAA,IAAI,CAAC,OAAA,EAAS;AACd,QAAA,IAAI,SAAS,KAAA,EAAO,OAAA,CAAQ,KAAA,CAAM,IAAA,CAAK,SAAS,KAAK,CAAA;AACrD,QAAA,IAAI,OAAO,QAAA,CAAS,SAAA,KAAc,QAAA,EAAU;AAC1C,UAAA,OAAA,CAAQ,YAAY,QAAA,CAAS,SAAA;AAAA,QAC/B;AACA,QAAA,IAAI,OAAO,QAAA,CAAS,SAAA,KAAc,QAAA,EAAU;AAC1C,UAAA,OAAA,CAAQ,YAAY,QAAA,CAAS,SAAA;AAAA,QAC/B;AACA,QAAA,OAAA,CAAQ,WAAA,GAAc,IAAA;AAAA,MACxB;AAEA,MAAA,UAAA,EAAW;AAAA,IACb,CAAA;AAAA,EACF,CAAA,EAAG;AAAA,IACD,eAAA;AAAA,IACA,QAAA;AAAA,IACA,cAAA;AAAA,IACA,aAAA;AAAA,IACA,UAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,EAAA;AAAA,IACA,UAAA;AAAA,IACA,cAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AASO,SAAS,gBAAA,CAAiB;AAAA,EAC/B,WAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAA;AAAA,EACA,MAAA;AAAA,EACA,cAAA;AAAA,EACA,sBAAA;AAAA,EACA,QAAA;AAAA,EACA,eAAA,GAAkB,IAAA;AAAA,EAClB,GAAG;AACL,CAAA,EAA0B;AACxB,EAAA,MAAM,WAAW,mBAAA,CAAoB;AAAA,IACnC,WAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,GAAA;AAAA,IACA,MAAA;AAAA,IACA,cAAA,EAAgB;AAAA,GACjB,CAAA;AACD,EAAA,MAAM,gBAAA,GACJ,OAAO,UAAA,CAAW,QAAA,KAAa,QAAA,IAAY,WAAW,QAAA,KAAa,IAAA,GAC/D,UAAA,CAAW,QAAA,GACX,EAAC;AAEP,EAAA,uBACE,IAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACE,GAAG,UAAA;AAAA,MACJ,QAAA,EAAU;AAAA,QACR,GAAG,gBAAA;AAAA,QACH,GAAG,QAAA,CAAS;AAAA,OACd;AAAA,MAEC,QAAA,EAAA;AAAA,QAAA,QAAA;AAAA,QACA,QAAA,IAAY,CAAC,eAAA,GAAkB,IAAA,uBAAQ,gBAAA,EAAA,EAAiB,CAAA;AAAA,QACxD;AAAA;AAAA;AAAA,GACH;AAEJ;AAEO,SAAS,mBAAA,CAAoB;AAAA,EAClC,WAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAA4D;AAC1D,EAAA,MAAM,mBAAA,GAAsB,OAAA;AAAA,IAC1B,MACE,gBACC,QAAA,GACG,4BAAA,CAA6B,UAAU,EAAE,QAAA,EAAU,CAAA,GACnD,MAAA,CAAA;AAAA,IACN,CAAC,WAAA,EAAa,QAAA,EAAU,QAAQ;AAAA,GAClC;AACA,EAAA,MAAM,cAAc,GAAA,IAAO,mBAAA,EAAqB,KAAA,CAAM,GAAA,IAAO,UAAU,KAAA,EAAO,GAAA;AAC9E,EAAA,MAAM,iBACJ,MAAA,IACA,mBAAA,EAAqB,MAAM,MAAA,IAC3B,QAAA,EAAU,OAAO,MAAA,IACjB,KAAA;AACF,EAAA,MAAM,yBACJ,cAAA,IACA,mBAAA,EAAqB,cAAA,IACrB,QAAA,EAAU,OAAO,cAAA,IACjB,MAAA;AAEF,EAAA,OAAO,OAAA;AAAA,IACL,OAAO;AAAA,MACL,GAAA,EAAK,WAAA;AAAA,MACL,MAAA,EAAQ,cAAA;AAAA,MACR,cAAA,EAAgB,sBAAA;AAAA,MAChB,UAAU,8BAAA,CAA+B;AAAA,QACvC,WAAA,EAAa,mBAAA;AAAA,QACb,GAAA,EAAK,WAAA;AAAA,QACL,MAAA,EAAQ,cAAA;AAAA,QACR,cAAA,EAAgB;AAAA,OACjB;AAAA,KACH,CAAA;AAAA,IACA,CAAC,mBAAA,EAAqB,WAAA,EAAa,cAAA,EAAgB,sBAAsB;AAAA,GAC3E;AACF;AAOO,SAAS,4BAAA,CACd,QAAA,EACA,OAAA,GAKI,EAAC,EACqC;AAC1C,EAAA,MAAM,QAAQ,QAAA,CAAS,KAAA;AACvB,EAAA,MAAM,iBAAiB,KAAA,EAAO,cAAA;AAE9B,EAAA,IAAI,CAAC,OAAO,OAAA,IAAW,CAAC,MAAM,GAAA,IAAO,CAAC,gBAAgB,OAAA,EAAS;AAC7D,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,OAAA,CAAQ,EAAA,IAAM,QAAA,CAAS,EAAA,IAAM,mBAAA;AAAA,IACjC,KAAA,EAAO,OAAA,CAAQ,KAAA,IAAS,QAAA,CAAS,KAAA,IAAS,4BAAA;AAAA,IAC1C,WAAA,EACE,QAAQ,WAAA,KACP,QAAA,CAAS,cACN,CAAA,OAAA,EAAU,QAAA,CAAS,WAAW,CAAA,wCAAA,CAAA,GAC9B,MAAA,CAAA;AAAA,IACN,KAAA,EAAO;AAAA,MACL,KAAK,KAAA,CAAM,GAAA;AAAA,MACX,MAAA,EAAQ,MAAM,MAAA,IAAU,KAAA;AAAA,MACxB,UAAU,OAAA,CAAQ;AAAA,KACpB;AAAA,IACA,cAAA,EAAgB;AAAA,MACd,GAAG,cAAA;AAAA,MACH,SAAS,cAAA,CAAe;AAAA;AAC1B,GACF;AACF;AAEA,SAAS,yBAAyB,KAAA,EAA+D;AAC/F,EAAA,OAAO,CAAC,CAAC,KAAA,IAAS,gBAAA,IAAoB,SAAS,OAAA,IAAW,KAAA;AAC5D;AAEA,SAAS,iBAAA,CAAkB,aAA0B,IAAA,EAAsB;AACzE,EAAA,MAAM,MAAM,WAAA,CAAY,GAAA;AACxB,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,MAAM,OAAO,GAAA,CAAI,QAAA,CAAS,GAAG,CAAA,GAAI,MAAM,GAAA,GAAM,GAAA;AAC7C,EAAA,IAAI,IAAA,CAAK,WAAW,IAAI,CAAA,SAAU,IAAA,CAAK,KAAA,CAAM,KAAK,MAAM,CAAA;AACxD,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,YAAY,KAAA,EAAoC;AACvD,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA,EAAG;AACpB,IAAA,IAAA,CAAK,IAAI,IAAI,CAAA;AACb,IAAA,MAAA,CAAO,KAAK,IAAI,CAAA;AAAA,EAClB;AACA,EAAA,OAAO,MAAA;AACT;AASO,SAAS,oBAAA,CACd,WAAA,EACA,KAAA,EACA,OAAA,GAA4C,EAAC,EAChC;AACb,EAAA,MAAM,WAAA,GAAc,yBAAyB,KAAK,CAAA,GAC9C,QACA,KAAA,GACE,4BAAA,CAA6B,KAAA,EAAO,OAAO,CAAA,GAC3C,MAAA;AACN,EAAA,MAAM,OAAA,GAAU,aAAa,cAAA,CAAe,OAAA;AAC5C,EAAA,IAAI,CAAC,SAAS,OAAO,WAAA;AAErB,EAAA,OAAO;AAAA,IACL,GAAG,WAAA;AAAA,IACH,kBAAkB,WAAA,CAAY;AAAA,MAC5B,GAAI,WAAA,CAAY,gBAAA,IAAoB,EAAC;AAAA,MACrC,iBAAA,CAAkB,aAAa,OAAO;AAAA,KACvC;AAAA,GACH;AACF;AAEO,SAAS,8BAAA,CAA+B;AAAA,EAC7C,WAAA;AAAA,EACA,GAAA;AAAA,EACA,MAAA,GAAS,KAAA;AAAA,EACT;AACF,CAAA,EAKG;AACD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,mBAAA;AAAA,IACN,eAAe,WAAA,EAAa,EAAA;AAAA,IAC5B,kBAAkB,WAAA,EAAa,KAAA;AAAA,IAC/B,QAAA,EAAU,GAAA;AAAA,IACV,WAAA,EAAa,MAAA;AAAA,IACb,aAAA,EAAe,aAAa,KAAA,CAAM,QAAA;AAAA,IAClC,oBAAA,EAAsB,gBAAgB,MAAA,IAAU,SAAA;AAAA,IAChD,uBAAuB,cAAA,EAAgB,OAAA;AAAA,IACvC,wBAAA,EAA0B,cAAA,EAAgB,UAAA,IAAc;AAAC,GAC3D;AACF;AAEO,SAAS,yBAAA,CAA0B;AAAA,EACxC,SAAA;AAAA,EACA;AACF,CAAA,EAGG;AACD,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,SAAA,EAAW,2BAA2B,CAAA;AAC1D,EAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,OAAA,EAAS,QAAQ,CAAA;AACtC,EAAA,OAAO,SAAA,CAAU,UAAA,CAAW,MAAM,CAAA,GAAI,GAAA,CAAI,QAAA,EAAS,GAAI,CAAA,EAAG,GAAA,CAAI,QAAQ,CAAA,EAAG,GAAA,CAAI,MAAM,CAAA,CAAA;AACrF;AAEA,SAAS,gBAAA,GAAmB;AAC1B,EAAA,uBACE,GAAA,CAAC,WACC,QAAA,kBAAA,IAAA,CAAC,MAAA,EAAA,EAAK,UAAU,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EACxB,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,iBAAY,IAAA,EAAM,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG,CAAA,EAAG,CAAA;AAAA,oBACpC,GAAA;AAAA,MAAC,mBAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAW,IAAA;AAAA,QACX,OAAA,EAAS,IAAA;AAAA,QACT,SAAA,EAAS,IAAA;AAAA,QACT,IAAA,EAAY,KAAA,CAAA;AAAA;AAAA;AACd,GAAA,EACF,CAAA,EACF,CAAA;AAEJ;AAEA,SAAS,iBAAA,CACP,QAAA,EACA,UAAA,EACA,OAAA,EACA,MAAA,EACA;AACA,EAAA,IAAI,QAAA,CAAS,aAAa,WAAA,EAAa;AACrC,IAAA,OAAO,IAAU,KAAA,CAAA,GAAA;AAAA,MACf,UAAA,IAAc,qBAAA,CAAsB,QAAA,CAAS,QAAQ,CAAA;AAAA,MACrD,OAAA,IAAW,GAAA;AAAA,MACX,MAAA,IAAU;AAAA,KACZ;AAAA,EACF;AAEA,EAAA,IAAI,QAAA,CAAS,aAAa,WAAA,EAAa;AACrC,IAAA,OAAO,IAAU,KAAA,CAAA,GAAA;AAAA,MACf,UAAA,IAAc,qBAAA,CAAsB,QAAA,CAAS,QAAQ,CAAA;AAAA,MACrD,OAAA,IAAW,CAAA;AAAA,MACX,MAAA,IAAU;AAAA,KACZ;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,sBAAA,CACP,KAAA,EACA,QAAA,EACA,SAAA,EAQA,cAAA,EACA;AACA,EAAA,MAAM,YAAY,QAAA,CAAS,SAAA;AAC3B,EAAA,IAAI,CAAC,SAAA,EAAW;AAEhB,EAAA,KAAA,CAAM,QAAA,CAAS,CAAC,MAAA,KAAW;AACzB,IAAA,IAAI,EAAE,kBAAwB,KAAA,CAAA,IAAA,CAAA,EAAO;AACnC,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,QAAA,IAAY,kBAAA,CAAmB,MAAA,CAAO,QAAQ,CAAA,EAAG;AAC1D,MAAA,MAAM,OAAA,GAAU,2BAA2B,QAAQ,CAAA;AACnD,MAAA,IAAI,CAAC,OAAA,EAAS;AACd,MAAA,IAAI,kBAAkB,CAAC,cAAA,CAAe,EAAE,MAAA,EAAQ,QAAA,EAAU,CAAA,EAAG;AAE7D,MAAA,IAAI,CAAC,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG;AAC5B,QAAA,SAAA,CAAU,IAAI,QAAA,EAAU;AAAA,UACtB,KAAA,EAAO,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAM;AAAA,UAC3B,WAAW,OAAA,CAAQ,SAAA;AAAA,UACnB,WAAW,OAAA,CAAQ;AAAA,SACpB,CAAA;AAAA,MACH;AAEA,MAAA,qBAAA,CAAsB,OAAA,EAAS,MAAA,EAAQ,QAAA,EAAU,SAAS,CAAA;AAAA,IAC5D;AAAA,EACF,CAAC,CAAA;AACH;AAEA,SAAS,qBAAA,CACP,QAAA,EACA,MAAA,EACA,QAAA,EACA,SAAA,EACA;AACA,EAAA,MAAM,IAAA,GAAO,SAAS,IAAA,IAAQ,CAAA;AAC9B,EAAA,MAAM,SAAA,GAAY,CAAA,EAAG,QAAA,CAAS,EAAA,IAAM,UAAU,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAA,CAAA,EAAI,QAAA,CAAS,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AACtF,EAAA,MAAM,SAAA,GAAY,mBAAmB,SAAS,CAAA;AAE9C,EAAA,IAAI,UAAU,qBAAA,EAAuB;AACnC,IAAA,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,SAAA,EAAW,IAAA,EAAM,IAAI,CAAA;AAAA,EAC7C;AAEA,EAAA,IAAI,UAAU,sBAAA,EAAwB;AACpC,IAAA,QAAA,CAAS,SAAA,GAAY,OAAA;AAAA,MACnB,SAAA,CAAU,SAAA,IAAa,IAAA,GAAO,SAAA,GAAY;AAAA,KAC5C;AACA,IAAA,QAAA,CAAS,SAAA,GAAY,OAAA;AAAA,MACnB,SAAA,CAAU,aAAa,SAAA,GAAY;AAAA,KACrC;AAAA,EACF;AAEA,EAAA,QAAA,CAAS,WAAA,GAAc,IAAA;AACzB;AAEA,SAAS,mBACP,QAAA,EACkB;AAClB,EAAA,OAAO,MAAM,OAAA,CAAQ,QAAQ,CAAA,GAAI,QAAA,GAAW,CAAC,QAAQ,CAAA;AACvD;AAEA,SAAS,2BACP,QAAA,EACgE;AAChE,EAAA,IACE,QAAA,YAA0B,KAAA,CAAA,oBAAA,IAC1B,QAAA,YAA0B,KAAA,CAAA,oBAAA,EAC1B;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,mBAAmB,KAAA,EAAe;AACzC,EAAA,IAAI,IAAA,GAAO,UAAA;AACX,EAAA,KAAA,IAAS,QAAQ,CAAA,EAAG,KAAA,GAAQ,KAAA,CAAM,MAAA,EAAQ,SAAS,CAAA,EAAG;AACpD,IAAA,IAAA,IAAQ,KAAA,CAAM,WAAW,KAAK,CAAA;AAC9B,IAAA,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,QAAQ,CAAA;AAAA,EACjC;AACA,EAAA,OAAA,CAAQ,SAAS,CAAA,IAAK,UAAA;AACxB;AAEA,SAAS,QAAQ,KAAA,EAAe;AAC9B,EAAA,OAAO,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,CAAC,CAAA;AACvC","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"]}
|