Skip to main content

motion_core/authoring/
node.rs

1//! Immutable scene nodes and bounded layout traversal.
2
3use super::*;
4
5/// A cheaply cloned snapshot of authored grouping and compositing choices.
6///
7/// [`Drawable`] values convert directly into leaves with content bounds.
8/// Grouping and modifiers retain geometry and explicit drawing policy.
9///
10/// ```
11/// use motion_core::prelude::*;
12/// use motion_core::{
13///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
14///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
15/// };
16/// let artwork = Rect::new(0.0, 0.0, 32.0, 24.0).fill(Color::rgb(20, 150, 125));
17/// let node = Node::group([Node::from(artwork).z_index(2)])
18///     .opacity(0.5)
19///     .named("overlay");
20/// node.compile(
21///     Size::new(32.0, 24.0),
22///     PixelSize::new(32, 24),
23///     ValidationLimits::default(),
24/// )?;
25/// # Ok::<(), motion_core::ValidationError>(())
26/// ```
27#[derive(Clone, Debug)]
28pub struct Node {
29    pub(super) data: Arc<NodeData>,
30}
31
32#[derive(Clone, Debug)]
33pub(super) struct NodeData {
34    pub(super) time: Option<Time>,
35    pub(super) kind: NodeKind,
36    pub(super) placement: Placement,
37    pub(super) text_metrics: Option<(f64, Rect)>,
38    pub(super) filters: Vec<crate::Filter>,
39    pub(super) opacity: f64,
40    pub(super) blend_mode: BlendMode,
41    pub(super) clip: Option<crate::PathClip>,
42    pub(super) rect_clip: Option<crate::RectClip>,
43    pub(super) shape_clip: Option<crate::ShapeClip>,
44    pub(super) mask: Option<Box<NodeMask>>,
45    pub(super) z_index: i32,
46    pub(super) name: Option<Arc<str>>,
47
48    pub(super) shutter_sampled: bool,
49}
50
51#[derive(Clone, Debug)]
52pub(super) struct NodeMask {
53    pub(super) node: Node,
54    pub(super) mode: crate::drawing::MaskMode,
55}
56
57#[derive(Clone)]
58pub(super) enum NodeKind {
59    Empty,
60    Leaf(Arc<dyn Drawable>),
61    Group(Vec<Node>),
62    Accumulate(Vec<Node>),
63    Layout(Layout, Vec<Node>),
64    Relative(super::spatial::Relative),
65}
66
67impl std::fmt::Debug for NodeKind {
68    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Empty => formatter.write_str("Empty"),
71            Self::Leaf(_) => formatter.write_str("Leaf(..)"),
72            Self::Relative(relative) => formatter.debug_tuple("Relative").field(relative).finish(),
73            Self::Group(children) => formatter.debug_tuple("Group").field(children).finish(),
74            Self::Accumulate(children) => {
75                formatter.debug_tuple("Accumulate").field(children).finish()
76            }
77            Self::Layout(layout, children) => formatter
78                .debug_tuple("Layout")
79                .field(layout)
80                .field(children)
81                .finish(),
82        }
83    }
84}
85
86impl Default for Node {
87    fn default() -> Self {
88        Self::empty()
89    }
90}
91
92/// Iterable or heterogeneous tuple children accepted by `Node::group`.
93pub trait IntoNodeGroup<Kind> {
94    /// Freezes child snapshots in source order.
95    fn into_nodes(self) -> Vec<Node>;
96}
97
98/// Distinguishes iterable children from tuple children during type inference.
99pub struct IteratorGroup<Child>(std::marker::PhantomData<Child>);
100
101/// Distinguishes heterogeneous tuple children from iterable children.
102pub struct TupleGroup;
103
104impl<Child: Into<Node>, Children: IntoIterator<Item = Child>> IntoNodeGroup<IteratorGroup<Child>>
105    for Children
106{
107    fn into_nodes(self) -> Vec<Node> {
108        self.into_iter().map(Into::into).collect()
109    }
110}
111
112impl IntoNodeGroup<TupleGroup> for () {
113    fn into_nodes(self) -> Vec<Node> {
114        Vec::new()
115    }
116}
117
118impl From<()> for Node {
119    fn from(_: ()) -> Self {
120        Self::empty()
121    }
122}
123
124macro_rules! tuple_nodes {
125    (@impl $($kind:ident:$value:ident,)+) => {
126        impl<$($kind: Into<Node>),+> IntoNodeGroup<TupleGroup> for ($($kind,)+) {
127            fn into_nodes(self) -> Vec<Node> {
128                let ($($value,)+) = self;
129                vec![$($value.into()),+]
130            }
131        }
132        impl<$($kind: Into<Node>),+> From<($($kind,)+)> for Node {
133            fn from(values: ($($kind,)+)) -> Self {
134                Self::new(NodeKind::Group(IntoNodeGroup::<TupleGroup>::into_nodes(values)))
135            }
136        }
137    };
138    (@grow [$($kind:ident:$value:ident,)+] $next:ident:$next_value:ident $(,$tail:ident:$tail_value:ident)*) => {
139        tuple_nodes!(@impl $($kind:$value,)+ $next:$next_value,);
140        tuple_nodes!(@grow [$($kind:$value,)+ $next:$next_value,] $($tail:$tail_value),*);
141    };
142    (@grow [$($kind:ident:$value:ident,)+]) => {};
143    ($first:ident:$first_value:ident $(,$tail:ident:$tail_value:ident)*) => {
144        tuple_nodes!(@impl $first:$first_value,);
145        tuple_nodes!(@grow [$first:$first_value,] $($tail:$tail_value),*);
146    };
147}
148
149tuple_nodes!(First:first, Second:second, Third:third, Fourth:fourth, Fifth:fifth, Sixth:sixth, Seventh:seventh, Eighth:eighth, Ninth:ninth, Tenth:tenth, Eleventh:eleventh, Twelfth:twelfth, Thirteenth:thirteenth, Fourteenth:fourteenth, Fifteenth:fifteenth, Sixteenth:sixteenth);
150
151impl Node {
152    pub(crate) fn shutter_sampled(mut self) -> Self {
153        Arc::make_mut(&mut self.data).shutter_sampled = true;
154        self
155    }
156
157    pub(crate) fn contains_shutter(&self, depth: usize) -> bool {
158        if depth > 256 || self.data.shutter_sampled {
159            return true;
160        }
161        if self
162            .data
163            .mask
164            .as_ref()
165            .is_some_and(|mask| mask.node.contains_shutter(depth + 1))
166        {
167            return true;
168        }
169        match &self.data.kind {
170            NodeKind::Group(children)
171            | NodeKind::Accumulate(children)
172            | NodeKind::Layout(_, children) => children
173                .iter()
174                .any(|child| child.contains_shutter(depth + 1)),
175            NodeKind::Relative(relative) => {
176                relative.node.contains_shutter(depth + 1)
177                    || relative.reference.contains_shutter(depth + 1)
178            }
179            _ => false,
180        }
181    }
182
183    /// Associates scene-local time with deferred content in this subtree.
184    pub fn with_scene_time(mut self, time: Time) -> Self {
185        Arc::make_mut(&mut self.data).time = Some(time);
186        self
187    }
188
189    pub(super) fn new(kind: NodeKind) -> Self {
190        Self {
191            data: Arc::new(NodeData {
192                time: None,
193                kind,
194                placement: Placement::default(),
195                text_metrics: None,
196                filters: Vec::new(),
197                opacity: 1.0,
198                blend_mode: BlendMode::SourceOver,
199                clip: None,
200                rect_clip: None,
201                shape_clip: None,
202                mask: None,
203                z_index: 0,
204                name: None,
205                shutter_sampled: false,
206            }),
207        }
208    }
209
210    /// Creates a node with no artwork.
211    pub fn empty() -> Self {
212        Self::new(NodeKind::Empty)
213    }
214
215    /// Positions a snapshot relative to compile-time measured reference bounds.
216    pub fn next_to(self, reference: &Node, side: Side, gap: f64) -> Self {
217        spatial::relative(self, reference.clone(), side, Some(gap))
218    }
219
220    /// Aligns an edge with a compile-time measured reference snapshot.
221    pub fn align_to(self, reference: &Node, edge: Side) -> Self {
222        spatial::relative(self, reference.clone(), edge, None)
223    }
224
225    /// Averages 1..=64 independently rendered samples in linear premultiplied RGBA.
226    /// Empty samples retain their weight; sibling z order cannot reorder samples.
227    pub fn accumulate(samples: impl IntoIterator<Item = Node>) -> Result<Self, ValidationError> {
228        let samples = samples.into_iter().take(65).collect::<Vec<_>>();
229        let count = samples.len();
230        Self::weighted_sum(samples.into_iter().map(|node| (node, 1.0 / count as f64)))
231    }
232
233    /// Composites 1..=64 isolated samples with explicit linear-light weights.
234    /// Each weight must be finite in `0..=1`; weights are not normalized.
235    pub fn weighted_sum(
236        samples: impl IntoIterator<Item = (Node, f64)>,
237    ) -> Result<Self, ValidationError> {
238        let samples = samples.into_iter().take(65).collect::<Vec<_>>();
239        if samples.is_empty()
240            || samples.len() > 64
241            || samples
242                .iter()
243                .any(|(_, weight)| !weight.is_finite() || !(0.0..=1.0).contains(weight))
244        {
245            return Err(ValidationError::invalid(
246                "accumulate",
247                "samples",
248                "Require 1..=64 samples and finite weights in 0..=1",
249            ));
250        }
251        Ok(Self::new(NodeKind::Accumulate(
252            samples
253                .into_iter()
254                .map(|(node, weight)| {
255                    Node::group([node])
256                        .opacity(weight)
257                        .blend_mode(BlendMode::LinearPlus)
258                })
259                .collect(),
260        )))
261    }
262
263    /// Attaches measured paragraph metrics for horizontal baseline layout.
264    /// Content implementations must provide conservative local bounds and a real baseline.
265    pub fn with_text_metrics(mut self, baseline: f64, bounds: Rect) -> Self {
266        Arc::make_mut(&mut self.data).text_metrics = Some((baseline, bounds));
267        self
268    }
269
270    /// Applies a full local affine matrix in a new group, before subsequent
271    /// placement modifiers.
272    pub fn transformed(self, transform: crate::Transform) -> Self {
273        let mut group = Self::group([self]);
274        Arc::make_mut(&mut group.data).placement.affine = transform;
275        group
276    }
277
278    /// Groups snapshots; sibling z indices are stably sorted during conversion.
279    pub fn group<Kind>(children: impl IntoNodeGroup<Kind>) -> Self {
280        Self::new(NodeKind::Group(children.into_nodes()))
281    }
282
283    /// Appends a drawable snapshot; nongroup nodes retain their original subtree.
284    pub fn push(&mut self, child: impl Into<Node>) {
285        let child = child.into();
286        if let NodeKind::Group(children) = &mut Arc::make_mut(&mut self.data).kind {
287            children.push(child);
288        } else {
289            *self = Self::group([self.clone(), child]);
290        }
291    }
292
293    pub(super) fn layout<Child: Into<Node>>(
294        layout: Layout,
295        children: impl IntoIterator<Item = Child>,
296    ) -> Self {
297        Self::new(NodeKind::Layout(
298            layout,
299            children.into_iter().map(Into::into).collect(),
300        ))
301    }
302
303    /// Measures content or declared layout geometry in parent coordinates.
304    ///
305    /// Uses f64 throughout, includes zero-area declared bounds and ignores empty
306    /// nodes. Structure and parameters validate without pixel narrowing.
307    /// Opacity does not change layout bounds. leaves call
308    /// Drawable::bounds, not draw. Uses default drawing inputs at device scale one.
309    pub fn measure(&self, limits: ValidationLimits) -> Result<Rect, ValidationError> {
310        self.measure_with(&DrawContext::default(), limits)
311    }
312
313    /// Validates structure and measured geometry without recording draw commands.
314    /// Fonts, when required by content, must be supplied through the drawing context.
315    pub fn preflight_with(
316        &self,
317        context: &DrawContext,
318        limits: ValidationLimits,
319    ) -> Result<crate::SceneStats, ValidationError> {
320        let context = &context.clone().with_validation_limits(limits);
321        let mut state = ValidationState::new(limits)?;
322        self.measure_bounds("node", 1, &mut state, context)?;
323        Ok(state.stats)
324    }
325
326    /// Measures with explicit, time-independent drawing inputs.
327    ///
328    /// Device scale is metadata for content; this adapter does not apply the
329    /// future scene-to-device root transform or infer resource environments.
330    pub fn measure_with(
331        &self,
332        context: &DrawContext,
333        limits: ValidationLimits,
334    ) -> Result<Rect, ValidationError> {
335        let context = &context.clone().with_validation_limits(limits);
336        let mut state = ValidationState::new(limits)?;
337        self.measure_bounds("node", 1, &mut state, context)?
338            .rect("node")
339    }
340
341    pub(super) fn measure_bounds(
342        &self,
343        path: &str,
344        depth: usize,
345        state: &mut ValidationState,
346        context: &DrawContext,
347    ) -> Result<Bounds, ValidationError> {
348        let context = &context
349            .clone()
350            .with_time(self.data.time.unwrap_or(context.time()));
351        let path = self
352            .data
353            .name
354            .as_ref()
355            .map_or_else(|| path.to_owned(), |name| format!("{path}(name={name})"));
356        state.enter(&path, depth)?;
357        self.validate_clip(&path, depth, state)?;
358        if !self.data.opacity.is_finite() || !(0.0..=1.0).contains(&self.data.opacity) {
359            return Err(ValidationError::invalid(
360                &path,
361                "opacity",
362                "Require finite opacity in 0..=1",
363            ));
364        }
365        if let Some(mask) = &self.data.mask {
366            mask.node
367                .measure_bounds(&format!("{path}.mask"), depth + 1, state, context)?;
368        }
369        let bounds = match &self.data.kind {
370            NodeKind::Empty => Bounds::Empty,
371            NodeKind::Relative(relative) => {
372                let subject = relative.node.measure_bounds(
373                    &format!("{path}.subject"),
374                    depth + 1,
375                    state,
376                    context,
377                )?;
378                let reference = relative.reference.measure_bounds(
379                    &format!("{path}.reference"),
380                    depth + 1,
381                    state,
382                    context,
383                )?;
384                let translation =
385                    relative.translation(subject.rect(&path)?, reference.rect(&path)?)?;
386                subject.transformed(translation, &path)?
387            }
388            NodeKind::Leaf(drawable) => {
389                if let Some(resolved) = drawable.resolve_node(context)? {
390                    resolved.measure_bounds(&path, depth + 1, state, context)?
391                } else {
392                    let bounds = drawable.try_bounds(context)?;
393                    crate::drawing::validate_rect(bounds, &path, "bounds")?;
394                    Bounds::Known(bounds)
395                }
396            }
397            NodeKind::Group(children)
398            | NodeKind::Layout(_, children)
399            | NodeKind::Accumulate(children) => {
400                let mut geometry = Vec::new();
401                for (index, child) in children.iter().enumerate() {
402                    geometry.push((
403                        index,
404                        child.measure_bounds(
405                            &format!("{path}.children[{index}]"),
406                            depth + 1,
407                            state,
408                            context,
409                        )?,
410                    ));
411                }
412                if let NodeKind::Layout(layout, _) = self.data.kind {
413                    let baselines = layout.baselines(children, context, &path)?;
414                    let placements = layout.placements(&geometry, &baselines, &path)?;
415                    for ((_, bounds), placement) in geometry.iter_mut().zip(placements) {
416                        if let Some((_, placed)) = placement {
417                            *bounds = placed;
418                        }
419                    }
420                }
421                geometry
422                    .into_iter()
423                    .fold(Bounds::Empty, |bounds, (_, child)| bounds.union(child))
424            }
425        };
426        let placement = self.data.placement.resolve(bounds, &path)?;
427        bounds.transformed(placement.transform, &path)
428    }
429
430    pub(super) fn validate_clip(
431        &self,
432        path: &str,
433        depth: usize,
434        state: &mut ValidationState,
435    ) -> Result<(), ValidationError> {
436        for (index, filter) in self.data.filters.iter().enumerate() {
437            let path = format!("{path}.filters[{index}]");
438            filter.validate(&path)?;
439            for operation in 0..filter.structural_cost() {
440                state.enter_with(|| format!("{path}[{operation}]"), depth + 1)?;
441            }
442        }
443        if let Some(clip) = self.data.rect_clip {
444            let path = format!("{path}.rect_clip");
445            crate::display_list::mapped_bounds(clip.bounds, clip.transform, &path)?;
446            state.enter(&path, depth + 1)?;
447        }
448        if let Some(clip) = &self.data.shape_clip {
449            let path = format!("{path}.shape_clip");
450            let verbs = clip.validate(&path)?;
451            state.enter(&path, depth + 1)?;
452            for index in 0..verbs {
453                state.enter_with(|| format!("{path}.verbs[{index}]"), depth + 1)?;
454            }
455        }
456        if let Some(clip) = &self.data.clip {
457            let path = format!("{path}.clip");
458            crate::display_list::validate_path(&clip.path, clip.transform, &path)?;
459            for index in 0..clip.path.verbs().len() {
460                state.enter_with(|| format!("{path}.verbs[{index}]"), depth + 1)?;
461            }
462        }
463        Ok(())
464    }
465
466    pub(super) fn baseline(
467        &self,
468        context: &DrawContext,
469        path: &str,
470    ) -> Result<Option<f64>, ValidationError> {
471        let (baseline, local_bounds) = if let Some(metrics) = self.data.text_metrics {
472            metrics
473        } else {
474            let NodeKind::Leaf(drawable) = &self.data.kind else {
475                return Ok(None);
476            };
477            let Some(baseline) = drawable.baseline(context) else {
478                return Ok(None);
479            };
480            (baseline, drawable.try_bounds(context)?)
481        };
482        if !baseline.is_finite() {
483            return Err(ValidationError::invalid(
484                path,
485                "baseline",
486                "Text baseline must be finite",
487            ));
488        }
489        if self.data.placement.rotation != 0.0 || self.data.placement.scale.y <= 0.0 {
490            return Err(ValidationError::unsupported(
491                path,
492                "baseline",
493                "Baseline rows require horizontal, upright text",
494            ));
495        }
496        let bounds = Bounds::declared(local_bounds, path)?;
497        let placement = self.data.placement.resolve(bounds, path)?;
498        let baseline = placement.transform.apply(Point::new(0.0, baseline)).y;
499        if !baseline.is_finite() {
500            return Err(ValidationError::invalid(
501                path,
502                "baseline",
503                "Transformed baseline must be finite",
504            ));
505        }
506        Ok(Some(baseline))
507    }
508}
509
510impl<Content: Drawable + 'static> From<Content> for Node {
511    fn from(content: Content) -> Self {
512        Self::new(NodeKind::Leaf(Arc::new(content)))
513    }
514}
515
516impl From<Arc<dyn Drawable>> for Node {
517    fn from(content: Arc<dyn Drawable>) -> Self {
518        Self::new(NodeKind::Leaf(content))
519    }
520}
521
522impl<Value: Into<Node>> From<Option<Value>> for Node {
523    fn from(value: Option<Value>) -> Self {
524        value.map_or_else(Self::empty, Into::into)
525    }
526}
527
528impl FromIterator<Node> for Node {
529    fn from_iter<Iter: IntoIterator<Item = Node>>(iter: Iter) -> Self {
530        Self::group(iter)
531    }
532}