Files
cadcraft/crates/render/src/hatch.rs
T
Brandon ThomasandClaude Opus 5.5 d9ab00c53c M0: CADCraft skeleton and vertical slice
Workspace (geom, dxf, color, doc, fonts, render, io, engine, ui-egui, mcp; apps cadcraft,
cadcraft-cli, cadcraft-web; xtask), AutoCAD-for-Mac style UI with code-drawn icons, command line
with prompts/keywords/AutoComplete, draw and modify commands, object snaps, polar/ortho,
selection, undo, layers and properties palettes, DXF read/write, SVG/PNG export, MCP server,
JSON control channel, CLI, wasm build, README, ROADMAP, AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:22:25 -04:00

96 lines
3.9 KiB
Rust

//! Pattern hatch line generation: each pattern line family is swept across the boundary
//! (even-odd clipped), then dashed.
use cadcraft_doc::Hatch;
use cadcraft_doc::library::{HatchPattern, pattern};
use cadcraft_geom::{Bounds2, Polyline, Vec2};
/// Hatch line runs in world coordinates (single-point runs are dots).
pub fn pattern_lines(h: &Hatch, tol: f64) -> Vec<Vec<Vec2>> {
let pat = pattern(&h.pattern).unwrap_or_else(|| pattern("ANSI31").unwrap_or(HatchPattern { name: "ANSI31", description: "", lines: vec![] }));
let loops: Vec<Vec<Vec2>> = h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
let scale = if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 };
let mut out = Vec::new();
for fam in &pat.lines {
let ang = fam.angle.to_radians() + h.angle;
let origin = h.origin + Vec2::new(fam.origin.0, fam.origin.1).rotate(h.angle) * scale;
let delta = Vec2::new(fam.delta.0, fam.delta.1).rotate(ang) * scale;
let spacing = (Vec2::new(fam.delta.0, fam.delta.1).y * scale).abs();
if spacing < 1e-9 {
continue;
}
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 {
break;
}
}
out
}
fn family(loops: &[Vec<Vec2>], origin: Vec2, ang: f64, delta: Vec2, spacing: f64, dashes: &[f64], out: &mut Vec<Vec<Vec2>>) {
// Work in a frame where the family's lines are horizontal.
let to_local = |p: Vec2| (p - origin).rotate(-ang);
let to_world = |p: Vec2| origin + p.rotate(ang);
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| to_local(*p)).collect()).collect();
let bb = Bounds2::from_points(local.iter().flatten().copied());
if bb.is_empty() {
return;
}
let shift_per_line = delta.rotate(-ang).x;
let k0 = (bb.min.y / spacing).floor() as i64;
let k1 = (bb.max.y / spacing).ceil() as i64;
if k1.saturating_sub(k0) > 20_000 {
return;
}
let pat_len: f64 = dashes.iter().map(|d| d.abs()).sum();
for k in k0..=k1 {
let y = k as f64 * spacing;
let mut xs: Vec<f64> = Vec::new();
for l in &local {
let n = l.len();
for i in 0..n {
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
if (a.y > y) != (b.y > y) {
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
}
}
}
xs.sort_by(f64::total_cmp);
let offset = shift_per_line * k as f64;
for span in xs.chunks(2) {
let [x0, x1] = span else { continue };
if dashes.is_empty() || pat_len < 1e-12 {
out.push(vec![to_world(Vec2::new(*x0, y)), to_world(Vec2::new(*x1, y))]);
continue;
}
// Dash phase anchored at x = offset.
let start_rep = ((x0 - offset) / pat_len).floor();
let mut x = offset + start_rep * pat_len;
let mut guard = 0;
'outer: while x < *x1 {
for d in dashes {
guard += 1;
if guard > 100_000 {
break 'outer;
}
let len = d.abs();
if *d > 0.0 {
let a = x.max(*x0);
let b = (x + len).min(*x1);
if b > a {
out.push(vec![to_world(Vec2::new(a, y)), to_world(Vec2::new(b, y))]);
}
} else if *d == 0.0 && x >= *x0 && x <= *x1 {
out.push(vec![to_world(Vec2::new(x, y))]);
}
x += len;
if x >= *x1 {
break 'outer;
}
}
}
}
}
}