Skip to main content

motion_content/svg/
view.rs

1//! The `Svg` node builder with lazy loading, viewport fitting, and failure placeholders.
2
3use super::*;
4use motion_core::{
5    Canvas, DeferredContent, DrawContext, Drawable, Node, NodeModifiers, ValidationError,
6};
7
8type Selection = Arc<dyn Fn(Node) -> Node + Send + Sync>;
9
10#[derive(Clone)]
11enum Input {
12    Source(motion_core::Source),
13    Lazy(motion_core::Handle<SvgAsset>),
14    Loaded(Arc<SvgAsset>),
15}
16
17/// resource-backed SVG operations; build/compile uses the same Node path
18/// for preview, still and export. Callbacks run synchronously on the CPU.
19#[derive(Clone)]
20pub struct Svg {
21    input: Input,
22    size: Option<Size>,
23    progress: Option<(f64, crate::WriteOptions)>,
24    selections: Vec<(Arc<str>, Selection)>,
25}
26
27impl Svg {
28    /// Captures a source resolved using session resources and fonts during compilation.
29    pub fn open(source: impl Into<motion_core::Source>) -> Self {
30        Self {
31            input: Input::Source(source.into()),
32            size: None,
33            progress: None,
34            selections: Vec::new(),
35        }
36    }
37
38    /// Captures a lazy resource without reading or parsing it.
39    pub fn new(handle: motion_core::Handle<SvgAsset>) -> Self {
40        Self {
41            input: Input::Lazy(handle),
42            size: None,
43            progress: None,
44            selections: Vec::new(),
45        }
46    }
47
48    /// Captures an already validated immutable vector asset.
49    pub fn from_asset(asset: Arc<SvgAsset>) -> Self {
50        Self {
51            input: Input::Loaded(asset),
52            size: None,
53            progress: None,
54            selections: Vec::new(),
55        }
56    }
57
58    /// Centers a contain mapping in this finite positive viewport.
59    pub fn fit(mut self, size: Size) -> Self {
60        self.size = Some(size);
61        self
62    }
63
64    /// Uses the common cumulative vector handwriting effect.
65    pub fn write(mut self, progress: f64) -> Self {
66        self.progress = Some((progress, crate::WriteOptions::default()));
67        self
68    }
69
70    /// Uses explicit outline geometry for the common cumulative handwriting effect.
71    pub fn write_with(mut self, progress: f64, options: crate::WriteOptions) -> Self {
72        self.progress = Some((progress, options));
73        self
74    }
75
76    /// Applies a normal modifier callback to an authored ID (# is optional).
77    /// Unknown IDs fail rather than silently selecting nothing. Callbacks follow
78    /// declaration order; resource IDs in masks are scoped to their resource.
79    pub fn select(
80        mut self,
81        id: impl Into<Arc<str>>,
82        callback: impl Fn(Node) -> Node + Send + Sync + 'static,
83    ) -> Self {
84        let id = id.into();
85        self.selections.push((
86            Arc::from(id.strip_prefix('#').unwrap_or(&id)),
87            Arc::new(callback),
88        ));
89        self
90    }
91
92    fn asset(&self, context: &DrawContext) -> motion_core::Result<Arc<SvgAsset>> {
93        match &self.input {
94            Input::Source(source) => context
95                .assets()
96                .load::<SvgAsset>(source.clone())
97                .get_with(context.diagnostics()),
98            Input::Lazy(handle) => handle.get_with(context.diagnostics()),
99            Input::Loaded(asset) => Ok(asset.clone()),
100        }
101    }
102
103    /// Returns the canonical shared vector model for inspection/effects.
104    pub fn vector_tree(&self) -> motion_core::Result<Arc<VectorTree>> {
105        Ok(self.asset(&DrawContext::default())?.tree.clone())
106    }
107
108    /// Builds checked content and reports invalid sizing/IDs/progress.
109    pub fn try_node(&self) -> motion_core::Result<Node> {
110        self.node_with(&DrawContext::default())
111    }
112
113    fn node_with(&self, context: &DrawContext) -> motion_core::Result<Node> {
114        let asset = self.asset(context)?;
115        let size = self.size.unwrap_or(asset.size());
116        if !size.width.is_finite()
117            || !size.height.is_finite()
118            || size.width <= 0.0
119            || size.height <= 0.0
120            || self.selections.len() > 256
121        {
122            return Err(unsupported(
123                "SVG fit requires positive finite size; at most 256 selections",
124            ));
125        }
126        for (id, _) in &self.selections {
127            if !asset.tree.ids().contains_key(id.as_ref()) {
128                return Err(unsupported(format!("SVG selection ID not found: {id}")));
129            }
130        }
131        let mut callbacks_by_address =
132            std::collections::BTreeMap::<Vec<usize>, Vec<&Selection>>::new();
133        for (id, callback) in &self.selections {
134            callbacks_by_address
135                .entry(asset.tree.ids()[id.as_ref()].clone())
136                .or_default()
137                .push(callback);
138        }
139        let map = |address: &[usize], mut node: Node| {
140            if let Some(callbacks) = callbacks_by_address.get(address) {
141                for callback in callbacks {
142                    node = callback(node);
143                }
144            }
145            node
146        };
147        let mut node = if let Some((progress, options)) = self.progress {
148            asset.tree.write_mapped(progress, options, &map)?
149        } else {
150            asset.tree.node_mapped(&map)
151        };
152        let scale = (size.width / asset.size().width).min(size.height / asset.size().height);
153        let transform = Transform::scale(scale).then(Transform::translate(
154            (size.width - asset.size().width * scale) * 0.5,
155            (size.height - asset.size().height * scale) * 0.5,
156        ));
157        node = node
158            .clip_with(asset.size().to_rect(), motion_core::ClipMode::Geometric)
159            .transformed(transform)
160            .clip_with(size.to_rect(), motion_core::ClipMode::Geometric);
161        Ok(Node::group([node, Node::from(Viewport(size.to_rect()))])
162            .pivot(motion_core::Pivot::Point(size.to_rect().center())))
163    }
164}
165
166#[derive(Clone)]
167struct Viewport(Rect);
168
169impl Drawable for Viewport {
170    fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
171        Ok(self.0)
172    }
173
174    fn draw(&self, _: &DrawContext, _: &mut Canvas<'_>) -> Result<(), ValidationError> {
175        Ok(())
176    }
177}
178
179#[derive(Clone)]
180struct FailedResource(Svg);
181
182impl Drawable for FailedResource {
183    fn bounds(&self, context: &DrawContext) -> Result<Rect, ValidationError> {
184        let _ = self.0.asset(context);
185        Ok(Rect::new(
186            0.0,
187            0.0,
188            self.0.size.map_or(64.0, |size| size.width),
189            self.0.size.map_or(64.0, |size| size.height),
190        ))
191    }
192
193    fn draw(&self, context: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
194        let _ = self.0.asset(context);
195        canvas.fill(
196            &self.bounds(context)?,
197            Color::rgb(255, 0, 255),
198            motion_core::FillRule::NonZero,
199        )
200    }
201}
202
203#[derive(Clone)]
204struct DeferredSvg(Svg);
205
206impl DeferredContent for DeferredSvg {
207    fn resolve(&self, context: &DrawContext) -> Result<Node, ValidationError> {
208        match self.0.node_with(context) {
209            Ok(node) => Ok(node),
210            Err(error) => {
211                if error.kind() == motion_core::ErrorKind::Asset {
212                    Ok(Node::from(FailedResource(self.0.clone())))
213                } else {
214                    Err(ValidationError::invalid(
215                        "svg",
216                        "content",
217                        error.to_string(),
218                    ))
219                }
220            }
221        }
222    }
223}
224
225impl From<Svg> for Node {
226    fn from(svg: Svg) -> Node {
227        Node::deferred(DeferredSvg(svg))
228    }
229}