motion_core/authoring/timeline/
track.rs1use super::{BoundaryAdjustment, BoundaryEdge, MAX_CLIPS, Scene, add_time, align, on_track};
4use crate::authoring::{scene, transition};
5use crate::{Duration, Error, FrameRate, SceneContext, Time};
6use std::{fmt, sync::Arc};
7
8#[derive(Clone)]
10pub struct Clip {
11 pub(super) id: usize,
12 pub(super) start: Time,
13 pub(super) duration: Duration,
14 pub(super) scene: Arc<dyn Scene>,
15 pub(super) transition_in: Option<TransitionIn>,
16}
17
18#[derive(Clone)]
19pub(super) struct TransitionIn {
20 pub(super) effect: Arc<dyn transition::Transition>,
21 pub(super) duration: Duration,
22}
23
24impl fmt::Debug for Clip {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 formatter
27 .debug_struct("Clip")
28 .field("id", &self.id)
29 .field("start", &self.start)
30 .field("duration", &self.duration)
31 .finish_non_exhaustive()
32 }
33}
34
35impl Clip {
36 pub fn id(&self) -> usize {
38 self.id
39 }
40
41 pub fn start(&self) -> Time {
43 self.start
44 }
45
46 pub fn duration(&self) -> Duration {
48 self.duration
49 }
50
51 pub fn end(&self) -> Time {
53 self.start + self.duration
54 }
55
56 pub fn scene(&self) -> &dyn Scene {
58 self.scene.as_ref()
59 }
60
61 pub fn transition_in(&self) -> Option<(&dyn transition::Transition, Duration)> {
63 self.transition_in
64 .as_ref()
65 .map(|value| (value.effect.as_ref(), value.duration))
66 }
67}
68
69#[derive(Clone, Debug)]
71pub struct Track {
72 pub(super) clips: Arc<[Clip]>,
73 pub(super) duration: Duration,
74}
75
76impl Track {
77 pub fn clips(&self) -> &[Clip] {
79 &self.clips
80 }
81
82 pub fn duration(&self) -> Duration {
84 self.duration
85 }
86
87 pub fn active_clip(&self, time: Time) -> Option<&Clip> {
90 let index = self
91 .clips
92 .partition_point(|clip| clip.start <= time)
93 .checked_sub(1)?;
94 self.clips.get(index).filter(|clip| time < clip.end())
95 }
96}
97
98pub struct TrackBuilder {
101 pub(super) index: usize,
102 pub(super) rate: FrameRate,
103 pub(super) clips: Vec<Clip>,
104 pub(super) cursor: Time,
105 pub(super) adjustments: Vec<BoundaryAdjustment>,
106 pub(super) pending: Option<Arc<dyn transition::Transition>>,
107 pub(super) errors: Vec<Arc<Error>>,
108}
109
110impl TrackBuilder {
111 fn defer(mut self, operation: impl FnOnce(&mut Self) -> crate::Result<()>) -> Self {
112 if let Err(error) = operation(&mut self)
113 && self.errors.len() < 1024
114 {
115 self.errors.push(Arc::new(on_track(
116 self.index,
117 format!("clip[{}]: {error}", self.clips.len()),
118 )));
119 }
120 self
121 }
122
123 pub fn push(self, scene: impl Scene + 'static) -> Self {
125 let cursor = self.cursor;
126 self.defer(|builder| builder.place_checked(cursor, scene))
127 }
128
129 pub fn scene<Output: scene::IntoSceneOutput + 'static>(
131 self,
132 duration: Duration,
133 build: impl Fn(&SceneContext<'_>) -> Output + Send + Sync + 'static,
134 ) -> Self {
135 self.push(scene::scene(build).with_duration(duration))
136 }
137
138 pub fn transition(
140 self,
141 effect: impl transition::Transition + 'static,
142 duration: Duration,
143 ) -> Self {
144 self.defer(|builder| builder.transition_checked(effect, duration))
145 }
146
147 pub fn place(self, start: Time, scene: impl Scene + 'static) -> Self {
149 self.defer(|builder| builder.place_checked(start, scene))
150 }
151
152 pub fn gap(self, duration: Duration) -> Self {
154 self.defer(|builder| builder.gap_checked(duration))
155 }
156
157 pub fn try_transition(
159 mut self,
160 effect: impl transition::Transition + 'static,
161 duration: Duration,
162 ) -> crate::Result<Self> {
163 self.transition_checked(effect, duration)?;
164 Ok(self)
165 }
166
167 pub fn try_place(mut self, start: Time, scene: impl Scene + 'static) -> crate::Result<Self> {
169 self.place_checked(start, scene)?;
170 Ok(self)
171 }
172
173 pub fn try_gap(mut self, duration: Duration) -> crate::Result<Self> {
175 self.gap_checked(duration)?;
176 Ok(self)
177 }
178
179 pub fn try_push(self, scene: impl Scene + 'static) -> crate::Result<Self> {
181 let cursor = self.cursor;
182 self.try_place(cursor, scene)
183 }
184
185 fn transition_checked(
188 &mut self,
189 effect: impl transition::Transition + 'static,
190 duration: Duration,
191 ) -> crate::Result<()> {
192 if self.pending.is_some() || duration <= Time::ZERO {
193 return Err(on_track(
194 self.index,
195 "Transition needs positive duration and no pending transition",
196 ));
197 }
198 let previous = self
199 .clips
200 .last()
201 .ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
202 if align(self.cursor, self.rate)?.0 != previous.end() {
203 return Err(on_track(self.index, "Transition cannot follow a gap"));
204 }
205 let start = self
206 .cursor
207 .ticks()
208 .checked_sub(duration.ticks())
209 .map(Time::from_ticks)
210 .filter(|time| *time >= Time::ZERO)
211 .ok_or_else(|| on_track(self.index, "Transition exceeds the preceding clip"))?;
212 let aligned = align(start, self.rate)?.0;
213 if aligned < previous.start
214 || aligned >= previous.end()
215 || previous
216 .transition_in
217 .as_ref()
218 .is_some_and(|incoming| aligned < previous.start + incoming.duration)
219 {
220 return Err(on_track(
221 self.index,
222 "Transition would be empty or overlap another transition",
223 ));
224 }
225 self.cursor = start;
226 self.pending = Some(Arc::new(effect));
227 Ok(())
228 }
229
230 fn place_checked(&mut self, start: Time, scene: impl Scene + 'static) -> crate::Result<()> {
234 if self.clips.len() >= MAX_CLIPS {
235 return Err(on_track(self.index, "At most 16384 clips are allowed"));
236 }
237 if start < Time::ZERO {
238 return Err(on_track(self.index, "Clip start must be nonnegative"));
239 }
240 let duration = scene.duration().ok_or_else(|| {
241 on_track(
242 self.index,
243 "Scene has no finite duration; use with_duration",
244 )
245 })?;
246 if duration <= Time::ZERO {
247 return Err(on_track(self.index, "Scene duration must be positive"));
248 }
249 let requested_end =
250 add_time(start, duration).map_err(|error| on_track(self.index, error))?;
251 let (aligned_start, start_frame) =
252 align(start, self.rate).map_err(|error| on_track(self.index, error))?;
253 let (aligned_end, end_frame) =
254 align(requested_end, self.rate).map_err(|error| on_track(self.index, error))?;
255 if aligned_end <= aligned_start {
256 return Err(on_track(
257 self.index,
258 "Clip becomes empty after frame alignment; increase its duration",
259 ));
260 }
261 let index = self
262 .clips
263 .partition_point(|clip| clip.start <= aligned_start);
264 let id = self.clips.len();
265 let overlaps = index
266 .checked_sub(1)
267 .and_then(|previous| self.clips.get(previous))
268 .filter(|clip| clip.end() > aligned_start)
269 .or_else(|| {
270 self.clips
271 .get(index)
272 .filter(|clip| clip.start < aligned_end)
273 });
274 let transition_in = if let Some(effect) = &self.pending {
275 let previous = self
276 .clips
277 .last()
278 .ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
279 if start != self.cursor
280 || index != self.clips.len()
281 || aligned_start < previous.start
282 || aligned_start >= previous.end()
283 || aligned_end < previous.end()
284 || previous
285 .transition_in
286 .as_ref()
287 .is_some_and(|incoming| aligned_start < previous.start + incoming.duration)
288 {
289 return Err(on_track(
290 self.index,
291 "Incoming clip must cover the adjacent overlap without a triple overlap",
292 ));
293 }
294 Some(TransitionIn {
295 effect: effect.clone(),
296 duration: previous.end() - aligned_start,
297 })
298 } else {
299 None
300 };
301 if let Some(other) = overlaps.filter(|_| transition_in.is_none()) {
302 return Err(on_track(
303 self.index,
304 format!(
305 "clip[{id}] [{}, {}) overlaps clip[{}] [{}, {}); use an explicit adjacent transition or separate track",
306 aligned_start.ticks(),
307 aligned_end.ticks(),
308 other.id,
309 other.start.ticks(),
310 other.end().ticks()
311 ),
312 ));
313 }
314 for (edge, requested, aligned, frame) in [
315 (BoundaryEdge::Start, start, aligned_start, start_frame),
316 (BoundaryEdge::End, requested_end, aligned_end, end_frame),
317 ] {
318 if requested != aligned {
319 self.adjustments.push(BoundaryAdjustment {
320 track: Some(self.index),
321 clip: Some(id),
322 edge,
323 requested,
324 aligned,
325 frame,
326 });
327 }
328 }
329 self.clips.insert(
330 index,
331 Clip {
332 id,
333 start: aligned_start,
334 duration: aligned_end - aligned_start,
335 scene: Arc::new(scene),
336 transition_in,
337 },
338 );
339 self.cursor = self.cursor.max(requested_end);
340 self.pending = None;
341 Ok(())
342 }
343
344 fn gap_checked(&mut self, duration: Duration) -> crate::Result<()> {
346 if self.pending.is_some() {
347 return Err(on_track(
348 self.index,
349 "A pending transition must be followed by a clip, not a gap",
350 ));
351 }
352 if duration < Time::ZERO {
353 return Err(on_track(self.index, "Gap must be nonnegative"));
354 }
355 self.cursor =
356 add_time(self.cursor, duration).map_err(|error| on_track(self.index, error))?;
357 Ok(())
358 }
359
360 pub fn cursor(&self) -> Time {
362 self.cursor
363 }
364
365 pub(super) fn finish(mut self) -> crate::Result<(Track, Vec<BoundaryAdjustment>)> {
366 if !self.errors.is_empty() {
367 return Err(Error::BuildErrors(self.errors));
368 }
369 if self.pending.is_some() {
370 return Err(on_track(
371 self.index,
372 "Pending transition has no incoming clip",
373 ));
374 }
375 let (duration, frame) =
376 align(self.cursor, self.rate).map_err(|error| on_track(self.index, error))?;
377 if duration != self.cursor {
378 self.adjustments.push(BoundaryAdjustment {
379 track: Some(self.index),
380 clip: None,
381 edge: BoundaryEdge::Cursor,
382 requested: self.cursor,
383 aligned: duration,
384 frame,
385 });
386 }
387 Ok((
388 Track {
389 clips: self.clips.into(),
390 duration,
391 },
392 self.adjustments,
393 ))
394 }
395}