import type { KeyboardEvent as ReactKeyboardEvent } from "react"; import { HOURS_PER_DAY, formatTimeOfDayHours } from "../document/project-time-settings"; import type { ProjectDialogueLibrary } from "../dialogues/project-dialogues"; import { formatControlEffectValue, getControlTargetRefKey } from "../controls/control-surface"; import { ProjectSequencesPanel } from "./ProjectSequencesPanel"; import { formatProjectScheduleDaySelection, getProjectScheduleTimelineSegments, type ProjectScheduler, type ProjectScheduleRoutine } from "../sequencer/project-sequencer"; import { getProjectScheduleEffectOptionId, getProjectScheduleTargetOptionForRoutine, listProjectScheduleEffectOptions, type ProjectScheduleEffectOptionId, type ProjectScheduleTargetOption } from "../sequencer/project-sequencer-control-options"; import { findHeldSequenceControlEffect, getProjectScheduleRoutineHeldSteps, getProjectSequenceImpulseSteps, getProjectSequenceHeldSteps } from "../sequencer/project-sequence-steps"; import { getProjectSequences, type ProjectSequenceLibrary } from "../sequencer/project-sequences"; interface ProjectSequencerPaneProps { mode: "timeline" | "sequence"; onSetMode(mode: "timeline" | "sequence"): void; targetOptions: ProjectScheduleTargetOption[]; teleportTargetOptions: Array<{ entityId: string; label: string; }>; sceneTransitionTargetOptions: Array<{ targetKey: string; label: string; }>; visibilityTargetOptions: Array<{ targetKey: string; label: string; }>; modelAnimationTargetOptions: Array<{ targetKey: string; label: string; }>; soundTargetOptions: Array<{ targetKey: string; label: string; }>; scheduler: ProjectScheduler; sequences: ProjectSequenceLibrary; dialogues: ProjectDialogueLibrary; selectedRoutineId: string | null; selectedSequenceId: string | null; onSelectRoutine(routineId: string | null): void; onSelectSequence(sequenceId: string | null): void; onAddRoutine(targetKey: string): void; onAddSequence(): void; onDeleteRoutine(routineId: string): void; onDeleteSequence(sequenceId: 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; onSetRoutineSequenceId(routineId: string, sequenceId: string | null): void; onSetRoutineEffectOption( routineId: string, effectOptionId: ProjectScheduleEffectOptionId ): 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; onSetActorRoutinePresence(routineId: string, active: boolean): void; onSetActorRoutineAnimationClip( routineId: string, clipName: string | null ): void; onSetActorRoutineAnimationLoop(routineId: string, loop: boolean): void; onSetActorRoutinePath(routineId: string, pathId: string | null): void; onSetActorRoutinePathSpeed(routineId: string, speed: number): void; onSetActorRoutinePathLoop(routineId: string, loop: boolean): void; onSetSequenceTitle(sequenceId: string, title: string): void; onAddHeldControlStep(sequenceId: string, targetKey: string): void; onAddImpulseControlStep(sequenceId: string, targetKey: string): void; onAddDialogueStep(sequenceId: string, dialogueId: string): void; onAddTeleportStep(sequenceId: string, targetEntityId: string): void; onAddSceneTransitionStep(sequenceId: string, targetKey: string): void; onAddVisibilityStep(sequenceId: string, targetKey: string): void; onAddPlayAnimationStep(sequenceId: string, targetKey: string): void; onAddStopAnimationStep(sequenceId: string, targetKey: string): void; onAddPlaySoundStep(sequenceId: string, targetKey: string): void; onAddStopSoundStep(sequenceId: string, targetKey: string): void; onDeleteStep(sequenceId: string, stepIndex: number): void; onSetControlStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetControlStepEffectOption( sequenceId: string, stepIndex: number, effectOptionId: ProjectScheduleEffectOptionId ): void; onSetControlStepNumericValue( sequenceId: string, stepIndex: number, value: number ): void; onSetControlStepColorValue( sequenceId: string, stepIndex: number, colorHex: string ): void; onSetControlStepAnimationClip( sequenceId: string, stepIndex: number, clipName: string ): void; onSetControlStepAnimationLoop( sequenceId: string, stepIndex: number, loop: boolean ): void; onSetDialogueStepDialogueId( sequenceId: string, stepIndex: number, dialogueId: string ): void; onSetTeleportStepTarget( sequenceId: string, stepIndex: number, targetEntityId: string ): void; onSetSceneTransitionStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetVisibilityStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetVisibilityStepMode( sequenceId: string, stepIndex: number, mode: "toggle" | "show" | "hide" ): void; } function handleCommitOnEnter( event: ReactKeyboardEvent, commit: () => void ) { if (event.key !== "Enter") { return; } event.currentTarget.blur(); commit(); } function parseTimeOfDayInputHours(value: string, label: string): number { const match = /^(?\d{1,2}):(?\d{2})$/.exec(value.trim()); if (match?.groups === undefined) { throw new Error(`${label} must use HH:MM.`); } const hours = Number(match.groups.hours); const minutes = Number(match.groups.minutes); if ( !Number.isFinite(hours) || !Number.isFinite(minutes) || hours < 0 || hours >= HOURS_PER_DAY || minutes < 0 || minutes >= 60 ) { throw new Error(`${label} must be a valid time of day.`); } return hours + minutes / 60; } function getRoutineSummary( routine: ProjectScheduleRoutine, sequences: ProjectSequenceLibrary ): string { const summaryParts = [ formatProjectScheduleDaySelection(routine.days), `${formatTimeOfDayHours(routine.startHour)}-${formatTimeOfDayHours(routine.endHour)}` ]; const heldSteps = getProjectScheduleRoutineHeldSteps(routine, sequences); if (routine.target.kind === "actor") { const presenceEffect = findHeldSequenceControlEffect( heldSteps, "setActorPresence" ); const animationEffect = findHeldSequenceControlEffect( heldSteps, "playActorAnimation" ); const pathEffect = findHeldSequenceControlEffect( heldSteps, "followActorPath" ); summaryParts.push(presenceEffect?.active === false ? "Hidden" : "Present"); if (animationEffect !== null) { summaryParts.push(formatControlEffectValue(animationEffect)); } if (pathEffect !== null) { summaryParts.push(formatControlEffectValue(pathEffect)); } } else { const effect = heldSteps[0]; if (routine.target.kind === "global" && routine.sequenceId !== null) { summaryParts.push("Impulse Sequence"); } else if (effect?.type === "controlEffect") { summaryParts.push(formatControlEffectValue(effect.effect)); } } summaryParts.push(`P${routine.priority}`); return summaryParts.join(" · "); } function isRoutineEffectInactive( routine: ProjectScheduleRoutine, sequences: ProjectSequenceLibrary ): boolean { const heldSteps = getProjectScheduleRoutineHeldSteps(routine, sequences); if (routine.target.kind === "actor") { return ( findHeldSequenceControlEffect(heldSteps, "setActorPresence")?.active === false ); } const effect = heldSteps[0]?.type === "controlEffect" ? heldSteps[0].effect : undefined; if (effect === undefined) { return false; } switch (effect.type) { case "stopModelAnimation": case "stopSound": return true; case "setModelInstanceVisible": return !effect.visible; case "setInteractionEnabled": case "setLightEnabled": return !effect.enabled; default: return false; } } function getRoutineNumericValue(routine: ProjectScheduleRoutine): number | null { const effect = routine.effects[0]; if (effect === undefined) { return null; } switch (effect.type) { case "setSoundVolume": return effect.volume; case "setLightIntensity": case "setAmbientLightIntensity": case "setSunLightIntensity": return effect.intensity; default: return null; } } function getRoutineColorValue(routine: ProjectScheduleRoutine): string | null { const effect = routine.effects[0]; if (effect === undefined) { return null; } switch (effect.type) { case "setLightColor": case "setAmbientLightColor": case "setSunLightColor": return 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 ProjectSequencerPane({ mode, onSetMode, targetOptions, teleportTargetOptions, sceneTransitionTargetOptions, visibilityTargetOptions, modelAnimationTargetOptions, soundTargetOptions, scheduler, sequences, dialogues, selectedRoutineId, selectedSequenceId, onSelectRoutine, onSelectSequence, onAddRoutine, onAddSequence, onDeleteRoutine, onDeleteSequence, onClose, onSetRoutineTarget, onSetRoutineTitle, onSetRoutineEnabled, onSetRoutineStartHour, onSetRoutineEndHour, onSetRoutinePriority, onSetRoutineSequenceId, onSetRoutineEffectOption, onSetRoutineNumericValue, onSetRoutineColorValue, onSetRoutineAnimationClip, onSetRoutineAnimationLoop, onSetActorRoutinePresence, onSetActorRoutineAnimationClip, onSetActorRoutineAnimationLoop, onSetActorRoutinePath, onSetActorRoutinePathSpeed, onSetActorRoutinePathLoop, onSetSequenceTitle, onAddHeldControlStep, onAddImpulseControlStep, onAddDialogueStep, onAddTeleportStep, onAddSceneTransitionStep, onAddVisibilityStep, onAddPlayAnimationStep, onAddStopAnimationStep, onAddPlaySoundStep, onAddStopSoundStep, onDeleteStep, onSetControlStepTarget, onSetControlStepEffectOption, onSetControlStepNumericValue, onSetControlStepColorValue, onSetControlStepAnimationClip, onSetControlStepAnimationLoop, onSetDialogueStepDialogueId, onSetTeleportStepTarget, onSetSceneTransitionStepTarget, onSetVisibilityStepTarget, onSetVisibilityStepMode }: ProjectSequencerPaneProps) { const selectedRoutine = selectedRoutineId === null ? null : scheduler.routines[selectedRoutineId] ?? null; const selectedRoutineHeldSteps = selectedRoutine === null ? [] : getProjectScheduleRoutineHeldSteps(selectedRoutine, sequences); const selectedTargetOption = selectedRoutine === null ? null : getProjectScheduleTargetOptionForRoutine( targetOptions, selectedRoutine.target ); const selectedEffectOptionId = selectedRoutine === null || selectedRoutine.target.kind === "actor" || selectedRoutine.sequenceId !== null ? null : getProjectScheduleEffectOptionId(selectedRoutine.effects[0]!); const selectedEffectOptions = selectedTargetOption === null || selectedTargetOption.target.kind === "actor" ? [] : listProjectScheduleEffectOptions(selectedTargetOption); const selectedActorPresenceEffect = selectedRoutine === null || selectedRoutine.target.kind !== "actor" ? null : findHeldSequenceControlEffect(selectedRoutineHeldSteps, "setActorPresence"); const selectedActorAnimationEffect = selectedRoutine === null || selectedRoutine.target.kind !== "actor" ? null : findHeldSequenceControlEffect(selectedRoutineHeldSteps, "playActorAnimation"); const selectedActorPathEffect = selectedRoutine === null || selectedRoutine.target.kind !== "actor" ? null : findHeldSequenceControlEffect(selectedRoutineHeldSteps, "followActorPath"); const compatibleHeldSequences = selectedRoutine === null ? [] : selectedRoutine.target.kind === "global" ? getProjectSequences(sequences).filter( (sequence) => getProjectSequenceImpulseSteps(sequence).length > 0 ) : getProjectSequences(sequences).filter((sequence) => { const heldSteps = getProjectSequenceHeldSteps(sequence); if (heldSteps.length === 0) { return false; } return heldSteps.every( (step) => step.type === "controlEffect" && getControlTargetRefKey(step.effect.target) === getControlTargetRefKey(selectedRoutine.target) ); }); const hourTicks = Array.from({ length: HOURS_PER_DAY }, (_, hour) => hour); return (
Sequencer
{mode === "timeline" ? "Place sequences over global project time." : "Compose reusable sequences from engine effects for timeline and interaction playback."}
{mode !== "timeline" ? ( ) : null} {mode !== "sequence" ? ( ) : null}
{mode === "sequence" ? (
) : ( <>
Targets
{hourTicks.map((hour) => (
{String(hour).padStart(2, "0")}
))}
{targetOptions.length === 0 ? (
No sequencer-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) => ( ) ) )}
); }) )}
)}
); } export const ProjectSchedulePane = ProjectSequencerPane;