motion_core/authoring/timeline/
builder.rs1use 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#[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 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 pub fn background(mut self, color: Color) -> Self {
113 self.background = Some(color);
114 self
115 }
116
117 pub fn track(self, build: impl FnOnce(TrackBuilder) -> TrackBuilder) -> Self {
119 self.defer(|builder| builder.try_track(|track| Ok(build(track))))
120 }
121
122 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 pub fn audio(self, clip: crate::AudioClip) -> Self {
166 self.defer(|builder| builder.try_audio(clip))
167 }
168
169 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 pub fn master(mut self, master: crate::audio::Master) -> Self {
184 self.soundtrack.master = master;
185 self
186 }
187
188 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 pub fn duration(self, duration: Duration) -> Self {
196 self.defer(|builder| builder.try_duration(duration))
197 }
198
199 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 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}