From 36f81d74b0ec693b25626c55991f4d089e340da8 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 01:13:32 +0200 Subject: [PATCH] Refactor sequence handling and add new sequences panel --- src/app/App.tsx | 181 ++---- src/app/ProjectSchedulePane.tsx | 38 +- src/app/ProjectSequencesPanel.tsx | 528 ++++++++++++++++++ src/app/app.css | 143 ----- src/document/migrate-scene-document.ts | 6 +- .../domain/runtime-interaction-system.test.ts | 3 - .../domain/runtime-project-scheduler.test.ts | 133 +---- .../domain/scene-document-validation.test.ts | 3 - 8 files changed, 585 insertions(+), 450 deletions(-) create mode 100644 src/app/ProjectSequencesPanel.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index b3c370f3..9e6a8036 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -369,10 +369,6 @@ import { Panel } from "../shared-ui/Panel"; import { ProjectDialoguesPanel } from "./ProjectDialoguesPanel"; import { ProjectSequencerPane } from "./ProjectSequencerPane"; import { - DEFAULT_HELD_SEQUENCE_CLIP_DURATION_MINUTES, - DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES, - DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES, - getNextSequenceClipLane, getProjectSequenceImpulseSteps } from "../sequencer/project-sequence-steps"; import { @@ -4246,7 +4242,7 @@ export function App({ store, initialStatusMessage }: AppProps) { editorState.projectDocument.scheduler.routines[routineId] ?? null; if (currentRoutine === null) { - setStatusMessage("Selected sequencer clip no longer exists."); + setStatusMessage("Selected sequence placement no longer exists."); return; } @@ -4291,7 +4287,7 @@ export function App({ store, initialStatusMessage }: AppProps) { if (targetOption === null) { setStatusMessage( - "Author a sequencer-addressable control target before creating a clip." + "Author a sequencer-addressable control target before creating a sequence placement." ); return; } @@ -4324,8 +4320,8 @@ export function App({ store, initialStatusMessage }: AppProps) { applyProjectScheduler( nextScheduler, - "Create project sequencer clip", - `Created sequencer clip for ${targetOption.label}.` + "Create sequence placement", + `Created a sequence placement for ${targetOption.label}.` ); setSchedulePaneOpen(true); setSequencerMode("timeline"); @@ -4343,8 +4339,8 @@ export function App({ store, initialStatusMessage }: AppProps) { applyProjectScheduler( nextScheduler, - "Delete project sequencer clip", - "Deleted sequencer clip." + "Delete sequence placement", + "Deleted sequence placement." ); if (selectedScheduleRoutineId === routineId) { setSelectedScheduleRoutineId(null); @@ -4384,11 +4380,8 @@ export function App({ store, initialStatusMessage }: AppProps) { const createDefaultProjectSequenceControlStep = ( stepClass: "held" | "impulse", targetKey: string, - previousStep?: Extract | null, - timing?: Partial< - Pick - > - ): Extract => { + previousStep?: Extract | null + ): Extract => { const targetOption = resolveSequenceControlTargetOption(targetKey); if (targetOption === null) { @@ -4404,13 +4397,6 @@ export function App({ store, initialStatusMessage }: AppProps) { } return { - startMinute: timing?.startMinute ?? 0, - durationMinutes: - timing?.durationMinutes ?? - (stepClass === "held" - ? DEFAULT_HELD_SEQUENCE_CLIP_DURATION_MINUTES - : DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES), - lane: timing?.lane ?? 0, stepClass, type: "controlEffect", effect: createProjectScheduleEffectFromOption({ @@ -4421,9 +4407,6 @@ export function App({ store, initialStatusMessage }: AppProps) { }; }; - const getNextProjectSequenceClipLane = (sequence: ProjectSequence): number => - getNextSequenceClipLane(sequence.clips); - const updateProjectSequence = ( sequenceId: string, label: string, @@ -4446,13 +4429,13 @@ export function App({ store, initialStatusMessage }: AppProps) { stepIndex: number, label: string, successMessage: string, - mutate: (step: ProjectSequence["clips"][number]) => void + mutate: (step: ProjectSequence["effects"][number]) => void ) => { updateProjectSequence(sequenceId, label, successMessage, (sequence) => { - const step = sequence.clips[stepIndex]; + const step = sequence.effects[stepIndex]; if (step === undefined) { - throw new Error("Selected project sequence clip no longer exists."); + throw new Error("Selected project sequence effect no longer exists."); } mutate(step); @@ -4460,9 +4443,7 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const handleAddProjectSequence = () => { - const nextSequence = createProjectSequence({ - durationMinutes: DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES - }); + const nextSequence = createProjectSequence(); updateProjectSequences( "Add project sequence", @@ -4540,21 +4521,14 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectSequence( sequenceId, stepClass === "held" - ? "Add held project sequence control clip" - : "Add impulse project sequence control clip", + ? "Add held project sequence effect" + : "Add impulse project sequence effect", stepClass === "held" - ? "Added held control clip." - : "Added impulse control clip.", + ? "Added held effect." + : "Added impulse effect.", (sequence) => { - sequence.clips.push( - createDefaultProjectSequenceControlStep(stepClass, targetKey, null, { - startMinute: 0, - durationMinutes: - stepClass === "held" - ? DEFAULT_HELD_SEQUENCE_CLIP_DURATION_MINUTES - : DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES, - lane: getNextProjectSequenceClipLane(sequence) - }) + sequence.effects.push( + createDefaultProjectSequenceControlStep(stepClass, targetKey) ); } ); @@ -4567,12 +4541,9 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectSequence( sequenceId, "Add project sequence dialogue clip", - "Added dialogue clip.", + "Added dialogue effect.", (sequence) => { - sequence.clips.push({ - startMinute: 0, - durationMinutes: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES, - lane: getNextProjectSequenceClipLane(sequence), + sequence.effects.push({ stepClass: "impulse", type: "startDialogue", dialogueId @@ -4587,10 +4558,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ) => { updateProjectSequence( sequenceId, - "Delete project sequence clip", - "Deleted sequence clip.", + "Delete project sequence effect", + "Deleted sequence effect.", (sequence) => { - sequence.clips.splice(stepIndex, 1); + sequence.effects.splice(stepIndex, 1); } ); }; @@ -4794,74 +4765,6 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updateProjectSequenceDurationMinutes = ( - sequenceId: string, - durationMinutes: number - ) => { - updateProjectSequence( - sequenceId, - "Set project sequence duration", - "Updated sequence duration.", - (sequence) => { - if (!Number.isFinite(durationMinutes) || durationMinutes < 1) { - throw new Error( - "Sequence duration must be a finite number of minutes greater than zero." - ); - } - - sequence.durationMinutes = Math.max(1, Math.trunc(durationMinutes)); - - for (const clip of sequence.clips) { - if (clip.startMinute >= sequence.durationMinutes) { - clip.startMinute = Math.max(0, sequence.durationMinutes - 1); - } - - if (clip.startMinute + clip.durationMinutes > sequence.durationMinutes) { - clip.durationMinutes = Math.max( - 1, - sequence.durationMinutes - clip.startMinute - ); - } - } - } - ); - }; - - const updateProjectSequenceClipTiming = ( - sequenceId: string, - stepIndex: number, - timing: { - startMinute: number; - durationMinutes: number; - lane: number; - } - ) => { - updateProjectSequence(sequenceId, "Set project sequence clip timing", "Updated sequence clip timing.", (sequence) => { - const clip = sequence.clips[stepIndex]; - - if (clip === undefined) { - throw new Error("Selected project sequence clip no longer exists."); - } - - if ( - !Number.isFinite(timing.startMinute) || - !Number.isFinite(timing.durationMinutes) || - !Number.isFinite(timing.lane) - ) { - throw new Error("Sequence clip timing must use finite minute values."); - } - - clip.startMinute = Math.min( - Math.max(0, Math.trunc(timing.startMinute)), - Math.max(0, sequence.durationMinutes - 1) - ); - clip.durationMinutes = Math.min( - Math.max(1, Math.trunc(timing.durationMinutes)), - Math.max(1, sequence.durationMinutes - clip.startMinute) - ); - clip.lane = Math.max(0, Math.trunc(timing.lane)); - }); - }; const updateWorldTimeOfDaySettings = ( label: string, @@ -7473,7 +7376,7 @@ export function App({ store, initialStatusMessage }: AppProps) { if (defaultSequence === null) { openSequencerSequenceEditor(); setStatusMessage( - "Open the Sequencer sequence editor and author a sequence with at least one impulse clip before adding a sequence link." + "Open the Sequencer sequence editor and author a sequence with at least one impulse effect before adding a sequence link." ); return; } @@ -8008,7 +7911,7 @@ export function App({ store, initialStatusMessage }: AppProps) { if (defaultSequence === null) { openSequencerSequenceEditor(); setStatusMessage( - "Open the Sequencer sequence editor and author a sequence with at least one impulse clip before switching this link to run sequence." + "Open the Sequencer sequence editor and author a sequence with at least one impulse effect before switching this link to run sequence." ); return; } @@ -8787,7 +8690,7 @@ export function App({ store, initialStatusMessage }: AppProps) {
Run Sequence links can only reference sequences that - contain at least one impulse clip. + contain at least one impulse effect.
); @@ -11431,7 +11334,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer target", - "Retargeted sequencer clip.", + "Retargeted sequence placement.", (routine) => { const targetOption = resolveProjectScheduleTargetOption(targetKey); @@ -11484,8 +11387,8 @@ export function App({ store, initialStatusMessage }: AppProps) { onSetRoutineTitle={(routineId, title) => updateProjectScheduleRoutine( routineId, - "Rename project sequencer clip", - "Updated sequencer clip title.", + "Rename sequence placement", + "Updated sequence placement title.", (routine) => { routine.title = title.trim(); } @@ -11494,10 +11397,10 @@ export function App({ store, initialStatusMessage }: AppProps) { onSetRoutineEnabled={(routineId, enabled) => updateProjectScheduleRoutine( routineId, - "Toggle project sequencer clip", + "Toggle sequence placement", enabled - ? "Enabled sequencer clip." - : "Disabled sequencer clip.", + ? "Enabled sequence placement." + : "Disabled sequence placement.", (routine) => { routine.enabled = enabled; } @@ -11507,7 +11410,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer start time", - "Updated sequencer clip start time.", + "Updated sequence placement start time.", (routine) => { routine.startHour = normalizeTimeOfDayHours(startHour); if (routine.startHour === routine.endHour) { @@ -11522,7 +11425,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer end time", - "Updated sequencer clip end time.", + "Updated sequence placement end time.", (routine) => { routine.endHour = normalizeTimeOfDayHours(endHour); if (routine.startHour === routine.endHour) { @@ -11537,7 +11440,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer priority", - "Updated sequencer clip priority.", + "Updated sequence placement priority.", (routine) => { if (!Number.isFinite(priority)) { throw new Error( @@ -11554,7 +11457,7 @@ export function App({ store, initialStatusMessage }: AppProps) { routineId, "Set project sequencer sequence", sequenceId === null - ? "Timeline clip now uses inline held clips." + ? "Sequence placement now uses inline held effects." : "Timeline clip now resolves a project sequence.", (routine) => { routine.sequenceId = sequenceId; @@ -11565,7 +11468,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer effect", - "Updated sequencer clip effect.", + "Updated sequence placement effect.", (routine) => { if (routine.target.kind === "actor") { throw new Error( @@ -11598,7 +11501,7 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer numeric value", - "Updated sequencer clip value.", + "Updated sequence placement value.", (routine) => { if (!Number.isFinite(value) || value < 0) { throw new Error( @@ -11610,7 +11513,7 @@ export function App({ store, initialStatusMessage }: AppProps) { if (effect === undefined) { throw new Error( - "The current sequencer clip does not expose a numeric value." + "The current sequence placement does not expose a numeric value." ); } @@ -11635,13 +11538,13 @@ export function App({ store, initialStatusMessage }: AppProps) { updateProjectScheduleRoutine( routineId, "Set project sequencer color", - "Updated sequencer clip color.", + "Updated sequence placement color.", (routine) => { const effect = routine.effects[0]; if (effect === undefined) { throw new Error( - "The current sequencer clip does not expose a color value." + "The current sequence placement does not expose a color value." ); } @@ -11888,7 +11791,6 @@ export function App({ store, initialStatusMessage }: AppProps) { } ) } - onSetSequenceDurationMinutes={updateProjectSequenceDurationMinutes} onAddHeldControlStep={(sequenceId, targetKey) => handleAddProjectSequenceControlStep( sequenceId, @@ -11905,7 +11807,6 @@ export function App({ store, initialStatusMessage }: AppProps) { } onAddDialogueStep={handleAddProjectSequenceDialogueStep} onDeleteStep={handleDeleteProjectSequenceStep} - onSetClipTiming={updateProjectSequenceClipTiming} onSetControlStepTarget={updateProjectSequenceControlStepTarget} onSetControlStepEffectOption={ updateProjectSequenceControlStepEffectOption diff --git a/src/app/ProjectSchedulePane.tsx b/src/app/ProjectSchedulePane.tsx index a55f48aa..abec066a 100644 --- a/src/app/ProjectSchedulePane.tsx +++ b/src/app/ProjectSchedulePane.tsx @@ -71,20 +71,10 @@ interface ProjectSequencerPaneProps { onSetActorRoutinePathSpeed(routineId: string, speed: number): void; onSetActorRoutinePathLoop(routineId: string, loop: boolean): void; onSetSequenceTitle(sequenceId: string, title: string): void; - onSetSequenceDurationMinutes(sequenceId: string, durationMinutes: number): void; onAddHeldControlStep(sequenceId: string, targetKey: string): void; onAddImpulseControlStep(sequenceId: string, targetKey: string): void; onAddDialogueStep(sequenceId: string, dialogueId: string): void; onDeleteStep(sequenceId: string, stepIndex: number): void; - onSetClipTiming( - sequenceId: string, - stepIndex: number, - timing: { - startMinute: number; - durationMinutes: number; - lane: number; - } - ): void; onSetControlStepTarget( sequenceId: string, stepIndex: number, @@ -324,12 +314,10 @@ export function ProjectSequencerPane({ onSetActorRoutinePathSpeed, onSetActorRoutinePathLoop, onSetSequenceTitle, - onSetSequenceDurationMinutes, onAddHeldControlStep, onAddImpulseControlStep, onAddDialogueStep, onDeleteStep, - onSetClipTiming, onSetControlStepTarget, onSetControlStepEffectOption, onSetControlStepNumericValue, @@ -399,8 +387,8 @@ export function ProjectSequencerPane({
Sequencer
{mode === "timeline" - ? "Place sequence clips over global project time." - : "Compose reusable sequences from clips for timeline and interaction playback."} + ? "Place sequences over global project time." + : "Compose reusable sequences from engine effects for timeline and interaction playback."}
@@ -459,12 +447,10 @@ export function ProjectSequencerPane({ onAddSequence={onAddSequence} onDeleteSequence={onDeleteSequence} onSetSequenceTitle={onSetSequenceTitle} - onSetSequenceDurationMinutes={onSetSequenceDurationMinutes} onAddHeldControlStep={onAddHeldControlStep} onAddImpulseControlStep={onAddImpulseControlStep} onAddDialogueStep={onAddDialogueStep} onDeleteStep={onDeleteStep} - onSetClipTiming={onSetClipTiming} onSetControlStepTarget={onSetControlStepTarget} onSetControlStepEffectOption={onSetControlStepEffectOption} onSetControlStepNumericValue={onSetControlStepNumericValue} @@ -562,12 +548,12 @@ export function ProjectSequencerPane({
Day-specific routine filters are preserved for compatibility. - New sequencer authoring should prefer timeline clips; a later + New sequencer authoring should prefer sequence placements; a later multi-day timeline will replace this legacy filter.
diff --git a/src/app/ProjectSequencesPanel.tsx b/src/app/ProjectSequencesPanel.tsx new file mode 100644 index 00000000..a9af73a2 --- /dev/null +++ b/src/app/ProjectSequencesPanel.tsx @@ -0,0 +1,528 @@ +import type { KeyboardEvent as ReactKeyboardEvent } from "react"; + +import { formatControlEffectValue, getControlTargetRefKey } from "../controls/control-surface"; +import { type ProjectDialogueLibrary, getProjectDialogues } from "../dialogues/project-dialogues"; +import { + getProjectScheduleEffectOptionId, + getProjectScheduleTargetOptionByKey, + listProjectScheduleEffectOptions, + type ProjectScheduleEffectOptionId, + type ProjectScheduleTargetOption +} from "../scheduler/project-schedule-control-options"; +import { + getProjectSequenceHeldSteps, + getProjectSequenceImpulseSteps, + getSequenceEffectLabel, + type SequenceEffect +} from "../sequencer/project-sequence-steps"; +import { + getProjectSequences, + type ProjectSequenceLibrary +} from "../sequencer/project-sequences"; + +interface ProjectSequencesPanelProps { + sequences: ProjectSequenceLibrary; + dialogues: ProjectDialogueLibrary; + targetOptions: ProjectScheduleTargetOption[]; + selectedSequenceId: string | null; + onSelectSequence(sequenceId: string | null): void; + onAddSequence(): void; + onDeleteSequence(sequenceId: string): 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; + 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; +} + +function commitOnEnter( + event: ReactKeyboardEvent, + commit: () => void +) { + if (event.key !== "Enter") { + return; + } + + event.currentTarget.blur(); + commit(); +} + +function getControlEffectNumericValue( + effect: Extract +): number | null { + switch (effect.effect.type) { + case "setSoundVolume": + return effect.effect.volume; + case "setLightIntensity": + case "setAmbientLightIntensity": + case "setSunLightIntensity": + return effect.effect.intensity; + default: + return null; + } +} + +function getControlEffectColorValue( + effect: Extract +): string | null { + switch (effect.effect.type) { + case "setLightColor": + case "setAmbientLightColor": + case "setSunLightColor": + return effect.effect.colorHex; + default: + return null; + } +} + +export function ProjectSequencesPanel({ + sequences, + dialogues, + targetOptions, + selectedSequenceId, + onSelectSequence, + onAddSequence, + onDeleteSequence, + onSetSequenceTitle, + onAddHeldControlStep, + onAddImpulseControlStep, + onAddDialogueStep, + onDeleteStep, + onSetControlStepTarget, + onSetControlStepEffectOption, + onSetControlStepNumericValue, + onSetControlStepColorValue, + onSetControlStepAnimationClip, + onSetControlStepAnimationLoop, + onSetDialogueStepDialogueId +}: ProjectSequencesPanelProps) { + const sequenceList = getProjectSequences(sequences); + const dialogueList = getProjectDialogues(dialogues); + const selectedSequence = + selectedSequenceId === null + ? null + : sequences.sequences[selectedSequenceId] ?? null; + + return ( +
+
Sequences
+ {sequenceList.length === 0 ? ( +
No project sequences authored yet.
+ ) : ( +
+ {sequenceList.map((sequence) => ( +
+
+ + +
+
+ ))} +
+ )} + +
+ +
+ + {selectedSequence === null ? ( +
+ Select a sequence to edit its title and effects. +
+ ) : ( +
+
+ A sequence is a reusable bundle of engine effects. Held effects stay + active for the whole scheduled window. Impulse effects fire when the + sequence is started from an interaction. +
+ + +
Effects
+ {selectedSequence.effects.length === 0 ? ( +
+ Add held control, impulse control, or dialogue effects. +
+ ) : ( +
+ {selectedSequence.effects.map((effect, effectIndex) => { + if (effect.type === "controlEffect") { + const targetKey = getControlTargetRefKey(effect.effect.target); + const targetOption = + getProjectScheduleTargetOptionByKey(targetOptions, targetKey); + const effectOptions = + targetOption === null + ? [] + : listProjectScheduleEffectOptions(targetOption); + const effectOptionId = + targetOption === null + ? null + : (() => { + try { + return getProjectScheduleEffectOptionId(effect.effect); + } catch { + return null; + } + })(); + + return ( +
+
+
+ {getSequenceEffectLabel(effect)} +
+ +
+ + {targetOption === null || effectOptionId === null ? ( +
+ {formatControlEffectValue(effect.effect)}. This effect is + preserved, but the current editor can only edit targets + and effects exposed through the existing control catalog. +
+ ) : ( + <> +
+ + +
+ + + {effectOptions.find((option) => option.id === effectOptionId) + ?.valueKind === "number" ? ( + + ) : null} + + {effectOptions.find((option) => option.id === effectOptionId) + ?.valueKind === "color" ? ( + + ) : null} + + {effect.effect.type === "playModelAnimation" ? ( + <> + + + + ) : null} + + )} +
+ ); + } + + if (effect.type === "startDialogue") { + return ( +
+
+
+ {getSequenceEffectLabel(effect)} +
+ +
+ +
+ ); + } + + return ( +
+
+
+ {getSequenceEffectLabel(effect)} +
+ +
+
+ This impulse effect is preserved, but the current editor only + exposes direct editing for dialogue and control effects. +
+
+ ); + })} +
+ )} + +
+ + + +
+
+ )} +
+ ); +} diff --git a/src/app/app.css b/src/app/app.css index 5eac4280..b3373e1f 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -1119,149 +1119,6 @@ button:disabled { white-space: nowrap; } -.sequence-timeline { - display: flex; - flex-direction: column; - gap: 10px; -} - -.sequence-timeline__summary { - color: var(--color-muted); - font-size: 0.8rem; -} - -.sequence-timeline__ruler { - position: relative; - height: 24px; - margin-left: 72px; -} - -.sequence-timeline__hour { - position: absolute; - top: 0; - transform: translateX(-50%); - color: var(--color-muted); - font-size: 0.7rem; - white-space: nowrap; -} - -.sequence-timeline__track { - position: relative; - margin-left: 72px; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01)), - rgba(9, 13, 18, 0.9); - overflow: hidden; -} - -.sequence-timeline__lane { - position: absolute; - left: 0; - right: 0; - height: 52px; - border-top: 1px solid rgba(255, 255, 255, 0.04); -} - -.sequence-timeline__lane:first-child { - border-top: 0; -} - -.sequence-timeline__lane-label { - position: absolute; - left: -68px; - top: 50%; - width: 56px; - transform: translateY(-50%); - color: var(--color-muted); - font-size: 0.72rem; - text-align: right; -} - -.sequence-timeline__minor-grid, -.sequence-timeline__major-grid { - position: absolute; - top: 0; - bottom: 0; - width: 1px; - pointer-events: none; -} - -.sequence-timeline__minor-grid { - background: rgba(255, 255, 255, 0.04); -} - -.sequence-timeline__major-grid { - background: rgba(255, 255, 255, 0.11); -} - -.sequence-timeline__clip { - position: absolute; - min-width: 18px; - height: 36px; - display: flex; - align-items: center; - gap: 8px; - padding: 0 10px; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.12); - cursor: grab; - overflow: hidden; -} - -.sequence-timeline__clip:active { - cursor: grabbing; -} - -.sequence-timeline__clip--held { - background: linear-gradient(135deg, #f5b66f, #d17f4a); - color: #24150d; -} - -.sequence-timeline__clip--impulse { - background: linear-gradient(135deg, #8ec7d9, #5d8aa1); - color: #0d1a22; -} - -.sequence-timeline__clip--selected { - box-shadow: 0 0 0 2px rgba(255, 250, 240, 0.35); -} - -.sequence-timeline__clip-title { - min-width: 0; - flex: 1 1 auto; - overflow: hidden; - font-size: 0.76rem; - font-weight: 700; - text-overflow: ellipsis; - white-space: nowrap; -} - -.sequence-timeline__clip-time { - font-size: 0.68rem; - opacity: 0.8; - white-space: nowrap; -} - -.sequence-timeline__clip-handle { - position: absolute; - top: 4px; - bottom: 4px; - width: 8px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.45); - cursor: ew-resize; -} - -.sequence-timeline__clip-handle--start { - left: 4px; -} - -.sequence-timeline__clip-handle--end { - right: 4px; -} - .schedule-days { display: flex; flex-wrap: wrap; diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 6177874f..2d0bd026 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -3541,7 +3541,7 @@ function readProjectSequenceEffect(value: unknown, label: string): SequenceClip }; case "startDialogue": if (stepClass !== "impulse") { - throw new Error(`${label}.startDialogue clips must use the impulse class.`); + throw new Error(`${label}.startDialogue effects must use the impulse class.`); } return { @@ -3551,7 +3551,7 @@ function readProjectSequenceEffect(value: unknown, label: string): SequenceClip }; case "teleportPlayer": if (stepClass !== "impulse") { - throw new Error(`${label}.teleportPlayer clips must use the impulse class.`); + throw new Error(`${label}.teleportPlayer effects must use the impulse class.`); } return { @@ -3564,7 +3564,7 @@ function readProjectSequenceEffect(value: unknown, label: string): SequenceClip }; case "toggleVisibility": if (stepClass !== "impulse") { - throw new Error(`${label}.toggleVisibility clips must use the impulse class.`); + throw new Error(`${label}.toggleVisibility effects must use the impulse class.`); } return { diff --git a/tests/domain/runtime-interaction-system.test.ts b/tests/domain/runtime-interaction-system.test.ts index 1905e531..1ec54442 100644 --- a/tests/domain/runtime-interaction-system.test.ts +++ b/tests/domain/runtime-interaction-system.test.ts @@ -649,9 +649,6 @@ describe("RuntimeInteractionSystem", () => { title: "Console Dialogue Sequence", steps: [ { - startMinute: 0, - durationMinutes: 1, - lane: 0, stepClass: "impulse", type: "startDialogue", dialogueId: "dialogue-sequence" diff --git a/tests/domain/runtime-project-scheduler.test.ts b/tests/domain/runtime-project-scheduler.test.ts index 0d1685e5..2ab0b430 100644 --- a/tests/domain/runtime-project-scheduler.test.ts +++ b/tests/domain/runtime-project-scheduler.test.ts @@ -22,8 +22,7 @@ import { } from "../../src/sequencer/project-sequences"; import { applyRuntimeProjectScheduleToControlState, - resolveRuntimeProjectScheduleState, - type RuntimeProjectSchedulePathDefinition + resolveRuntimeProjectScheduleState } from "../../src/runtime-three/runtime-project-scheduler"; describe("runtime project scheduler", () => { @@ -180,9 +179,6 @@ describe("runtime project scheduler", () => { title: "Vendor Open Sequence", steps: [ { - startMinute: 0, - durationMinutes: 480, - lane: 0, stepClass: "held", type: "controlEffect", effect: createSetActorPresenceControlEffect({ @@ -191,9 +187,6 @@ describe("runtime project scheduler", () => { }) }, { - startMinute: 0, - durationMinutes: 480, - lane: 1, stepClass: "held", type: "controlEffect", effect: createPlayActorAnimationControlEffect({ @@ -240,130 +233,6 @@ describe("runtime project scheduler", () => { ); }); - it("resolves timed held sequence clips against local routine minutes", () => { - const actorTarget = createActorControlTargetRef("actor-timed-sequence"); - const scheduler = createEmptyProjectScheduler(); - const sequences = createEmptyProjectSequenceLibrary(); - sequences.sequences["sequence-timed-open"] = createProjectSequence({ - id: "sequence-timed-open", - title: "Timed Open Sequence", - durationMinutes: 180, - clips: [ - { - startMinute: 30, - durationMinutes: 60, - lane: 0, - stepClass: "held", - type: "controlEffect", - effect: createSetActorPresenceControlEffect({ - target: actorTarget, - active: true - }) - }, - { - startMinute: 60, - durationMinutes: 90, - lane: 1, - stepClass: "held", - type: "controlEffect", - effect: createFollowActorPathControlEffect({ - target: actorTarget, - pathId: "path-market", - speed: 2, - loop: false, - progressMode: "deriveFromTime" - }) - } - ] - }); - scheduler.routines["routine-timed-open"] = createProjectScheduleRoutine({ - id: "routine-timed-open", - title: "Timed Open", - target: actorTarget, - startHour: 9, - endHour: 12, - sequenceId: "sequence-timed-open", - effects: [ - createSetActorPresenceControlEffect({ - target: actorTarget, - active: false - }) - ] - }); - const pathsById = new Map([ - [ - "path-market", - { - id: "path-market", - loop: false, - points: [ - { position: { x: 0, y: 0, z: 0 } }, - { position: { x: 4, y: 0, z: 0 } } - ], - segments: [ - { - start: { x: 0, y: 0, z: 0 }, - end: { x: 4, y: 0, z: 0 }, - length: 4, - distanceStart: 0, - distanceEnd: 4, - tangent: { x: 1, y: 0, z: 0 } - } - ], - totalLength: 4 - } - ] - ]); - - const beforePresence = resolveRuntimeProjectScheduleState({ - scheduler, - sequences, - actorIds: ["actor-timed-sequence"], - dayNumber: 1, - timeOfDayHours: 9.25, - pathsById - }); - const duringPresence = resolveRuntimeProjectScheduleState({ - scheduler, - sequences, - actorIds: ["actor-timed-sequence"], - dayNumber: 1, - timeOfDayHours: 9.75, - pathsById - }); - const duringPath = resolveRuntimeProjectScheduleState({ - scheduler, - sequences, - actorIds: ["actor-timed-sequence"], - dayNumber: 1, - timeOfDayHours: 10.5, - pathsById - }); - - expect(beforePresence.actors[0]).toEqual( - expect.objectContaining({ - active: false - }) - ); - expect(duringPresence.actors[0]).toEqual( - expect.objectContaining({ - active: true - }) - ); - expect(duringPath.actors[0].pathEffect).toEqual( - expect.objectContaining({ - type: "followActorPath", - pathId: "path-market" - }) - ); - expect(duringPath.actors[0].resolvedPath).toEqual( - expect.objectContaining({ - elapsedHours: 0.5, - distance: 1 - }) - ); - }); - it("applies non-actor scheduler effects over baseline control state and restores defaults when inactive", () => { const lightTarget = createLightControlTargetRef( "pointLight", diff --git a/tests/domain/scene-document-validation.test.ts b/tests/domain/scene-document-validation.test.ts index c1e1f066..594cfb1a 100644 --- a/tests/domain/scene-document-validation.test.ts +++ b/tests/domain/scene-document-validation.test.ts @@ -267,9 +267,6 @@ describe("validateSceneDocument", () => { title: "Held Only", steps: [ { - startMinute: 0, - durationMinutes: 120, - lane: 0, stepClass: "held", type: "controlEffect", effect: createSetSoundVolumeControlEffect({