Hatches: draw the pattern definition stored in the file; scale library patterns in metric drawings (#643)

Squash of PR #643 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aldozar 2026-10-11 17:54:18 -06:00 committed by GitHub
parent 9477f37503
commit 135c928fea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 115 additions and 38 deletions

View File

@ -203,28 +203,73 @@ pub fn standard_patterns() -> Vec<HatchPattern> {
description: "Staircase effect",
lines: vec![pl(0.0, (0.0, 0.0), (0.125, 0.125), &[0.125, -0.125]), pl(90.0, (0.125, 0.0), (0.125, 0.125), &[0.125, -0.125])],
},
HatchPattern {
name: "AR-CONC",
description: "Random dot and stone pattern",
lines: vec![
pl(50.0, (0.0, 0.0), (0.4, 0.25), &[0.15, -0.6]),
pl(355.0, (0.0, 0.0), (0.27, 0.3), &[0.12, -0.5]),
pl(100.0, (0.12, 0.05), (0.35, 0.33), &[0.1, -0.55]),
pl(0.0, (0.07, 0.02), (0.21, 0.17), &[0.0, -0.4]),
],
},
HatchPattern {
name: "AR-SAND",
description: "Random dot pattern",
lines: vec![
pl(37.5, (0.0, 0.0), (0.112, 0.0912), &[0.0, -0.22, 0.0, -0.14]),
pl(7.5, (0.0, 0.0), (0.157, 0.0724), &[0.0, -0.19, 0.0, -0.31]),
pl(-32.5, (0.0, 0.0), (0.091, 0.1312), &[0.0, -0.27, 0.0, -0.11]),
],
},
// Drawn at the scale of the pattern CAD programs ship under this name (about 16x
// coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense).
scaled_pattern(
HatchPattern {
name: "AR-CONC",
description: "Random dot and stone pattern",
lines: vec![
pl(50.0, (0.0, 0.0), (0.4, 0.25), &[0.15, -0.6]),
pl(355.0, (0.0, 0.0), (0.27, 0.3), &[0.12, -0.5]),
pl(100.0, (0.12, 0.05), (0.35, 0.33), &[0.1, -0.55]),
pl(0.0, (0.07, 0.02), (0.21, 0.17), &[0.0, -0.4]),
],
},
16.0,
),
// Drawn at the scale of the pattern CAD programs ship under this name (about 16x
// coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense).
scaled_pattern(
HatchPattern {
name: "AR-SAND",
description: "Random dot pattern",
lines: vec![
pl(37.5, (0.0, 0.0), (0.112, 0.0912), &[0.0, -0.22, 0.0, -0.14]),
pl(7.5, (0.0, 0.0), (0.157, 0.0724), &[0.0, -0.19, 0.0, -0.31]),
pl(-32.5, (0.0, 0.0), (0.091, 0.1312), &[0.0, -0.27, 0.0, -0.11]),
],
},
16.0,
),
]
}
pub fn pattern(name: &str) -> Option<HatchPattern> {
standard_patterns().into_iter().find(|p| p.name.eq_ignore_ascii_case(name))
}
/// The library pattern `name` as drawn in `d`. In a metric drawing (`MEASUREMENT` 1) every
/// length is multiplied by 25.4, as CAD programs use ISO-scaled pattern definitions there (as
/// for linetypes, see [`standard_linetypes_for`]): a hatch at scale 1 measures in millimetres
/// what it measures in inches in an imperial drawing.
pub fn pattern_for(d: &Drawing, name: &str) -> Option<HatchPattern> {
let p = pattern(name)?;
Some(if d.header.i64("MEASUREMENT", 0) == 1 { scaled_pattern(p, 25.4) } else { p })
}
/// `p` with every length (origins, offsets, dashes) multiplied by `k`.
fn scaled_pattern(mut p: HatchPattern, k: f64) -> HatchPattern {
for l in &mut p.lines {
l.origin = (l.origin.0 * k, l.origin.1 * k);
l.delta = (l.delta.0 * k, l.delta.1 * k);
for d in &mut l.dashes {
*d *= k;
}
}
p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metric_drawings_draw_iso_scaled_patterns() {
let imp = pattern_for(&Drawing::new_imperial(), "ANSI31").map(|p| p.lines[0].delta.1);
let met = pattern_for(&Drawing::new_metric(), "ansi31").map(|p| p.lines[0].delta.1);
assert_eq!(imp, Some(0.125));
assert!(met.is_some_and(|m| (m - 0.125 * 25.4).abs() < 1e-12), "{met:?}");
assert!(pattern_for(&Drawing::new_metric(), "NOPE").is_none());
}
}

View File

