Skip to main content

motion_core/authoring/
scene.rs

1//! Stateless scenes and explicit durations.
2
3use super::*;
4
5#[cfg(test)]
6mod authoring_helpers_tests;
7
8/// Converts ordinary drawable snapshots or recoverable scene failures.
9pub trait IntoSceneOutput {
10    /// Preserves the original failure category and source.
11    fn into_scene_output(self) -> crate::Result<Node>;
12}
13
14impl<Output: Into<Node>> IntoSceneOutput for Output {
15    fn into_scene_output(self) -> crate::Result<Node> {
16        Ok(self.into())
17    }
18}
19
20impl<Output: Into<Node>, Failure: Into<crate::Error>> IntoSceneOutput
21    for std::result::Result<Output, Failure>
22{
23    fn into_scene_output(self) -> crate::Result<Node> {
24        self.map(Into::into).map_err(Into::into)
25    }
26}
27
28struct SceneFailure {
29    error: Arc<crate::Error>,
30    report: Arc<crate::Error>,
31}
32
33impl SceneFailure {
34    fn new(error: crate::Error) -> Self {
35        let error = Arc::new(error);
36        let report = Arc::new(crate::Error::BuildErrors(vec![error.clone()]));
37        Self { error, report }
38    }
39
40    fn diagnostic(&self, context: &DrawContext) -> ValidationError {
41        context.diagnostics().record(self.report.clone());
42        if let Some(error) = self.error.downcast_ref::<ValidationError>() {
43            return error.clone();
44        }
45        ValidationError::invalid("scene", "output", self.error.to_string())
46    }
47}
48
49pub(crate) fn failure(error: impl Into<crate::Error>) -> Node {
50    Node::from(SceneFailure::new(error.into()))
51}
52
53impl Drawable for SceneFailure {
54    fn bounds(&self, _: &DrawContext) -> Rect {
55        Rect::new(0.0, 0.0, 0.0, 0.0)
56    }
57
58    fn try_bounds(&self, context: &DrawContext) -> Result<Rect, ValidationError> {
59        Err(self.diagnostic(context))
60    }
61
62    fn draw(
63        &self,
64        context: &DrawContext,
65        _: &mut crate::Canvas<'_>,
66    ) -> Result<(), ValidationError> {
67        Err(self.diagnostic(context))
68    }
69}
70
71/// Gives closures the context bound needed for inference and defers failures to rendering.
72pub fn scene<Build, Output>(build: Build) -> impl Fn(&SceneContext<'_>) -> Node + Send + Sync
73where
74    Build: Fn(&SceneContext<'_>) -> Output + Send + Sync,
75    Output: IntoSceneOutput,
76{
77    move |context| match build(context).into_scene_output() {
78        Ok(node) => node,
79        Err(error) => {
80            let failure = SceneFailure::new(error);
81            if let Some(diagnostics) = context.diagnostics() {
82                diagnostics.record(failure.report.clone());
83            }
84            Node::from(failure)
85        }
86    }
87}
88
89/// Groups heterogeneous drawable or node values in source order.
90#[macro_export]
91macro_rules! group {
92    () => { $crate::Node::empty() };
93    ($($child:expr),* $(,)?) => {
94        $crate::Node::group([$($crate::Node::from($child)),*])
95    };
96}
97
98/// A stateless scene evaluated from explicit authoring inputs.
99///
100/// Closures implement this canonical root [`crate::Scene`] protocol.
101///
102/// ```
103/// use motion_core::prelude::*;
104/// use motion_core::{
105///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
106///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
107/// };
108///
109/// let child = (|context: &SceneContext<'_>| {
110///     assert_eq!(context.time(), 1.seconds());
111///     assert_eq!(context.global_time(), 2.seconds());
112///     Node::from(Rect::new(0.0, 0.0, 32.0, 24.0).fill(Color::rgb(20, 150, 125)))
113/// })
114/// .with_duration(2.seconds());
115/// let cache = SceneCache::new(0);
116/// let context = SceneContext::new(
117///     2.seconds(),
118///     FrameRate::from(30),
119///     Size::new(640.0, 360.0),
120///     &cache,
121/// );
122/// let node = context.during(Time::ZERO, 4.seconds(), |local| {
123///     local.embed(&child, 1.seconds())
124/// });
125/// let frame = node.compile(
126///     context.size(),
127///     PixelSize::new(64, 36),
128///     ValidationLimits::default(),
129/// )?;
130/// # Ok::<(), motion_core::ValidationError>(())
131/// ```
132pub trait Scene: Send + Sync {
133    /// Validates deferred authoring errors without evaluating a frame.
134    fn validate(&self) -> crate::Result<()> {
135        Ok(())
136    }
137
138    /// Builds one snapshot without retaining previous-frame state.
139    fn render(&self, context: &SceneContext<'_>) -> Node;
140
141    /// Natural finite duration, or None when a caller must provide one.
142    fn duration(&self) -> Option<Duration> {
143        None
144    }
145
146    /// Returns pure audio metadata for precomposition/export, not playback.
147    /// Default scenes are silent; callers must preserve local clip coordinates.
148    fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
149        Ok(Vec::new())
150    }
151
152    /// Optional nested timeline ownership, preserved when its audio is relocated.
153    fn audio_composition(&self) -> Option<&crate::Timeline> {
154        None
155    }
156
157    /// Overrides clip metadata without retiming or clamping the callback.
158    ///
159    /// Panics for negative duration. Zero duration has an empty active interval.
160    fn with_duration(self, duration: Duration) -> WithDuration<Self>
161    where
162        Self: Sized,
163    {
164        assert!(duration >= Time::ZERO, "scene duration must be nonnegative");
165        WithDuration {
166            scene: self,
167            duration,
168        }
169    }
170}
171
172impl<Callback> Scene for Callback
173where
174    Callback: Fn(&SceneContext<'_>) -> Node + Send + Sync,
175{
176    fn render(&self, context: &SceneContext<'_>) -> Node {
177        self(context)
178    }
179}
180
181impl Scene for Arc<dyn Scene> {
182    fn audio_composition(&self) -> Option<&crate::Timeline> {
183        self.as_ref().audio_composition()
184    }
185
186    fn validate(&self) -> crate::Result<()> {
187        self.as_ref().validate()
188    }
189
190    fn render(&self, context: &SceneContext<'_>) -> Node {
191        self.as_ref().render(context)
192    }
193
194    fn duration(&self) -> Option<Duration> {
195        self.as_ref().duration()
196    }
197
198    fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
199        self.as_ref().audio_clips()
200    }
201}
202
203/// A scene with an explicit finite authoring duration.
204pub struct WithDuration<Content> {
205    scene: Content,
206    duration: Duration,
207}
208
209impl<Content: Scene> Scene for WithDuration<Content> {
210    fn audio_composition(&self) -> Option<&crate::Timeline> {
211        self.scene.audio_composition()
212    }
213
214    fn validate(&self) -> crate::Result<()> {
215        self.scene.validate()
216    }
217
218    fn render(&self, context: &SceneContext<'_>) -> Node {
219        self.scene.render(context)
220    }
221
222    fn duration(&self) -> Option<Duration> {
223        Some(self.duration)
224    }
225
226    fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
227        self.scene
228            .audio_clips()?
229            .iter()
230            .filter_map(|clip| clip.relocated(Time::ZERO, self.duration).transpose())
231            .collect::<Result<Vec<_>, _>>()
232            .map_err(Into::into)
233    }
234}
235
236/// Explicit sound-declaration scene; the time-zero metadata pass is cached once.
237pub struct SoundScene<Build> {
238    build: Build,
239    duration: Duration,
240    size: crate::Size,
241    rate: crate::FrameRate,
242    sounds: std::sync::Mutex<Option<Vec<crate::AudioClip>>>,
243}
244
245/// Opts a scene into local `SceneContext::sound` declarations without changing silent closure scenes.
246/// The declaration pass has time zero and no rendering/device initialization.
247pub fn scene_with_sound<Build, Output>(
248    build: Build,
249    duration: Duration,
250    size: crate::Size,
251    rate: impl Into<crate::FrameRate>,
252) -> SoundScene<Build>
253where
254    Build: Fn(&SceneContext<'_>) -> Output + Send + Sync,
255    Output: IntoSceneOutput,
256{
257    SoundScene {
258        build,
259        duration,
260        size,
261        rate: rate.into(),
262        sounds: std::sync::Mutex::new(None),
263    }
264}
265
266impl<Build, Output> Scene for SoundScene<Build>
267where
268    Build: Fn(&SceneContext<'_>) -> Output + Send + Sync,
269    Output: IntoSceneOutput,
270{
271    fn validate(&self) -> crate::Result {
272        if self.duration <= Time::ZERO || self.duration.as_seconds() > 86400.0 {
273            return Err(ValidationError::invalid(
274                "scene_audio",
275                "duration",
276                "Require duration in (0,24h]",
277            )
278            .into());
279        }
280        SceneContext::try_new(
281            Time::ZERO,
282            self.rate,
283            self.size,
284            &crate::SceneCache::default(),
285        )?;
286        Ok(())
287    }
288
289    fn duration(&self) -> Option<Duration> {
290        Some(self.duration)
291    }
292
293    fn render(&self, context: &SceneContext<'_>) -> Node {
294        match (self.build)(context).into_scene_output() {
295            Ok(node) => node,
296            Err(error) => failure(error),
297        }
298    }
299
300    fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
301        self.validate()?;
302        let mut cached = self.sounds.lock().map_err(|_| {
303            ValidationError::invalid("scene_audio", "lock", "Sound scene cache is poisoned")
304        })?;
305        if let Some(sounds) = cached.as_ref() {
306            return Ok(sounds.clone());
307        }
308        let cache = crate::SceneCache::default();
309        let sounds = std::sync::Mutex::new(Vec::new());
310        let context = SceneContext::try_new(Time::ZERO, self.rate, self.size, &cache)?
311            .with_clip_duration(self.duration)
312            .collecting_sounds(&sounds);
313        (self.build)(&context).into_scene_output()?;
314        let sounds = sounds.into_inner().map_err(|_| {
315            ValidationError::invalid("scene_audio", "lock", "Sound declaration lock is poisoned")
316        })?;
317        *cached = Some(sounds.clone());
318        Ok(sounds)
319    }
320}