Skip to main content

motion_core/authoring/
compile.rs

1//! Scene compilation: bounded layout/recording followed by device emission.
2
3use std::fmt::Write;
4
5use super::spatial::Bounds;
6use super::{Node, NodeKind};
7use crate::display_list::{Builder, mapped_bounds};
8use crate::drawing::BlendMode;
9use crate::validation::ValidationState;
10use crate::{
11    Canvas, DrawCommand, DrawContext, PixelSize, Size, Transform, ValidatedDisplayList,
12    ValidationError, ValidationLimits,
13};
14
15mod inspection;
16#[cfg(test)]
17mod tests;
18
19pub use inspection::NodeInspection;
20
21/// Spare bytes reserved after a parent path for one `.children[n]` or `.commands[n]` suffix.
22const CHILD_PATH_SLACK: usize = 24;
23
24impl Node {
25    /// Compiles content to flat device-space commands without rendering.
26    ///
27    /// Logical design space is contained and centered in the output dimensions.
28    /// bounds/draw callbacks receive the resulting device scale once per
29    /// occurrence. Layout and stable sibling z order precede matrix composition.
30    /// Nonunit opacity isolates its whole subtree without f32 narrowing.
31    ///
32    /// All input branches and emitted work validate, including empty, transparent
33    /// and offscreen content. There is no culling yet. Source and output have independent
34    /// instances of the supplied limits. This does not prepare coverage/pixels.
35    ///
36    /// ```
37    /// use motion_core::prelude::*;
38    /// use motion_core::{
39    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
40    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
41    /// };
42    /// use motion_core::authoring::Scene as _;
43    /// let cache = SceneCache::new(0);
44    /// let context = SceneContext::new(
45    ///     Time::ZERO,
46    ///     fps!(30),
47    ///     Size::new(100.0, 50.0),
48    ///     &cache,
49    /// );
50    /// let scene = |context: &SceneContext<'_>| {
51    ///     let path = Path2D::from_rect(Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
52    ///     Row::new()
53    ///         .gap(4.0)
54    ///         .children([
55    ///             Node::from(Rect::new(0.0, 0.0, 10.0, 20.0).fill(Color::WHITE)),
56    ///             Node::from(path.fill(Color::from_hex(0x24a49b))),
57    ///         ])
58    ///         .at(context.center())
59    ///         .opacity(0.5)
60    /// };
61    /// let list = scene.render(&context).compile(
62    ///     context.size(),
63    ///     PixelSize::new(200, 200),
64    ///     ValidationLimits::default(),
65    /// )?;
66    /// assert_eq!(list.commands.len(), 4);
67    /// assert!(matches!(
68    ///     list.commands[0],
69    ///     DrawCommand::PushLayer { opacity: 0.5, .. }
70    /// ));
71    /// assert!(matches!(
72    ///     list.commands[2],
73    ///     DrawCommand::Fill {
74    ///         rule: FillRule::NonZero,
75    ///         ..
76    ///     }
77    /// ));
78    /// list.preflight(ValidationLimits::default())?;
79    /// # Ok::<(), motion_core::ValidationError>(())
80    /// ```
81    pub fn compile(
82        &self,
83        design_size: Size,
84        output_size: PixelSize,
85        limits: ValidationLimits,
86    ) -> Result<ValidatedDisplayList, ValidationError> {
87        self.compile_in_fonts(design_size, output_size, limits, None)
88    }
89
90    /// Compiles with one immutable font environment shared by paragraph callbacks.
91    pub fn compile_with_fonts(
92        &self,
93        design_size: Size,
94        output_size: PixelSize,
95        limits: ValidationLimits,
96        fonts: impl Into<crate::asset::environment::FontEnvironment>,
97    ) -> Result<ValidatedDisplayList, ValidationError> {
98        let fonts = fonts.into();
99        self.compile_in_fonts(design_size, output_size, limits, Some(&fonts))
100    }
101
102    fn compile_in_fonts(
103        &self,
104        design_size: Size,
105        output_size: PixelSize,
106        limits: ValidationLimits,
107        fonts: Option<&crate::asset::environment::FontEnvironment>,
108    ) -> Result<ValidatedDisplayList, ValidationError> {
109        let mut context = DrawContext::default();
110        if let Some(fonts) = fonts {
111            context = context.with_fonts(fonts.clone());
112        }
113        self.compile_with_context(design_size, output_size, limits, &context)
114    }
115
116    /// Compiles with explicit resources and a reusable session layout cache.
117    pub fn compile_with_context(
118        &self,
119        design_size: Size,
120        output_size: PixelSize,
121        limits: ValidationLimits,
122        drawing: &DrawContext,
123    ) -> Result<ValidatedDisplayList, ValidationError> {
124        let (root, scale) = output_size.mapping(design_size)?;
125        let mut context = drawing.clone().with_validation_limits(limits);
126        context.device_scale = scale;
127        let mut source = ValidationState::new(limits)?;
128        let plan = plan(self, "node", 1, &context, &mut source)?;
129        let mut output = Builder::new(output_size, limits)?;
130        plan.emit(root, &mut output)?;
131        output.finish_validated()
132    }
133}
134
135struct Plan {
136    transform: Transform,
137    bounds: Bounds,
138    opacity: f64,
139    blend_mode: BlendMode,
140    filters: Vec<crate::Filter>,
141    clip: Option<crate::PathClip>,
142    rect_clip: Option<crate::RectClip>,
143    shape_clip: Option<crate::ShapeClip>,
144    mask: Option<(Box<Plan>, crate::drawing::MaskMode)>,
145    path: String,
146    content: Content,
147    has_commands: bool,
148    accumulate: bool,
149}
150
151enum Content {
152    Leaf(Vec<DrawCommand>),
153    Children(Vec<Plan>),
154}
155
156fn plan(
157    node: &Node,
158    path: &str,
159    depth: usize,
160    context: &DrawContext,
161    state: &mut ValidationState,
162) -> Result<Plan, ValidationError> {
163    let context = &context
164        .clone()
165        .with_time(node.data.time.unwrap_or(context.time()));
166    let path = node
167        .data
168        .name
169        .as_ref()
170        .map_or_else(|| path.to_owned(), |name| format!("{path}(name={name})"));
171    state.enter(&path, depth)?;
172    let opacity = node.data.opacity;
173    if !opacity.is_finite() || !(0.0..=1.0).contains(&opacity) {
174        return Err(ValidationError::invalid(
175            &path,
176            "opacity",
177            "Require finite opacity in 0..=1",
178        ));
179    }
180    node.validate_clip(&path, depth, state)?;
181    let mask = node
182        .data
183        .mask
184        .as_ref()
185        .map(|mask| {
186            plan(
187                &mask.node,
188                &format!("{path}.mask"),
189                depth + 1,
190                context,
191                state,
192            )
193            .map(|planned| (Box::new(planned), mask.mode))
194        })
195        .transpose()?;
196    let (content, bounds, has_commands) = match &node.data.kind {
197        NodeKind::Empty => (Content::Children(Vec::new()), Bounds::Empty, false),
198        NodeKind::Relative(relative) => {
199            let mut subject = plan(
200                &relative.node,
201                &format!("{path}.subject"),
202                depth + 1,
203                context,
204                state,
205            )?;
206            let reference = relative.reference.measure_bounds(
207                &format!("{path}.reference"),
208                depth + 1,
209                state,
210                context,
211            )?;
212            let translation =
213                relative.translation(subject.bounds.rect(&path)?, reference.rect(&path)?)?;
214            subject.transform = subject.transform.then(translation);
215            subject.bounds = subject.bounds.transformed(translation, &path)?;
216            let bounds = subject.bounds;
217            let has_commands = subject.has_commands;
218            (Content::Children(vec![subject]), bounds, has_commands)
219        }
220        NodeKind::Leaf(drawable) => {
221            let leaf = record_leaf(drawable.as_ref(), &path, depth, context, state);
222            let (bounds, commands) = match leaf {
223                Err(error)
224                    if context.marks_invalid_geometry()
225                        && error.kind == crate::ValidationErrorKind::InvalidValue =>
226                {
227                    invalid_geometry_marker(&path, depth, state)?
228                }
229                leaf => leaf?,
230            };
231            let has_commands = !commands.is_empty();
232            (Content::Leaf(commands), Bounds::Known(bounds), has_commands)
233        }
234        NodeKind::Deferred(content) => {
235            let resolved = content.resolve(context).map_err(|mut error| {
236                error.path = format!("{path}.{}", error.path);
237                error
238            })?;
239            let expanded = plan(
240                &resolved,
241                &format!("{path}.resolved"),
242                depth + 1,
243                context,
244                state,
245            )?;
246            let bounds = expanded.bounds;
247            let has_commands = expanded.has_commands;
248            (Content::Children(vec![expanded]), bounds, has_commands)
249        }
250        NodeKind::Group(children)
251        | NodeKind::Layout(_, children)
252        | NodeKind::Accumulate(children) => {
253            let mut planned = Vec::with_capacity(children.len());
254            let mut child_path = String::with_capacity(path.len() + CHILD_PATH_SLACK);
255            for (index, child) in children.iter().enumerate() {
256                child_path.clear();
257                let _ = write!(child_path, "{path}.children[{index}]");
258                planned.push((index, plan(child, &child_path, depth + 1, context, state)?));
259            }
260            if let NodeKind::Layout(layout, _) = node.data.kind {
261                let geometry: Vec<_> = planned
262                    .iter()
263                    .map(|(index, child)| (*index, child.bounds))
264                    .collect();
265                let baselines = layout.baselines(children, context, &path)?;
266                for ((_, child), placement) in planned
267                    .iter_mut()
268                    .zip(layout.placements(&geometry, &baselines, &path)?)
269                {
270                    if let Some((placement, bounds)) = placement {
271                        child.transform = child.transform.then(placement.transform);
272                        child.bounds = bounds;
273                    }
274                }
275            }
276            planned.sort_by_key(|(index, _)| children[*index].data.z_index);
277            let mut bounds = Bounds::Empty;
278            let mut has_commands = false;
279            let children = planned
280                .into_iter()
281                .map(|(_, child)| {
282                    bounds = bounds.union(child.bounds);
283                    has_commands |= child.has_commands;
284                    child
285                })
286                .collect();
287            (Content::Children(children), bounds, has_commands)
288        }
289    };
290    let placement = node.data.placement.resolve(bounds, &path)?;
291    let bounds = bounds.transformed(placement.transform, &path)?;
292    Ok(Plan {
293        transform: placement.transform,
294        bounds,
295        opacity,
296        blend_mode: node.data.blend_mode,
297        filters: node.data.filters.clone(),
298        clip: node.data.clip.clone(),
299        rect_clip: node.data.rect_clip,
300        shape_clip: node.data.shape_clip.clone(),
301        mask,
302        path,
303        content,
304        has_commands,
305        accumulate: matches!(node.data.kind, NodeKind::Accumulate(_)),
306    })
307}
308
309fn record_leaf(
310    drawable: &dyn crate::Drawable,
311    path: &str,
312    depth: usize,
313    context: &DrawContext,
314    state: &mut ValidationState,
315) -> Result<(crate::Rect, Vec<DrawCommand>), ValidationError> {
316    let bounds = drawable.bounds(context)?;
317    crate::drawing::validate_rect(bounds, path, "bounds")?;
318    let commands = Canvas::new(state, path, depth + 1).record(drawable, context)?;
319    Ok((bounds, commands))
320}
321
322/// Magenta square at the leaf's local origin standing in for geometry that failed validation.
323fn invalid_geometry_marker(
324    path: &str,
325    depth: usize,
326    state: &mut ValidationState,
327) -> Result<(crate::Rect, Vec<DrawCommand>), ValidationError> {
328    let bounds = crate::Rect::new(0.0, 0.0, 24.0, 24.0);
329    let mut canvas = Canvas::new(state, path, depth + 1);
330    canvas.fill_rect(bounds, crate::Color::from_rgb_float(1.0, 0.0, 1.0))?;
331    Ok((bounds, canvas.finish()?))
332}
333
334impl Plan {
335    fn input_bounds(&self, parent: Transform) -> Result<Bounds, ValidationError> {
336        let transform = self.transform.then(parent);
337        match &self.content {
338            Content::Leaf(_) => self.bounds.transformed(parent, &self.path),
339            Content::Children(children) => {
340                let mut bounds = Bounds::Empty;
341                for child in children {
342                    bounds = bounds.union(child.visual_bounds(transform)?);
343                }
344                Ok(bounds)
345            }
346        }
347    }
348
349    fn visual_bounds(&self, parent: Transform) -> Result<Bounds, ValidationError> {
350        let bounds = self.input_bounds(parent)?;
351        if let Bounds::Known(mut bounds) = bounds {
352            for filter in &self.filters {
353                bounds = filter
354                    .device(self.transform.then(parent), &self.path)?
355                    .extent(bounds, &self.path)?
356                    .0;
357            }
358            Ok(Bounds::Known(bounds))
359        } else {
360            Ok(bounds)
361        }
362    }
363
364    fn emit(self, parent: Transform, output: &mut Builder) -> Result<(), ValidationError> {
365        let transform = self.transform.then(parent);
366        if !transform.is_finite() {
367            return Err(ValidationError::invalid(
368                &self.path,
369                "transform",
370                "Composed device transform must remain finite",
371            ));
372        }
373        let input_bounds = self.input_bounds(parent)?;
374        let visual_bounds = self.visual_bounds(parent)?;
375        let filters = self
376            .filters
377            .iter()
378            .map(|filter| filter.device(transform, &self.path))
379            .collect::<Result<Vec<_>, _>>()?;
380        let rect_clip = self
381            .rect_clip
382            .map(|mut clip| {
383                clip.transform = clip.transform.then(transform);
384                mapped_bounds(
385                    clip.bounds,
386                    clip.transform,
387                    &format!("{}.rect_clip", self.path),
388                )?;
389                Ok::<_, ValidationError>(clip)
390            })
391            .transpose()?;
392        let shape_clip = self
393            .shape_clip
394            .map(|mut clip| {
395                clip.transform = clip.transform.then(transform);
396                clip.validate(&format!("{}.shape_clip", self.path))?;
397                Ok::<_, ValidationError>(clip)
398            })
399            .transpose()?;
400        let isolate = (self.accumulate
401            || self.opacity != 1.0
402            || self.blend_mode != BlendMode::SourceOver
403            || self.clip.is_some()
404            || self.rect_clip.is_some()
405            || shape_clip.is_some()
406            || self.mask.is_some()
407            || !filters.is_empty())
408            && self.has_commands;
409        if isolate {
410            let bounds = visual_bounds.rect(&self.path)?;
411            let clip = self.clip.map(|mut clip| {
412                clip.transform = clip.transform.then(transform);
413                clip
414            });
415            let command = if let Some(shape_clip) = shape_clip {
416                DrawCommand::PushShapeLayer {
417                    opacity: self.opacity,
418                    bounds,
419                    blend_mode: self.blend_mode,
420                    clip,
421                    rect_clip,
422                    shape_clip,
423                }
424            } else if let Some(rect_clip) = rect_clip {
425                DrawCommand::PushRectLayer {
426                    opacity: self.opacity,
427                    bounds,
428                    blend_mode: self.blend_mode,
429                    clip,
430                    rect_clip,
431                }
432            } else {
433                DrawCommand::PushLayer {
434                    opacity: self.opacity,
435                    bounds,
436                    blend_mode: self.blend_mode,
437                    clip,
438                }
439            };
440            output.push(command, &format!("{}.push_layer", self.path))?;
441        }
442        if self.has_commands && !filters.is_empty() {
443            output.push(
444                DrawCommand::PushFilterLayer {
445                    bounds: input_bounds.rect(&self.path)?,
446                    filters,
447                },
448                &format!("{}.filters", self.path),
449            )?;
450        }
451        match self.content {
452            Content::Leaf(commands) => {
453                let mut command_path = String::with_capacity(self.path.len() + CHILD_PATH_SLACK);
454                for (index, mut command) in commands.into_iter().enumerate() {
455                    match &mut command {
456                        DrawCommand::FillRect {
457                            transform: local, ..
458                        }
459                        | DrawCommand::Fill {
460                            transform: local, ..
461                        }
462                        | DrawCommand::ParticleInstances {
463                            transform: local, ..
464                        } => *local = local.then(transform),
465                        DrawCommand::PushFilterLayer { .. }
466                        | DrawCommand::PushLayer { .. }
467                        | DrawCommand::PushRectLayer { .. }
468                        | DrawCommand::PushShapeLayer { .. }
469                        | DrawCommand::PopLayer
470                        | DrawCommand::BeginMask { .. }
471                        | DrawCommand::EndMask => {
472                            return Err(ValidationError::invalid(
473                                &self.path,
474                                "commands",
475                                "Local drawables cannot supply compiled layers",
476                            ));
477                        }
478                    }
479                    command_path.clear();
480                    let _ = write!(command_path, "{}.commands[{index}]", self.path);
481                    output.push(command, &command_path)?;
482                }
483            }
484            Content::Children(children) => {
485                for child in children {
486                    child.emit(transform, output)?;
487                }
488            }
489        }
490        if self.has_commands && !self.filters.is_empty() {
491            output.push(DrawCommand::PopLayer, &format!("{}.filters.pop", self.path))?;
492        }
493        if isolate {
494            if let Some((mask, mode)) = self.mask {
495                output.push(
496                    DrawCommand::BeginMask { mode },
497                    &format!("{}.begin_mask", self.path),
498                )?;
499                mask.emit(transform, output)?;
500                output.push(DrawCommand::EndMask, &format!("{}.end_mask", self.path))?;
501            }
502            output.push(DrawCommand::PopLayer, &format!("{}.pop_layer", self.path))?;
503        }
504        Ok(())
505    }
506}
507
508#[cfg(test)]
509mod deferred_content_tests {
510    use super::*;
511    use crate::Shape;
512    use crate::{Color, DeferredContent, NodeModifiers, Rect, TimeLiteral};
513
514    struct Deferred;
515
516    impl DeferredContent for Deferred {
517        fn resolve(&self, context: &DrawContext) -> Result<Node, ValidationError> {
518            Ok(Rect::new(0.0, 0.0, 2.0 + context.time().as_seconds(), 1.0)
519                .fill(Color::WHITE)
520                .into())
521        }
522    }
523
524    #[test]
525    fn deferred_content_preserves_outer_placement_and_inherited_local_time() {
526        let node = Node::deferred(Deferred)
527            .translate(10.0, 0.0)
528            .with_scene_time(1.seconds().after_origin());
529        assert_eq!(
530            node.measure(ValidationLimits::default()).unwrap(),
531            Rect::new(10.0, 0.0, 13.0, 1.0)
532        );
533        let list = node
534            .compile(
535                Size::new(20.0, 4.0),
536                PixelSize::new(20, 4),
537                ValidationLimits::default(),
538            )
539            .unwrap();
540        assert_eq!(list.commands.len(), 1);
541    }
542}