2026-04-14 01:56:23 +02:00
|
|
|
import {
|
|
|
|
|
applyControlEffectToResolvedState,
|
|
|
|
|
cloneControlEffect,
|
|
|
|
|
cloneRuntimeResolvedControlState,
|
|
|
|
|
createActorControlTargetRef,
|
|
|
|
|
createDefaultResolvedControlSource,
|
|
|
|
|
createSchedulerResolvedControlSource,
|
|
|
|
|
createSetActorPresenceControlEffect,
|
2026-04-14 03:01:21 +02:00
|
|
|
getControlEffectResolutionKey,
|
|
|
|
|
type ControlEffect,
|
|
|
|
|
type ControlTargetRef,
|
2026-04-14 01:56:23 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 03:01:21 +02:00
|
|
|
export interface RuntimeResolvedScheduledControlRoutine {
|
|
|
|
|
routineId: string;
|
|
|
|
|
title: string;
|
|
|
|
|
target: ControlTargetRef;
|
|
|
|
|
effect: ControlEffect;
|
|
|
|
|
resolutionKey: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 01:56:23 +02:00
|
|
|
export interface RuntimeResolvedProjectScheduleState {
|
|
|
|
|
actors: RuntimeResolvedActorScheduleState[];
|
2026-04-14 03:01:21 +02:00
|
|
|
controls: RuntimeResolvedScheduledControlRoutine[];
|
2026-04-14 01:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-04-14 03:01:21 +02:00
|
|
|
actors: state.actors.map(cloneRuntimeResolvedActorScheduleState),
|
|
|
|
|
controls: state.controls.map((routine) => ({
|
|
|
|
|
routineId: routine.routineId,
|
|
|
|
|
title: routine.title,
|
|
|
|
|
target: cloneControlEffect(routine.effect).target,
|
|
|
|
|
effect: cloneControlEffect(routine.effect),
|
|
|
|
|
resolutionKey: routine.resolutionKey
|
|
|
|
|
}))
|
2026-04-14 01:56:23 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createEmptyRuntimeResolvedProjectScheduleState(): RuntimeResolvedProjectScheduleState {
|
|
|
|
|
return {
|
2026-04-14 03:01:21 +02:00
|
|
|
actors: [],
|
|
|
|
|
controls: []
|
2026-04-14 01:56:23 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
})
|
2026-04-14 03:01:21 +02:00
|
|
|
),
|
|
|
|
|
controls: resolveRuntimeScheduledControlRoutines(options)
|
2026-04-14 01:56:23 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 03:01:21 +02:00
|
|
|
function resolveRuntimeScheduledControlRoutines(options: {
|
|
|
|
|
scheduler: ProjectScheduler;
|
|
|
|
|
dayNumber: number;
|
|
|
|
|
timeOfDayHours: number;
|
|
|
|
|
}): RuntimeResolvedScheduledControlRoutine[] {
|
|
|
|
|
const activeRoutines = Object.values(options.scheduler.routines)
|
|
|
|
|
.filter((routine) =>
|
|
|
|
|
isProjectScheduleRoutineActiveAt(
|
|
|
|
|
routine,
|
|
|
|
|
options.dayNumber,
|
|
|
|
|
options.timeOfDayHours
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.sort(compareProjectScheduleRoutinePriority);
|
|
|
|
|
const seenResolutionKeys = new Set<string>();
|
|
|
|
|
const resolved: RuntimeResolvedScheduledControlRoutine[] = [];
|
|
|
|
|
|
|
|
|
|
for (const routine of activeRoutines) {
|
|
|
|
|
if (routine.effect.type === "setActorPresence") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolutionKey = getControlEffectResolutionKey(routine.effect);
|
|
|
|
|
|
|
|
|
|
if (seenResolutionKeys.has(resolutionKey)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seenResolutionKeys.add(resolutionKey);
|
|
|
|
|
resolved.push({
|
|
|
|
|
routineId: routine.id,
|
|
|
|
|
title: routine.title,
|
|
|
|
|
target: cloneControlEffect(routine.effect).target,
|
|
|
|
|
effect: cloneControlEffect(routine.effect),
|
|
|
|
|
resolutionKey
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resolved;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 01:56:23 +02:00
|
|
|
export function applyRuntimeProjectScheduleToControlState(
|
|
|
|
|
resolved: RuntimeResolvedControlState,
|
|
|
|
|
schedule: RuntimeResolvedProjectScheduleState
|
|
|
|
|
): RuntimeResolvedControlState {
|
|
|
|
|
let nextResolved = cloneRuntimeResolvedControlState(resolved);
|
|
|
|
|
nextResolved.discrete = nextResolved.discrete.filter(
|
2026-04-14 03:01:21 +02:00
|
|
|
(state) => state.type !== "actorPresence" && state.source.kind !== "scheduler"
|
|
|
|
|
);
|
|
|
|
|
nextResolved.channels = nextResolved.channels.filter(
|
|
|
|
|
(channel) => channel.source.kind !== "scheduler"
|
2026-04-14 01:56:23 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 03:01:21 +02:00
|
|
|
for (const controlRoutine of schedule.controls) {
|
|
|
|
|
nextResolved = applyControlEffectToResolvedState(
|
|
|
|
|
nextResolved,
|
|
|
|
|
controlRoutine.effect,
|
|
|
|
|
createSchedulerResolvedControlSource(controlRoutine.routineId)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 01:56:23 +02:00
|
|
|
return nextResolved;
|
|
|
|
|
}
|