1#[cfg(feature = "render")]
2use ranim_render::extract::{Extract, ExtractContext};
3
4use color::{AlphaColor, Srgb, palette::css, rgb8, rgba};
5use glam::{DAffine2, DAffine3, DMat3, DVec3, dvec3};
6use ranim_core::anchor::Aabb;
7use ranim_core::traits::{ApplyTransform, FillColor, Opacity, StrokeColor, StrokeWidth};
8use ranim_core::traits::{PointsFunc, Rigid, TransformGroup};
9use ranim_core::transformed::Transformed;
10use ranim_core::utils::bezier::PathBuilder;
11use ranim_core::{color, components::width::Width, glam};
12use tracing::warn;
13
14use super::VItem;
15use crate::hierarchy::{Node, PlacedLeaves};
16
17#[derive(Clone, Debug)]
30pub struct SvgItem(Transformed<Node<VItem>, DAffine3>);
31
32impl From<SvgItem> for Vec<VItem> {
33 fn from(value: SvgItem) -> Self {
37 value
38 .0
39 .leaves()
40 .map(|(world, leaf)| {
41 let mut vitem = leaf.clone();
42 vitem.apply_affine3(world);
43 vitem
44 })
45 .collect()
46 }
47}
48
49impl SvgItem {
50 pub fn new(svg: impl AsRef<str>) -> Self {
57 let tree =
58 usvg::Tree::from_str(svg.as_ref(), &usvg::Options::default()).expect("invalid svg");
59 let mut item = Self::from_tree(&tree);
60 let [min, max] = item.0.aabb();
61 let center = (min + max) * 0.5;
62 item.0.compose_outer(
63 Rigid::from_axis_angle(DVec3::X, std::f64::consts::PI)
64 .compose(&Rigid::from_translation(-center)),
65 );
66 item
67 }
68
69 pub fn from_tree(tree: &usvg::Tree) -> Self {
76 Self(build_group_node(tree.root(), usvg::Transform::identity()))
77 }
78
79 pub fn tree(&self) -> &Transformed<Node<VItem>, DAffine3> {
81 &self.0
82 }
83
84 pub fn tree_mut(&mut self) -> &mut Transformed<Node<VItem>, DAffine3> {
86 &mut self.0
87 }
88
89 pub fn into_tree(self) -> Transformed<Node<VItem>, DAffine3> {
91 self.0
92 }
93
94 pub fn by_id(&self, id: &str) -> Option<&VItem> {
100 let node = self.0.inner.by_id(id)?;
101 node.inner.item().or_else(|| node.inner.first_leaf())
102 }
103
104 pub fn by_id_mut(&mut self, id: &str) -> Option<&mut VItem> {
108 let node = self.0.inner.by_id_mut(id)?;
109 if node.inner.item_mut().is_some() {
110 node.inner.item_mut()
111 } else {
112 node.inner.first_leaf_mut()
113 }
114 }
115}
116
117impl Aabb for SvgItem {
119 fn aabb(&self) -> [glam::DVec3; 2] {
120 self.0.aabb()
121 }
122}
123
124impl<G: Into<glam::DAffine3>> ApplyTransform<G> for SvgItem {
125 fn apply(&mut self, transform: G) -> &mut Self {
126 self.0.apply(transform.into());
127 self
128 }
129}
130
131impl FillColor for SvgItem {
132 fn fill_color(&self) -> AlphaColor<Srgb> {
133 self.0.fill_color()
134 }
135 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
136 self.0.set_fill_color(color);
137 self
138 }
139 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
140 self.0.set_fill_opacity(opacity);
141 self
142 }
143}
144
145impl StrokeColor for SvgItem {
146 fn stroke_color(&self) -> AlphaColor<Srgb> {
147 self.0.stroke_color()
148 }
149 fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
150 self.0.set_stroke_color(color);
151 self
152 }
153 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
154 self.0.set_stroke_opacity(opacity);
155 self
156 }
157}
158
159impl Opacity for SvgItem {
160 fn set_opacity(&mut self, opacity: f32) -> &mut Self {
161 self.0.set_opacity(opacity);
162 self
163 }
164}
165
166impl StrokeWidth for SvgItem {
167 fn stroke_width(&self) -> f32 {
168 self.0.stroke_width()
169 }
170 fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
171 self.0.apply_stroke_func(f);
172 self
173 }
174 fn set_stroke_width(&mut self, width: f32) -> &mut Self {
175 self.0.set_stroke_width(width);
176 self
177 }
178}
179
180#[cfg(feature = "render")]
182impl Extract for SvgItem {
183 fn extract(&self, ctx: &mut ExtractContext<'_>) {
184 self.0.extract(ctx);
185 }
186}
187
188fn parse_paint(paint: &usvg::Paint) -> AlphaColor<Srgb> {
190 match paint {
191 usvg::Paint::Color(color) => rgb8(color.red, color.green, color.blue),
192 _ => css::GREEN,
193 }
194}
195
196fn build_group_node(
203 group: &usvg::Group,
204 parent_abs: usvg::Transform,
205) -> Transformed<Node<VItem>, DAffine3> {
206 let abs = group.abs_transform();
207 let children: Vec<Transformed<Node<VItem>, DAffine3>> = group
208 .children()
209 .iter()
210 .filter_map(|node| match node {
211 usvg::Node::Group(group) => Some(build_group_node(group, abs)),
212 usvg::Node::Path(path) => build_path_leaf(path, abs),
213 usvg::Node::Image(_) | usvg::Node::Text(_) => None,
215 })
216 .collect();
217 let mut node = Node::group(children);
218 if !group.id().is_empty() {
219 node.id = Some(group.id().to_string());
220 }
221 Transformed::new(node, widen_to_daffine3(rel_transform(parent_abs, abs)))
222}
223
224fn build_path_leaf(
232 path: &usvg::Path,
233 parent_abs: usvg::Transform,
234) -> Option<Transformed<Node<VItem>, DAffine3>> {
235 let mut builder = PathBuilder::new();
236 for segment in path.data().segments() {
237 match segment {
238 usvg::tiny_skia_path::PathSegment::MoveTo(p) => {
239 builder.move_to(dvec3(p.x as f64, p.y as f64, 0.0))
240 }
241 usvg::tiny_skia_path::PathSegment::LineTo(p) => {
242 builder.line_to(dvec3(p.x as f64, p.y as f64, 0.0))
243 }
244 usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => builder.quad_to(
245 dvec3(p1.x as f64, p1.y as f64, 0.0),
246 dvec3(p2.x as f64, p2.y as f64, 0.0),
247 ),
248 usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => builder.cubic_to(
249 dvec3(p1.x as f64, p1.y as f64, 0.0),
250 dvec3(p2.x as f64, p2.y as f64, 0.0),
251 dvec3(p3.x as f64, p3.y as f64, 0.0),
252 ),
253 usvg::tiny_skia_path::PathSegment::Close => builder.close_path(),
254 };
255 }
256 if builder.is_empty() {
257 warn!("empty path");
258 return None;
259 }
260
261 let mut vitem = VItem::from_vpoints(builder.vpoints().to_vec());
262 let fill_color = if let Some(fill) = path.fill() {
263 parse_paint(fill.paint()).with_alpha(fill.opacity().get())
264 } else {
265 rgba(0.0, 0.0, 0.0, 0.0)
266 };
267 vitem.set_fill_color(fill_color);
268 if let Some(stroke) = path.stroke() {
269 let color = parse_paint(stroke.paint()).with_alpha(stroke.opacity().get());
270 vitem.set_stroke_color(color);
271 vitem.set_stroke_width(stroke.width().get());
272 } else {
273 vitem.set_stroke_color(fill_color.with_alpha(0.0));
274 vitem.set_stroke_width(0.0);
275 }
276
277 let mut node = Node::leaf(vitem);
278 if !path.id().is_empty() {
279 node.id = Some(path.id().to_string());
280 }
281 Some(Transformed::new(
282 node,
283 widen_to_daffine3(rel_transform(parent_abs, path.abs_transform())),
284 ))
285}
286
287fn rel_transform(parent_abs: usvg::Transform, abs: usvg::Transform) -> DAffine2 {
293 let parent = usvg_transform_to_daffine2(parent_abs);
294 if parent.matrix2.determinant() == 0.0 {
295 warn!("singular svg transform cannot be inverted, using identity");
296 return DAffine2::IDENTITY;
297 }
298 parent.inverse() * usvg_transform_to_daffine2(abs)
299}
300
301fn usvg_transform_to_daffine2(transform: usvg::Transform) -> DAffine2 {
303 DAffine2::from_cols_array(&[
304 transform.sx as f64,
305 transform.ky as f64,
306 transform.kx as f64,
307 transform.sy as f64,
308 transform.tx as f64,
309 transform.ty as f64,
310 ])
311}
312
313fn widen_to_daffine3(affine: DAffine2) -> DAffine3 {
315 DAffine3::from_mat3_translation(
316 DMat3::from_cols(
317 affine.matrix2.x_axis.extend(0.0),
318 affine.matrix2.y_axis.extend(0.0),
319 DVec3::Z,
320 ),
321 affine.translation.extend(0.0),
322 )
323}
324
325pub fn vitems_from_svg(svg: &str) -> Vec<VItem> {
328 let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap();
329 vitems_from_tree(&tree)
330}
331
332pub fn vitems_from_tree(tree: &usvg::Tree) -> Vec<VItem> {
334 Vec::<VItem>::from(SvgItem::from_tree(tree))
335}
336
337#[cfg(test)]
338mod tests {
339 use std::f64::consts::PI;
340
341 use glam::dvec2;
342 use ranim_core::traits::ShiftTransform;
343
344 use super::*;
345
346 const NESTED_SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
350 <g transform="translate(10) scale(2)">
351 <g id="inner" transform="rotate(45)">
352 <path id="leaf-a" d="M 10 10 L 20 10" fill="#ff0000" stroke="#00ff00" stroke-width="0.5"/>
353 </g>
354 <path id="leaf-b" d="M 0 0 L 5 0" fill="#0000ff"/>
355 </g>
356 </svg>"##;
357
358 fn parse(svg: &str) -> usvg::Tree {
359 usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap()
360 }
361
362 fn outer_rel() -> DAffine2 {
364 DAffine2::from_translation(dvec2(10.0, 0.0)) * DAffine2::from_scale(dvec2(2.0, 2.0))
365 }
366
367 fn assert_affine3_eq(actual: DAffine3, expected: DAffine3) {
370 assert!(actual.matrix3.abs_diff_eq(expected.matrix3, 1e-6));
371 assert!(actual.translation.abs_diff_eq(expected.translation, 1e-6));
372 }
373
374 #[test]
375 fn structure_ids_and_relative_transforms_match_the_source() {
376 let svg = SvgItem::from_tree(&parse(NESTED_SVG));
377 let root = svg.tree();
378 let outer = &root.inner.children()[0];
379 let inner = &outer.inner.children()[0];
380 assert_eq!(inner.inner.id.as_deref(), Some("inner"));
381 assert_affine3_eq(outer.transform, widen_to_daffine3(outer_rel()));
382 assert_affine3_eq(
383 inner.transform,
384 widen_to_daffine3(DAffine2::from_angle(PI / 4.0)),
385 );
386 let leaf = &inner.inner.children()[0];
387 assert_eq!(leaf.inner.id.as_deref(), Some("leaf-a"));
388 assert_eq!(
389 leaf.inner.item().unwrap().vpoints.0[0],
390 dvec3(10.0, 10.0, 0.0)
391 );
392 assert_eq!(
393 leaf.inner.item().unwrap().vpoints.0[2],
394 dvec3(20.0, 10.0, 0.0)
395 );
396 }
397
398 #[test]
399 fn flattened_world_points_match_hand_composed_affine() {
400 let svg = SvgItem::from_tree(&parse(NESTED_SVG));
401 let vitems = Vec::<VItem>::from(svg.clone());
402 assert_eq!(vitems.len(), 2);
403 for (item, id, affine) in [
404 (
405 &vitems[0],
406 "leaf-a",
407 outer_rel() * DAffine2::from_angle(PI / 4.0),
408 ),
409 (&vitems[1], "leaf-b", outer_rel()),
410 ] {
411 let raw = &svg.by_id(id).unwrap().vpoints.0;
412 assert_eq!(item.vpoints.len(), raw.len());
413 let affine = widen_to_daffine3(affine);
414 for (world, raw) in item.vpoints.iter().zip(raw) {
415 assert!(world.abs_diff_eq(affine.transform_point3(*raw), 1e-5));
416 }
417 }
418 }
419
420 #[test]
421 fn new_centers_the_aabb_and_flips_after_centering() {
422 let raw_items = Vec::<VItem>::from(SvgItem::from_tree(&parse(NESTED_SVG)));
424 let [min, max] = raw_items.aabb();
425 let center = (min + max) * 0.5;
426
427 let new_items = Vec::<VItem>::from(SvgItem::new(NESTED_SVG));
428
429 let flip = DVec3::new(1.0, -1.0, -1.0);
431 for (new, raw) in new_items.iter().zip(raw_items.iter()) {
432 for (new_p, raw_p) in new.vpoints.0.iter().zip(raw.vpoints.0.iter()) {
433 let expected = (raw_p - center) * flip;
434 assert!(
435 new_p.abs_diff_eq(expected, 1e-9),
436 "new {new_p:?} vs expected {expected:?}"
437 );
438 }
439 }
440
441 let [min, max] = new_items.aabb();
443 assert!(((min + max) * 0.5).abs_diff_eq(DVec3::ZERO, 1e-9));
444 }
445
446 #[test]
447 fn by_id_lookups_read_and_write_only_the_named_leaf() {
448 let mut svg = SvgItem::from_tree(&parse(NESTED_SVG));
449 assert!(svg.by_id("missing").is_none());
450
451 let sibling_before = svg.by_id("leaf-b").unwrap().vpoints.0.clone();
452 svg.by_id_mut("leaf-a").unwrap().shift(DVec3::X);
453 assert!(
454 svg.by_id("leaf-a").unwrap().vpoints.0[0].abs_diff_eq(dvec3(11.0, 10.0, 0.0), 1e-9)
455 );
456 assert_eq!(
457 svg.by_id("leaf-b").unwrap().vpoints.0,
458 sibling_before,
459 "the sibling leaf must be untouched"
460 );
461
462 assert_eq!(
464 svg.by_id("inner").unwrap().vpoints.0[0],
465 dvec3(11.0, 10.0, 0.0)
466 );
467 }
468}
469
470#[cfg(all(test, feature = "typst"))]
471mod typst_tests {
472 use crate::vitem::typst::typst_svg;
473
474 use super::*;
475
476 #[test]
477 fn typst_svg_converts_to_vitems() {
478 let svg = typst_svg("R");
479 let vitems = vitems_from_svg(&svg);
480
481 assert!(!vitems.is_empty(), "typst output produced no vitems");
482 assert!(vitems.iter().all(|item| !item.vpoints.is_empty()));
483 }
484}