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