82 lines
3.6 KiB
Rust
82 lines
3.6 KiB
Rust
//! Strokes in SVG export look as on the canvas: arrowheads, width profiles, dots and brushes are
|
|
//! written as the canvas's filled outlines or art, aligned strokes are clipped or masked, and open
|
|
//! paths stroke centred. Checked by rendering the exported SVG with resvg.
|
|
|
|
use vectorcraft_color::{Color, Paint};
|
|
use vectorcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, Document, LineCap, LineJoin, Node, StrokeAlign, StrokeLayer, WidthProfile};
|
|
use vectorcraft_geom::{PathData, Point, SubPath, shapes};
|
|
use vectorcraft_svg::{ExportOptions, export};
|
|
use vectorcraft_testkit::raster::{Image, assert_similar, render_artboard};
|
|
|
|
fn add(d: &mut Document, path: PathData, f: impl FnOnce(&mut StrokeLayer)) {
|
|
let mut st = StrokeLayer::new(Paint::solid(Color::rgb(0.1, 0.2, 0.6)), 4.0);
|
|
f(&mut st);
|
|
let n = Node::path(d.alloc_id(), path, Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
|
|
let l = d.layers[0].id;
|
|
d.insert(Some(l), usize::MAX, n).unwrap();
|
|
}
|
|
|
|
fn line(x0: f64, y: f64, x1: f64) -> PathData {
|
|
shapes::line(Point::new(x0, y), Point::new(x1, y))
|
|
}
|
|
|
|
/// The exported SVG rendered by resvg on white at 1 px/pt.
|
|
fn resvg_render(svg: &str, w: u32, h: u32) -> Image {
|
|
let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default()).expect("parse");
|
|
let mut pm = resvg::tiny_skia::Pixmap::new(w, h).unwrap();
|
|
pm.fill(resvg::tiny_skia::Color::WHITE);
|
|
resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pm.as_mut());
|
|
Image { width: w, height: h, rgba: pm.data().to_vec() }
|
|
}
|
|
|
|
#[test]
|
|
fn exported_strokes_render_like_the_canvas() {
|
|
let mut d = Document::new(200.0, 200.0);
|
|
add(&mut d, line(20.0, 20.0, 150.0), |s| {
|
|
s.end_arrow = Some(Arrowhead::Triangle);
|
|
s.start_arrow = Some(Arrowhead::CircleOpen);
|
|
s.opacity = 0.5;
|
|
});
|
|
add(&mut d, line(20.0, 50.0, 180.0), |s| {
|
|
s.width = 14.0;
|
|
s.profile = Some(WidthProfile::lens());
|
|
});
|
|
add(&mut d, line(20.0, 80.0, 180.0), |s| {
|
|
s.cap = LineCap::Square;
|
|
s.dash = Some(Dash { pattern: vec![0.0, 12.0], offset: 0.0, align_corners: false });
|
|
});
|
|
add(&mut d, line(20.0, 110.0, 180.0), |s| s.brush = Some("Tapered Stroke".into()));
|
|
let tri = SubPath::polyline(&[Point::new(60.0, 185.0), Point::new(100.0, 185.0), Point::new(80.0, 135.0)], true);
|
|
add(&mut d, PathData::single(tri), |s| {
|
|
s.align = StrokeAlign::Outside;
|
|
s.join = LineJoin::Miter;
|
|
s.width = 6.0;
|
|
});
|
|
let svg = export(&d, &ExportOptions::default());
|
|
assert!(!svg.contains("stroke-dasharray"), "dots are outlines:\n{svg}");
|
|
let canvas = render_artboard(&d);
|
|
let written = resvg_render(&svg, 200, 200);
|
|
assert_similar(&canvas, &written, 24.0, 0.002);
|
|
// The comparison is meaningful: every stroke drew something.
|
|
for y in [20, 50, 80, 110, 160] {
|
|
assert!((0..200).any(|x| canvas.over_white(x, y).iter().any(|c| *c < 200)), "row {y} has ink");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_open_path_with_inside_alignment_exports_a_plain_stroke() {
|
|
let mut d = Document::new(200.0, 100.0);
|
|
add(&mut d, line(20.0, 50.0, 180.0), |s| s.align = StrokeAlign::Inside);
|
|
let svg = export(&d, &ExportOptions::default());
|
|
assert!(svg.contains("stroke-width=\"4\""), "{svg}");
|
|
assert!(!svg.contains("clip-path") && !svg.contains("mask"), "{svg}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_lens_profile_exports_a_fill() {
|
|
let mut d = Document::new(200.0, 100.0);
|
|
add(&mut d, line(20.0, 50.0, 180.0), |s| s.profile = Some(WidthProfile::lens()));
|
|
let svg = export(&d, &ExportOptions::default());
|
|
assert!(svg.contains("fill=\"#1a3399\"") && !svg.contains("stroke="), "{svg}");
|
|
}
|