Skip to main content

motion_content/
vector.rs

1//! Shared immutable semantic vectors for text, SVG, formulas and vector effects.
2//!
3//! ```
4//! use motion_content::prelude::*;
5//! use motion_core::prelude::*;
6//! use motion_core::{
7//!     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
8//!     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
9//! };
10//! use motion_render::{Composition, RenderPrepare, RenderOptions, BackendKind, render_still};
11//! let tree = VectorTree::new(
12//!     Rect::new(0.0, 0.0, 16.0, 16.0),
13//!     VectorGroup {
14//!         children: vec![VectorItem::Path(VectorPath::filled(
15//!             Path2D::from_rect(Rect::new(2.0, 2.0, 14.0, 14.0))?,
16//!             Color::WHITE,
17//!         ))],
18//!         ..VectorGroup::default()
19//!     },
20//! )?;
21//! assert_eq!(tree.surface()?.area(), 144.0);
22//! assert_eq!(tree.sample_points(32, 42)?, tree.sample_points(32, 42)?);
23//! let ink = tree.write(0.5, WriteOptions::default())?;
24//! let morph = Morph::new(&tree, &tree)?;
25//! let frame = morph
26//!     .sample(0.5)?
27//!     .compile(
28//!         Size::new(16.0, 16.0),
29//!         PixelSize::new(16, 16),
30//!         ValidationLimits::default(),
31//!     )?
32//!     .prepare(ValidationLimits::default(), RasterLimits::default())?
33//!     .render_linear(RasterLimits::default())?;
34//! assert_eq!(frame.linear_pixel(8, 8), Some([1.0; 4]));
35//! # Ok::<(), motion_core::Error>(())
36//! ```
37
38use motion_core::{
39    DrawContext, Drawable, FillRule, Node, NodeModifiers, Paint, Path2D, Rect, StrokeStyle,
40    Transform, ValidationError, ValidationLimits,
41};
42use std::{collections::BTreeMap, ops::Range, sync::Arc};
43
44mod deferred;
45mod effects;
46#[cfg(feature = "ink")]
47pub mod ink;
48mod morph;
49mod surface;
50#[cfg(test)]
51mod tests;
52
53pub use deferred::{CompileCache, VectorSelection};
54pub use effects::WriteOptions;
55pub use morph::Morph;
56pub use surface::{SurfaceTriangle, VectorSurface};
57
58fn invalid(field: &str, message: &str) -> ValidationError {
59    ValidationError::invalid("vector_tree", field, message)
60}
61
62fn limited(message: &str) -> ValidationError {
63    ValidationError::limit("vector_tree", "structure", message)
64}
65
66/// A painted path in its containing group's local coordinates.
67#[derive(Clone, Debug)]
68pub struct VectorPath {
69    /// Optional unique selection ID.
70    pub id: Option<Arc<str>>,
71    /// Original curves, shared without flattening.
72    pub path: Arc<Path2D>,
73    /// Fill paint and contour interpretation; None is genuinely unfilled.
74    pub fill: Option<(Paint, FillRule)>,
75    /// Local stroke style/paint, drawn after fill.
76    pub stroke: Option<(StrokeStyle, Paint)>,
77    /// Optional original UTF-8 source interval for reading-order operations.
78    pub source: Option<Range<usize>>,
79}
80
81impl VectorPath {
82    /// Creates a nonzero solid/spatial filled path without an ID or stroke.
83    pub fn filled(path: Path2D, paint: impl Into<Paint>) -> Self {
84        Self {
85            id: None,
86            path: Arc::new(path),
87            fill: Some((paint.into(), FillRule::NonZero)),
88            stroke: None,
89            source: None,
90        }
91    }
92}
93
94impl Drawable for VectorPath {
95    fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
96        let mut bounds = self.path.bounding_box();
97        if let Some((style, _)) = &self.stroke {
98            bounds = bounds.union(
99                style
100                    .outline(&self.path, "vector_tree.stroke")?
101                    .bounding_box(),
102            );
103        }
104        Ok(bounds)
105    }
106
107    fn draw(
108        &self,
109        _: &DrawContext,
110        canvas: &mut motion_core::Canvas<'_>,
111    ) -> Result<(), ValidationError> {
112        if let Some((paint, rule)) = &self.fill {
113            canvas.fill_path(self.path.clone(), Transform::IDENTITY, paint.clone(), *rule)?;
114        }
115        if let Some((style, paint)) = &self.stroke {
116            canvas.stroke(self.path.as_ref(), style, paint.clone())?;
117        }
118        Ok(())
119    }
120}
121
122/// A bitmap leaf; its surrounding group supplies any affine placement.
123#[derive(Clone, Debug)]
124pub struct VectorImage {
125    /// Optional unique selection ID.
126    pub id: Option<Arc<str>>,
127    /// Immutable linear-premultiplied mip data.
128    pub image: Arc<crate::ImageAsset>,
129    /// Local destination rectangle; pixels stretch to this rectangle.
130    pub rect: Rect,
131    /// Source-pixel-to-local mapping, independent of the filled rectangle.
132    pub pixel_transform: Transform,
133    /// Shared reconstruction rule.
134    pub sampling: crate::Sampling,
135    /// Sampling outside source coordinates.
136    pub extend: motion_core::GradientExtend,
137    /// Whether source-pixel bounds discard samples before reconstruction.
138    pub decal: bool,
139    /// Optional source provenance.
140    pub source: Option<Range<usize>>,
141}
142
143impl VectorImage {
144    /// Creates a image stretched to a local rectangle, with trilinear/Pad
145    /// sampling and a source decal. Fonts/importers may retain a distinct mapping.
146    pub fn new(image: Arc<crate::ImageAsset>, rect: Rect) -> Self {
147        let pixel_transform = Transform::scale_non_uniform(
148            rect.width() / f64::from(image.width()),
149            rect.height() / f64::from(image.height()),
150        )
151        .then(Transform::translate(rect.left, rect.top));
152        Self {
153            id: None,
154            image,
155            rect,
156            pixel_transform,
157            sampling: crate::Sampling::Trilinear,
158            extend: motion_core::GradientExtend::Pad,
159            decal: true,
160            source: None,
161        }
162    }
163
164    pub(crate) fn pattern(&self) -> motion_core::ImagePattern {
165        let mut pattern = motion_core::ImagePattern::from_asset(self.image.clone())
166            .transform(self.pixel_transform)
167            .sampling(self.sampling)
168            .extend(self.extend);
169        pattern.decal = self.decal;
170        pattern
171    }
172}
173
174impl Drawable for VectorImage {
175    fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
176        Ok(self.rect)
177    }
178
179    fn draw(
180        &self,
181        _: &DrawContext,
182        canvas: &mut motion_core::Canvas<'_>,
183    ) -> Result<(), ValidationError> {
184        if self.rect.width() == 0.0 || self.rect.height() == 0.0 {
185            return Ok(());
186        }
187        canvas.fill_path(
188            Arc::new(Path2D::from_rect(self.rect)?),
189            Transform::IDENTITY,
190            self.pattern(),
191            FillRule::NonZero,
192        )
193    }
194}
195
196/// A semantic leaf or group, retained in paint order.
197#[derive(Clone, Debug)]
198pub enum VectorItem {
199    /// Nested isolation/coordinate system.
200    Group(VectorGroup),
201    /// Painted curves.
202    Path(VectorPath),
203    /// immutable image.
204    Image(VectorImage),
205}
206
207/// A shared coordinate/isolation group, including source provenance.
208#[derive(Clone, Debug)]
209pub struct VectorMask {
210    /// Immutable local mask artwork; its IDs are scoped to this resource.
211    pub tree: Arc<VectorTree>,
212    /// Alpha/luminance extraction policy.
213    pub mode: motion_core::MaskMode,
214    /// Local mask region, outside which coverage is zero.
215    pub rect: Rect,
216}
217
218/// A shared coordinate/isolation group, including source provenance.
219#[derive(Clone, Debug)]
220pub struct VectorGroup {
221    /// Optional unique selection ID.
222    pub id: Option<Arc<str>>,
223    /// Local-to-parent full f64 affine mapping.
224    pub transform: Transform,
225    /// Whole-group alpha, not per-child alpha.
226    pub opacity: f64,
227    /// Optional local path coverage, applied once to the completed group.
228    pub clip: Option<motion_core::PathClip>,
229    /// Content coverage applied once after source filters and before clipping.
230    pub mask: Option<VectorMask>,
231    /// Whole-group blending against its immediate backdrop.
232    pub blend_mode: motion_core::BlendMode,
233    /// Isolates descendant blending even with unit opacity and SourceOver.
234    pub isolate: bool,
235    /// Ordered source-only filters.
236    pub filters: Vec<motion_core::Filter>,
237    /// Source provenance inherited by descendants without their own interval.
238    pub source: Option<Range<usize>>,
239    /// Paint order, independent of source reading order.
240    pub children: Vec<VectorItem>,
241}
242
243impl Default for VectorGroup {
244    fn default() -> Self {
245        Self {
246            id: None,
247            transform: Transform::IDENTITY,
248            opacity: 1.0,
249            clip: None,
250            mask: None,
251            blend_mode: motion_core::BlendMode::SourceOver,
252            isolate: false,
253            filters: Vec::new(),
254            source: None,
255            children: Vec::new(),
256        }
257    }
258}
259
260impl VectorGroup {
261    fn node(&self) -> Node {
262        self.decorate(self.children.iter().map(item_node))
263    }
264
265    pub(crate) fn decorate(&self, children: impl IntoIterator<Item = Node>) -> Node {
266        let content = Node::group(children);
267        let content = if self.isolate {
268            Node::accumulate([content]).expect("one isolated content sample")
269        } else {
270            content
271        };
272        let mut node = content.opacity(self.opacity).blend_mode(self.blend_mode);
273        for filter in &self.filters {
274            node = node.filter(filter.clone());
275        }
276        if let Some(mask) = &self.mask {
277            node = node.mask(
278                mask.tree
279                    .node()
280                    .clip_with(mask.rect, motion_core::ClipMode::Geometric),
281                mask.mode,
282            );
283        }
284        if let Some(clip) = &self.clip {
285            node = node.clip(clip.clone());
286        }
287        node = node.transformed(self.transform);
288        if let Some(id) = &self.id {
289            node = node.named(id);
290        }
291        node
292    }
293}
294
295pub(crate) fn item_node(item: &VectorItem) -> Node {
296    match item {
297        VectorItem::Group(group) => group.node(),
298        VectorItem::Path(path) => Node::from(path.clone()),
299        VectorItem::Image(image) => Node::from(image.clone()),
300    }
301}
302
303/// A leaf occurrence with complete world coordinates and inherited metadata.
304#[derive(Clone, Debug)]
305pub struct VectorLeaf<'a> {
306    /// Path/image occurrence, not a copied geometry.
307    pub item: &'a VectorItem,
308    /// Local-to-tree coordinates.
309    pub transform: Transform,
310    /// Product of ancestor group opacities.
311    pub opacity: f64,
312    /// Nearest source interval; None uses paint order as reading order.
313    pub source: Option<Range<usize>>,
314    /// Child indices starting at the root group.
315    pub address: Vec<usize>,
316    pub(crate) clips: Vec<motion_core::PathClip>,
317}
318
319/// Immutable, cheaply cloned group/path/image tree with unique ID lookup.
320/// Construction validates every branch, including invisible ones, under bounded
321/// depth (48), items (8192), IDs (1024 bytes each) and aggregate verbs (262144).
322#[derive(Clone, Debug)]
323pub struct VectorTree {
324    data: Arc<TreeData>,
325}
326
327#[derive(Clone, Debug)]
328struct TreeData {
329    view_box: Rect,
330    root: VectorGroup,
331    ids: BTreeMap<Arc<str>, Vec<usize>>,
332    baseline: Option<f64>,
333    surface: std::sync::OnceLock<Result<Arc<VectorSurface>, ValidationError>>,
334}
335
336impl VectorTree {
337    /// Freezes a checked semantic snapshot. No fonts, devices or files are loaded.
338    pub fn new(view_box: Rect, root: VectorGroup) -> Result<Self, ValidationError> {
339        motion_core::drawing::validate_rect(view_box, "vector_tree", "view_box")?;
340        let mut ids = BTreeMap::new();
341        let mut count = 0;
342        let mut verbs = 0;
343        validate_group(&root, &mut Vec::new(), &mut ids, &mut count, &mut verbs)?;
344        let tree = Self {
345            data: Arc::new(TreeData {
346                view_box,
347                root,
348                ids,
349                baseline: None,
350                surface: std::sync::OnceLock::new(),
351            }),
352        };
353        tree.node().measure(ValidationLimits::default())?;
354        Ok(tree)
355    }
356
357    /// Original logical viewport, not a raster or an inferred visible pixel box.
358    pub fn view_box(&self) -> Rect {
359        self.data.view_box
360    }
361
362    /// Read-only paint-order root.
363    pub fn root(&self) -> &VectorGroup {
364        &self.data.root
365    }
366
367    /// Stable ID-to-child-address index; the root group has an empty address.
368    pub fn ids(&self) -> &BTreeMap<Arc<str>, Vec<usize>> {
369        &self.data.ids
370    }
371
372    /// Looks up a non-root occurrence by ID without searching or mutating the tree.
373    pub fn get(&self, id: &str) -> Option<&VectorItem> {
374        let address = self.data.ids.get(id)?;
375        let (last, parents) = address.split_last()?;
376        let mut group = &self.data.root;
377        for index in parents {
378            let VectorItem::Group(child) = &group.children[*index] else {
379                return None;
380            };
381            group = child;
382        }
383        group.children.get(*last)
384    }
385
386    /// Converts the same content to ordinary Node groups and drawables.
387    pub fn node(&self) -> Node {
388        self.node_mapped(&|_, node| node)
389    }
390
391    /// Builds nodes while applying an address-aware immutable modifier.
392    /// Addresses follow the original shared tree, not the modified output nodes.
393    pub fn node_mapped(&self, map: &impl Fn(&[usize], Node) -> Node) -> Node {
394        fn build(
395            group: &VectorGroup,
396            address: &mut Vec<usize>,
397            map: &impl Fn(&[usize], Node) -> Node,
398        ) -> Node {
399            let children = group
400                .children
401                .iter()
402                .enumerate()
403                .map(|(index, item)| {
404                    address.push(index);
405                    let node = match item {
406                        VectorItem::Group(group) => build(group, address, map),
407                        _ => map(address, item_node(item)),
408                    };
409                    address.pop();
410                    node
411                })
412                .collect::<Vec<_>>();
413            map(address, group.decorate(children))
414        }
415        let node = build(&self.data.root, &mut Vec::new(), map);
416        if let Some(baseline) = self.data.baseline {
417            Node::group([node, Node::from(TextBounds(self.data.view_box))])
418                .with_text_metrics(baseline, self.data.view_box)
419        } else {
420            node
421        }
422    }
423
424    /// Records a content importer's logical baseline for text-aware placement.
425    pub fn with_baseline(mut self, baseline: f64) -> Self {
426        Arc::make_mut(&mut self.data).baseline = Some(baseline);
427        self
428    }
429
430    /// Paint-order occurrences with complete transforms and clip stacks.
431    pub fn paint_leaves(&self) -> Vec<VectorLeaf<'_>> {
432        let mut leaves = Vec::new();
433        gather(
434            &self.data.root,
435            Transform::IDENTITY,
436            1.0,
437            None,
438            &[],
439            &mut Vec::new(),
440            &mut leaves,
441        );
442        leaves
443    }
444
445    /// Stable source reading order, independent of visual order (for example bidi).
446    /// Leaves without provenance retain paint order after source-bearing leaves.
447    pub fn leaves(&self) -> Vec<VectorLeaf<'_>> {
448        let mut leaves = self.paint_leaves();
449        leaves.sort_by_key(|leaf| {
450            leaf.source
451                .as_ref()
452                .map(|range| range.start)
453                .unwrap_or(usize::MAX)
454        });
455        leaves
456    }
457}
458
459#[derive(Clone)]
460struct TextBounds(Rect);
461
462impl Drawable for TextBounds {
463    fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
464        Ok(self.0)
465    }
466
467    fn draw(
468        &self,
469        _: &DrawContext,
470        _: &mut motion_core::Canvas<'_>,
471    ) -> Result<(), ValidationError> {
472        Ok(())
473    }
474}
475
476fn metadata(
477    id: &Option<Arc<str>>,
478    source: &Option<Range<usize>>,
479    address: &[usize],
480    ids: &mut BTreeMap<Arc<str>, Vec<usize>>,
481) -> Result<(), ValidationError> {
482    if source.as_ref().is_some_and(|range| range.start > range.end) {
483        return Err(invalid("source", "Source ranges must not be inverted"));
484    }
485    if let Some(id) = id
486        && (id.is_empty()
487            || id.len() > 1024
488            || id.chars().any(char::is_control)
489            || ids.insert(id.clone(), address.to_vec()).is_some())
490    {
491        return Err(invalid(
492            "id",
493            "IDs must be unique nonempty strings of at most 1024 bytes without controls",
494        ));
495    }
496    Ok(())
497}
498
499fn validate_group(
500    group: &VectorGroup,
501    address: &mut Vec<usize>,
502    ids: &mut BTreeMap<Arc<str>, Vec<usize>>,
503    count: &mut usize,
504    verbs: &mut usize,
505) -> Result<(), ValidationError> {
506    *count += 1;
507    if address.len() > 48 || *count > 8192 {
508        return Err(limited("Vector tree depth/item allowance exhausted"));
509    }
510    metadata(&group.id, &group.source, address, ids)?;
511    if group.filters.len() > 32 {
512        return Err(limited("Vector group exceeds 32 filters"));
513    }
514    for filter in &group.filters {
515        filter.validate("vector_tree.filters")?;
516    }
517    if let Some(mask) = &group.mask {
518        motion_core::drawing::validate_rect(mask.rect, "vector_tree.mask", "rect")?;
519        address.push(usize::MAX);
520        validate_group(
521            mask.tree.root(),
522            address,
523            &mut BTreeMap::new(),
524            count,
525            verbs,
526        )?;
527        address.pop();
528    }
529    if !group.transform.is_finite()
530        || !group.opacity.is_finite()
531        || !(0.0..=1.0).contains(&group.opacity)
532    {
533        return Err(invalid(
534            "group",
535            "Require finite matrices and opacity in 0..=1",
536        ));
537    }
538    if let Some(clip) = &group.clip {
539        *verbs += clip.path.verbs().len();
540    }
541    for (index, item) in group.children.iter().enumerate() {
542        address.push(index);
543        match item {
544            VectorItem::Group(child) => validate_group(child, address, ids, count, verbs)?,
545            VectorItem::Path(path) => {
546                *count += 1;
547                *verbs += path.path.verbs().len();
548                metadata(&path.id, &path.source, address, ids)?;
549                if let Some((paint, _)) = &path.fill {
550                    paint.validate()?;
551                }
552                if let Some((style, paint)) = &path.stroke {
553                    style.validate("vector_tree.stroke")?;
554                    paint.validate()?;
555                }
556            }
557            VectorItem::Image(image) => {
558                *count += 1;
559                metadata(&image.id, &image.source, address, ids)?;
560                motion_core::drawing::validate_rect(image.rect, "vector_tree.image", "rect")?;
561                if !image.pixel_transform.is_finite() {
562                    return Err(invalid("transform", "Image pixel mapping must be finite"));
563                }
564                if image.rect.width() > 0.0 && image.rect.height() > 0.0 {
565                    Paint::from(image.pattern()).validate()?;
566                }
567            }
568        }
569        address.pop();
570        if *count > 8192 || *verbs > 262144 {
571            return Err(limited("Vector tree item/verb allowance exhausted"));
572        }
573    }
574    Ok(())
575}
576
577fn gather<'a>(
578    group: &'a VectorGroup,
579    parent: Transform,
580    opacity: f64,
581    source: Option<Range<usize>>,
582    clips: &[motion_core::PathClip],
583    address: &mut Vec<usize>,
584    leaves: &mut Vec<VectorLeaf<'a>>,
585) {
586    let transform = group.transform.then(parent);
587    let opacity = opacity * group.opacity;
588    let source = group.source.clone().or(source);
589    let mut clips = clips.to_vec();
590    if let Some(mut clip) = group.clip.clone() {
591        clip.transform = clip.transform.then(transform);
592        clips.push(clip);
593    }
594    for (index, item) in group.children.iter().enumerate() {
595        address.push(index);
596        match item {
597            VectorItem::Group(child) => gather(
598                child,
599                transform,
600                opacity,
601                source.clone(),
602                &clips,
603                address,
604                leaves,
605            ),
606            _ => {
607                let own = match item {
608                    VectorItem::Path(path) => path.source.clone(),
609                    VectorItem::Image(image) => image.source.clone(),
610                    _ => None,
611                };
612                leaves.push(VectorLeaf {
613                    item,
614                    transform,
615                    opacity,
616                    source: own.or(source.clone()),
617                    address: address.clone(),
618                    clips: clips.clone(),
619                });
620            }
621        }
622        address.pop();
623    }
624}