Files
cadcraft/crates/render/tests/plot_styles.rs
T
Valentin KleinandClaude Opus 5.5 ee34f423cc Plot styles: colour-dependent tables apply to PDF plots and layouts (part of #410) (#412)
A PlotStyleTable model in doc with our own built-in tables (default.ctb keeps
object properties, monochrome.ctb prints black, grayscale.ctb prints the grey of
each colour's luminance). The render builder resolves each object's style by its
index colour (true colours by the nearest index) and applies colour, screening,
linetype and lineweight. PLOT/EXPORTPDF use the page setup's table (or the
plotStyleTable option); layouts with Display plot styles (PAGESETUP
displayPlotStyles, DXF plot flag 2) show it on screen and in PNG/SVG exports.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:19:48 -04:00

118 lines
5.4 KiB
Rust

//! Plot style tables applied to the display list (issue #410): colour-dependent tables map an
//! object's colour to its printed colour, screening, linetype and lineweight; layouts show them
//! when "Display plot styles" is on.
use std::sync::Arc;
use cadcraft_color::{Color, Rgb};
use cadcraft_doc::{Common, Drawing, EntityKind, Line, Lineweight, PlotStyle, PlotStyleKind, PlotStyleTable, Space};
use cadcraft_geom::Vec3;
use cadcraft_render::{DisplayList, Options, build, build_plot};
const BLACK: Rgb = Rgb(0, 0, 0);
fn line(y: f64) -> EntityKind {
EntityKind::Line(Line { a: Vec3::new(0.0, y, 0.0), b: Vec3::new(10.0, y, 0.0) })
}
/// Lines in red (1), green (3), colour 7 (ByLayer on layer 0) and a true orange, in `space`.
fn drawing(space: &Space) -> Drawing {
let mut d = Drawing::new_metric();
let c = |color| Common { color, ..Default::default() };
d.add(space, c(Color::Index(1)), line(0.0)).unwrap();
d.add(space, c(Color::Index(3)), line(1.0)).unwrap();
d.add(space, Common::default(), line(2.0)).unwrap();
d.add(space, c(Color::True(Rgb(255, 128, 0))), line(3.0)).unwrap();
d
}
fn with_table(t: PlotStyleTable) -> Options {
Options { plot_style_table: Some(Arc::new(t)), lineweights: true, ..Default::default() }
}
fn colours(l: &DisplayList) -> Vec<Rgb> {
l.prims.iter().map(|p| p.display_rgb(Rgb(255, 255, 255))).collect()
}
#[test]
fn monochrome_prints_every_colour_black() {
let d = drawing(&Space::Model);
let l = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("monochrome.ctb").unwrap()));
assert_eq!(l.prims.len(), 4);
assert!(l.prims.iter().all(|p| p.color == BLACK && !p.aci7), "{:?}", l.prims);
// Without a table the colours are the objects'.
let plain = build_plot(&d, &Space::Model, &Options::default());
assert_eq!(colours(&plain), [Rgb(255, 0, 0), Rgb(0, 255, 0), BLACK, Rgb(255, 128, 0)]);
}
#[test]
fn grayscale_and_default_tables() {
let d = drawing(&Space::Model);
let l = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("grayscale.ctb").unwrap()));
for c in colours(&l) {
assert!(c.0 == c.1 && c.1 == c.2, "grey: {c:?}");
}
let c = colours(&l);
assert!(c[1].0 > c[0].0, "green is lighter than red");
assert_eq!(c[2], BLACK, "colour 7 prints black");
let keep = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("default.ctb").unwrap()));
assert_eq!(colours(&keep), colours(&build_plot(&d, &Space::Model, &Options::default())));
}
#[test]
fn screening_lineweight_and_linetype_overrides() {
let mut d = Drawing::new_metric();
d.header.set_f64("LTSCALE", 0.05);
d.add(&Space::Model, Common { color: Color::Index(1), lineweight: Lineweight::Mm100(13), ..Default::default() }, line(0.0)).unwrap();
d.add(&Space::Model, Common { color: Color::Index(5), ..Default::default() }, line(1.0)).unwrap();
let table = PlotStyleTable::color_dependent("pens.ctb", "", |i| {
(i == 1).then(|| PlotStyle {
color: Some(BLACK),
screening: 50,
lineweight: Some(70),
linetype: Some("DASHED".into()),
..PlotStyle::default()
})
});
let l = build_plot(&d, &Space::Model, &with_table(table.clone()));
let red: Vec<_> = l.prims.iter().filter(|p| p.color == Rgb(128, 128, 128)).collect();
assert!(red.len() > 1, "dashed into several pieces: {}", red.len());
assert!(red.iter().all(|p| p.lw == 0.7), "pen lineweight replaces 0.13 mm");
let blue: Vec<_> = l.prims.iter().filter(|p| p.color == Rgb(0, 0, 255)).collect();
assert_eq!(blue.len(), 1, "colour 5 keeps its own style");
assert_eq!(blue[0].lw, 0.25, "object lineweight (LWDEFAULT)");
// With lineweights off nothing gets a width.
let thin = build_plot(&d, &Space::Model, &Options { lineweights: false, ..with_table(table) });
assert!(thin.prims.iter().all(|p| p.lw == 0.0));
}
#[test]
fn layout_display_plot_styles_flag() {
let space = Space::Paper("Layout1".into());
let mut d = drawing(&space);
d.layouts[0].page.plot_style_table = "monochrome.ctb".into();
let red = |l: &DisplayList| l.prims.iter().any(|p| p.color == Rgb(255, 0, 0));
assert!(red(&build(&d, &space, &Options::default())), "flag off: object colours on screen");
d.layouts[0].page.show_plot_styles = true;
assert!(!red(&build(&d, &space, &Options::default())), "flag on: monochrome on screen");
// Plotting takes its table from the options (the plotter passes the page's).
assert!(red(&build_plot(&d, &space, &Options::default())));
// Model space and unknown tables draw object colours.
d.layouts[0].page.plot_style_table = "unknown.ctb".into();
assert!(red(&build(&d, &space, &Options::default())));
}
#[test]
fn hostile_tables_do_not_panic() {
let d = drawing(&Space::Model);
for styles in [Vec::new(), vec![PlotStyle { screening: 255, lineweight: Some(u16::MAX), linetype: Some("NOPE".into()), ..PlotStyle::default() }]]
{
let t = PlotStyleTable { name: "x.ctb".into(), styles, ..PlotStyleTable::default() };
let l = build_plot(&d, &Space::Model, &with_table(t));
assert_eq!(l.prims.len(), 4);
assert!(l.prims.iter().all(|p| p.lw <= 2.11));
}
let named = PlotStyleTable { kind: PlotStyleKind::Named, styles: vec![PlotStyle::object("Normal")], ..PlotStyleTable::default() };
assert_eq!(build_plot(&d, &Space::Model, &with_table(named)).prims.len(), 4);
}