1use color::{AlphaColor, Srgb, palette::css, rgb8, rgba};
2use glam::{DAffine2, DAffine3, DMat3, DVec3, dvec3};
3use ranim_core::anchor::Aabb;
4use ranim_core::core_item::CoreItem;
5use ranim_core::core_item::transformed::Transformed;
6use ranim_core::traits::{ApplyTransform, FillColor, Opacity, StrokeColor, StrokeWidth};
7use ranim_core::traits::{PointsFunc, Rigid, TransformGroup};
8use ranim_core::utils::bezier::PathBuilder;
9use ranim_core::{Extract, color, components::width::Width, glam};
10use tracing::warn;
11
12use super::VItem;
13use crate::hierarchy::{Node, PlacedLeaves};
14
15#[derive(Clone, Debug)]
28pub struct SvgItem(Transformed<Node<VItem>, DAffine3>);
29
30impl From<SvgItem> for Vec<VItem> {
31 fn from(value: SvgItem) -> Self {
35 value
36 .0
37 .leaves()
38 .map(|(world, leaf)| {
39 let mut vitem = leaf.clone();
40 vitem.apply_affine3(world);
41 vitem
42 })
43 .collect()
44 }
45}
46
47impl SvgItem {
48 pub fn new(svg: impl AsRef<str>) -> Self {
55 let tree =
56 usvg::Tree::from_str(svg.as_ref(), &usvg::Options::default()).expect("invalid svg");
57 let mut item = Self::from_tree(&tree);
58 let [min, max] = item.0.aabb();
59 let center = (min + max) * 0.5;
60 item.0.compose_outer(
61 Rigid::from_axis_angle(DVec3::X, std::f64::consts::PI)
62 .compose(&Rigid::from_translation(-center)),
63 );
64 item
65 }
66
67 pub fn from_tree(tree: &usvg::Tree) -> Self {
74 Self(build_group_node(tree.root(), usvg::Transform::identity()))
75 }
76
77 pub fn tree(&self) -> &Transformed<Node<VItem>, DAffine3> {
79 &self.0
80 }
81
82 pub fn tree_mut(&mut self) -> &mut Transformed<Node<VItem>, DAffine3> {
84 &mut self.0
85 }
86
87 pub fn into_tree(self) -> Transformed<Node<VItem>, DAffine3> {
89 self.0
90 }
91
92 pub fn by_id(&self, id: &str) -> Option<&VItem> {
98 let node = self.0.inner.by_id(id)?;
99 node.inner.item().or_else(|| node.inner.first_leaf())
100 }
101
102 pub fn by_id_mut(&mut self, id: &str) -> Option<&mut VItem> {
106 let node = self.0.inner.by_id_mut(id)?;
107 if node.inner.item_mut().is_some() {
108 node.inner.item_mut()
109 } else {
110 node.inner.first_leaf_mut()
111 }
112 }
113}
114
115impl Aabb for SvgItem {
117 fn aabb(&self) -> [glam::DVec3; 2] {
118 self.0.aabb()
119 }
120}
121
122impl<G: Into<glam::DAffine3>> ApplyTransform<G> for SvgItem {
123 fn apply(&mut self, transform: G) -> &mut Self {
124 self.0.apply(transform.into());
125 self
126 }
127}
128
129impl FillColor for SvgItem {
130 fn fill_color(&self) -> AlphaColor<Srgb> {
131 self.0.fill_color()
132 }
133 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
134 self.0.set_fill_color(color);
135 self
136 }
137 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
138 self.0.set_fill_opacity(opacity);
139 self
140 }
141}
142
143impl StrokeColor for SvgItem {
144 fn stroke_color(&self) -> AlphaColor<Srgb> {
145 self.0.stroke_color()
146 }
147 fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
148 self.0.set_stroke_color(color);
149 self
150 }
151 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
152 self.0.set_stroke_opacity(opacity);
153 self
154 }
155}
156
157impl Opacity for SvgItem {
158 fn set_opacity(&mut self, opacity: f32) -> &mut Self {
159 self.0.set_opacity(opacity);
160 self
161 }
162}
163
164impl StrokeWidth for SvgItem {
165 fn stroke_width(&self) -> f32 {
166 self.0.stroke_width()
167 }
168 fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
169 self.0.apply_stroke_func(f);
170 self
171 }
172 fn set_stroke_width(&mut self, width: f32) -> &mut Self {
173 self.0.set_stroke_width(width);
174 self
175 }
176}
177
178impl Extract for SvgItem {
180 type Target = CoreItem;
181 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
182 self.0.extract_into(buf);
183 }
184}
185
186fn parse_paint(paint: &usvg::Paint) -> AlphaColor<Srgb> {
188 match paint {
189 usvg::Paint::Color(color) => rgb8(color.red, color.green, color.blue),
190 _ => css::GREEN,
191 }
192}
193
194fn build_group_node(
201 group: &usvg::Group,
202 parent_abs: usvg::Transform,
203) -> Transformed<Node<VItem>, DAffine3> {
204 let abs = group.abs_transform();
205 let children: Vec<Transformed<Node<VItem>, DAffine3>> = group
206 .children()
207 .iter()
208 .filter_map(|node| match node {
209 usvg::Node::Group(group) => Some(build_group_node(group, abs)),
210 usvg::Node::Path(path) => build_path_leaf(path, abs),
211 usvg::Node::Image(_) | usvg::Node::Text(_) => None,
213 })
214 .collect();
215 let mut node = Node::group(children);
216 if !group.id().is_empty() {
217 node.id = Some(group.id().to_string());
218 }
219 Transformed::new(node, widen_to_daffine3(rel_transform(parent_abs, abs)))
220}
221
222fn build_path_leaf(
230 path: &usvg::Path,
231 parent_abs: usvg::Transform,
232) -> Option<Transformed<Node<VItem>, DAffine3>> {
233 let mut builder = PathBuilder::new();
234 for segment in path.data().segments() {
235 match segment {
236 usvg::tiny_skia_path::PathSegment::MoveTo(p) => {
237 builder.move_to(dvec3(p.x as f64, p.y as f64, 0.0))
238 }
239 usvg::tiny_skia_path::PathSegment::LineTo(p) => {
240 builder.line_to(dvec3(p.x as f64, p.y as f64, 0.0))
241 }
242 usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => builder.quad_to(
243 dvec3(p1.x as f64, p1.y as f64, 0.0),
244 dvec3(p2.x as f64, p2.y as f64, 0.0),
245 ),
246 usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => builder.cubic_to(
247 dvec3(p1.x as f64, p1.y as f64, 0.0),
248 dvec3(p2.x as f64, p2.y as f64, 0.0),
249 dvec3(p3.x as f64, p3.y as f64, 0.0),
250 ),
251 usvg::tiny_skia_path::PathSegment::Close => builder.close_path(),
252 };
253 }
254 if builder.is_empty() {
255 warn!("empty path");
256 return None;
257 }
258
259 let mut vitem = VItem::from_vpoints(builder.vpoints().to_vec());
260 let fill_color = if let Some(fill) = path.fill() {
261 parse_paint(fill.paint()).with_alpha(fill.opacity().get())
262 } else {
263 rgba(0.0, 0.0, 0.0, 0.0)
264 };
265 vitem.set_fill_color(fill_color);
266 if let Some(stroke) = path.stroke() {
267 let color = parse_paint(stroke.paint()).with_alpha(stroke.opacity().get());
268 vitem.set_stroke_color(color);
269 vitem.set_stroke_width(stroke.width().get());
270 } else {
271 vitem.set_stroke_color(fill_color.with_alpha(0.0));
272 vitem.set_stroke_width(0.0);
273 }
274
275 let mut node = Node::leaf(vitem);
276 if !path.id().is_empty() {
277 node.id = Some(path.id().to_string());
278 }
279 Some(Transformed::new(
280 node,
281 widen_to_daffine3(rel_transform(parent_abs, path.abs_transform())),
282 ))
283}
284
285fn rel_transform(parent_abs: usvg::Transform, abs: usvg::Transform) -> DAffine2 {
291 let parent = usvg_transform_to_daffine2(parent_abs);
292 if parent.matrix2.determinant() == 0.0 {
293 warn!("singular svg transform cannot be inverted, using identity");
294 return DAffine2::IDENTITY;
295 }
296 parent.inverse() * usvg_transform_to_daffine2(abs)
297}
298
299fn usvg_transform_to_daffine2(transform: usvg::Transform) -> DAffine2 {
301 DAffine2::from_cols_array(&[
302 transform.sx as f64,
303 transform.ky as f64,
304 transform.kx as f64,
305 transform.sy as f64,
306 transform.tx as f64,
307 transform.ty as f64,
308 ])
309}
310
311fn widen_to_daffine3(affine: DAffine2) -> DAffine3 {
313 DAffine3::from_mat3_translation(
314 DMat3::from_cols(
315 affine.matrix2.x_axis.extend(0.0),
316 affine.matrix2.y_axis.extend(0.0),
317 DVec3::Z,
318 ),
319 affine.translation.extend(0.0),
320 )
321}
322
323pub fn vitems_from_svg(svg: &str) -> Vec<VItem> {
326 let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap();
327 vitems_from_tree(&tree)
328}
329
330pub fn vitems_from_tree(tree: &usvg::Tree) -> Vec<VItem> {
332 Vec::<VItem>::from(SvgItem::from_tree(tree))
333}
334
335#[cfg(test)]
336mod tests {
337 use std::f64::consts::PI;
338
339 use glam::dvec2;
340 use ranim_core::traits::ShiftTransform;
341
342 use super::*;
343
344 const NESTED_SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
348 <g transform="translate(10) scale(2)">
349 <g id="inner" transform="rotate(45)">
350 <path id="leaf-a" d="M 10 10 L 20 10" fill="#ff0000" stroke="#00ff00" stroke-width="0.5"/>
351 </g>
352 <path id="leaf-b" d="M 0 0 L 5 0" fill="#0000ff"/>
353 </g>
354 </svg>"##;
355
356 fn parse(svg: &str) -> usvg::Tree {
357 usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap()
358 }
359
360 fn outer_rel() -> DAffine2 {
362 DAffine2::from_translation(dvec2(10.0, 0.0)) * DAffine2::from_scale(dvec2(2.0, 2.0))
363 }
364
365 fn assert_affine3_eq(actual: DAffine3, expected: DAffine3) {
368 assert!(
369 actual.translation.abs_diff_eq(expected.translation, 1e-6),
370 "translation {:?} vs {:?}",
371 actual.translation,
372 expected.translation
373 );
374 for i in 0..3 {
375 assert!(
376 actual
377 .matrix3
378 .col(i)
379 .abs_diff_eq(expected.matrix3.col(i), 1e-6),
380 "matrix3 column {i} diverges"
381 );
382 }
383 }
384
385 #[test]
386 fn structure_ids_and_relative_transforms_match_the_source() {
387 let svg = SvgItem::from_tree(&parse(NESTED_SVG));
388 let root = svg.tree();
389
390 assert!(root.inner.is_group());
391 assert_eq!(root.inner.id, None);
392 assert_eq!(root.transform, DAffine3::IDENTITY);
395
396 let children = root.inner.children();
397 assert_eq!(children.len(), 1);
398 let outer = &children[0];
399 assert!(outer.inner.is_group());
400 assert_eq!(outer.inner.id, None, "the outer <g> has no id attribute");
401 assert_affine3_eq(outer.transform, widen_to_daffine3(outer_rel()));
402
403 let inner = &outer.inner.children()[0];
404 assert_eq!(inner.inner.id.as_deref(), Some("inner"));
405 assert!(inner.inner.is_group());
406 assert_affine3_eq(
407 inner.transform,
408 widen_to_daffine3(DAffine2::from_angle(45.0f64.to_radians())),
409 );
410
411 let leaf_a = &inner.inner.children()[0];
412 assert_eq!(leaf_a.inner.id.as_deref(), Some("leaf-a"));
413 assert!(leaf_a.inner.is_leaf());
414 assert_eq!(leaf_a.transform, DAffine3::IDENTITY);
415 let item = leaf_a.inner.item().unwrap();
419 assert_eq!(item.vpoints.0[0], dvec3(10.0, 10.0, 0.0));
420 assert_eq!(item.vpoints.0[2], dvec3(20.0, 10.0, 0.0));
421 }
422
423 #[test]
424 fn flattened_world_points_match_hand_composed_affine() {
425 let svg = SvgItem::from_tree(&parse(NESTED_SVG));
426 let vitems = Vec::<VItem>::from(svg.clone());
427
428 let expected_a = widen_to_daffine3(outer_rel() * DAffine2::from_angle(PI / 4.0));
431 let raw_a = svg.by_id("leaf-a").unwrap().vpoints.0.clone();
432 for (world, raw) in vitems[0].vpoints.0.iter().zip(raw_a.iter()) {
433 assert!(
434 world.abs_diff_eq(expected_a.transform_point3(*raw), 1e-5),
435 "world {world:?} vs expected {:?}",
436 expected_a.transform_point3(*raw)
437 );
438 }
439
440 let expected_b = widen_to_daffine3(outer_rel());
442 let raw_b = svg.by_id("leaf-b").unwrap().vpoints.0.clone();
443 for (world, raw) in vitems[1].vpoints.0.iter().zip(raw_b.iter()) {
444 assert!(
445 world.abs_diff_eq(expected_b.transform_point3(*raw), 1e-5),
446 "world {world:?} vs expected {:?}",
447 expected_b.transform_point3(*raw)
448 );
449 }
450 }
451
452 #[test]
453 fn dfs_leaf_order_matches_source_order() {
454 let vitems = Vec::<VItem>::from(SvgItem::from_tree(&parse(NESTED_SVG)));
457 assert_eq!(vitems.len(), 2);
458
459 assert!(
462 vitems[0].vpoints.0[0]
463 .abs_diff_eq(dvec3(10.0, 20.0 * std::f64::consts::SQRT_2, 0.0), 1e-5)
464 );
465 assert!(vitems[1].vpoints.0[0].abs_diff_eq(dvec3(10.0, 0.0, 0.0), 1e-5));
467 }
468
469 #[test]
470 fn new_centers_the_aabb_and_flips_after_centering() {
471 let raw_items = Vec::<VItem>::from(SvgItem::from_tree(&parse(NESTED_SVG)));
473 let [min, max] = raw_items.aabb();
474 let center = (min + max) * 0.5;
475
476 let new_items = Vec::<VItem>::from(SvgItem::new(NESTED_SVG));
477
478 let flip = DVec3::new(1.0, -1.0, -1.0);
480 for (new, raw) in new_items.iter().zip(raw_items.iter()) {
481 for (new_p, raw_p) in new.vpoints.0.iter().zip(raw.vpoints.0.iter()) {
482 let expected = (raw_p - center) * flip;
483 assert!(
484 new_p.abs_diff_eq(expected, 1e-9),
485 "new {new_p:?} vs expected {expected:?}"
486 );
487 }
488 }
489
490 let [min, max] = new_items.aabb();
492 assert!(((min + max) * 0.5).abs_diff_eq(DVec3::ZERO, 1e-9));
493 }
494
495 #[test]
496 fn by_id_lookups_read_and_write_only_the_named_leaf() {
497 let mut svg = SvgItem::from_tree(&parse(NESTED_SVG));
498 assert!(svg.by_id("missing").is_none());
499
500 let sibling_before = svg.by_id("leaf-b").unwrap().vpoints.0.clone();
501 svg.by_id_mut("leaf-a").unwrap().shift(DVec3::X);
502 assert!(
503 svg.by_id("leaf-a").unwrap().vpoints.0[0].abs_diff_eq(dvec3(11.0, 10.0, 0.0), 1e-9)
504 );
505 assert_eq!(
506 svg.by_id("leaf-b").unwrap().vpoints.0,
507 sibling_before,
508 "the sibling leaf must be untouched"
509 );
510
511 assert_eq!(
513 svg.by_id("inner").unwrap().vpoints.0[0],
514 dvec3(11.0, 10.0, 0.0)
515 );
516 }
517
518 #[test]
519 fn ghostscript_tiger_keeps_leaf_count_and_centering() {
520 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
521 .join("../../assets/Ghostscript_Tiger.svg");
522 let source = std::fs::read_to_string(&path).expect("the tiger asset should exist");
523
524 let svg = SvgItem::new(&source);
525 let vitems = Vec::<VItem>::from(svg.clone());
526
527 let leaf_count = svg.tree().leaves().count();
528 assert!(leaf_count > 100, "unexpectedly few leaves: {leaf_count}");
529 assert_eq!(vitems.len(), leaf_count);
530
531 let [min, max] = vitems.aabb();
532 assert!(
533 ((min + max) * 0.5).abs_diff_eq(DVec3::ZERO, 1e-6),
534 "aabb is not centered: min {min:?}, max {max:?}"
535 );
536 }
537}
538
539#[cfg(all(test, feature = "typst"))]
540mod typst_tests {
541 use std::f64::consts::PI;
542
543 use glam::dvec3;
544
545 use crate::vitem::{geometry::Arc, typst::typst_svg};
546 use ranim_core::traits::{RotateTransform, ShiftTransform};
547
548 use super::*;
549
550 #[test]
551 fn typst_svg_converts_to_vitems() {
552 let svg = typst_svg("R");
553 let vitems = vitems_from_svg(&svg);
554
555 assert!(!vitems.is_empty(), "typst output produced no vitems");
556 assert!(vitems.iter().all(|item| !item.vpoints.is_empty()));
557 }
558
559 #[test]
560 fn arc_points_are_transformed_after_conversion_to_vitem() {
561 let angle = PI / 3.0 * 2.0;
562 let mut arc = VItem::from(Arc::new(angle, 2.0));
563 arc.rotate_on_axis(DVec3::Z, PI / 2.0 - angle / 2.0)
564 .shift(dvec3(2.0, 2.0, 0.0));
565 assert!(arc.vpoints[0].abs_diff_eq(dvec3(3.732050807568877, 3.0, 0.0), 1e-10));
566 assert!(
567 arc.vpoints[arc.vpoints.len() - 1]
568 .abs_diff_eq(dvec3(0.2679491924311228, 3.0, 0.0), 1e-10)
569 );
570 }
571}