From 26c9250fa644ab3c80db3f0a5cd4443918bf3d60 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 20:43:38 +0200 Subject: [PATCH] Add runtime scene editor simulation logic --- .../runtime-scene-editor-simulation.ts | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/runtime-three/runtime-scene-editor-simulation.ts diff --git a/src/runtime-three/runtime-scene-editor-simulation.ts b/src/runtime-three/runtime-scene-editor-simulation.ts new file mode 100644 index 00000000..afaceab4 --- /dev/null +++ b/src/runtime-three/runtime-scene-editor-simulation.ts @@ -0,0 +1,159 @@ +import type { + RuntimeResolvedControlChannelValue, + RuntimeResolvedDiscreteControlState +} from "../controls/control-surface"; + +import type { RuntimeSceneDefinition } from "./runtime-scene-build"; + +function mutateRuntimeLight( + runtimeScene: RuntimeSceneDefinition, + entityId: string, + mutate: ( + light: + | RuntimeSceneDefinition["localLights"]["pointLights"][number] + | RuntimeSceneDefinition["localLights"]["spotLights"][number] + ) => void +) { + const pointLight = runtimeScene.localLights.pointLights.find( + (candidate) => candidate.entityId === entityId + ); + + if (pointLight !== undefined) { + mutate(pointLight); + return; + } + + const spotLight = runtimeScene.localLights.spotLights.find( + (candidate) => candidate.entityId === entityId + ); + + if (spotLight !== undefined) { + mutate(spotLight); + } +} + +function applyResolvedDiscreteControlStateToRuntimeScene( + runtimeScene: RuntimeSceneDefinition, + state: RuntimeResolvedDiscreteControlState +) { + switch (state.type) { + case "actorPresence": + case "actorAnimationPlayback": + case "actorPathAssignment": + // Runtime scene build already resolves scheduler-owned actor state. + return; + case "modelVisibility": { + const modelInstance = runtimeScene.modelInstances.find( + (candidate) => candidate.instanceId === state.target.modelInstanceId + ); + + if (modelInstance !== undefined) { + modelInstance.visible = state.value; + } + return; + } + case "soundPlayback": { + const soundEmitter = runtimeScene.entities.soundEmitters.find( + (candidate) => candidate.entityId === state.target.entityId + ); + + if (soundEmitter !== undefined) { + soundEmitter.autoplay = state.value; + } + return; + } + case "modelAnimationPlayback": { + const modelInstance = runtimeScene.modelInstances.find( + (candidate) => candidate.instanceId === state.target.modelInstanceId + ); + + if (modelInstance !== undefined) { + modelInstance.animationClipName = state.clipName ?? undefined; + modelInstance.animationAutoplay = state.clipName !== null; + modelInstance.animationLoop = + state.clipName === null ? undefined : state.loop; + } + return; + } + case "lightEnabled": + mutateRuntimeLight(runtimeScene, state.target.entityId, (light) => { + light.enabled = state.value; + }); + return; + case "lightColor": + mutateRuntimeLight(runtimeScene, state.target.entityId, (light) => { + light.colorHex = state.value; + }); + return; + case "interactionEnabled": + if (state.target.interactionKind === "interactable") { + const interactable = runtimeScene.entities.interactables.find( + (candidate) => candidate.entityId === state.target.entityId + ); + + if (interactable !== undefined) { + interactable.interactionEnabled = state.value; + } + return; + } + + const sceneExit = runtimeScene.entities.sceneExits.find( + (candidate) => candidate.entityId === state.target.entityId + ); + + if (sceneExit !== undefined) { + sceneExit.interactionEnabled = state.value; + } + return; + case "ambientLightColor": + runtimeScene.world.ambientLight.colorHex = state.value; + return; + case "sunLightColor": + runtimeScene.world.sunLight.colorHex = state.value; + return; + } +} + +function applyResolvedControlChannelValueToRuntimeScene( + runtimeScene: RuntimeSceneDefinition, + channelValue: RuntimeResolvedControlChannelValue +) { + switch (channelValue.type) { + case "lightIntensity": + mutateRuntimeLight(runtimeScene, channelValue.descriptor.target.entityId, (light) => { + light.intensity = channelValue.value; + }); + return; + case "soundVolume": { + const soundEmitter = runtimeScene.entities.soundEmitters.find( + (candidate) => + candidate.entityId === channelValue.descriptor.target.entityId + ); + + if (soundEmitter !== undefined) { + soundEmitter.volume = channelValue.value; + } + return; + } + case "ambientLightIntensity": + runtimeScene.world.ambientLight.intensity = channelValue.value; + return; + case "sunLightIntensity": + runtimeScene.world.sunLight.intensity = channelValue.value; + return; + } +} + +export function applyResolvedControlStateToRuntimeScene( + runtimeScene: RuntimeSceneDefinition +): RuntimeSceneDefinition { + for (const state of runtimeScene.control.resolved.discrete) { + applyResolvedDiscreteControlStateToRuntimeScene(runtimeScene, state); + } + + for (const channelValue of runtimeScene.control.resolved.channels) { + applyResolvedControlChannelValueToRuntimeScene(runtimeScene, channelValue); + } + + return runtimeScene; +}