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:
parent
9477f37503
commit
135c928fea
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue