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