@ -374,6 +374,8 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
h.pattern_lines = super::hatch::lines_for(v);
}
h.pattern = v.to_ascii_uppercase();
// The old pattern's definition lines no longer apply.
h.pattern_lines.clear();
h.solid = h.pattern == "SOLID";
if !h.solid {
// A named pattern replaces the gradient fill; the renderer draws the gradient first when it is set.

View File

@ -742,9 +742,10 @@ fn hatch(tags: &[Tag], t: &T) -> Option<EntityKind> {
}
let solid = solid || pattern.eq_ignore_ascii_case("SOLID");
let (scale, angle) = (t.fd(41, 1.0), t.fd(52, 0.0).to_radians());
// Definition lines matter only for a pattern the library doesn't define.
let pattern_lines =
if solid || cadcraft_doc::library::pattern(&pattern).is_some() { Vec::new() } else { pattern_definition(tags, scale, angle, origin) };
// Keep the file's definition lines even for a name the library knows: the library's versions
// of some patterns (AR-CONC) only approximate AutoCAD's, and drawing them at the file's scale
// could be hundreds of times denser than the hatch the author saw.
let pattern_lines = if solid { Vec::new() } else { pattern_definition(tags, scale, angle, origin) };
Some(EntityKind::Hatch(Hatch {
pattern: pattern.to_ascii_uppercase(),
solid,

View File

@ -924,9 +924,12 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
w.f(52, h.angle.to_degrees());
w.f(41, h.scale);
w.i(77, 0);
let pat = cadcraft_doc::library::pattern(&h.pattern);
// A pattern the library doesn't define keeps the lines read from its file.
let lines = pat.map(|p| p.lines).unwrap_or_else(|| h.pattern_lines.clone());
// The lines read from the file, else the library's pattern of that name.
let lines = if h.pattern_lines.is_empty() {
cadcraft_doc::library::pattern_for(d, &h.pattern).map(|p| p.lines).unwrap_or_default()
} else {
h.pattern_lines.clone()
};
w.i(78, lines.len() as i64);
for pl in lines {
let ang = pl.angle.to_radians() + h.angle;

View File

@ -98,9 +98,27 @@ fn custom_pattern_lines_roundtrip_scaled_and_rotated() {
}
#[test]
fn library_patterns_keep_no_definition() {
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!(h.pattern_lines.is_empty());
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]

View File

@ -4,8 +4,8 @@
use super::{Builder, Ctx, Ink, fill, ink};
use cadcraft_color::{Color, Rgb};
use cadcraft_doc::library::pattern;
use cadcraft_doc::{Entity, Gradient, Hatch};
use cadcraft_doc::library::pattern_for;
use cadcraft_doc::{Drawing, Entity, Gradient, Hatch};
use cadcraft_geom::{Bounds2, Polyline, Vec2, point_in_polygon};
/// Draw a hatch entity: `rgb` is its resolved colour, `tol` the chord tolerance.
@ -29,7 +29,7 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
{
b.tris(ctx, ink(bg, layer_color, ctx.block_color), &fill::triangulate_evenodd(&loops));
}
for seg in pattern_lines(h, &loops) {
for seg in pattern_lines(ctx.d, h, &loops) {
if seg.len() == 1 {
if let Some(p) = seg.first() {
b.point(ctx, rgb, *p);
@ -209,13 +209,18 @@ impl Field {
/// Upper bound on the line families drawn from a pattern definition read from a file.
const MAX_PATTERN_LINES: usize = 1024;
/// Upper bound on line runs drawn for one hatch (AutoCAD's HPMAXLINES default): a pattern far
/// too dense for its area stops here instead of exhausting memory.
const MAX_HATCH_SEGMENTS: usize = 1_000_000;
/// Hatch line runs in world coordinates (single-point runs are dots) for the fill `loops`.
pub fn pattern_lines(h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
// A pattern the library doesn't define draws with the lines its file defined, else as ANSI31.
let lines = match pattern(&h.pattern) {
Some(p) => p.lines,
None if !h.pattern_lines.is_empty() => h.pattern_lines.iter().take(MAX_PATTERN_LINES).cloned().collect(),
None => pattern("ANSI31").map(|p| p.lines).unwrap_or_default(),
pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
// The lines the file defined (as its author's CAD drew them), else the library's pattern of
// that name, else ANSI31.
let lines = if h.pattern_lines.is_empty() {
pattern_for(d, &h.pattern).or_else(|| pattern_for(d, "ANSI31")).map(|p| p.lines).unwrap_or_default()
} else {
h.pattern_lines.iter().take(MAX_PATTERN_LINES).cloned().collect()
};
let scale = if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 };
let mut out = Vec::new();
@ -229,7 +234,7 @@ pub fn pattern_lines(h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
}
let dashes: Vec<f64> = fam.dashes.iter().map(|d| d * scale).collect();
family(loops, origin, ang, delta, spacing, &dashes, &mut out);
if out.len() > 500_000 {
if out.len() >= MAX_HATCH_SEGMENTS {
break;
}
}
@ -253,6 +258,9 @@ fn family(loops: &[Vec<Vec2>], origin: Vec2, ang: f64, delta: Vec2, spacing: f64
}
let pat_len: f64 = dashes.iter().map(|d| d.abs()).sum();
for k in k0..=k1 {
if out.len() >= MAX_HATCH_SEGMENTS {
return;
}
let y = k as f64 * spacing;
let mut xs: Vec<f64> = Vec::new();
for l in &local {
@ -279,7 +287,7 @@ fn family(loops: &[Vec<Vec2>], origin: Vec2, ang: f64, delta: Vec2, spacing: f64
'outer: while x < *x1 {
for d in dashes {
guard += 1;
if guard > 100_000 {
if guard > 100_000 || out.len() >= MAX_HATCH_SEGMENTS {
break 'outer;
}
let len = d.abs();