Skip to main content

tellur_core/timeline_component/
placed.rs

1//! [`Placement`] / [`Placed`] and the `.at(..)` / `.fill()` / `.trim(..)`
2//! placement verbs — where a component sits on the parent clock.
3
4use std::ops::Range;
5
6use crate::geometry::Vec2;
7use crate::raster::{RasterImage, Resolution};
8use crate::render_context::RenderContext;
9use crate::time::{LocalTime, Time};
10
11use super::*;
12
13// ── Placement ───────────────────────────────────────────────────────────────
14
15/// Where a component sits on the PARENT clock.
16///
17/// `From<f32>` = a start point (it plays for its own
18/// [`duration`](TimelineComponent::duration) at native speed); `From<Range<f32>>`
19/// = an explicit `start..end` window. For a TIMELESS visual/subtitle the window
20/// just gives it that interval. For a TIMED component a window ≠ its length is a
21/// STRETCH that time-scales the (trimmed) source to fill the window, so
22/// `speed = content_duration / (b - a)` (decided semantics, `.sketch/01` A.3).
23/// There is no separate `.speed()` — to merely truncate, [`Timed::trim`] the
24/// source.
25#[derive(Debug, Clone, Copy, crate::Keyable)]
26pub struct Placement {
27    /// Relative start on the parent clock.
28    start: f32,
29    /// Explicit window end (exclusive); `None` for a bare start point, whose
30    /// end is the inner component's own duration.
31    end: Option<f32>,
32    /// Whether this placement is a [`fill`](Timed::fill): it takes the
33    /// CONTAINER's resolved length (set by the overlay `Timeline` in its
34    /// second sub-pass) and is EXCLUDED from the container's length measure
35    /// (the load-bearing acyclicity invariant, `.sketch/02 §5/§9`). A fill
36    /// child inside a `Sequence` is a resolve error (ZONE C #1).
37    fill: bool,
38}
39
40impl Placement {
41    /// Relative start on the parent clock.
42    pub fn start(&self) -> f32 {
43        self.start
44    }
45
46    /// Explicit window end, if this placement is a `start..end` window.
47    pub fn end(&self) -> Option<f32> {
48        self.end
49    }
50
51    /// Whether this is a [`fill`](Timed::fill) placement (stretches to the
52    /// container's resolved length).
53    pub fn is_fill(&self) -> bool {
54        self.fill
55    }
56
57    /// A fill placement — takes the container's resolved length. Constructed by
58    /// [`Timed::fill`]; not reachable through the `From` conversions (a window /
59    /// point is never implicitly a fill).
60    fn fill() -> Self {
61        Self {
62            start: 0.0,
63            end: None,
64            fill: true,
65        }
66    }
67}
68
69impl From<f32> for Placement {
70    fn from(start: f32) -> Self {
71        Self {
72            start,
73            end: None,
74            fill: false,
75        }
76    }
77}
78
79impl From<Range<f32>> for Placement {
80    fn from(window: Range<f32>) -> Self {
81        Self {
82            start: window.start,
83            end: Some(window.end),
84            fill: false,
85        }
86    }
87}
88
89/// A component placed at a parent time — the temporal twin of
90/// [`Positioned`](crate::placement::Positioned). A [`TimelineComponent`] itself,
91/// so it nests.
92///
93/// Per the DECIDED stretch semantics (`.sketch/01` A.3): `.at(a..b)` footprint
94/// is the window; the implied speed factor is recorded for later sampling.
95#[derive(crate::Keyable)]
96pub struct Placed {
97    /// Where on the parent clock this sits.
98    placement: Placement,
99    /// The placed component.
100    child: Box<dyn TimelineComponent + Send>,
101}
102
103impl Placed {
104    /// Constructs a placement of `child` at `placement` on the parent clock.
105    pub fn new(placement: Placement, child: Box<dyn TimelineComponent + Send>) -> Self {
106        Self { placement, child }
107    }
108
109    /// Where this sits on the parent clock.
110    pub fn placement(&self) -> Placement {
111        self.placement
112    }
113
114    /// Whether this is a [`fill`](Timed::fill) placement. Containers read this
115    /// to exclude fill children from their length measure and resolve them
116    /// against the container's resolved length (`.sketch/02 §5/§6`).
117    pub fn is_fill(&self) -> bool {
118        self.placement.fill
119    }
120
121    /// The placed child, borrowed. Lets a container drive the child's own
122    /// `resolve` against the container's resolved length for a fill child.
123    pub fn child(&self) -> &(dyn TimelineComponent + Send) {
124        &*self.child
125    }
126
127    /// Speed factor implied by a stretch window over a timed child, i.e.
128    /// `content_duration / (b - a)`; `1.0` for a bare start point or a window
129    /// over a timeless child. Recorded for later sampling (`.sketch/01` A.3).
130    pub fn speed(&self) -> f32 {
131        match (self.placement.end, self.child.duration()) {
132            (Some(end), Some(content)) => {
133                let window = end - self.placement.start;
134                if window > 0.0 {
135                    content / window
136                } else {
137                    // TODO(task 3): a zero/negative window is a resolve error.
138                    1.0
139                }
140            }
141            _ => 1.0,
142        }
143    }
144}
145
146impl TimelineComponent for Placed {
147    fn duration(&self) -> Option<f32> {
148        match self.placement.end {
149            // An explicit window fixes the length regardless of the child's.
150            Some(end) => Some(end - self.placement.start),
151            // A bare start point plays for the child's own duration.
152            None => self.child.duration(),
153        }
154    }
155
156    fn measure(&self) -> Option<f32> {
157        // The measure-pass footprint is the relative start plus the resolved
158        // length (`.sketch/02 §5`): `b` for a window, `start + inner` for a
159        // point.
160        match self.placement.end {
161            Some(end) => Some(end),
162            None => self
163                .child
164                .measure()
165                .map(|inner| self.placement.start + inner),
166        }
167    }
168
169    fn resolve(&self, abs_start: f32, out: &mut ResolveCtx) -> f32 {
170        // A zero/negative explicit window has no determinate speed (it would
171        // stretch the child by ∞); surface it as an authoring error instead of
172        // silently degrading to `speed() == 1.0`.
173        if let Some(end) = self.placement.end {
174            if end - self.placement.start <= 0.0 {
175                out.error(format!(
176                    "a placement window must have positive length, got .at({}..{})",
177                    self.placement.start, end
178                ));
179            }
180        }
181        // Recurse at the child's absolute start, folding the window stretch in
182        // (`.sketch/01 §A.3`). The relative start is in THIS level's local
183        // seconds, so scale it to absolute by the enclosing `local_scale`; the
184        // child then runs at this window's `speed()`, so any interior offsets it
185        // records shrink by that factor (a `.at(0..1)` over a 2s child has
186        // `speed = 2`, so the child's own seconds are half a parent second each).
187        let scale = out.local_scale;
188        let child_abs = abs_start + self.placement.start * scale;
189        let saved = out.local_scale;
190        out.local_scale = scale / self.speed();
191        let child_len = self.child.resolve(child_abs, out);
192        out.local_scale = saved;
193        self.duration().unwrap_or(self.placement.start + child_len)
194    }
195
196    fn frame(
197        &self,
198        clock: Clock<'_>,
199        canvas: Vec2,
200        target: Resolution,
201        ctx: &mut dyn RenderContext,
202    ) -> Option<RasterImage> {
203        let t = clock.local().seconds();
204        // Temporal gate: a placed clip contributes ONLY within its resolved
205        // parent-clock interval `[start, end)` (half-open, so abutting clips and
206        // Sequence slots never double-draw at the seam). A `.fill()` spans its
207        // container and is never self-gated; an open-ended timeless point (no
208        // window, no child duration) can't know its end, so it stays active.
209        if !self.is_fill() {
210            let active = match self.placement.end {
211                Some(end) => t >= self.placement.start && t < end,
212                None => match self.child.duration() {
213                    Some(d) => t >= self.placement.start && t < self.placement.start + d,
214                    None => true,
215                },
216            };
217            if !active {
218                return None;
219            }
220        }
221        // Rebase + stretch (`.sketch/02 §8`, `.sketch/01 §A.3`): shift the child's
222        // local axis to its relative start, then time-scale by the window's
223        // implied `speed()`. A `.at(0.0..1.0)` over a 2s source has `speed = 2.0`,
224        // so source-local advances twice as fast — at parent local `0.5` the child
225        // sees `1.0`. `global` / `triggers` are unchanged (the trigger axis is
226        // global, never remapped).
227        let rebased = (t - self.placement.start) * self.speed();
228        // Surface the child's LOCAL window length so end-relative effects
229        // (`clock.envelope` / `clock.remaining`) know where the clip closes. A
230        // window is `(end - start) * speed` = the child's own post-stretch seconds
231        // (content_dur for a timed stretch, `b - a` for a timeless one), matching
232        // the units of the rebased local axis above. A bare point takes the
233        // child's own duration; a fill is open-ended (`None`).
234        let window = if self.is_fill() {
235            None
236        } else {
237            match self.placement.end {
238                Some(end) => Some((end - self.placement.start) * self.speed()),
239                None => self.child.duration(),
240            }
241        };
242        let child_clock = clock.with_local_window(LocalTime::new(rebased), window);
243        self.child.frame(child_clock, canvas, target, ctx)
244    }
245
246    fn samples(&self, clock: Clock<'_>, window: f32) -> Option<AudioBuffer> {
247        // The mix-down uses `mix_into`; this per-window seam just forwards.
248        self.child.samples(clock, window)
249    }
250
251    fn mix_into(&self, mix: &mut crate::audio::AudioMix, start_secs: f32, speed: f32) {
252        // Shift the child to its relative start and fold the window stretch into
253        // the speed — exactly the rebase + time-scale `frame` applies, but on
254        // the sample axis (`.sketch/02 §8`). A fill child has relative start 0.
255        self.child
256            .mix_into(mix, start_secs + self.placement.start, speed * self.speed());
257    }
258
259    fn cues(&self, offset: f32) -> Vec<Cue> {
260        let child_offset = offset + self.placement.start;
261        let mut cues = self.child.cues(child_offset);
262        // A TIMELESS child (e.g. a `Subtitle`) has no intrinsic length, so its
263        // cues come out as zero-length points; the placement WINDOW is what
264        // gives the cue its interval (`.sketch/02 §10`). Stamp the window end
265        // onto those cues. A timed child already carries its own ends, and a
266        // fill child's length is supplied by the container's own `cues` walk.
267        if self.child.duration().is_none() {
268            if let Some(end) = self.placement.end {
269                let abs_end = offset + end;
270                for cue in &mut cues {
271                    cue.end = abs_end;
272                }
273            }
274        }
275        cues
276    }
277
278    fn arrangement(&self, offset: f32) -> Arrangement {
279        // Produce the child's node at its absolute start (`offset + relative`).
280        let child_offset = offset + self.placement.start;
281        let mut node = self.child.arrangement(child_offset);
282        // A TIMELESS child (e.g. a `Subtitle` / a bare visual) comes out
283        // 0-length, so the placement WINDOW is what gives it its interval —
284        // stamp the window end (mirrors `Placed::cues`). A timed child already
285        // carries its own end; a fill child's end is supplied by the container.
286        if self.child.duration().is_none() {
287            if let Some(end) = self.placement.end {
288                node.end = offset + end;
289            }
290        }
291        node
292    }
293}
294
295/// A `.at(..)` / `.fill()` result drops straight into a container's
296/// `child(impl Into<Box<dyn TimelineComponent + Send>>)` setter, mirroring
297/// raster `Positioned`'s `From<Positioned> for Box<dyn RasterComponent>`.
298impl From<Placed> for Box<dyn TimelineComponent + Send> {
299    fn from(placed: Placed) -> Self {
300        Box::new(placed)
301    }
302}
303
304// ── Placement verbs (split: component-side + builder-side, audit B2) ─────────
305
306/// Placement verbs on a built [`TimelineComponent`]. Blanket-implemented for
307/// every `T: TimelineComponent`, mirroring `RasterBuilderPlacement`.
308///
309/// The two clocks are deliberately different (`.sketch/01` A.3):
310/// - `.at(..)` — PARENT clock: where it sits (point or window).
311/// - [`trim`](Self::trim) — SOURCE clock: which of its own seconds play.
312///
313/// SPEED is emergent, not a verb: `.at(a..b)` over a timed component stretches
314/// its (trimmed) length to fill the window. [`fill`](Self::fill) is valid ONLY
315/// in an overlay `Timeline` — inside a `Sequence` it is a compile/resolve error.
316pub trait Timed: TimelineComponent + Sized + Send + 'static {
317    /// Place on the parent clock. `.at(2.0)` plays at native speed for the
318    /// component's own duration; `.at(0.0..3.0)` places it into an explicit
319    /// window (a stretch for a timed component, an interval for a timeless one).
320    fn at(self, placement: impl Into<Placement>) -> Placed {
321        Placed::new(placement.into(), Box::new(self))
322    }
323
324    /// Stretch to the CONTAINER's resolved length — the one declarative length
325    /// verb. Valid ONLY in an overlay `Timeline` (inside a `Sequence` it is a
326    /// compile/resolve error). Fill children are excluded from the container's
327    /// length measure (the load-bearing invariant), so this never forms a cycle.
328    fn fill(self) -> Placed {
329        // A fill placement: the overlay `Timeline` resolves it against its own
330        // length in its second sub-pass, and excludes it from the length
331        // measure (`.sketch/02 §5/§6`). A `Sequence` rejects it at resolve time.
332        Placed::new(Placement::fill(), Box::new(self))
333    }
334
335    /// Use only SOURCE seconds `a..b` (the in/out crop). The way to truncate (a
336    /// short `.at` window stretches, it does not cut).
337    ///
338    /// Honoured ONLY by the media leaves (`VideoFile` / `AudioFile`), which shadow
339    /// this blanket with an inherent `trim` that records the crop and reports
340    /// `b - a` from [`duration`](TimelineComponent::duration). A source-time crop
341    /// is meaningless for a synthetic (timeless / clock-driven) component, so on
342    /// everything else this is intentionally a no-op returning `self` unchanged.
343    fn trim(self, _r: Range<f32>) -> Self {
344        self
345    }
346}
347
348impl<T: TimelineComponent + Send + 'static> Timed for T {}
349
350/// Buildless twin of [`Timed`], over complete builders. Same method names; lets
351/// `Caption::builder().line(..).fill()` work with no `.build()`. Returns
352/// built/placed types exactly as `VectorBuilderPlacement::place_at` returns
353/// `Positioned`.
354pub trait TimedBuilder: TimelineBuilder
355where
356    Self::Output: Send,
357{
358    fn at(self, placement: impl Into<Placement>) -> Placed {
359        self.build_component().at(placement)
360    }
361
362    fn fill(self) -> Placed {
363        self.build_component().fill()
364    }
365
366    fn trim(self, r: Range<f32>) -> Self::Output {
367        self.build_component().trim(r)
368    }
369}
370
371impl<B> TimedBuilder for B
372where
373    B: TimelineBuilder,
374    B::Output: Send,
375{
376}