1use super::*;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10pub struct WriteOptions {
11 pub width: f64,
13 pub outline_fraction: f64,
16}
17
18impl Default for WriteOptions {
19 fn default() -> Self {
20 Self {
21 width: 1.0,
22 outline_fraction: 0.8,
23 }
24 }
25}
26
27impl VectorTree {
28 pub fn write(&self, progress: f64, options: WriteOptions) -> Result<Node, ValidationError> {
33 self.write_mapped(progress, options, &|_, node| node)
34 }
35
36 pub fn write_mapped(
38 &self,
39 progress: f64,
40 options: WriteOptions,
41 map: &impl Fn(&[usize], Node) -> Node,
42 ) -> Result<Node, ValidationError> {
43 if !progress.is_finite()
44 || !options.width.is_finite()
45 || options.width <= 0.0
46 || !options.outline_fraction.is_finite()
47 || !(0.0..1.0).contains(&options.outline_fraction)
48 || options.outline_fraction == 0.0
49 {
50 return Err(invalid(
51 "write",
52 "Require finite progress, positive width and outline fraction in (0,1)",
53 ));
54 }
55 let progress = progress.clamp(0.0, 1.0);
56 if progress == 1.0 {
57 return Ok(self.node_mapped(map));
58 }
59 let handoff = ((progress - options.outline_fraction) / (1.0 - options.outline_fraction))
60 .clamp(0.0, 1.0);
61 let mut paths = Vec::new();
62 let mut total = 0.0;
63 for leaf in self.leaves() {
64 if let VectorItem::Path(path) = leaf.item
65 && (path.fill.is_some() || path.stroke.is_some())
66 {
67 let world = path.path.transformed(&leaf.transform)?;
68 let length = world.try_length()?;
69 total += length;
70 if !total.is_finite() {
71 return Err(invalid("length", "Accumulated vector length overflow"));
72 }
73 paths.push((leaf, world, length));
74 }
75 }
76 let mut remaining = total * (progress / options.outline_fraction).min(1.0);
77 let mut drawings = BTreeMap::new();
78 for (leaf, world, length) in paths {
79 let amount = if length == 0.0 {
80 0.0
81 } else {
82 (remaining / length).clamp(0.0, 1.0)
83 };
84 remaining = (remaining - length).max(0.0);
85 let VectorItem::Path(path) = leaf.item else {
86 unreachable!("only path leaves are collected")
87 };
88 let mut children = Vec::new();
89 if amount > 0.0 && handoff < 1.0 {
90 let inverse = leaf.transform.inverse().ok_or_else(|| {
91 invalid(
92 "transform",
93 "Writing requires an invertible local-to-tree matrix",
94 )
95 })?;
96 let outline = StrokeStyle {
97 width: options.width,
98 cap: motion_core::LineCap::Round,
99 join: motion_core::LineJoin::Round,
100 }
101 .outline(&world.try_trim(0.0, amount)?, "vector_tree.write")?
102 .transformed(&inverse)?;
103 let paint = path
104 .stroke
105 .as_ref()
106 .map(|(_, paint)| paint)
107 .or_else(|| path.fill.as_ref().map(|(paint, _)| paint))
108 .expect("painted path")
109 .clone();
110 children
111 .push(Node::from(VectorPath::filled(outline, paint)).opacity(1.0 - handoff));
112 }
113 if handoff > 0.0 {
114 children.push(Node::from(path.clone()).opacity(handoff));
115 }
116 drawings.insert(leaf.address, Node::group(children));
117 }
118
119 fn build(
120 group: &VectorGroup,
121 address: &mut Vec<usize>,
122 drawings: &BTreeMap<Vec<usize>, Node>,
123 handoff: f64,
124 map: &impl Fn(&[usize], Node) -> Node,
125 ) -> Node {
126 let children = group
127 .children
128 .iter()
129 .enumerate()
130 .map(|(index, item)| {
131 address.push(index);
132 let node = match item {
133 VectorItem::Group(child) => build(child, address, drawings, handoff, map),
134 VectorItem::Path(_) => {
135 drawings.get(address).cloned().unwrap_or_else(Node::empty)
136 }
137 VectorItem::Image(_) => item_node(item).opacity(handoff),
138 };
139 let node = if matches!(item, VectorItem::Group(_)) {
140 node
141 } else {
142 map(address, node)
143 };
144 address.pop();
145 node
146 })
147 .collect::<Vec<_>>();
148 map(address, group.decorate(children))
149 }
150 let mut node = build(self.root(), &mut Vec::new(), &drawings, handoff, map);
151 if let Some(baseline) = self.data.baseline {
152 node = Node::group([node, Node::from(TextBounds(self.view_box()))])
153 .with_text_metrics(baseline, self.view_box());
154 }
155 Ok(node.pivot(motion_core::Pivot::Point(
156 self.node().measure(ValidationLimits::default())?.center(),
157 )))
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use motion_core::{Color, PixelSize, RasterLimits, Size};
165 use motion_render::RenderPrepare;
166
167 fn pixels(node: Node) -> motion_core::Result<motion_render::FrameBuffer> {
168 Ok(node
169 .compile(
170 Size::new(16.0, 8.0),
171 PixelSize::new(16, 8),
172 ValidationLimits::default(),
173 )?
174 .prepare(ValidationLimits::default(), RasterLimits::default())?
175 .render_linear(RasterLimits::default())?)
176 }
177
178 #[test]
179 fn writing_uses_total_world_length_then_hands_off_to_exact_original() -> motion_core::Result<()>
180 {
181 let tree = VectorTree::new(
182 Rect::new(0.0, 0.0, 16.0, 8.0),
183 VectorGroup {
184 children: vec![
185 VectorItem::Path(VectorPath::filled(
186 Path2D::from_rect(Rect::new(1.0, 1.0, 5.0, 5.0))?,
187 Color::WHITE,
188 )),
189 VectorItem::Path(VectorPath::filled(
190 Path2D::from_rect(Rect::new(7.0, 1.0, 15.0, 5.0))?,
191 Color::rgb(255, 0, 0),
192 )),
193 ],
194 ..Default::default()
195 },
196 )?;
197 let options = WriteOptions::default();
198 let early = pixels(tree.write(0.8 * 16.0 / 40.0, options)?)?;
199 assert!(early.linear_pixel(1, 1).unwrap()[3] > 0.0);
200 assert_eq!(early.linear_pixel(3, 3), Some([0.0; 4]));
201 assert_eq!(early.linear_pixel(7, 1), Some([0.0; 4]));
202 assert!(
203 pixels(tree.write(0.9, options)?)?
204 .linear_pixel(3, 3)
205 .unwrap()[3]
206 > 0.0
207 );
208 assert_eq!(pixels(tree.write(1.0, options)?)?, pixels(tree.node())?);
209 assert!(
210 pixels(tree.write(0.0, options)?)?
211 .linear_pixels()
212 .all(|pixel| pixel == [0.0; 4])
213 );
214 assert!(tree.write(f64::NAN, options).is_err());
215 Ok(())
216 }
217}