//! Simple shapes export as SVG basic shapes (#4652): axis-aligned rectangles, circles and //! ellipses, straight lines, and polygons/polylines of straight segments. Everything else, and //! rectangles and ellipses whose dashes would start elsewhere, stays ``; the picture and the //! reimported geometry don't change. use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, FillLayer, Node, StrokeLayer}; use vectorcraft_geom::{Affine, PathData, Point, Rect, SubPath, shapes}; use vectorcraft_svg::{ExportOptions, export, import}; use vectorcraft_testkit::raster::{Image, assert_similar, render_artboard}; fn painted(dash: bool) -> Appearance { let mut st = StrokeLayer::new(Paint::solid(Color::rgb(0.1, 0.2, 0.6)), 3.0); if dash { st.dash = Some(Dash { pattern: vec![6.0, 4.0], offset: 0.0, align_corners: false }); } Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::rgb(0.9, 0.5, 0.1)))), AppearanceItem::Stroke(st)], ..Default::default() } } fn doc(paths: Vec<(PathData, bool)>) -> Document { let mut d = Document::new(200.0, 200.0); let l = d.layers[0].id; for (p, dash) in paths { let n = Node::path(d.alloc_id(), p, painted(dash)); d.insert(Some(l), usize::MAX, n).unwrap(); } d } fn svg_of(p: PathData, dash: bool) -> String { export(&doc(vec![(p, dash)]), &ExportOptions::default()) } fn poly(pts: &[(f64, f64)], closed: bool) -> PathData { let pts: Vec = pts.iter().map(|&(x, y)| Point::new(x, y)).collect(); PathData::single(SubPath::polyline(&pts, closed)) } #[test] fn each_simple_shape_writes_its_element() { let cases = [ (shapes::rectangle(Rect::new(10.0, 20.0, 110.0, 70.0)), " Image { let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default()).expect("parse"); let mut pm = resvg::tiny_skia::Pixmap::new(200, 200).unwrap(); pm.fill(resvg::tiny_skia::Color::WHITE); resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pm.as_mut()); Image { width: 200, height: 200, rgba: pm.data().to_vec() } } fn shapes_doc() -> Document { doc(vec![ (shapes::rectangle(Rect::new(10.0, 20.0, 110.0, 70.0)), false), (shapes::rectangle(Rect::new(120.0, 20.0, 190.0, 70.0)), true), (shapes::ellipse(Rect::new(10.0, 80.0, 50.0, 120.0)), false), (shapes::ellipse(Rect::new(60.0, 80.0, 140.0, 120.0)), false), (shapes::line(Point::new(150.0, 80.0), Point::new(190.0, 120.0)), true), (poly(&[(20.0, 185.0), (60.0, 185.0), (40.0, 135.0)], true), false), (poly(&[(80.0, 140.0), (120.0, 180.0), (160.0, 140.0)], false), true), ]) } #[test] fn shape_elements_render_like_the_canvas() { let d = shapes_doc(); let svg = export(&d, &ExportOptions::default()); assert!(!svg.contains(" 1e-3); assert!(!far, "{a:?} came back as {b:?}"); } } /// The paths of the objects in `d`, in order. fn paths(d: &Document) -> Vec { let mut v = Vec::new(); d.walk(|n| v.extend(n.path_data().cloned())); v } #[test] fn circles_and_ellipses_import_as_the_ellipse_drawn() { // What export writes comes back with the anchors and handles it was written from. let drawn = [shapes::ellipse(Rect::new(10.0, 80.0, 50.0, 120.0)), shapes::ellipse(Rect::new(60.0, 80.0, 140.0, 120.0))]; let d = doc(drawn.iter().map(|p| (p.clone(), false)).collect()); let back = paths(&import(&export(&d, &ExportOptions::default())).unwrap()); assert_eq!(back.len(), 2); for (want, got) in drawn.iter().zip(&back) { let [w, g] = [want, got].map(|p| &p.subpaths[0].anchors); assert!(got.subpaths.len() == 1 && got.subpaths[0].closed && g.len() == 4, "{got:?}"); // The same four anchors, starting on the right as an SVG ellipse does. for a in g { let same = w.iter().any(|b| a.p.distance(b.p) < 1e-4 && a.h_in.distance(b.h_in) < 1e-4 && a.h_out.distance(b.h_out) < 1e-4); assert!(same, "{a:?} is not one of {w:?}"); } assert!((g[0].p.x - want.bounds().unwrap().x1).abs() < 1e-4, "{g:?}"); } // From another app, with a transform: four anchors on the axes, transformed. let svg = r#" "#; let back = paths(&import(svg).unwrap()); assert_eq!(back.len(), 5); let circle = &back[0].subpaths[0].anchors; let want: Vec = [(100.0, 70.0), (60.0, 110.0), (20.0, 70.0), (60.0, 30.0)].map(|(x, y)| Point::new(x, y)).into(); assert_eq!(circle.len(), 4); for (a, w) in circle.iter().zip(&want) { assert!(a.p.distance(*w) < 1e-4, "{circle:?}"); } let turned = &back[1].subpaths[0].anchors; let r = Affine::rotate_about(30f64.to_radians(), Point::new(100.0, 100.0)); assert!(turned.len() == 4 && turned[0].p.distance(r * Point::new(160.0, 100.0)) < 1e-3, "{turned:?}"); // However many curves the arcs of a flat ellipse make. let flat = &back[2].subpaths[0].anchors; assert!(flat.len() == 4 && flat[0].p.distance(Point::new(72.51, 154.118)) < 1e-3, "{flat:?}"); // A small circle far from the origin too. let far = &back[3].subpaths[0].anchors; assert!(far.len() == 4 && far[0].p.distance(Point::new(40_000_400.0, 0.0)) < 8.0, "{far:?}"); // A radius of a few of f32's steps at its coordinates, with handles of the standard length. let thin = &back[4].subpaths[0].anchors; let handle = thin.first().map(|a| a.h_out - a.p); assert!(thin.len() == 4 && thin[0].p.distance(Point::new(1_000_036.0, 1_000_000.0)) < 0.25, "{thin:?}"); assert!(handle.is_some_and(|h| h.x.abs() < 1e-6 && (h.y - shapes::KAPPA).abs() < 1e-6), "{thin:?}"); } #[test] fn circles_import_without_changing_ids_or_style_matches() { // The file's own ids stay unique: the `` shows the rectangle, not the circle. let svg = r##" "##; let img = render_artboard(&import(svg).unwrap()); assert_eq!(img.over_white(30, 30), [0, 0, 255]); assert_eq!(img.over_white(140, 140), [255, 0, 0]); // A style sheet's `[id]` selector still matches only the circle that has an id. let svg = r##" "##; let img = render_artboard(&import(svg).unwrap()); assert_eq!(img.over_white(30, 30), [0, 0, 255]); assert_eq!(img.over_white(130, 30), [255, 0, 0]); } #[test] fn circles_import_without_an_id_the_file_names() { // A style sheet's rule for an id no element has does not reach the circle. let svg = r##" "##; let img = render_artboard(&import(svg).unwrap()); assert_eq!(img.over_white(30, 30), [0, 0, 0]); } #[test] fn circles_import_without_an_id_a_use_names() { // A `` of an id no element has does not show the circle, nor does one that writes the // id with a character reference. for href in ["#vectorcraft-id-0", "#vectorcraft-id-0"] { let svg = format!( r##" "## ); let img = render_artboard(&import(&svg).unwrap()); assert_eq!(img.over_white(30, 30), [0, 0, 0], "{href}"); assert_eq!(img.over_white(130, 30), [255, 255, 255], "{href}"); } }