Files
cadcraft/crates/io/tests/hatch_pattern_def.rs

149 lines
6.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! HATCH pattern definition lines (groups 78, 53, 43–46, 79, 49) of a pattern the library
//! doesn't define survive a DXF read and save (issue #290).
use cadcraft_doc::library::PatternLine;
use cadcraft_doc::*;
use cadcraft_geom::{PolyVertex, Vec2};
use cadcraft_io::{read_dxf, write_dxf};
fn square() -> HatchLoop {
let v = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)].into_iter().map(|(x, y)| PolyVertex::new(Vec2::new(x, y))).collect();
HatchLoop { vertices: v, outer: true, sources: Vec::new() }
}
fn hatch(pattern: &str, scale: f64, angle: f64, origin: Vec2, lines: Vec<PatternLine>) -> Hatch {
Hatch {
pattern: pattern.into(),
solid: false,
loops: vec![square()],
scale,
angle,
associative: false,
style: 0,
elevation: 0.0,
gradient: None,
origin,
background: None,
pattern_lines: lines,
}
}
fn custom_lines() -> Vec<PatternLine> {
vec![
PatternLine { angle: 0.0, origin: (0.0, 0.0), delta: (0.5, 1.0), dashes: vec![0.75, -0.25, 0.0, -0.25] },
PatternLine { angle: 90.0, origin: (0.25, 0.0), delta: (0.0, 1.0), dashes: vec![] },
]
}
fn save(h: Hatch) -> String {
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Hatch(h)).expect("add");
write_dxf(&d)
}
fn first_hatch(text: &str) -> Hatch {
let d = read_dxf(text.as_bytes()).expect("read");
d.model.iter().find_map(|e| if let EntityKind::Hatch(h) = &e.kind { Some(h.clone()) } else { None }).expect("hatch")
}
/// (group code, value) pairs of a DXF text.
fn pairs(text: &str) -> Vec<(i32, String)> {
let lines: Vec<&str> = text.lines().collect();
lines.chunks(2).filter_map(|c| Some((c.first()?.trim().parse().ok()?, c.get(1)?.trim().to_string()))).collect()
}
/// Index of the HATCH's 78 group.
fn pattern_data(p: &[(i32, String)]) -> usize {
let hatch = p.iter().position(|(c, v)| *c == 0 && v == "HATCH").expect("HATCH");
p.iter().skip(hatch).position(|(c, _)| *c == 78).map(|k| hatch + k).expect("78")
}
fn join(p: &[(i32, String)]) -> String {
p.iter().map(|(c, v)| format!("{c}\n{v}\n")).collect()
}
fn near(a: &[PatternLine], b: &[PatternLine]) -> bool {
let close = |x: f64, y: f64| (x - y).abs() < 1e-9;
a.len() == b.len()
&& a.iter().zip(b).all(|(p, q)| {
close(p.angle, q.angle)
&& close(p.origin.0, q.origin.0)
&& close(p.origin.1, q.origin.1)
&& close(p.delta.0, q.delta.0)
&& close(p.delta.1, q.delta.1)
&& p.dashes.len() == q.dashes.len()
&& p.dashes.iter().zip(&q.dashes).all(|(x, y)| close(*x, *y))
})
}
#[test]
fn custom_pattern_lines_roundtrip_scaled_and_rotated() {
let angle = 30f64.to_radians();
let text = save(hatch("MYPAT", 2.0, angle, Vec2::new(1.0, 2.0), custom_lines()));
// In the file the lines are as drawn: rotated with the hatch, scaled, offset as a world vector.
let p = pairs(&text);
let at = pattern_data(&p);
assert_eq!(p[at].1, "2");
let num = |k: usize| p[at + k].1.parse::<f64>().expect("number");
assert!((num(1) - 30.0).abs() < 1e-9, "53 angle");
let off = Vec2::new(0.5, 1.0).rotate(angle) * 2.0;
assert!((num(2) - 1.0).abs() < 1e-9 && (num(3) - 2.0).abs() < 1e-9, "43/44 base point at the origin");
assert!((num(4) - off.x).abs() < 1e-9 && (num(5) - off.y).abs() < 1e-9, "45/46 offset");
assert_eq!(p[at + 6], (79, "4".into()));
assert!((num(7) - 1.5).abs() < 1e-9, "49 dash scaled");
// Read back to the same definition, and saved again unchanged.
let h = first_hatch(&text);
assert!(near(&h.pattern_lines, &custom_lines()), "{:?}", h.pattern_lines);
assert!(near(&first_hatch(&save(h)).pattern_lines, &custom_lines()));
}
#[test]
fn library_named_patterns_keep_the_files_definition() {
// The library's AR-CONC only approximates AutoCAD's: the file's own lines are kept and saved.
let h = first_hatch(&save(hatch("AR-CONC", 2.0, 0.0, Vec2::ZERO, custom_lines())));
assert!(near(&h.pattern_lines, &custom_lines()), "{:?}", h.pattern_lines);
// A hatch without lines of its own saves the library's definition, which reads back as lines.
let h = first_hatch(&save(hatch("ANSI31", 1.0, 0.0, Vec2::ZERO, vec![])));
assert_eq!(h.pattern_lines.len(), 1);
}
#[test]
fn file_definition_is_drawn_instead_of_the_librarys() {
let prims = |lines: Vec<PatternLine>| {
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Hatch(hatch("ANSI31", 1.0, 0.0, Vec2::ZERO, lines))).expect("add");
cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default()).prims.len()
};
// One horizontal family 5 units apart from y = 2.5 across the 10 × 10 square: two lines.
let sparse = vec![PatternLine { angle: 0.0, origin: (0.0, 2.5), delta: (0.0, 5.0), dashes: vec![] }];
assert_eq!(prims(sparse), 2);
// Without file lines, the library's ANSI31 (0.125 apart) is drawn.
assert!(prims(vec![]) > 100);
}
#[test]
fn hostile_pattern_definitions_are_capped() {
let text = save(hatch("MYPAT", 1.0, 0.0, Vec2::ZERO, custom_lines()));
let p = pairs(&text);
let at = pattern_data(&p);
let end = p.iter().skip(at).position(|(c, _)| *c == 98).map(|k| at + k).expect("98");
let mut hostile = vec![(78, "2000000000".to_string())];
// A line with a NaN offset, a line with a million dashes, then far too many lines.
hostile.extend([(53, "0"), (43, "0"), (44, "0"), (45, "nan"), (46, "1"), (79, "1"), (49, "1")].map(|(c, v)| (c, v.to_string())));
hostile.extend([(53, "0"), (43, "0"), (44, "0"), (45, "0"), (46, "1"), (79, "1000000")].map(|(c, v)| (c, v.to_string())));
hostile.extend((0..1_000).map(|_| (49, "0.5".to_string())));
for _ in 0..3_000 {
hostile.extend([(53, "45"), (43, "0"), (44, "0"), (45, "0"), (46, "1"), (79, "0")].map(|(c, v)| (c, v.to_string())));
}
let mut q = p[..at].to_vec();
q.extend(hostile);
q.extend_from_slice(&p[end..]);
let h = first_hatch(&join(&q));
assert!(h.pattern_lines.len() <= 1024 && h.pattern_lines.len() > 100, "{}", h.pattern_lines.len());
assert!(h.pattern_lines.iter().all(|l| l.delta.0.is_finite() && l.dashes.len() <= 256));
// Drawing the capped definition stays bounded.
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Hatch(h)).expect("add");
let _ = cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default());
}