diff --git a/src/app/ProjectSchedulePane.tsx b/src/app/ProjectSchedulePane.tsx new file mode 100644 index 00000000..1ced2c66 --- /dev/null +++ b/src/app/ProjectSchedulePane.tsx @@ -0,0 +1,660 @@ +import type { KeyboardEvent as ReactKeyboardEvent } from "react"; + +import { + HOURS_PER_DAY, + formatTimeOfDayHours +} from "../document/project-time-settings"; +import { formatControlEffectValue, getControlTargetRefKey } from "../controls/control-surface"; +import { + PROJECT_SCHEDULE_WEEKDAYS, + formatProjectScheduleDaySelection, + formatProjectScheduleWeekdayLabel, + getProjectScheduleTimelineSegments, + type ProjectScheduler, + type ProjectScheduleRoutine, + type ProjectScheduleWeekday +} from "../scheduler/project-scheduler"; +import { + getProjectScheduleEffectOptionId, + getProjectScheduleTargetOptionForRoutine, + listProjectScheduleEffectOptions, + type ProjectScheduleEffectOptionId, + type ProjectScheduleTargetOption +} from "../scheduler/project-schedule-control-options"; + +interface ProjectSchedulePaneProps { + targetOptions: ProjectScheduleTargetOption[]; + scheduler: ProjectScheduler; + selectedRoutineId: string | null; + onSelectRoutine(routineId: string | null): void; + onAddRoutine(targetKey: string): void; + onDeleteRoutine(routineId: string): void; + onClose(): void; + onSetRoutineTarget(routineId: string, targetKey: string): void; + onSetRoutineTitle(routineId: string, title: string): void; + onSetRoutineEnabled(routineId: string, enabled: boolean): void; + onSetRoutineStartHour(routineId: string, startHour: number): void; + onSetRoutineEndHour(routineId: string, endHour: number): void; + onSetRoutinePriority(routineId: string, priority: number): void; + onSetRoutineEffectOption( + routineId: string, + effectOptionId: ProjectScheduleEffectOptionId + ): void; + onSetRoutineDays( + routineId: string, + mode: "everyDay" | "selectedDays", + days: ProjectScheduleWeekday[] + ): void; + onSetRoutineNumericValue(routineId: string, value: number): void; + onSetRoutineColorValue(routineId: string, colorHex: string): void; + onSetRoutineAnimationClip(routineId: string, clipName: string): void; + onSetRoutineAnimationLoop(routineId: string, loop: boolean): void; +} + +function handleCommitOnEnter( + event: ReactKeyboardEvent, + commit: () => void +) { + if (event.key !== "Enter") { + return; + } + + event.currentTarget.blur(); + commit(); +} + +function getRoutineSummary(routine: ProjectScheduleRoutine): string { + return `${formatProjectScheduleDaySelection(routine.days)} · ${formatTimeOfDayHours(routine.startHour)}-${formatTimeOfDayHours(routine.endHour)} · ${formatControlEffectValue(routine.effect)} · P${routine.priority}`; +} + +function isRoutineEffectInactive(routine: ProjectScheduleRoutine): boolean { + switch (routine.effect.type) { + case "setActorPresence": + return !routine.effect.active; + case "stopModelAnimation": + case "stopSound": + return true; + case "setModelInstanceVisible": + return !routine.effect.visible; + case "setInteractionEnabled": + case "setLightEnabled": + return !routine.effect.enabled; + default: + return false; + } +} + +function getRoutineNumericValue(routine: ProjectScheduleRoutine): number | null { + switch (routine.effect.type) { + case "setSoundVolume": + return routine.effect.volume; + case "setLightIntensity": + case "setAmbientLightIntensity": + case "setSunLightIntensity": + return routine.effect.intensity; + default: + return null; + } +} + +function getRoutineColorValue(routine: ProjectScheduleRoutine): string | null { + switch (routine.effect.type) { + case "setLightColor": + case "setAmbientLightColor": + case "setSunLightColor": + return routine.effect.colorHex; + default: + return null; + } +} + +function groupTargetOptions( + targetOptions: ProjectScheduleTargetOption[] +): Array<{ groupLabel: string; options: ProjectScheduleTargetOption[] }> { + const grouped = new Map(); + + for (const option of targetOptions) { + const entries = grouped.get(option.groupLabel) ?? []; + entries.push(option); + grouped.set(option.groupLabel, entries); + } + + return [...grouped.entries()].map(([groupLabel, options]) => ({ + groupLabel, + options + })); +} + +export function ProjectSchedulePane({ + targetOptions, + scheduler, + selectedRoutineId, + onSelectRoutine, + onAddRoutine, + onDeleteRoutine, + onClose, + onSetRoutineTarget, + onSetRoutineTitle, + onSetRoutineEnabled, + onSetRoutineStartHour, + onSetRoutineEndHour, + onSetRoutinePriority, + onSetRoutineEffectOption, + onSetRoutineDays, + onSetRoutineNumericValue, + onSetRoutineColorValue, + onSetRoutineAnimationClip, + onSetRoutineAnimationLoop +}: ProjectSchedulePaneProps) { + const selectedRoutine = + selectedRoutineId === null ? null : scheduler.routines[selectedRoutineId] ?? null; + const selectedTargetOption = + selectedRoutine === null + ? null + : getProjectScheduleTargetOptionForRoutine( + targetOptions, + selectedRoutine.target + ); + const selectedEffectOptionId = + selectedRoutine === null + ? null + : getProjectScheduleEffectOptionId(selectedRoutine.effect); + const selectedEffectOptions = + selectedTargetOption === null + ? [] + : listProjectScheduleEffectOptions(selectedTargetOption); + const selectedRoutineDays = + selectedRoutine === null + ? PROJECT_SCHEDULE_WEEKDAYS + : selectedRoutine.days.mode === "everyDay" + ? PROJECT_SCHEDULE_WEEKDAYS + : selectedRoutine.days.days; + const hourTicks = Array.from({ length: HOURS_PER_DAY }, (_, hour) => hour); + + return ( +
+
+
+
Schedule
+
+ Scheduler-owned orchestration over typed control targets and global + project time. +
+
+
+ + +
+
+ +
+
+
+
Targets
+
+ {hourTicks.map((hour) => ( +
+ {String(hour).padStart(2, "0")} +
+ ))} +
+
+ + {targetOptions.length === 0 ? ( +
+ No scheduler-addressable control targets are authored in this + project yet. +
+ ) : ( + targetOptions.map((targetOption) => { + const routines = Object.values(scheduler.routines) + .filter( + (routine) => + getControlTargetRefKey(routine.target) === targetOption.key + ) + .sort((left, right) => left.startHour - right.startHour); + + return ( +
+
+ +
+
{targetOption.label}
+
+ {targetOption.groupLabel} · {targetOption.subtitle} +
+
+
+
+
+ {routines.map((routine) => + getProjectScheduleTimelineSegments(routine).map( + (segment) => ( + + ) + ) + )} +
+
+ ); + }) + )} +
+ + +
+
+ ); +}