From 05d513464e73dcf6b6ada488a18b327057de3c2a Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 01:52:22 +0200 Subject: [PATCH] Update control surface to include actor presence controls --- src/controls/control-surface.ts | 94 +++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/controls/control-surface.ts b/src/controls/control-surface.ts index 997a63ae..edd2effa 100644 --- a/src/controls/control-surface.ts +++ b/src/controls/control-surface.ts @@ -8,6 +8,7 @@ export const CONTROL_INTERACTION_TARGET_KINDS = [ "sceneExit" ] as const; export const CONTROL_CAPABILITY_KINDS = [ + "actorPresence", "animationPlayback", "soundPlayback", "interactionAvailability", @@ -82,6 +83,12 @@ export interface PlayModelAnimationControlEffect { loop?: boolean; } +export interface SetActorPresenceControlEffect { + type: "setActorPresence"; + target: ActorControlTargetRef; + active: boolean; +} + export interface StopModelAnimationControlEffect { type: "stopModelAnimation"; target: ModelInstanceControlTargetRef; @@ -116,6 +123,7 @@ export interface SetLightIntensityControlEffect { } export type ControlEffect = + | SetActorPresenceControlEffect | PlayModelAnimationControlEffect | StopModelAnimationControlEffect | PlaySoundControlEffect @@ -159,6 +167,13 @@ export interface RuntimeResolvedLightEnabledState { source: RuntimeResolvedControlSource; } +export interface RuntimeResolvedActorPresenceState { + type: "actorPresence"; + target: ActorControlTargetRef; + value: boolean; + source: RuntimeResolvedControlSource; +} + export interface RuntimeResolvedInteractionEnabledState { type: "interactionEnabled"; target: InteractionControlTargetRef; @@ -167,6 +182,7 @@ export interface RuntimeResolvedInteractionEnabledState { } export type RuntimeResolvedDiscreteControlState = + | RuntimeResolvedActorPresenceState | RuntimeResolvedLightEnabledState | RuntimeResolvedInteractionEnabledState; @@ -428,6 +444,17 @@ export function createDefaultResolvedControlSource(): DefaultResolvedControlSour }; } +export function createSchedulerResolvedControlSource( + scheduleId: string +): SchedulerResolvedControlSource { + assertNonEmptyString(scheduleId, "Resolved control scheduler id"); + + return { + kind: "scheduler", + scheduleId + }; +} + export function createInteractionLinkResolvedControlSource( linkId: string ): InteractionLinkResolvedControlSource { @@ -439,6 +466,19 @@ export function createInteractionLinkResolvedControlSource( }; } +export function createSetActorPresenceControlEffect(options: { + target: ActorControlTargetRef; + active: boolean; +}): SetActorPresenceControlEffect { + assertBoolean(options.active, "Control actor active"); + + return { + type: "setActorPresence", + target: cloneControlTargetRef(options.target) as ActorControlTargetRef, + active: options.active + }; +} + export function createResolvedLightEnabledState(options: { target: LightControlTargetRef; value: boolean; @@ -454,6 +494,21 @@ export function createResolvedLightEnabledState(options: { }; } +export function createResolvedActorPresenceState(options: { + target: ActorControlTargetRef; + value: boolean; + source: RuntimeResolvedControlSource; +}): RuntimeResolvedActorPresenceState { + assertBoolean(options.value, "Resolved control actor active"); + + return { + type: "actorPresence", + target: cloneControlTargetRef(options.target) as ActorControlTargetRef, + value: options.value, + source: cloneResolvedControlSource(options.source) + }; +} + export function createResolvedInteractionEnabledState(options: { target: InteractionControlTargetRef; value: boolean; @@ -552,6 +607,8 @@ function getResolvedDiscreteControlStateKey( state: RuntimeResolvedDiscreteControlState ): string { switch (state.type) { + case "actorPresence": + return `state:${state.type}:${getControlTargetRefKey(state.target)}`; case "lightEnabled": return `state:${state.type}:${getControlTargetRefKey(state.target)}`; case "interactionEnabled": @@ -632,6 +689,12 @@ export function cloneControlEffect( effect: TEffect ): TEffect { switch (effect.type) { + case "setActorPresence": + return { + type: "setActorPresence", + target: cloneControlTargetRef(effect.target), + active: effect.active + } as TEffect; case "playModelAnimation": return { type: "playModelAnimation", @@ -693,6 +756,12 @@ export function cloneRuntimeResolvedDiscreteControlState< TState extends RuntimeResolvedDiscreteControlState >(state: TState): TState { switch (state.type) { + case "actorPresence": + return createResolvedActorPresenceState({ + target: state.target, + value: state.value, + source: state.source + }) as TState; case "lightEnabled": return createResolvedLightEnabledState({ target: state.target, @@ -755,6 +824,8 @@ export function areControlEffectsEqual( } switch (left.type) { + case "setActorPresence": + return left.active === (right as SetActorPresenceControlEffect).active; case "playModelAnimation": return ( left.clipName === (right as PlayModelAnimationControlEffect).clipName && @@ -779,6 +850,8 @@ export function areControlEffectsEqual( export function getControlEffectLabel(effect: ControlEffect): string { switch (effect.type) { + case "setActorPresence": + return "Set Actor Presence"; case "playModelAnimation": return "Play Animation"; case "stopModelAnimation": @@ -832,6 +905,8 @@ function formatTargetKindLabel( export function formatControlEffectValue(effect: ControlEffect): string { switch (effect.type) { + case "setActorPresence": + return effect.active ? "Present" : "Hidden"; case "playModelAnimation": return effect.loop === false ? `${effect.clipName} (Once)` @@ -858,6 +933,25 @@ export function applyControlEffectToResolvedState( const nextResolved = cloneRuntimeResolvedControlState(resolved); switch (effect.type) { + case "setActorPresence": { + const nextState = createResolvedActorPresenceState({ + target: effect.target, + value: effect.active, + source + }); + const stateKey = getResolvedDiscreteControlStateKey(nextState); + const existingIndex = nextResolved.discrete.findIndex( + (candidate) => + getResolvedDiscreteControlStateKey(candidate) === stateKey + ); + + if (existingIndex >= 0) { + nextResolved.discrete[existingIndex] = nextState; + } else { + nextResolved.discrete.push(nextState); + } + return nextResolved; + } case "setLightEnabled": { const nextState = createResolvedLightEnabledState({ target: effect.target,