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