Skip to main content

motion_core/authoring/timeline/
builder.rs

1//! Fluent construction of an immutable, validated [`Timeline`].
2
3use super::diagnostics::{BoundaryAdjustment, BoundaryEdge, Diagnostics, TimelineWarning};
4use super::{
5    MAX_ADJUSTMENTS, MAX_CLIPS, MAX_TRACKS, Timeline, Track, TrackBuilder, align, invalid, on_track,
6};
7use crate::ErrorReport;
8use crate::authoring::timeline::soundtrack::Soundtrack;
9use crate::{Color, Duration, Error, FrameRate, Size, TICKS_PER_SECOND, Time};
10use std::sync::Arc;
11
12/// Collects tracks, audio and settings, then [`build`](Self::build)s a [`Timeline`].
13///
14/// Fluent methods retain failures and report them all from `build`; `try_*`
15/// variants report immediately. Empty timelines last zero ticks; trailing gaps
16/// contribute duration. Without [`duration`](Self::duration) the timeline lasts
17/// until the end of its tracks and audio (including bus tails, rounded up to a
18/// frame); an explicit duration clips or extends the tracks without retiming
19/// their scenes.
20#[derive(Clone, Debug)]
21pub struct TimelineBuilder {
22    size: Size,
23    rate: FrameRate,
24    background: Option<Color>,
25    tracks: Arc<Vec<Track>>,
26    track_end: Duration,
27    adjustments: Arc<Vec<BoundaryAdjustment>>,
28    soundtrack: Soundtrack,
29    nested_audio: Arc<Vec<crate::AudioClip>>,
30    duration: Option<Duration>,
31    errors: Arc<ErrorReport>,
32}
33
34impl TimelineBuilder {
35    pub(super) fn empty(size: Size, rate: FrameRate) -> Self {
36        Self {
37            size,
38            rate,
39            background: None,
40            tracks: Arc::new(Vec::new()),
41            track_end: Duration::ZERO,
42            adjustments: Arc::new(Vec::new()),
43            soundtrack: Soundtrack::default(),
44            nested_audio: Arc::default(),
45            duration: None,
46            errors: Arc::default(),
47        }
48    }
49
50    /// Reopens a built timeline for further edits, dropping resolved audio.
51    pub(super) fn from_timeline(timeline: &Timeline) -> Self {
52        Self {
53            size: timeline.size,
54            rate: timeline.rate,
55            background: timeline.background,
56            tracks: timeline.tracks.clone(),
57            track_end: timeline
58                .tracks
59                .iter()
60                .map(|track| track.duration)
61                .max()
62                .unwrap_or(Duration::ZERO),
63            adjustments: Arc::new(
64                timeline
65                    .diagnostics
66                    .boundary_adjustments()
67                    .cloned()
68                    .collect(),
69            ),
70            soundtrack: timeline.soundtrack.clone(),
71            nested_audio: Arc::default(),
72            duration: (!timeline.inferred_duration).then_some(timeline.duration),
73            errors: Arc::default(),
74        }
75    }
76
77    pub(super) fn checked(size: Size, rate: FrameRate) -> crate::Result<Self> {
78        if !size.width.is_finite()
79            || !size.height.is_finite()
80            || size.width <= 0.0
81            || size.height <= 0.0
82        {
83            return Err(invalid(
84                "Timeline design dimensions must be finite and positive",
85            ));
86        }
87        if i128::from(rate.numerator())
88            > i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator())
89        {
90            return Err(invalid("Frame rate exceeds one frame per tick"));
91        }
92        Ok(Self::empty(size, rate))
93    }
94
95    pub(super) fn failed(mut self, error: Error) -> Self {
96        Arc::make_mut(&mut self.errors).record(error);
97        self
98    }
99
100    fn defer(self, operation: impl FnOnce(Self) -> crate::Result<Self>) -> Self {
101        let mut previous = self.clone();
102        match operation(self) {
103            Ok(value) => value,
104            Err(error) => {
105                Arc::make_mut(&mut previous.errors).record(error);
106                previous
107            }
108        }
109    }
110
111    /// Adds a full-design background below every track; alpha is retained.
112    pub fn background(mut self, color: Color) -> Self {
113        self.background = Some(color);
114        self
115    }
116
117    /// Constructs one track while accumulating all invalid operations.
118    pub fn track(self, build: impl FnOnce(TrackBuilder) -> TrackBuilder) -> Self {
119        self.defer(|builder| builder.try_track(|track| Ok(build(track))))
120    }
121
122    /// Atomically builds one track; all tracks together admit at most 16384
123    /// clips, 256 tracks and 65536 frame-adjustment warnings per snapshot.
124    pub fn try_track(
125        mut self,
126        build: impl FnOnce(TrackBuilder) -> crate::Result<TrackBuilder>,
127    ) -> crate::Result<Self> {
128        if self.tracks.len() >= MAX_TRACKS {
129            return Err(invalid("Timeline exceeds 256 tracks"));
130        }
131        let builder = TrackBuilder {
132            index: self.tracks.len(),
133            rate: self.rate,
134            clips: Vec::new(),
135            cursor: Time::ZERO,
136            adjustments: Vec::new(),
137            pending: None,
138            errors: ErrorReport::default(),
139            audio: Vec::new(),
140        };
141        let builder = build(builder)?;
142        if builder.index != self.tracks.len() || builder.rate != self.rate {
143            return Err(invalid(
144                "Track builder belongs to a different index or frame grid",
145            ));
146        }
147        let (track, adjustments, audio) = builder.finish()?;
148        let total = self
149            .tracks
150            .iter()
151            .map(|track| track.clips.len())
152            .sum::<usize>()
153            + track.clips.len();
154        if total > MAX_CLIPS || self.adjustments.len() + adjustments.len() > MAX_ADJUSTMENTS {
155            return Err(invalid("Timeline exceeds clip or adjustment allowance"));
156        }
157        self.track_end = self.track_end.max(track.duration);
158        Arc::make_mut(&mut self.tracks).push(track);
159        Arc::make_mut(&mut self.adjustments).extend(adjustments);
160        Arc::make_mut(&mut self.nested_audio).extend(audio);
161        Ok(self)
162    }
163
164    /// Adds timeline audio, deferring validation failures to `build`.
165    pub fn audio(self, clip: crate::AudioClip) -> Self {
166        self.defer(|builder| builder.try_audio(clip))
167    }
168
169    /// Adds audio without snapping its placement to the video grid. Audio
170    /// extends the inferred duration to the next frame boundary, never trims its tail.
171    pub fn try_audio(mut self, clip: crate::AudioClip) -> crate::Result<Self> {
172        if self.soundtrack.clips.len() >= 256 {
173            return Err(invalid("Timeline exceeds 256 audio clips"));
174        }
175        if !(clip.unresolved_file() && clip.parameters().duration.is_none()) {
176            self.soundtrack.clip_end(&clip, self.rate)?;
177        }
178        Arc::make_mut(&mut self.soundtrack.clips).push(clip);
179        Ok(self)
180    }
181
182    /// Sets optional master processing, shared by preview and export.
183    pub fn master(mut self, master: crate::audio::Master) -> Self {
184        self.soundtrack.master = master;
185        self
186    }
187
188    /// Declares a named offline submix before the master.
189    pub fn audio_bus(mut self, bus: crate::audio::AudioBus) -> Self {
190        Arc::make_mut(&mut self.soundtrack.buses).push(bus);
191        self
192    }
193
194    /// Overrides output duration, deferring validation failures to `build`.
195    pub fn duration(self, duration: Duration) -> Self {
196        self.defer(|builder| builder.try_duration(duration))
197    }
198
199    /// Overrides total duration on the nearest frame grid; zero is intentionally empty.
200    pub fn try_duration(mut self, duration: Duration) -> crate::Result<Self> {
201        let (aligned, frame) = align(duration.after_origin(), self.rate)?;
202        if duration > Duration::ZERO && aligned == Time::ZERO {
203            return Err(invalid(
204                "Timeline duration becomes empty after frame alignment",
205            ));
206        }
207        let adjustments = Arc::make_mut(&mut self.adjustments);
208        adjustments.retain(|adjustment| adjustment.edge != BoundaryEdge::Duration);
209        if duration != aligned.since_origin().magnitude() {
210            if adjustments.len() >= MAX_ADJUSTMENTS {
211                return Err(invalid("Timeline exceeds adjustment allowance"));
212            }
213            adjustments.push(BoundaryAdjustment {
214                track: None,
215                clip: None,
216                edge: BoundaryEdge::Duration,
217                requested: duration.after_origin(),
218                aligned,
219                frame,
220            });
221        }
222        self.duration = Some(aligned.since_origin().magnitude());
223        Ok(self)
224    }
225
226    /// Validates everything retained so far and freezes the timeline. All
227    /// deferred failures, invalid audio settings and invalid nested scenes are
228    /// reported together as one [`ErrorReport`].
229    pub fn build(self) -> Result<Timeline, ErrorReport> {
230        let mut errors = self.errors.as_ref().clone();
231        if let Err(error) = self.soundtrack.validate() {
232            errors.record(error);
233        }
234        for (track_index, track) in self.tracks.iter().enumerate() {
235            for clip in track.clips.iter() {
236                if let Err(error) = clip.scene.validate() {
237                    errors.record(on_track(track_index, format!("clip[{}]: {error}", clip.id)));
238                }
239            }
240        }
241        let audio_end = match self.soundtrack.end(self.rate) {
242            Ok(end) => end,
243            Err(error) => {
244                errors.record(error);
245                Duration::ZERO
246            }
247        };
248        if !errors.is_empty() {
249            return Err(errors);
250        }
251        let mut soundtrack = self.soundtrack;
252        if !self.nested_audio.is_empty() {
253            let mut clips = soundtrack.clips.as_ref().clone();
254            clips.extend(self.nested_audio.iter().cloned());
255            if clips.len() > 256 {
256                return Err(invalid("Nested timeline exceeds 256 audio clips").into());
257            }
258            soundtrack.clips = Arc::new(clips);
259        }
260        let content = self.track_end.max(audio_end);
261        let mut warnings = self
262            .adjustments
263            .iter()
264            .cloned()
265            .map(TimelineWarning::Boundary)
266            .collect::<Vec<_>>();
267        if let Some(duration) = self.duration.filter(|duration| *duration < content) {
268            warnings.push(TimelineWarning::Truncated { duration, content });
269        }
270        Ok(Timeline {
271            size: self.size,
272            rate: self.rate,
273            background: self.background,
274            tracks: self.tracks,
275            duration: self.duration.unwrap_or(content),
276            inferred_duration: self.duration.is_none(),
277            diagnostics: Diagnostics::new(warnings),
278            soundtrack,
279        })
280    }
281}