1use super::{BoundaryAdjustment, BoundaryEdge, MAX_CLIPS, Scene, add_time, align, on_track};
4use crate::ErrorReport;
5use crate::authoring::{scene, transition};
6use crate::{AudioClip, Duration, FrameRate, Node, SceneContext, Time, Timeline};
7use std::{fmt, sync::Arc};
8
9#[derive(Clone)]
11pub struct Clip {
12 pub(super) id: usize,
13 pub(super) start: Time,
14 pub(super) duration: Duration,
15 pub(super) offset: Duration,
16 pub(super) scene: Arc<dyn Scene>,
17 pub(super) transition_in: Option<TransitionIn>,
18}
19
20#[derive(Clone)]
21pub(super) struct TransitionIn {
22 pub(super) effect: Arc<dyn transition::Transition>,
23 pub(super) duration: Duration,
24}
25
26impl fmt::Debug for Clip {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter
29 .debug_struct("Clip")
30 .field("id", &self.id)
31 .field("start", &self.start)
32 .field("duration", &self.duration)
33 .finish_non_exhaustive()
34 }
35}
36
37impl Clip {
38 pub fn id(&self) -> usize {
40 self.id
41 }
42
43 pub fn start(&self) -> Time {
45 self.start
46 }
47
48 pub fn duration(&self) -> Duration {
50 self.duration
51 }
52
53 pub fn offset(&self) -> Duration {
55 self.offset
56 }
57
58 pub fn end(&self) -> Time {
60 self.start + self.duration
61 }
62
63 pub fn scene(&self) -> &dyn Scene {
65 self.scene.as_ref()
66 }
67
68 pub fn transition_in(&self) -> Option<(&dyn transition::Transition, Duration)> {
70 self.transition_in
71 .as_ref()
72 .map(|value| (value.effect.as_ref(), value.duration))
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct Track {
79 pub(super) clips: Arc<[Clip]>,
80 pub(super) duration: Duration,
81}
82
83impl Track {
84 pub fn clips(&self) -> &[Clip] {
86 &self.clips
87 }
88
89 pub fn duration(&self) -> Duration {
91 self.duration
92 }
93
94 pub fn active_clip(&self, time: Time) -> Option<&Clip> {
97 let index = self
98 .clips
99 .partition_point(|clip| clip.start <= time)
100 .checked_sub(1)?;
101 self.clips.get(index).filter(|clip| time < clip.end())
102 }
103
104 pub fn render(&self, context: &SceneContext<'_>) -> Option<Node> {
107 let index = self
108 .clips
109 .partition_point(|clip| clip.start <= context.time())
110 .checked_sub(1)?;
111 let clip = &self.clips[index];
112 if context.time() >= clip.end() {
113 return None;
114 }
115 let render = |clip: &Clip| {
116 context.during(clip.start, clip.duration, |local| {
117 if clip.offset == Duration::ZERO {
118 clip.scene.render(local)
119 } else {
120 clip.scene
121 .render(&local.shifted((Time::ZERO - clip.offset).since_origin()))
122 }
123 })
124 };
125 let node = if let Some(transition) = clip
126 .transition_in
127 .as_ref()
128 .filter(|transition| context.time() < clip.start + transition.duration)
129 {
130 let outgoing = render(&self.clips[index - 1]);
131 let incoming = render(clip);
132 let animation = crate::Tween::new(0.0f64, 1.0, transition.duration)
133 .easing(transition.effect.easing());
134 let progress =
135 crate::Signal::sample(&animation, context.time() - clip.start.since_origin());
136 if progress.is_finite() {
137 let local = context
138 .shifted(clip.start.since_origin())
139 .with_clip_duration(transition.duration);
140 transition
141 .effect
142 .compose(progress.clamp(0.0, 1.0), outgoing, incoming, &local)
143 } else {
144 transition::failure("easing", "Transition easing must produce finite progress")
145 }
146 } else {
147 render(clip)
148 };
149 Some(Node::group([node]))
150 }
151}
152
153pub struct TrackBuilder {
156 pub(super) index: usize,
157 pub(super) rate: FrameRate,
158 pub(super) clips: Vec<Clip>,
159 pub(super) cursor: Time,
160 pub(super) adjustments: Vec<BoundaryAdjustment>,
161 pub(super) pending: Option<Arc<dyn transition::Transition>>,
162 pub(super) errors: ErrorReport,
163 pub(super) audio: Vec<AudioClip>,
164}
165
166impl TrackBuilder {
167 fn defer(mut self, operation: impl FnOnce(&mut Self) -> crate::Result<()>) -> Self {
168 if let Err(error) = operation(&mut self) {
169 self.errors.record(on_track(
170 self.index,
171 format!("clip[{}]: {error}", self.clips.len()),
172 ));
173 }
174 self
175 }
176
177 pub fn push(self, scene: impl Scene + 'static) -> Self {
179 let cursor = self.cursor;
180 self.defer(|builder| builder.place_checked(cursor, None, Duration::ZERO, scene))
181 }
182
183 pub fn scene<Output: scene::IntoSceneOutput + 'static>(
185 self,
186 duration: Duration,
187 build: impl Fn(&SceneContext<'_>) -> Output + Send + Sync + 'static,
188 ) -> Self {
189 self.push(scene::scene(build).with_duration(duration))
190 }
191
192 pub fn push_for(self, duration: Duration, scene: impl Scene + 'static) -> Self {
194 let cursor = self.cursor;
195 self.defer(|builder| builder.place_checked(cursor, Some(duration), Duration::ZERO, scene))
196 }
197
198 pub fn place_for(self, start: Time, duration: Duration, scene: impl Scene + 'static) -> Self {
200 self.defer(|builder| builder.place_checked(start, Some(duration), Duration::ZERO, scene))
201 }
202
203 pub fn push_trimmed(
205 self,
206 offset: Duration,
207 duration: Duration,
208 scene: impl Scene + 'static,
209 ) -> Self {
210 let cursor = self.cursor;
211 self.defer(|builder| builder.place_checked(cursor, Some(duration), offset, scene))
212 }
213
214 pub fn try_push_for(
216 mut self,
217 duration: Duration,
218 scene: impl Scene + 'static,
219 ) -> crate::Result<Self> {
220 let cursor = self.cursor;
221 self.place_checked(cursor, Some(duration), Duration::ZERO, scene)?;
222 Ok(self)
223 }
224
225 pub fn try_place_for(
227 mut self,
228 start: Time,
229 duration: Duration,
230 scene: impl Scene + 'static,
231 ) -> crate::Result<Self> {
232 self.place_checked(start, Some(duration), Duration::ZERO, scene)?;
233 Ok(self)
234 }
235
236 pub fn try_push_trimmed(
238 mut self,
239 offset: Duration,
240 duration: Duration,
241 scene: impl Scene + 'static,
242 ) -> crate::Result<Self> {
243 let cursor = self.cursor;
244 self.place_checked(cursor, Some(duration), offset, scene)?;
245 Ok(self)
246 }
247
248 pub fn nest(self, child: Timeline) -> Self {
252 let cursor = self.cursor;
253 self.defer(|builder| builder.nest_checked(cursor, None, child))
254 }
255
256 pub fn nest_for(self, duration: Duration, child: Timeline) -> Self {
259 let cursor = self.cursor;
260 self.defer(|builder| builder.nest_checked(cursor, Some(duration), child))
261 }
262
263 pub fn place_timeline(self, start: Time, child: Timeline) -> Self {
265 self.defer(|builder| builder.nest_checked(start, None, child))
266 }
267
268 pub fn try_nest(mut self, child: Timeline) -> crate::Result<Self> {
270 let cursor = self.cursor;
271 self.nest_checked(cursor, None, child)?;
272 Ok(self)
273 }
274
275 pub fn try_nest_for(mut self, duration: Duration, child: Timeline) -> crate::Result<Self> {
277 let cursor = self.cursor;
278 self.nest_checked(cursor, Some(duration), child)?;
279 Ok(self)
280 }
281
282 pub fn try_place_timeline(mut self, start: Time, child: Timeline) -> crate::Result<Self> {
284 self.nest_checked(start, None, child)?;
285 Ok(self)
286 }
287
288 fn nest_checked(
289 &mut self,
290 start: Time,
291 duration: Option<Duration>,
292 child: Timeline,
293 ) -> crate::Result<()> {
294 let id = self.clips.len();
295 match duration {
296 Some(duration) => {
297 self.place_checked(start, Some(duration), Duration::ZERO, child.visual())?
298 }
299 None => self.place_checked(start, None, Duration::ZERO, child.visual())?,
300 }
301 let clip = self
302 .clips
303 .iter()
304 .find(|clip| clip.id == id)
305 .ok_or_else(|| on_track(self.index, "Nested clip was not placed"))?;
306 let (start, duration) = (clip.start, clip.duration);
307 let soundtrack = child.soundtrack();
308 let sources =
309 if soundtrack.master() != crate::Master::default() || !soundtrack.buses().is_empty() {
310 vec![AudioClip::composition(child.clone()).duration(duration)]
311 } else {
312 soundtrack.clips().to_vec()
313 };
314 for source in sources {
315 if let Some(source) = source.relocated(start.since_origin().magnitude(), duration)? {
316 self.audio.push(source);
317 }
318 }
319 Ok(())
320 }
321
322 pub fn transition(
324 self,
325 effect: impl transition::Transition + 'static,
326 duration: Duration,
327 ) -> Self {
328 self.defer(|builder| builder.transition_checked(effect, duration))
329 }
330
331 pub fn place(self, start: Time, scene: impl Scene + 'static) -> Self {
333 self.defer(|builder| builder.place_checked(start, None, Duration::ZERO, scene))
334 }
335
336 pub fn gap(self, duration: Duration) -> Self {
338 self.defer(|builder| builder.gap_checked(duration))
339 }
340
341 pub fn try_transition(
343 mut self,
344 effect: impl transition::Transition + 'static,
345 duration: Duration,
346 ) -> crate::Result<Self> {
347 self.transition_checked(effect, duration)?;
348 Ok(self)
349 }
350
351 pub fn try_place(mut self, start: Time, scene: impl Scene + 'static) -> crate::Result<Self> {
353 self.place_checked(start, None, Duration::ZERO, scene)?;
354 Ok(self)
355 }
356
357 pub fn try_gap(mut self, duration: Duration) -> crate::Result<Self> {
359 self.gap_checked(duration)?;
360 Ok(self)
361 }
362
363 pub fn try_push(self, scene: impl Scene + 'static) -> crate::Result<Self> {
365 let cursor = self.cursor;
366 self.try_place(cursor, scene)
367 }
368
369 fn transition_checked(
372 &mut self,
373 effect: impl transition::Transition + 'static,
374 duration: Duration,
375 ) -> crate::Result<()> {
376 if self.pending.is_some() || duration <= Duration::ZERO {
377 return Err(on_track(
378 self.index,
379 "Transition needs positive duration and no pending transition",
380 ));
381 }
382 let previous = self
383 .clips
384 .last()
385 .ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
386 if align(self.cursor, self.rate)?.0 != previous.end() {
387 return Err(on_track(self.index, "Transition cannot follow a gap"));
388 }
389 let start = self
390 .cursor
391 .checked_sub(duration)
392 .filter(|time| *time >= Time::ZERO)
393 .ok_or_else(|| on_track(self.index, "Transition exceeds the preceding clip"))?;
394 let aligned = align(start, self.rate)?.0;
395 if aligned < previous.start
396 || aligned >= previous.end()
397 || previous
398 .transition_in
399 .as_ref()
400 .is_some_and(|incoming| aligned < previous.start + incoming.duration)
401 {
402 return Err(on_track(
403 self.index,
404 "Transition would be empty or overlap another transition",
405 ));
406 }
407 self.cursor = start;
408 self.pending = Some(Arc::new(effect));
409 Ok(())
410 }
411
412 fn place_checked(
416 &mut self,
417 start: Time,
418 duration: Option<Duration>,
419 offset: Duration,
420 scene: impl Scene + 'static,
421 ) -> crate::Result<()> {
422 if self.clips.len() >= MAX_CLIPS {
423 return Err(on_track(self.index, "At most 16384 clips are allowed"));
424 }
425 if start < Time::ZERO {
426 return Err(on_track(self.index, "Clip start must be nonnegative"));
427 }
428 let duration = duration.or_else(|| scene.duration()).ok_or_else(|| {
429 on_track(
430 self.index,
431 "Scene has no finite duration; use push_for or with_duration",
432 )
433 })?;
434 if duration <= Duration::ZERO {
435 return Err(on_track(self.index, "Clip duration must be positive"));
436 }
437 let requested_end =
438 add_time(start, duration).map_err(|error| on_track(self.index, error))?;
439 let (aligned_start, start_frame) =
440 align(start, self.rate).map_err(|error| on_track(self.index, error))?;
441 let (aligned_end, end_frame) =
442 align(requested_end, self.rate).map_err(|error| on_track(self.index, error))?;
443 if aligned_end <= aligned_start {
444 return Err(on_track(
445 self.index,
446 "Clip becomes empty after frame alignment; increase its duration",
447 ));
448 }
449 let index = self
450 .clips
451 .partition_point(|clip| clip.start <= aligned_start);
452 let id = self.clips.len();
453 let overlaps = index
454 .checked_sub(1)
455 .and_then(|previous| self.clips.get(previous))
456 .filter(|clip| clip.end() > aligned_start)
457 .or_else(|| {
458 self.clips
459 .get(index)
460 .filter(|clip| clip.start < aligned_end)
461 });
462 let transition_in = if let Some(effect) = &self.pending {
463 let previous = self
464 .clips
465 .last()
466 .ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
467 if start != self.cursor
468 || index != self.clips.len()
469 || aligned_start < previous.start
470 || aligned_start >= previous.end()
471 || aligned_end < previous.end()
472 || previous
473 .transition_in
474 .as_ref()
475 .is_some_and(|incoming| aligned_start < previous.start + incoming.duration)
476 {
477 return Err(on_track(
478 self.index,
479 "Incoming clip must cover the adjacent overlap without a triple overlap",
480 ));
481 }
482 Some(TransitionIn {
483 effect: effect.clone(),
484 duration: (previous.end() - aligned_start).magnitude(),
485 })
486 } else {
487 None
488 };
489 if let Some(other) = overlaps.filter(|_| transition_in.is_none()) {
490 return Err(on_track(
491 self.index,
492 format!(
493 "clip[{id}] [{}, {}) overlaps clip[{}] [{}, {}); use an explicit adjacent transition or separate track",
494 aligned_start.ticks(),
495 aligned_end.ticks(),
496 other.id,
497 other.start.ticks(),
498 other.end().ticks()
499 ),
500 ));
501 }
502 for (edge, requested, aligned, frame) in [
503 (BoundaryEdge::Start, start, aligned_start, start_frame),
504 (BoundaryEdge::End, requested_end, aligned_end, end_frame),
505 ] {
506 if requested != aligned {
507 self.adjustments.push(BoundaryAdjustment {
508 track: Some(self.index),
509 clip: Some(id),
510 edge,
511 requested,
512 aligned,
513 frame,
514 });
515 }
516 }
517 self.clips.insert(
518 index,
519 Clip {
520 id,
521 start: aligned_start,
522 duration: (aligned_end - aligned_start).magnitude(),
523 offset,
524 scene: Arc::new(scene),
525 transition_in,
526 },
527 );
528 self.cursor = self.cursor.max(requested_end);
529 self.pending = None;
530 Ok(())
531 }
532
533 fn gap_checked(&mut self, duration: Duration) -> crate::Result<()> {
535 if self.pending.is_some() {
536 return Err(on_track(
537 self.index,
538 "A pending transition must be followed by a clip, not a gap",
539 ));
540 }
541 self.cursor =
542 add_time(self.cursor, duration).map_err(|error| on_track(self.index, error))?;
543 Ok(())
544 }
545
546 pub fn cursor(&self) -> Time {
548 self.cursor
549 }
550
551 pub(super) fn finish(
552 mut self,
553 ) -> Result<(Track, Vec<BoundaryAdjustment>, Vec<AudioClip>), ErrorReport> {
554 if !self.errors.is_empty() {
555 return Err(self.errors);
556 }
557 if self.pending.is_some() {
558 return Err(on_track(self.index, "Pending transition has no incoming clip").into());
559 }
560 let (end, frame) =
561 align(self.cursor, self.rate).map_err(|error| on_track(self.index, error))?;
562 if end != self.cursor {
563 self.adjustments.push(BoundaryAdjustment {
564 track: Some(self.index),
565 clip: None,
566 edge: BoundaryEdge::Cursor,
567 requested: self.cursor,
568 aligned: end,
569 frame,
570 });
571 }
572 Ok((
573 Track {
574 clips: self.clips.into(),
575 duration: end.since_origin().magnitude(),
576 },
577 self.adjustments,
578 self.audio,
579 ))
580 }
581}