main
Colby Knox adom-fields 0.10.6: grid covers the board (issue 27); one board thickness and solid-uncoupled (issue 28); decoupling co… cb236bf
//! Copper masks on a fine grid, pooled to the solve grid as copper fractions.
use crate::board::{Board, Poly, LAYERS};
use std::collections::HashMap;

pub struct Raster { pub w: usize, pub h: usize, pub res: f64, pub x0: f64, pub y0: f64, pub masks: HashMap<(String, String), Vec<u8>> }

impl Raster {
    pub fn new(b: &Board, res: f64) -> Raster { Raster::pooled(b, res, 1) }
    /// A fine grid that pools `pool` pixels into each solve cell: whole cells, rounded up so the grid covers the whole
    /// outline and a pad on the far edge has copper in it (issue 27). The 1e-6 keeps a board that is an exact number of
    /// cells from gaining a column to floating-point noise.
    pub fn pooled(b: &Board, res: f64, pool: usize) -> Raster {
        let px = |mm: f64| ((mm / (res * pool as f64) - 1e-6).ceil() as usize).max(1) * pool;
        Raster { w: px(b.width()), h: px(b.height()), res, x0: b.x0, y0: b.y0, masks: HashMap::new() }
    }
    fn mask(&mut self, net: &str, layer: &str) -> &mut Vec<u8> {
        let n = self.w * self.h;
        self.masks.entry((net.to_string(), layer.to_string())).or_insert_with(|| vec![0u8; n])
    }
    fn px(&self, x: f64, y: f64) -> (f64, f64) { ((x - self.x0) / self.res, (y - self.y0) / self.res) }
    pub fn fill_poly(&mut self, net: &str, layer: &str, pts: &[(f64, f64)]) {
        let (w, h) = (self.w, self.h);
        let p: Vec<(f64, f64)> = pts.iter().map(|(x, y)| self.px(*x, *y)).collect();
        let m = self.mask(net, layer);
        let ymin = p.iter().map(|q| q.1).fold(f64::MAX, f64::min).floor().max(0.0) as usize;
        let ymax = p.iter().map(|q| q.1).fold(f64::MIN, f64::max).ceil().min(h as f64 - 1.0) as usize;
        let mut xs: Vec<f64> = Vec::new();
        for row in ymin..=ymax {
            let yc = row as f64 + 0.5;
            xs.clear();
            for i in 0..p.len() {
                let (a, b) = (p[i], p[(i + 1) % p.len()]);
                if (a.1 <= yc && b.1 > yc) || (b.1 <= yc && a.1 > yc) { xs.push(a.0 + (yc - a.1) / (b.1 - a.1) * (b.0 - a.0)); }
            }
            xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
            for pair in xs.chunks(2) {
                if pair.len() < 2 { break; }
                let (xa, xb) = ((pair[0].round().max(0.0)) as usize, (pair[1].round().min(w as f64)) as usize);
                for col in xa..xb { m[row * w + col] = 1; }
            }
        }
    }
    pub fn disc(&mut self, net: &str, layer: &str, x: f64, y: f64, r: f64) {
        let (cx, cy) = self.px(x, y); let rp = r / self.res; let (w, h) = (self.w, self.h);
        let m = self.mask(net, layer);
        let (r0, r1) = (((cy - rp).floor().max(0.0)) as usize, ((cy + rp).ceil().min(h as f64 - 1.0)) as usize);
        let (c0, c1) = (((cx - rp).floor().max(0.0)) as usize, ((cx + rp).ceil().min(w as f64 - 1.0)) as usize);
        for row in r0..=r1 { for col in c0..=c1 { let (dx, dy) = (col as f64 + 0.5 - cx, row as f64 + 0.5 - cy); if dx * dx + dy * dy <= rp * rp { m[row * w + col] = 1; } } }
    }
    pub fn thick_line(&mut self, net: &str, layer: &str, a: (f64, f64), b: (f64, f64), wmm: f64) {
        let (ax, ay) = self.px(a.0, a.1); let (bx, by) = self.px(b.0, b.1); let r = wmm / 2.0 / self.res; let (w, h) = (self.w, self.h);
        let m = self.mask(net, layer);
        let (r0, r1) = (((ay.min(by) - r).floor().max(0.0)) as usize, ((ay.max(by) + r).ceil().min(h as f64 - 1.0)) as usize);
        let (c0, c1) = (((ax.min(bx) - r).floor().max(0.0)) as usize, ((ax.max(bx) + r).ceil().min(w as f64 - 1.0)) as usize);
        let (dx, dy) = (bx - ax, by - ay); let len2 = dx * dx + dy * dy;
        for row in r0..=r1 { for col in c0..=c1 {
            let (px, py) = (col as f64 + 0.5, row as f64 + 0.5);
            let t = if len2 > 0.0 { ((px - ax) * dx + (py - ay) * dy) / len2 } else { 0.0 }.clamp(0.0, 1.0);
            let (qx, qy) = (ax + t * dx, ay + t * dy);
            if (px - qx).powi(2) + (py - qy).powi(2) <= r * r { m[row * w + col] = 1; }
        } }
    }
    /// Draw everything on the board: filled polygons, tracks, pads, via annular rings.
    pub fn draw_board(&mut self, b: &Board) {
        let polys: Vec<Poly> = b.polys.iter().chain(b.pad_polys.iter()).cloned().collect();
        for p in &polys { self.fill_poly(&p.net, &p.layer, &p.pts); }
        for t in &b.tracks { let (net, layer, a, bb, w) = (t.net.clone(), t.layer.clone(), t.a, t.b, t.w); self.thick_line(&net, &layer, a, bb, w); }
        // a via's pad ring on the copper the board has, never on an empty slot (PR 5)
        let cu: Vec<&str> = if b.copper.is_empty() { LAYERS.to_vec() } else { b.copper.iter().map(String::as_str).collect() };
        for v in &b.vias { for &l in &cu { let (net, x, y, s) = (v.net.clone(), v.x, v.y, v.size); self.disc(&net, l, x, y, s / 2.0); } }
        self.draw_fills(b);
        self.fill_pours(b);
        // drill voids: no copper in a drilled hole on any net or layer, even where a track or pour crosses it (PR 9)
        let key = "\u{0}drills";
        for v in b.vias.iter().chain(b.plated_holes.iter()) { let (a, z) = crate::barrel::axis(v); self.thick_line(key, key, a, z, v.drill); }
        if let Some(holes) = self.masks.remove(&(key.to_string(), key.to_string())) { for m in self.masks.values_mut() { for (c, h) in m.iter_mut().zip(&holes) { if *h != 0 { *c = 0; } } } }
    }
    /// Pour islands the file carries already filled (Fusion): each outline minus its holes.
    fn draw_fills(&mut self, b: &Board) {
        let (w, h) = (self.w, self.h);
        for f in &b.fills {
            let (so, sh) = ("\u{0}fill", "\u{0}hole");
            self.masks.remove(&(so.to_string(), f.layer.clone())); self.masks.remove(&(sh.to_string(), f.layer.clone()));
            self.fill_poly(so, &f.layer, &f.outer);
            for hole in &f.holes { self.fill_poly(sh, &f.layer, hole); }
            let outer = self.masks.remove(&(so.to_string(), f.layer.clone())).unwrap_or_else(|| vec![0u8; w * h]);
            let holes = self.masks.remove(&(sh.to_string(), f.layer.clone())).unwrap_or_else(|| vec![0u8; w * h]);
            let m = self.mask(&f.net, &f.layer);
            for i in 0..w * h { if outer[i] != 0 && holes[i] == 0 { m[i] = 1; } }
        }
    }
    /// Fill the pours whose fill the file does not carry (Fusion/EAGLE polygons), the way Fusion
    /// computes it: the outline, inside the board, minus every other net's copper on that layer grown
    /// by the pour's isolate distance, keeping only the islands that touch the pour's own copper
    /// (Fusion's default: orphans off). Higher-priority pours (lower rank) are filled first and count
    /// as other-net copper for the ones after them.
    fn fill_pours(&mut self, b: &Board) {
        let (w, h) = (self.w, self.h);
        let mut pours: Vec<&crate::board::Pour> = b.pours.iter().collect();
        pours.sort_by_key(|p| p.rank);
        for p in pours {
            // the outline alone, in a scratch mask
            let scratch = "\u{0}pour";
            self.masks.remove(&(scratch.to_string(), p.layer.clone()));
            self.fill_poly(scratch, &p.layer, &p.pts);
            let Some(mut fill) = self.masks.remove(&(scratch.to_string(), p.layer.clone())) else { continue };
            // other nets' copper on this layer, grown by the isolate distance
            let grow = (p.isolate / self.res).ceil() as i64;
            let mut blocked = vec![0u8; w * h];
            for ((net, layer), m) in &self.masks {
                if layer != &p.layer || net == &p.net { continue; }
                for i in 0..w * h { if m[i] != 0 { blocked[i] = 1; } }
            }
            let blocked = dilate(&blocked, w, h, grow);
            for i in 0..w * h { if blocked[i] != 0 { fill[i] = 0; } }
            // keep the islands that touch the net's own copper
            let own = self.masks.get(&(p.net.clone(), p.layer.clone())).cloned().unwrap_or_else(|| vec![0u8; w * h]);
            let mut keep = vec![0u8; w * h];
            let mut stack: Vec<usize> = (0..w * h).filter(|i| fill[*i] != 0 && own[*i] != 0).collect();
            for i in &stack { keep[*i] = 1; }
            while let Some(i) = stack.pop() {
                let (r, c) = (i / w, i % w);
                let mut nb = Vec::with_capacity(4);
                if r > 0 { nb.push(i - w); } if r + 1 < h { nb.push(i + w); } if c > 0 { nb.push(i - 1); } if c + 1 < w { nb.push(i + 1); }
                for j in nb { if fill[j] != 0 && keep[j] == 0 { keep[j] = 1; stack.push(j); } }
            }
            let m = self.mask(&p.net, &p.layer);
            for i in 0..w * h { if keep[i] != 0 { m[i] = 1; } }
        }
    }
    /// The copper fraction of one net on one layer per solve cell (pool x pool fine pixels each).
    pub fn fraction(&self, net: &str, layer: &str, pool: usize) -> Grid {
        let (gw, gh) = (self.w / pool, self.h / pool);
        let mut g = Grid::new(gw, gh);
        if let Some(m) = self.masks.get(&(net.to_string(), layer.to_string())) {
            for r in 0..gh { for c in 0..gw {
                let mut s = 0u32;
                for i in 0..pool { for j in 0..pool { s += m[(r * pool + i) * self.w + c * pool + j] as u32; } }
                g.v[r * gw + c] = s as f32 / (pool * pool) as f32;
            } }
        }
        g
    }
    pub fn nets_on(&self, layer: &str) -> Vec<String> { let mut v: Vec<String> = self.masks.keys().filter(|(_, l)| l == layer).map(|(n, _)| n.clone()).collect(); v.sort(); v.dedup(); v }
}

