cadcraft/crates/render/tests/plot_line_styles.rs

180 lines
8.1 KiB
Rust

//! Plot style line ends, joins and fill patterns (issue #564): wide lines become outlines with
//! the style's ends and joins, solid fills become the style's pattern.
use std::sync::Arc;
use cadcraft_color::Color;
use cadcraft_doc::{
Common, Drawing, EndStyle, EntityKind, FillStyle, JoinStyle, Line, LwPolyline, PlotStyle, PlotStyleTable, Solid, Space, Viewport,
};
use cadcraft_geom::{PolyVertex, Vec2, Vec3};
use cadcraft_render::{DisplayList, Kind, Options, VIEWPORT_CONTENT, build, build_plot, pen};
/// A colour-dependent table whose colour 1 prints 1.00 mm wide with `style`'s end/join/fill.
fn table(end: EndStyle, join: JoinStyle, fill: FillStyle) -> Options {
let t = PlotStyleTable::color_dependent("pens.ctb", "", |i| {
(i == 1).then(|| PlotStyle { lineweight: Some(100), end, join, fill, ..PlotStyle::default() })
});
Options { plot_style_table: Some(Arc::new(t)), lineweights: true, ..Default::default() }
}
fn red() -> Common {
Common { color: Color::Index(1), ..Default::default() }
}
fn tri_area(l: &DisplayList) -> f64 {
l.prims
.iter()
.filter(|p| p.kind == Kind::Tris)
.flat_map(|p| l.points(p).as_chunks::<3>().0.iter().map(|[a, b, c]| (*b - *a).cross(*c - *a) / 2.0))
.sum()
}
/// A 10-unit line plotted at 1 mm per unit.
fn line_plot(end: EndStyle, join: JoinStyle) -> DisplayList {
let mut d = Drawing::new_metric();
d.add(&Space::Model, red(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(10.0, 0.0, 0.0) })).unwrap();
let mut l = build_plot(&d, &Space::Model, &table(end, join, FillStyle::Object));
pen::apply(&mut l, 1.0);
l
}
#[test]
fn line_ends_shape_wide_lines() {
let object = line_plot(EndStyle::Object, JoinStyle::Object);
assert_eq!(object.prims[0].kind, Kind::Polyline, "use-object ends keep the stroke");
assert_eq!(object.prims[0].lw, 1.0);
let butt = line_plot(EndStyle::Butt, JoinStyle::Object);
assert_eq!(butt.prims.len(), 1);
assert_eq!(butt.prims[0].kind, Kind::Tris);
assert!((tri_area(&butt) - 10.0).abs() < 1e-9, "butt: {}", tri_area(&butt));
let square = line_plot(EndStyle::Square, JoinStyle::Object);
assert!((tri_area(&square) - 11.0).abs() < 1e-9, "square: {}", tri_area(&square));
let diamond = line_plot(EndStyle::Diamond, JoinStyle::Object);
assert!((tri_area(&diamond) - 10.5).abs() < 1e-9, "diamond: {}", tri_area(&diamond));
let round = line_plot(EndStyle::Round, JoinStyle::Object);
assert!(tri_area(&round) > 10.7 && tri_area(&round) < 10.0 + std::f64::consts::PI / 4.0);
// Applying again changes nothing; pens are spent.
let mut again = butt.clone();
pen::apply(&mut again, 1.0);
assert_eq!(again.prims, butt.prims);
assert!(butt.pens.is_empty());
}
#[test]
fn joins_shape_the_corner() {
// An L: (0,0) → (10,0) → (10,10), 1 mm wide, butt ends.
let corner = |join| {
let mut d = Drawing::new_metric();
let v = |x, y| PolyVertex::new(Vec2::new(x, y));
let pl =
LwPolyline { vertices: vec![v(0.0, 0.0), v(10.0, 0.0), v(10.0, 10.0)], closed: false, const_width: 0.0, elevation: 0.0, plinegen: false };
d.add(&Space::Model, red(), EntityKind::LwPolyline(pl)).unwrap();
let mut l = build_plot(&d, &Space::Model, &table(EndStyle::Butt, join, FillStyle::Object));
pen::apply(&mut l, 1.0);
// The union of the outline: rasterise coarsely by sampling the outer corner region.
let inside = |p: Vec2| {
l.prims.iter().any(|pr| {
l.points(pr).as_chunks::<3>().0.iter().any(|[a, b, c]| {
let s = |u: Vec2, w: Vec2| (w - u).cross(p - u) >= -1e-12;
s(*a, *b) && s(*b, *c) && s(*c, *a)
})
})
};
(inside(Vec2::new(10.45, -0.45)), inside(Vec2::new(10.3, -0.3)), inside(Vec2::new(10.2, -0.2)))
};
assert_eq!(corner(JoinStyle::Miter), (true, true, true), "miter fills the outer corner");
assert_eq!(corner(JoinStyle::Bevel), (false, false, true), "bevel cuts it");
assert_eq!(corner(JoinStyle::Round), (false, true, true), "round rounds it");
assert_eq!(corner(JoinStyle::Diamond), (false, true, true), "diamond points into it");
}
#[test]
fn fill_patterns_replace_solid_fills() {
let mut d = Drawing::new_metric();
let c = [Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(0.0, 10.0, 0.0), Vec3::new(10.0, 10.0, 0.0)];
d.add(&Space::Model, red(), EntityKind::Solid(Solid { corners: c })).unwrap();
let area = |fill| {
let mut l = build_plot(&d, &Space::Model, &table(EndStyle::Object, JoinStyle::Object, fill));
assert_eq!(l.prims.len(), 1);
pen::apply(&mut l, 1.0);
assert_eq!(l.prims.len(), 1, "one primitive per object");
tri_area(&l)
};
assert!((area(FillStyle::Solid) - 100.0).abs() < 1e-9);
assert!((area(FillStyle::Object) - 100.0).abs() < 1e-9);
for (fill, expect) in [
(FillStyle::Checkerboard, 50.0),
(FillStyle::HorizontalBars, 100.0 / 3.0),
(FillStyle::VerticalBars, 100.0 / 3.0),
(FillStyle::SquareDots, 100.0 * 0.16),
] {
let a = area(fill);
assert!((a - expect).abs() < 1.5, "{fill:?}: {a} (expected about {expect})");
}
for fill in [FillStyle::Crosshatch, FillStyle::Diamonds, FillStyle::SlantLeft, FillStyle::SlantRight] {
let a = area(fill);
assert!(a > 15.0 && a < 60.0, "{fill:?}: {a}");
}
}
#[test]
fn layouts_draw_pens_while_building() {
let space = Space::Paper("Layout1".into());
let mut d = Drawing::new_metric();
d.add(&space, red(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(10.0, 0.0, 0.0) })).unwrap();
let opts = table(EndStyle::Square, JoinStyle::Object, FillStyle::Object);
let l = build(&d, &space, &opts);
assert_eq!(l.prims[0].kind, Kind::Tris);
assert!(l.pens.is_empty(), "pens are used up");
assert!((tri_area(&l) - 11.0).abs() < 1e-9);
// Thin lines stay strokes.
let thin = PlotStyleTable::color_dependent("thin.ctb", "", |i| {
(i == 1).then(|| PlotStyle { lineweight: Some(18), end: EndStyle::Square, ..PlotStyle::default() })
});
let l = build(&d, &space, &Options { plot_style_table: Some(Arc::new(thin)), lineweights: true, ..Default::default() });
assert_eq!(l.prims[0].kind, Kind::Polyline);
}
#[test]
fn hostile_scales_and_huge_fills_do_not_hang() {
let mut d = Drawing::new_metric();
let c = [Vec3::new(0.0, 0.0, 0.0), Vec3::new(1e12, 0.0, 0.0), Vec3::new(0.0, 1e12, 0.0), Vec3::new(1e12, 1e12, 0.0)];
d.add(&Space::Model, red(), EntityKind::Solid(Solid { corners: c })).unwrap();
d.add(&Space::Model, red(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(1e300, 0.0, 0.0) })).unwrap();
let l = build_plot(&d, &Space::Model, &table(EndStyle::Diamond, JoinStyle::Diamond, FillStyle::Checkerboard));
for mm in [1.0, 1e-300, 1e300, 0.0, -1.0, f64::NAN, f64::INFINITY] {
let mut x = l.clone();
pen::apply(&mut x, mm);
assert_eq!(x.prims.len(), l.prims.len());
}
}
#[test]
fn viewport_contents_get_their_pens() {
// A 2:1 viewport onto a red model-space line: 1 mm wide on paper, square ends.
let mut d = Drawing::new_metric();
d.add(&Space::Model, red(), EntityKind::Line(Line { a: Vec3::new(-10.0, 0.0, 0.0), b: Vec3::new(10.0, 0.0, 0.0) })).unwrap();
let vp = Viewport {
center: Vec3::new(50.0, 50.0, 0.0),
width: 80.0,
height: 60.0,
view_center: Vec2::ZERO,
view_height: 30.0,
id: 2,
locked: false,
frozen_layers: Vec::new(),
layer_colors: Vec::new(),
clip: None,
on: true,
};
let space = Space::Paper("Layout1".into());
d.add(&space, Common::default(), EntityKind::Viewport(vp)).unwrap();
let l = build_plot(&d, &space, &table(EndStyle::Square, JoinStyle::Object, FillStyle::Object));
let content: Vec<_> = l.prims.iter().filter(|p| p.handle == VIEWPORT_CONTENT).collect();
assert_eq!(content.len(), 1);
assert_eq!(content[0].kind, Kind::Tris);
// 40 mm long on paper plus two 0.5 mm square ends, 1 mm wide.
assert!((tri_area(&l) - 41.0).abs() < 1e-6, "{}", tri_area(&l));
}