From 8639566565eaac9ac4703f051d9303c9eb7a6316 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 01:34:43 +0200 Subject: [PATCH] Add runtime control surface definition and build function --- src/runtime-three/runtime-scene-build.ts | 159 +++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/runtime-three/runtime-scene-build.ts b/src/runtime-three/runtime-scene-build.ts index 9dd629f3..1fe030c3 100644 --- a/src/runtime-three/runtime-scene-build.ts +++ b/src/runtime-three/runtime-scene-build.ts @@ -1,6 +1,21 @@ import type { LoadedModelAsset } from "../assets/gltf-model-import"; import type { Vec3 } from "../core/vector"; import { getModelInstances } from "../assets/model-instances"; +import { + createControlTargetDescriptor, + createDefaultResolvedControlSource, + createEmptyRuntimeResolvedControlState, + createInteractionControlTargetRef, + createLightControlTargetRef, + createLightIntensityControlChannelDescriptor, + createModelInstanceControlTargetRef, + createResolvedInteractionEnabledState, + createResolvedLightEnabledState, + createResolvedLightIntensityChannelValue, + createRuntimeControlSurfaceDefinition, + createSoundEmitterControlTargetRef, + type RuntimeControlSurfaceDefinition +} from "../controls/control-surface"; import { cloneBoxBrushGeometry, cloneBoxBrushVolumeSettings, @@ -304,6 +319,7 @@ export interface RuntimeSpawnPoint { export interface RuntimeSceneDefinition { time: ProjectTimeSettings; world: WorldSettings; + control: RuntimeControlSurfaceDefinition; localLights: RuntimeLocalLightCollection; brushes: RuntimeBoxBrushInstance[]; volumes: RuntimeBoxVolumeCollection; @@ -802,6 +818,133 @@ interface RuntimeSceneCollections { npcDefinitions: RuntimeNpcDefinition[]; } +function buildRuntimeControlSurface( + document: SceneDocument, + collections: RuntimeSceneCollections, + modelInstances: RuntimeModelInstance[] +): RuntimeControlSurfaceDefinition { + const targets: RuntimeControlSurfaceDefinition["targets"] = []; + const channels: RuntimeControlSurfaceDefinition["channels"] = []; + const resolved = createEmptyRuntimeResolvedControlState(); + const defaultSource = createDefaultResolvedControlSource(); + + for (const pointLight of collections.localLights.pointLights) { + const target = createLightControlTargetRef("pointLight", pointLight.entityId); + const descriptor = createLightIntensityControlChannelDescriptor({ + target, + defaultValue: pointLight.intensity + }); + const sourceEntity = document.entities[pointLight.entityId]; + + targets.push( + createControlTargetDescriptor(target, ["lightEnabled", "lightIntensity"]) + ); + channels.push(descriptor); + resolved.discrete.push( + createResolvedLightEnabledState({ + target, + value: sourceEntity?.kind === "pointLight" ? sourceEntity.visible : true, + source: defaultSource + }) + ); + resolved.channels.push( + createResolvedLightIntensityChannelValue({ + descriptor, + value: pointLight.intensity, + source: defaultSource + }) + ); + } + + for (const spotLight of collections.localLights.spotLights) { + const target = createLightControlTargetRef("spotLight", spotLight.entityId); + const descriptor = createLightIntensityControlChannelDescriptor({ + target, + defaultValue: spotLight.intensity + }); + const sourceEntity = document.entities[spotLight.entityId]; + + targets.push( + createControlTargetDescriptor(target, ["lightEnabled", "lightIntensity"]) + ); + channels.push(descriptor); + resolved.discrete.push( + createResolvedLightEnabledState({ + target, + value: sourceEntity?.kind === "spotLight" ? sourceEntity.visible : true, + source: defaultSource + }) + ); + resolved.channels.push( + createResolvedLightIntensityChannelValue({ + descriptor, + value: spotLight.intensity, + source: defaultSource + }) + ); + } + + for (const soundEmitter of collections.entities.soundEmitters) { + targets.push( + createControlTargetDescriptor( + createSoundEmitterControlTargetRef(soundEmitter.entityId), + ["soundPlayback"] + ) + ); + } + + for (const interactable of collections.entities.interactables) { + const target = createInteractionControlTargetRef( + "interactable", + interactable.entityId + ); + + targets.push( + createControlTargetDescriptor(target, ["interactionAvailability"]) + ); + resolved.discrete.push( + createResolvedInteractionEnabledState({ + target, + value: interactable.interactionEnabled, + source: defaultSource + }) + ); + } + + for (const sceneExit of collections.entities.sceneExits) { + const target = createInteractionControlTargetRef( + "sceneExit", + sceneExit.entityId + ); + + targets.push( + createControlTargetDescriptor(target, ["interactionAvailability"]) + ); + resolved.discrete.push( + createResolvedInteractionEnabledState({ + target, + value: sceneExit.interactionEnabled, + source: defaultSource + }) + ); + } + + for (const modelInstance of modelInstances) { + targets.push( + createControlTargetDescriptor( + createModelInstanceControlTargetRef(modelInstance.instanceId), + ["animationPlayback"] + ) + ); + } + + return createRuntimeControlSurfaceDefinition({ + targets, + channels, + resolved + }); +} + function buildRuntimeSceneCollections( document: SceneDocument, timeOfDayHours: number @@ -1044,6 +1187,7 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options: document, runtimeTimeOfDayHours ); + const control = buildRuntimeControlSurface(document, collections, modelInstances); const enabledBrushIds = new Set(enabledBrushes.map((brush) => brush.id)); const enabledModelInstanceIds = new Set(enabledModelInstances.map((modelInstance) => modelInstance.id)); const enabledEntityIds = new Set( @@ -1068,6 +1212,20 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options: case "playSound": case "stopSound": return enabledEntityIds.has(link.action.targetSoundEmitterId); + case "control": + switch (link.action.effect.target.kind) { + case "entity": + case "interaction": + return enabledEntityIds.has(link.action.effect.target.entityId); + case "modelInstance": + return enabledModelInstanceIds.has( + link.action.effect.target.modelInstanceId + ); + case "actor": + case "scene": + case "global": + return true; + } } }) .map((link) => cloneInteractionLink(link)); @@ -1120,6 +1278,7 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options: return { time: cloneProjectTimeSettings(document.time), world: cloneWorldSettings(document.world), + control, localLights: collections.localLights, brushes, volumes,