#[derive(Clone, Debug)]
pub struct Grid { pub w: usize, pub h: usize, pub v: Vec<f32> }
impl Grid {
    pub fn new(w: usize, h: usize) -> Grid { Grid { w, h, v: vec![0.0; w * h] } }
    pub fn at(&self, r: usize, c: usize) -> f32 { self.v[r * self.w + c] }
    pub fn max_with(&mut self, o: &Grid) { for (a, b) in self.v.iter_mut().zip(o.v.iter()) { if *b > *a { *a = *b; } } }
    pub fn sum(&self) -> f64 { self.v.iter().map(|x| *x as f64).sum() }
    pub fn max(&self) -> f32 { self.v.iter().cloned().fold(0.0, f32::max) }
}

/// Grow a mask by r pixels (a disc), separably approximated by a square of the same radius then
/// trimmed to the disc.
fn dilate(m: &[u8], w: usize, h: usize, r: i64) -> Vec<u8> {
    if r <= 0 { return m.to_vec(); }
    let mut out = vec![0u8; w * h];
    let offs: Vec<(i64, i64)> = (-r..=r).flat_map(|dy| (-r..=r).map(move |dx| (dx, dy))).filter(|(dx, dy)| dx * dx + dy * dy <= r * r).collect();
    for i in 0..w * h {
        if m[i] == 0 { continue; }
        let (r0, c0) = ((i / w) as i64, (i % w) as i64);
        for (dx, dy) in &offs {
            let (rr, cc) = (r0 + dy, c0 + dx);
            if rr >= 0 && cc >= 0 && (rr as usize) < h && (cc as usize) < w { out[rr as usize * w + cc as usize] = 1; }
        }
    }
    out
}

