Skip to main content

motion_core/authoring/
compile.rs

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