From c1f3064e9e37fdbd72fcafaa364aca74d1b61cf5 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Thu, 2 Apr 2026 19:38:16 +0200 Subject: [PATCH] Add sound emitter validation and interaction handling --- src/document/scene-document-validation.ts | 43 ++++++++++++++++++- .../runtime-interaction-system.ts | 8 ++++ src/runtime-three/runtime-scene-build.ts | 12 ++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/document/scene-document-validation.ts b/src/document/scene-document-validation.ts index bc9130bd..24870e48 100644 --- a/src/document/scene-document-validation.ts +++ b/src/document/scene-document-validation.ts @@ -807,6 +807,47 @@ function validateInteractionLink(link: InteractionLink, path: string, document: ); } break; + case "playSound": + case "stopSound": { + const targetEntity = document.entities[link.action.targetSoundEmitterId]; + + if (targetEntity === undefined) { + diagnostics.push( + createDiagnostic( + "error", + "missing-sound-emitter-entity", + `Sound emitter entity ${link.action.targetSoundEmitterId} does not exist.`, + `${path}.action.targetSoundEmitterId` + ) + ); + break; + } + + if (targetEntity.kind !== "soundEmitter") { + diagnostics.push( + createDiagnostic( + "error", + "invalid-sound-emitter-kind", + "Sound playback actions must target a Sound Emitter entity.", + `${path}.action.targetSoundEmitterId` + ) + ); + break; + } + + if (targetEntity.audioAssetId === null) { + diagnostics.push( + createDiagnostic( + "error", + "missing-sound-emitter-audio-asset", + "Sound playback actions require a Sound Emitter that references an audio asset.", + `${path}.action.targetSoundEmitterId` + ) + ); + } + + break; + } default: diagnostics.push( createDiagnostic( @@ -946,7 +987,7 @@ export function validateSceneDocument(document: SceneDocument): SceneDocumentVal validatePlayerStartEntity(entity, path, diagnostics); break; case "soundEmitter": - validateSoundEmitterEntity(entity, path, diagnostics); + validateSoundEmitterEntity(entity, path, document, diagnostics); break; case "triggerVolume": validateTriggerVolumeEntity(entity, path, diagnostics); diff --git a/src/runtime-three/runtime-interaction-system.ts b/src/runtime-three/runtime-interaction-system.ts index 33847e2d..f316abc1 100644 --- a/src/runtime-three/runtime-interaction-system.ts +++ b/src/runtime-three/runtime-interaction-system.ts @@ -10,6 +10,8 @@ export interface RuntimeInteractionDispatcher { toggleBrushVisibility(brushId: string, visible: boolean | undefined, link: InteractionLink): void; playAnimation(instanceId: string, clipName: string, loop: boolean | undefined, link: InteractionLink): void; stopAnimation(instanceId: string, link: InteractionLink): void; + playSound(soundEmitterId: string, link: InteractionLink): void; + stopSound(soundEmitterId: string, link: InteractionLink): void; } export interface RuntimeInteractionPrompt { @@ -220,6 +222,12 @@ export class RuntimeInteractionSystem { case "stopAnimation": dispatcher.stopAnimation(link.action.targetModelInstanceId, link); break; + case "playSound": + dispatcher.playSound(link.action.targetSoundEmitterId, link); + break; + case "stopSound": + dispatcher.stopSound(link.action.targetSoundEmitterId, link); + break; } } } diff --git a/src/runtime-three/runtime-scene-build.ts b/src/runtime-three/runtime-scene-build.ts index e1764dd7..325b34de 100644 --- a/src/runtime-three/runtime-scene-build.ts +++ b/src/runtime-three/runtime-scene-build.ts @@ -49,8 +49,10 @@ export interface RuntimePlayerStart { export interface RuntimeSoundEmitter { entityId: string; position: Vec3; - radius: number; - gain: number; + audioAssetId: string | null; + volume: number; + refDistance: number; + maxDistance: number; autoplay: boolean; loop: boolean; } @@ -340,8 +342,10 @@ function buildRuntimeSceneCollections(document: SceneDocument): RuntimeSceneColl runtimeEntities.soundEmitters.push({ entityId: entity.id, position: cloneVec3(entity.position), - radius: entity.radius, - gain: entity.gain, + audioAssetId: entity.audioAssetId, + volume: entity.volume, + refDistance: entity.refDistance, + maxDistance: entity.maxDistance, autoplay: entity.autoplay, loop: entity.loop });