#[cfg(test)] mod drill_tests {
    use super::*;
    // issue 27: whole solve cells, rounded up so they cover the outline; an exact fit gains nothing
    #[test] fn pooled_grid_covers_the_outline() {
        let g = |x1: f64, y1: f64| { let r = Raster::pooled(&Board { x1, y1, ..Board::default() }, 0.1, 2); (r.w, r.h) };
        assert_eq!(g(49.53, 20.0), (496, 200)); assert_eq!(g(49.41, 20.03), (496, 202)); assert_eq!(g(50.0, 28.0), (500, 280)); assert_eq!(g(0.01, 0.01), (2, 2));
        // the same when a rounding error leaves the width a hair over a whole cell
        assert_eq!(g(16.200000000000003, 60.1 - 40.1), (162, 200));
    }
    // PR 9: a drill is a void on every layer even where a pour crosses it; a slot is void along its own axis
    #[test] fn drill_voids() {
        let mut b = Board { x1: 4.0, y1: 4.0, layer_names: vec!["F.Cu".into(), "B.Cu".into()], copper: vec!["F.Cu".into(), "B.Cu".into()], ..Board::default() };
        for l in ["F.Cu", "B.Cu"] { b.polys.push(Poly { net: "N".into(), layer: l.into(), pts: vec![(0., 0.), (4., 0.), (4., 4.), (0., 4.)] }); }
        b.vias.push(crate::board::Via { net: "N".into(), x: 2.0, y: 2.0, size: 1.4, drill: 0.8, drill_long: None, drill_angle: 0.0 });
        let mut r = Raster::new(&b, 0.025); r.draw_board(&b);
        for l in ["F.Cu", "B.Cu"] { let g = r.fraction("N", l, 2); assert_eq!(g.at(40, 40), 0.0); assert_eq!(g.at(40, 50), 1.0); assert!((16.0 - g.sum() * 0.05 * 0.05 - std::f64::consts::PI * 0.16).abs() < 0.025); }
    }
    #[test] fn rotated_slot() {
        let mut b = Board { x1: 4.0, y1: 4.0, layer_names: vec!["F.Cu".into()], copper: vec!["F.Cu".into()], ..Board::default() };
        b.polys.push(Poly { net: "N".into(), layer: "F.Cu".into(), pts: vec![(0., 0.), (4., 0.), (4., 4.), (0., 4.)] });
        b.plated_holes.push(crate::board::Via { net: "N".into(), x: 2., y: 2., size: 2., drill: 0.4, drill_long: Some(1.4), drill_angle: 90. });
        let mut r = Raster::new(&b, 0.025); r.draw_board(&b); let g = r.fraction("N", "F.Cu", 2);
        assert_eq!(g.at(30, 40), 0.0); assert_eq!(g.at(40, 30), 1.0);
    }
}