vectorcraft/crates/pdf/src/tests_stroke.rs

95 lines
4.0 KiB
Rust

//! Stroke geometry in PDF export: arrowheads and the trimmed line are written as the shared
//! `vectorcraft_effects::stroke` outlines, and they take the stroke opacity once.
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, ArrowAlign, Arrowhead, Document, Node, NodeId, NodeKind, StrokeLayer};
use vectorcraft_effects::stroke::{OUTLINE_TOL, line_outline, stroke_pieces};
use vectorcraft_geom::{Point, Rect, Shape, shapes};
use crate::*;
pub(crate) fn line_doc(st: StrokeLayer) -> (Document, Node) {
let mut d = Document::new(100.0, 100.0);
let mut n = Node::path(
NodeId(0),
shapes::line(Point::new(20.0, 50.0), Point::new(80.0, 50.0)),
Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() },
);
n.id = d.alloc_id();
let layer = d.default_layer().unwrap();
d.insert(Some(layer), 0, n.clone()).unwrap();
(d, n)
}
fn arrow_stroke(kind: Arrowhead, align: ArrowAlign, opacity: f32) -> StrokeLayer {
let mut st = StrokeLayer::new(Paint::solid(Color::BLACK), 4.0);
st.end_arrow = Some(kind);
st.arrow_align = align;
st.opacity = opacity;
st
}
fn close(a: Rect, b: Rect) -> bool {
[(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(p, q)| (p - q).abs() < 0.01)
}
/// Bounding boxes of the filled and the stroked paths of a PDF read back.
pub(crate) fn painted_boxes(d: &Document) -> (Vec<Rect>, Vec<Rect>) {
let back = import(&export(d, &PdfOptions::default()).unwrap()).unwrap();
let (mut fills, mut strokes) = (vec![], vec![]);
back.walk(|m| {
let Some(bb) = m.geometric_bounds().filter(|_| matches!(m.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })) else { return };
match m.appearance.items.first() {
Some(AppearanceItem::Fill(_)) => fills.push(bb),
Some(AppearanceItem::Stroke(_)) => strokes.push(bb),
None => {}
}
});
(fills, strokes)
}
#[test]
fn arrowheads_and_the_trimmed_line_match_the_shared_geometry() {
for align in [ArrowAlign::Extend, ArrowAlign::Tip] {
for kind in Arrowhead::ALL {
let (d, n) = line_doc(arrow_stroke(kind, align, 1.0));
let bp = n.path_data().unwrap().to_bezpath();
let st = n.appearance.stroke().unwrap();
let pieces = stroke_pieces(&bp, st);
let (fills, strokes) = painted_boxes(&d);
let head = pieces.heads[0].outline.bounding_box();
let line = line_outline(&pieces.line, st, st.width, OUTLINE_TOL).bounding_box();
assert!(strokes.is_empty(), "{kind:?} {align:?}: the line is written as its outline");
assert_eq!(fills.len(), 2, "{kind:?} {align:?}: the line, then the head");
assert!(close(fills[0], line), "{kind:?} {align:?}: {:?} vs {line:?}", fills[0]);
assert!(close(fills[1], head), "{kind:?} {align:?}: {:?} vs {head:?}", fills[1]);
if align == ArrowAlign::Tip {
assert!((fills[0].x1 - (80.0 - pieces.heads[0].inset)).abs() < 0.01, "{kind:?}: the stroke stops under the head");
}
}
}
}
#[test]
fn line_and_head_share_one_opacity_group() {
let (d, _) = line_doc(arrow_stroke(Arrowhead::Triangle, ArrowAlign::Tip, 0.5));
let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap();
let mut group_opacity = None;
let mut leaf_opacities = vec![];
back.walk(|m| match &m.kind {
NodeKind::Group { .. } if m.opacity < 1.0 => group_opacity = Some(m.opacity),
NodeKind::Path { .. } => {
leaf_opacities.push(m.opacity);
for i in &m.appearance.items {
leaf_opacities.push(match i {
AppearanceItem::Fill(f) => f.opacity,
AppearanceItem::Stroke(s) => s.opacity,
});
}
}
_ => {}
});
assert!(group_opacity.is_some_and(|o| (o - 0.5).abs() < 0.01), "{group_opacity:?}");
assert!(leaf_opacities.iter().all(|o| (o - 1.0).abs() < 1e-3), "{leaf_opacities:?}");
}