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}