Skip to main content

motion_core/authoring/
context.rs

1//! Stateless authoring time, design coordinates and optional shared computation.
2
3use std::hash::Hash;
4use std::sync::Arc;
5
6use crate::{
7    Duration, FrameRate, Interval, Node, Point, Random, Rect, Signal, Size, Time, TimeOffset,
8};
9
10mod cache;
11mod partition;
12#[cfg(test)]
13mod tests;
14
15pub use cache::SceneCache;
16pub use partition::{CacheLimits, CachePartition, RenderCache};
17
18/// Immutable scene evaluation inputs in signed ticks and logical coordinates.
19///
20/// The caller supplies time explicitly and can reuse one [`SceneCache`] across
21/// arbitrary frame orders and threads, without initializing rendering resources
22/// or interpreting design dimensions as output pixels.
23///
24/// ```
25/// use motion_core::prelude::*;
26/// use motion_core::{
27///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
28///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
29/// };
30/// use std::sync::Arc;
31///
32/// let cache = SceneCache::new(32);
33/// let tween = Tween::new(0.0, 100.0, 1.seconds());
34/// let evaluate = |time| {
35///     let context = SceneContext::new(time, fps!(30), Size::new(640.0, 360.0), &cache)
36///         .with_clip_duration(2.seconds());
37///     let positions = context.cache(("dots", 8_u32), || {
38///         (0..8_u32)
39///             .map(|index| Point::new(context.random(("x", index)) * 640.0, 180.0))
40///             .collect::<Vec<_>>()
41///     });
42///     let local = context.shifted(500.milliseconds().offset());
43///     assert_eq!(local.global_time(), time);
44///     (local.sample(&tween), positions)
45/// };
46/// let (late, first) = evaluate(1500.milliseconds().after_origin());
47/// let (early, second) = evaluate(500.milliseconds().after_origin());
48/// let (again, third) = evaluate(1500.milliseconds().after_origin());
49/// assert_eq!((late, early, again), (100.0, 0.0, 100.0));
50/// assert!(Arc::ptr_eq(&first, &second) && Arc::ptr_eq(&first, &third));
51/// ```
52#[derive(Clone)]
53pub struct SceneContext<'a> {
54    evaluation: Arc<crate::Evaluation>,
55    time: Time,
56    global_time: Time,
57    clip_duration: Option<Duration>,
58    frame_rate: FrameRate,
59    size: Size,
60    nested_scene: bool,
61
62    cache: &'a SceneCache,
63}
64
65impl<'a> SceneContext<'a> {
66    /// Creates root evaluation inputs with matching local/global time.
67    ///
68    /// Panics unless both design dimensions are finite and strictly positive.
69    pub fn new(time: Time, frame_rate: FrameRate, size: Size, cache: &'a SceneCache) -> Self {
70        Self::try_new(time, frame_rate, size, cache)
71            .unwrap_or_else(|error| panic!("{}", error.message))
72    }
73
74    /// Creates evaluation inputs with recoverable design-size validation.
75    pub fn try_new(
76        time: Time,
77        frame_rate: FrameRate,
78        size: Size,
79        cache: &'a SceneCache,
80    ) -> Result<Self, crate::ValidationError> {
81        if !size.width.is_finite()
82            || size.width <= 0.0
83            || !size.height.is_finite()
84            || size.height <= 0.0
85        {
86            return Err(crate::ValidationError::invalid(
87                "scene_context",
88                "size",
89                "scene design dimensions must be finite and positive",
90            ));
91        }
92        Ok(Self {
93            evaluation: Arc::default(),
94            time,
95            global_time: time,
96            clip_duration: None,
97            frame_rate,
98            size,
99            nested_scene: false,
100            cache,
101        })
102    }
103
104    /// Shares an evaluation environment; its own time is ignored in favor of local time.
105    pub fn with_evaluation(mut self, evaluation: Arc<crate::Evaluation>) -> Self {
106        self.evaluation = evaluation;
107        self
108    }
109
110    /// Shared quality, resources, diagnostics and caches, retained by shifted, sampled and
111    /// nested contexts.
112    pub fn evaluation(&self) -> &Arc<crate::Evaluation> {
113        &self.evaluation
114    }
115
116    fn edit(mut self, edit: impl FnOnce(crate::Evaluation) -> crate::Evaluation) -> Self {
117        let current = Arc::try_unwrap(std::mem::take(&mut self.evaluation))
118            .unwrap_or_else(|shared| crate::Evaluation::clone(&shared));
119        self.evaluation = Arc::new(edit(current));
120        self
121    }
122
123    /// Selects preview approximations, preserved by nested and sampled contexts.
124    pub fn with_quality(self, quality: crate::RenderQuality) -> Self {
125        self.edit(|evaluation| evaluation.with_quality(quality))
126    }
127
128    /// Explicit rendering quality; manually constructed contexts default to Final.
129    pub fn render_quality(&self) -> crate::RenderQuality {
130        self.evaluation.quality()
131    }
132
133    /// Always available resources; an unconfigured context uses an empty owner.
134    pub fn assets(&self) -> &crate::Assets {
135        self.evaluation.assets()
136    }
137
138    /// Attaches the common resource owner without reading or decoding.
139    pub fn with_assets(self, assets: &crate::Assets) -> Self {
140        self.edit(|evaluation| evaluation.with_assets(assets))
141    }
142
143    /// Attaches a request-owned sink suitable for explicit background resource reads.
144    pub fn with_diagnostics(self, diagnostics: &crate::RequestDiagnostics) -> Self {
145        self.edit(|evaluation| evaluation.with_diagnostics(diagnostics.clone()))
146    }
147
148    /// Diagnostic scope retained by shifted, sampled and nested contexts.
149    pub fn diagnostics(&self) -> &crate::RequestDiagnostics {
150        self.evaluation.diagnostics()
151    }
152
153    /// Sets finite clip metadata; zero denotes an instantaneous clip.
154    ///
155    /// Sampling time is not clamped to the clip.
156    pub fn with_clip_duration(mut self, duration: Duration) -> Self {
157        self.clip_duration = Some(duration);
158        self
159    }
160
161    /// Signed clip-local time.
162    pub fn time(&self) -> Time {
163        self.time
164    }
165
166    /// Clip-local time in floating-point seconds, for animation mathematics.
167    pub fn seconds(&self) -> f64 {
168        self.time.as_seconds()
169    }
170
171    /// Root evaluation time, unaffected by local shifts.
172    pub fn global_time(&self) -> Time {
173        self.global_time
174    }
175
176    /// Explicit clip duration, absent for an unconstrained scene.
177    pub fn clip_duration(&self) -> Option<Duration> {
178        self.clip_duration
179    }
180
181    /// Clip-local time as a duration since the clip start; zero before the start.
182    pub fn elapsed(&self) -> Duration {
183        self.time
184            .duration_since(Time::ZERO)
185            .unwrap_or(Duration::ZERO)
186    }
187
188    /// Clamped local progress; unconstrained scenes return zero.
189    ///
190    /// A zero-duration clip jumps from zero to one at local time zero.
191    pub fn progress(&self) -> f64 {
192        self.clip_duration
193            .map_or(0.0, |duration| self.progress_in(Time::ZERO, duration))
194    }
195
196    /// Clamped progress in a local interval, using integer boundary comparisons.
197    ///
198    /// Zero duration jumps at start. Wide integer
199    /// differences allow queries across opposite signed tick extremes.
200    pub fn progress_in(&self, start: Time, duration: Duration) -> f64 {
201        Interval::new(start, duration).progress(self.time)
202    }
203
204    /// Eased local interval progress, with the same clamping as progress_in.
205    pub fn ease_in(&self, start: Time, duration: Duration, easing: crate::Easing) -> f64 {
206        Interval::new(start, duration).ease(self.time, easing)
207    }
208
209    /// Exact rational authoring frame rate shared by still, preview and export clocks.
210    pub fn frame_rate(&self) -> FrameRate {
211        self.frame_rate
212    }
213
214    pub(crate) fn for_timeline(&self, size: Size, frame_rate: FrameRate) -> Self {
215        Self {
216            frame_rate,
217            size,
218            nested_scene: true,
219            ..self.clone()
220        }
221    }
222
223    pub(crate) fn is_nested_scene(&self) -> bool {
224        self.nested_scene
225    }
226
227    /// Converts a frame count directly to ticks without cumulative rounding.
228    pub fn frames(&self, count: u64) -> Duration {
229        Duration::from_frame(count, self.frame_rate)
230    }
231
232    /// Samples an animation using local, not global, time.
233    pub fn sample<Value>(&self, animation: &(impl Signal<Value> + ?Sized)) -> Value {
234        animation.sample(self.time)
235    }
236
237    /// Subtracts an offset from local time, preserving global time and clip metadata.
238    ///
239    /// Offsets can be negative. Panics if the resulting local ticks overflow.
240    pub fn shifted(&self, offset: TimeOffset) -> Self {
241        Self {
242            time: self.time - offset,
243            ..self.clone()
244        }
245    }
246
247    /// Subtracts a local offset without panicking on tick overflow.
248    pub fn try_shifted(&self, offset: TimeOffset) -> Result<Self, crate::ValidationError> {
249        let ticks = self
250            .time
251            .ticks()
252            .checked_sub(offset.ticks())
253            .ok_or_else(|| {
254                crate::ValidationError::invalid(
255                    "scene_context",
256                    "time",
257                    "Shifted local ticks overflow",
258                )
259            })?;
260        Ok(Self {
261            time: Time::from_ticks(ticks),
262            ..self.clone()
263        })
264    }
265
266    pub(crate) fn resampled(&self, offset: TimeOffset) -> Result<Self, crate::ValidationError> {
267        let shift = |time: Time| {
268            time.ticks()
269                .checked_add(offset.ticks())
270                .map(Time::from_ticks)
271                .ok_or_else(|| {
272                    crate::ValidationError::invalid(
273                        "motion_blur",
274                        "time",
275                        "Subframe local/global ticks overflow",
276                    )
277                })
278        };
279        Ok(Self {
280            time: shift(self.time)?,
281            global_time: shift(self.global_time)?,
282            ..self.clone()
283        })
284    }
285
286    /// Resamples a local subtree at 1..=64 centered integer shutter midpoints.
287    /// Both local/global clocks move; clip metadata, resources and cache persist.
288    /// Signed samples outside a clip are supplied unchanged for caller-owned gating.
289    pub fn try_motion_blur(
290        &self,
291        shutter: Duration,
292        samples: u32,
293        build: impl FnMut(&SceneContext<'_>) -> Node,
294    ) -> Result<Node, crate::ValidationError> {
295        crate::authoring::sampling::capture(self, shutter, samples, build)
296    }
297
298    /// Captures shutter samples, deferring invalid sampling parameters to compilation.
299    pub fn motion_blur(
300        &self,
301        shutter: Duration,
302        samples: u32,
303        build: impl FnMut(&SceneContext<'_>) -> Node,
304    ) -> Node {
305        self.try_motion_blur(shutter, samples, build)
306            .unwrap_or_else(super::scene::failure)
307    }
308
309    /// Builds only in [start, start + duration), with shifted local clip inputs.
310    ///
311    /// Outside the interval, including zero duration, returns an empty node and
312    /// does not call build. Global time stays unchanged. Integer differences avoid
313    /// overflowing the endpoint sum.
314    pub fn during(
315        &self,
316        start: Time,
317        duration: Duration,
318        build: impl FnOnce(&SceneContext<'a>) -> Node,
319    ) -> Node {
320        let interval = Interval::new(start, duration);
321        if !interval.contains(self.time) {
322            Node::empty()
323        } else {
324            build(&Self {
325                nested_scene: true,
326                ..self
327                    .shifted(start.since_origin())
328                    .with_clip_duration(duration)
329            })
330            .with_scene_time(Time::from_ticks(
331                (i128::from(self.time.ticks()) - i128::from(start.ticks())) as i64,
332            ))
333        }
334    }
335
336    /// Evaluates a child scene starting at a parent-local offset.
337    ///
338    /// Finite children use their half-open duration. Infinite children are active
339    /// from offset onward and clear inherited clip-duration metadata. The parent
340    /// context itself is never clamped; its caller controls the parent interval.
341    /// Panics for unrepresentable shifted local ticks.
342    /// Timeline children clip and contain their own design viewport;
343    /// changing the child frame grid never quantizes the elapsed parent ticks.
344    pub fn embed(&self, scene: &dyn crate::authoring::Scene, offset: Time) -> Node {
345        match scene.duration() {
346            Some(duration) => self.during(offset, duration, |context| scene.render(context)),
347            None if self.time < offset => Node::empty(),
348            None => scene.render(&Self {
349                clip_duration: None,
350                nested_scene: true,
351                ..self.shifted(offset.since_origin())
352            }),
353        }
354    }
355
356    /// Logical design dimensions, not device pixels.
357    pub fn size(&self) -> Size {
358        self.size
359    }
360
361    /// Opposite-corner logical bounds at the origin.
362    pub fn bounds(&self) -> Rect {
363        self.size.to_rect()
364    }
365
366    /// Center of the logical design bounds.
367    pub fn center(&self) -> Point {
368        self.bounds().center()
369    }
370
371    /// Measures content or declared layout in parent coordinates.
372    ///
373    /// Deferred content sees this context's local time, quality, assets and
374    /// diagnostics. Uses default structural limits without rendering or f32 pixel
375    /// conversion.
376    /// For explicit budgets use [`Node::measure`]. Leaves supply their own bounds
377    /// with the shared explicit font environment. This does not invoke drawing
378    /// or infer filter extents; command validation occurs during compilation.
379    pub fn measure(&self, node: &Node) -> Result<Rect, crate::ValidationError> {
380        let drawing = crate::DrawContext::default()
381            .with_evaluation(self.evaluation.clone())
382            .with_time(self.time);
383        node.measure_with(&drawing, crate::ValidationLimits::default())
384    }
385
386    /// Measures like [`measure`](Self::measure) but substitutes `fallback` when measurement fails,
387    /// for example `context.measure_or(&label, Rect::default())`. The failure still surfaces
388    /// when the node is compiled.
389    pub fn measure_or(&self, node: &Node, fallback: Rect) -> Rect {
390        self.measure(node).unwrap_or(fallback)
391    }
392
393    /// Places a measured snapshot next to a reference in parent coordinates.
394    ///
395    /// Vertical sides center horizontally; horizontal sides center vertically.
396    /// Finite negative gaps intentionally overlap. Returns errors for unknown
397    /// geometry, invalid gaps or unrepresentable offsets; inputs stay unchanged.
398    /// This is immediate snapshot positioning, not a live constraint to reference.
399    ///
400    /// ```
401    /// use motion_core::prelude::*;
402    /// use motion_core::{
403    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
404    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
405    /// };
406    /// let cache = SceneCache::new(0);
407    /// let context = SceneContext::new(
408    ///     Time::ZERO,
409    ///     fps!(30),
410    ///     Size::new(200.0, 100.0),
411    ///     &cache,
412    /// );
413    /// let reference = Node::from(Rect::new(10.0, 10.0, 50.0, 30.0).fill(Color::WHITE));
414    /// let card = Node::from(Rect::new(0.0, 0.0, 8.0, 4.0).fill(Color::WHITE));
415    /// let card = context.next_to(card, &reference, Side::Right, 6.0);
416    /// assert_eq!(context.measure(&card)?, Rect::new(56.0, 18.0, 64.0, 22.0));
417    /// let card = context.align_to(card, &reference, Side::Bottom);
418    /// assert_eq!(context.measure(&card)?, Rect::new(56.0, 26.0, 64.0, 30.0));
419    /// # Ok::<(), motion_core::ValidationError>(())
420    /// ```
421    pub fn try_next_to(
422        &self,
423        node: impl Into<Node>,
424        reference: &Node,
425        side: crate::authoring::Side,
426        gap: f64,
427    ) -> Result<Node, crate::ValidationError> {
428        if !gap.is_finite() {
429            return Err(crate::ValidationError::invalid(
430                "layout",
431                "gap",
432                "Relative layout gap must be finite",
433            ));
434        }
435        let node = node.into();
436        let offset = side.adjacent(self.measure(&node)?, self.measure(reference)?, gap);
437        self.translated_layout(node, offset)
438    }
439
440    /// Aligns one measured edge with a reference, preserving the other axis.
441    ///
442    /// Uses the already transformed parent-space AABBs. Existing position,
443    /// rotation, scale and pivot remain intact; only translation is added.
444    pub fn try_align_to(
445        &self,
446        node: impl Into<Node>,
447        reference: &Node,
448        edge: crate::authoring::Side,
449    ) -> Result<Node, crate::ValidationError> {
450        let node = node.into();
451        let offset = edge.aligned(self.measure(&node)?, self.measure(reference)?);
452        self.translated_layout(node, offset)
453    }
454
455    /// Measures and places the snapshot next to its reference during compilation.
456    pub fn next_to(
457        &self,
458        node: impl Into<Node>,
459        reference: &Node,
460        side: super::Side,
461        gap: f64,
462    ) -> Node {
463        node.into().next_to(reference, side, gap)
464    }
465
466    /// Aligns one edge using the current compile-time font and drawing environment.
467    pub fn align_to(&self, node: impl Into<Node>, reference: &Node, edge: super::Side) -> Node {
468        node.into().align_to(reference, edge)
469    }
470
471    fn translated_layout(
472        &self,
473        node: Node,
474        offset: crate::Vector2,
475    ) -> Result<Node, crate::ValidationError> {
476        use crate::NodeModifiers;
477        if !offset.x.is_finite() || !offset.y.is_finite() {
478            return Err(crate::ValidationError::invalid(
479                "layout",
480                "translation",
481                "Relative layout offset must remain finite",
482            ));
483        }
484        let node = node.translate(offset.x, offset.y);
485        self.measure(&node)?;
486        Ok(node)
487    }
488
489    /// Returns a key-derived value in [0, 1), independent of time and cache state.
490    pub fn random(&self, key: impl Hash) -> f64 {
491        self.random_generator(key).next_float()
492    }
493
494    /// Starts a fresh key-derived stream, independent of all other evaluations.
495    pub fn random_generator(&self, key: impl Hash) -> Random {
496        Random::from_key(key)
497    }
498
499    /// Reuses a pure value by calling location, value type and an equal typed key.
500    ///
501    /// Keys must distinguish all inputs, including resource revisions. Keys are
502    /// retained, so use owned data or static references rather than borrowed locals.
503    /// Hash collisions are resolved by key type and equality.
504    /// Builders may be rerun after eviction, clear, disabled caching or races.
505    #[track_caller]
506    pub fn cache<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
507        &self,
508        key: Key,
509        build: impl FnOnce() -> Value,
510    ) -> Arc<Value> {
511        self.cache.get(key, build)
512    }
513
514    /// Like [`cache`](Self::cache) for a fallible builder, for example a validated
515    /// `build()`. A failure is returned and not retained, so the next call tries again.
516    #[track_caller]
517    pub fn try_cache<
518        Key: Eq + Hash + Send + Sync + 'static,
519        Value: Send + Sync + 'static,
520        Error,
521    >(
522        &self,
523        key: Key,
524        build: impl FnOnce() -> Result<Value, Error>,
525    ) -> Result<Arc<Value>, Error> {
526        self.cache.try_get(key, build)
527    }
528
529    /// Reuses a pure static subtree without exposing its shared Arc handle.
530    #[track_caller]
531    pub fn memo<Key: Eq + Hash + Send + Sync + 'static, Output: Into<Node>>(
532        &self,
533        key: Key,
534        build: impl FnOnce() -> Output,
535    ) -> Node {
536        self.cache(key, || build().into()).as_ref().clone()
537    }
538
539    /// Caches a pure value with an explicit logical retained-byte estimate.
540    /// Cost includes the key, value and their owned heap payloads, with a minimum
541    /// of their combined inline sizes. Entry metadata, allocator overhead and
542    /// shared external pins are excluded.
543    /// Oversized values are returned without retention; cost runs outside the lock.
544    /// Keys retain the same equality-checked, callsite-scoped contract as cache().
545    #[track_caller]
546    pub fn cache_with_cost<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
547        &self,
548        key: Key,
549        build: impl FnOnce() -> Value,
550        cost: impl FnOnce(&Value) -> usize,
551    ) -> Arc<Value> {
552        self.cache.get_with_cost(key, build, cost)
553    }
554}