use i_overlay::core::fill_rule::FillRule;
use i_overlay::float::clip::FloatClip;
use i_overlay::string::clip::ClipRule;
use crate::Polygon;
use super::convert::{line_to_i, polygon_to_f};
#[derive(Debug, Clone, Copy)]
pub struct HatchOpts {
pub angle_deg: f64,
pub spacing: f64,
pub phase: f64,
pub origin: (f64, f64),
}
impl Default for HatchOpts {
fn default() -> Self {
Self {
angle_deg: 0.0,
spacing: 8.0,
phase: 0.0,
origin: (0.0, 0.0),
}
}
}
pub fn hatch_polygons(polys: &[Polygon], opts: &HatchOpts) -> Vec<Vec<(i32, i32)>> {
if polys.is_empty() || !opts.spacing.is_finite() || opts.spacing <= 0.0 {
return Vec::new();
}
let theta = opts.angle_deg.to_radians();
let (sin, cos) = theta.sin_cos();
let (ox, oy) = opts.origin;
let v_origin = -ox * sin + oy * cos;
let mut out = Vec::new();
for poly in polys {
if poly.exterior.is_empty() {
continue;
}
let mut v_min = f64::INFINITY;
let mut v_max = f64::NEG_INFINITY;
let mut u_min = f64::INFINITY;
let mut u_max = f64::NEG_INFINITY;
for &(x, y) in &poly.exterior {
let xf = x as f64;
let yf = y as f64;
let u = xf * cos + yf * sin;
let v = -xf * sin + yf * cos;
v_min = v_min.min(v);
v_max = v_max.max(v);
u_min = u_min.min(u);
u_max = u_max.max(u);
}
if !v_min.is_finite() || v_max <= v_min {
continue;
}
let v0 = ((v_min + v_origin) / opts.spacing).floor() * opts.spacing - v_origin
+ opts.phase * opts.spacing;
let mut lines: Vec<Vec<[f64; 2]>> = Vec::new();
let mut v = v0;
if v > v_min {
v -= opts.spacing;
}
while v <= v_max + opts.spacing {
let a_world = [u_min * cos - v * sin, u_min * sin + v * cos];
let b_world = [u_max * cos - v * sin, u_max * sin + v * cos];
lines.push(vec![a_world, b_world]);
v += opts.spacing;
}
if lines.is_empty() {
continue;
}
let shape = polygon_to_f(poly);
let clip_rule = ClipRule {
invert: false,
boundary_included: false,
};
let clipped = lines.clip_by(&shape, FillRule::EvenOdd, clip_rule);
for path in &clipped {
if path.len() >= 2 {
out.push(line_to_i(path));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn horizontal_hatch_fills_square() {
let p = Polygon {
exterior: vec![(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)],
holes: vec![],
};
let out = hatch_polygons(
&[p],
&HatchOpts {
angle_deg: 0.0,
spacing: 10.0,
phase: 0.0,
origin: (0.0, 0.0),
},
);
assert!(out.len() >= 8 && out.len() <= 12, "got {} lines", out.len());
for line in &out {
assert_eq!(line.len(), 2);
}
}
#[test]
fn zero_spacing_is_no_op() {
let p = Polygon {
exterior: vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
holes: vec![],
};
let out = hatch_polygons(
&[p],
&HatchOpts {
angle_deg: 0.0,
spacing: 0.0,
phase: 0.0,
origin: (0.0, 0.0),
},
);
assert!(out.is_empty());
}
}