From f986eb0282f24ee2b9218015592f81c49b0fb25b Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 01:56:23 +0200 Subject: [PATCH] Add runtime project scheduler implementation --- .../runtime-project-scheduler.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/runtime-three/runtime-project-scheduler.ts diff --git a/src/runtime-three/runtime-project-scheduler.ts b/src/runtime-three/runtime-project-scheduler.ts new file mode 100644 index 00000000..a0aa9af1 --- /dev/null +++ b/src/runtime-three/runtime-project-scheduler.ts @@ -0,0 +1,175 @@ +import { + applyControlEffectToResolvedState, + cloneControlEffect, + cloneRuntimeResolvedControlState, + createActorControlTargetRef, + createDefaultResolvedControlSource, + createSchedulerResolvedControlSource, + createSetActorPresenceControlEffect, + type RuntimeResolvedControlState, + type SetActorPresenceControlEffect +} from "../controls/control-surface"; +import { + cloneProjectScheduler, + compareProjectScheduleRoutinePriority, + isProjectScheduleRoutineActiveAt, + type ProjectScheduler, + type ProjectScheduleRoutine +} from "../scheduler/project-scheduler"; + +export interface RuntimeResolvedActorScheduleState { + actorId: string; + hasRules: boolean; + active: boolean; + activeRoutineId: string | null; + activeRoutineTitle: string | null; + effect: SetActorPresenceControlEffect | null; +} + +export interface RuntimeResolvedProjectScheduleState { + actors: RuntimeResolvedActorScheduleState[]; +} + +export interface RuntimeProjectSchedulerState { + document: ProjectScheduler; + resolved: RuntimeResolvedProjectScheduleState; +} + +function cloneRuntimeResolvedActorScheduleState( + state: RuntimeResolvedActorScheduleState +): RuntimeResolvedActorScheduleState { + return { + actorId: state.actorId, + hasRules: state.hasRules, + active: state.active, + activeRoutineId: state.activeRoutineId, + activeRoutineTitle: state.activeRoutineTitle, + effect: state.effect === null ? null : cloneControlEffect(state.effect) + }; +} + +export function cloneRuntimeResolvedProjectScheduleState( + state: RuntimeResolvedProjectScheduleState +): RuntimeResolvedProjectScheduleState { + return { + actors: state.actors.map(cloneRuntimeResolvedActorScheduleState) + }; +} + +export function createEmptyRuntimeResolvedProjectScheduleState(): RuntimeResolvedProjectScheduleState { + return { + actors: [] + }; +} + +export function createRuntimeProjectSchedulerState(options: { + document: ProjectScheduler; + resolved?: RuntimeResolvedProjectScheduleState; +}): RuntimeProjectSchedulerState { + return { + document: cloneProjectScheduler(options.document), + resolved: cloneRuntimeResolvedProjectScheduleState( + options.resolved ?? createEmptyRuntimeResolvedProjectScheduleState() + ) + }; +} + +function resolveActorScheduleRules( + scheduler: ProjectScheduler, + actorId: string +): ProjectScheduleRoutine[] { + return Object.values(scheduler.routines) + .filter( + (routine) => + routine.enabled && + routine.target.kind === "actor" && + routine.target.actorId === actorId && + routine.effect.type === "setActorPresence" + ) + .sort(compareProjectScheduleRoutinePriority); +} + +export function resolveRuntimeActorScheduleState(options: { + scheduler: ProjectScheduler; + actorId: string; + dayNumber: number; + timeOfDayHours: number; +}): RuntimeResolvedActorScheduleState { + const actorRules = resolveActorScheduleRules(options.scheduler, options.actorId); + const activeRoutine = + actorRules.find((routine) => + isProjectScheduleRoutineActiveAt( + routine, + options.dayNumber, + options.timeOfDayHours + ) + ) ?? null; + + if (activeRoutine === null) { + return { + actorId: options.actorId, + hasRules: actorRules.length > 0, + active: actorRules.length === 0, + activeRoutineId: null, + activeRoutineTitle: null, + effect: null + }; + } + + return { + actorId: options.actorId, + hasRules: true, + active: activeRoutine.effect.active, + activeRoutineId: activeRoutine.id, + activeRoutineTitle: activeRoutine.title, + effect: cloneControlEffect(activeRoutine.effect) + }; +} + +export function resolveRuntimeProjectScheduleState(options: { + scheduler: ProjectScheduler; + actorIds: string[]; + dayNumber: number; + timeOfDayHours: number; +}): RuntimeResolvedProjectScheduleState { + const actorIds = [...new Set(options.actorIds)]; + + return { + actors: actorIds.map((actorId) => + resolveRuntimeActorScheduleState({ + scheduler: options.scheduler, + actorId, + dayNumber: options.dayNumber, + timeOfDayHours: options.timeOfDayHours + }) + ) + }; +} + +export function applyRuntimeProjectScheduleToControlState( + resolved: RuntimeResolvedControlState, + schedule: RuntimeResolvedProjectScheduleState +): RuntimeResolvedControlState { + let nextResolved = cloneRuntimeResolvedControlState(resolved); + nextResolved.discrete = nextResolved.discrete.filter( + (state) => state.type !== "actorPresence" + ); + + for (const actorState of schedule.actors) { + const effect = + actorState.effect ?? + createSetActorPresenceControlEffect({ + target: createActorControlTargetRef(actorState.actorId), + active: actorState.active + }); + nextResolved = applyControlEffectToResolvedState( + nextResolved, + effect, + actorState.activeRoutineId === null + ? createDefaultResolvedControlSource() + : createSchedulerResolvedControlSource(actorState.activeRoutineId) + ); + } + + return nextResolved; +}