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
//! Pour continuity (issue 15): for each poured net and layer, how many separate pieces the fill broke into, whether the
//! net's vias join them through the other layers, and for a split pour the narrowest gap between its main piece and
//! the next one with the part (or the other net's copper) sitting in that gap, as a move the caller can make.
use crate::board::{Board, LAYERS};
use crate::fields::{Fields, Issue, Spec};
use crate::flow::is_ground;
use serde_json::{json, Value};

fn find(p: &mut [usize], mut a: usize) -> usize { while p[a] != a { p[a] = p[p[a]]; a = p[a]; } a }
fn r2(x: f64) -> f64 { (x * 100.0).round() / 100.0 }

pub fn review(f: &Fields, b: &Board, spec: &Spec, issues: &mut Vec<Issue>) -> Value {
    let (w, h, d) = (f.w, f.h, f.d); let n = w * h; let nl = LAYERS.len();
    let cell = |x: f64, y: f64| -> Option<usize> { crate::fields::cell_at(x, y, f.x0, f.y0, d, w, h).map(|(r, c)| r * w + c) };
    let at = |i: usize| [f.x0 + ((i % w) as f64 + 0.5) * d, f.y0 + ((i / w) as f64 + 0.5) * d];
    // a piece smaller than this is a sliver of fill, not a pour fragment worth a move
    let min_cells = ((0.5 / (d * d)).ceil() as usize).max(2);
    let mut nets: Vec<String> = b.polys.iter().map(|p| p.net.clone()).filter(|x| !x.is_empty()).collect(); nets.sort(); nets.dedup();
    let mut out = Vec::new();
    for net in &nets {
        let Some(id) = f.net_names.iter().position(|x| x == net) else { continue }; let id = id as u16;
        // the net's copper per layer, 4-connected pieces
        let mut lab = vec![u32::MAX; nl * n]; let mut size: Vec<usize> = Vec::new(); let mut layer_of: Vec<usize> = Vec::new();
        for k in 0..nl { for s in 0..n {
            if f.net_ids[k][s] != id || lab[k * n + s] != u32::MAX { continue; }
            let c = size.len() as u32; lab[k * n + s] = c; let mut st = vec![s]; let mut cnt = 0;
            while let Some(i) = st.pop() { cnt += 1; let (r, cc) = (i / w, i % w);
                for (ok, j) in [(cc > 0, i.wrapping_sub(1)), (cc + 1 < w, i + 1), (r > 0, i.wrapping_sub(w)), (r + 1 < h, i + w)] { if ok && f.net_ids[k][j] == id && lab[k * n + j] == u32::MAX { lab[k * n + j] = c; st.push(j); } } }
            size.push(cnt); layer_of.push(k);
        } }
        // the pieces across the layers: a via or plated hole of the net joins every layer it lands copper on (the drill
        // void is no copper, so its ring of cells is searched)
        let mut par: Vec<usize> = (0..size.len()).collect();
        for v in b.vias.iter().chain(b.plated_holes.iter()).filter(|v| v.net == *net) {
            let Some(i) = cell(v.x, v.y) else { continue }; let rad = ((v.size / 2.0 / d).ceil() as i64).max(1); let (r0, c0) = ((i / w) as i64, (i % w) as i64);
            let mut hit: Vec<usize> = Vec::new();
            for k in 0..nl { 'k: for dr in -rad..=rad { for dc in -rad..=rad { let (r, c) = (r0 + dr, c0 + dc); if r < 0 || c < 0 || r >= h as i64 || c >= w as i64 { continue; } let l = lab[k * n + r as usize * w + c as usize]; if l != u32::MAX { hit.push(l as usize); break 'k; } } } }
            for p in hit.windows(2) { let (a, z) = (find(&mut par, p[0]), find(&mut par, p[1])); if a != z { par[a] = z; } }
        }
        let important = spec.loads.contains_key(net) || is_ground(net);
        for k in 0..nl {
            // the pieces this layer's pour polygons landed in
            let mut pieces: Vec<usize> = Vec::new();
            for p in b.polys.iter().filter(|p| p.net == *net && p.layer == LAYERS[k]) {
                for &(x, y) in p.pts.iter().step_by((p.pts.len() / 16).max(1)) { let Some(i) = cell(x, y) else { continue }; let (r0, c0) = ((i / w) as i64, (i % w) as i64);
                    'v: for dr in -1..=1i64 { for dc in -1..=1i64 { let (r, c) = (r0 + dr, c0 + dc); if r < 0 || c < 0 || r >= h as i64 || c >= w as i64 { continue; } let l = lab[k * n + r as usize * w + c as usize]; if l != u32::MAX { pieces.push(l as usize); break 'v; } } } }
            }
            pieces.sort(); pieces.dedup(); pieces.retain(|&p| size[p] >= min_cells);
            if pieces.is_empty() { continue; }
            pieces.sort_by(|a, z| size[*z].cmp(&size[*a]));
            let main = pieces[0];
            let mut rec = json!({"net": net, "layer": LAYERS[k], "pieces": pieces.len(), "areasMm2": pieces.iter().map(|&p| r2(size[p] as f64 * d * d)).collect::<Vec<_>>()});
            if pieces.len() > 1 {
                // the gap from the main piece to each other piece: one breadth-first sweep from the main piece over the
                // layer, carrying where each cell was reached from
                let mut src = vec![usize::MAX; n]; let mut q = std::collections::VecDeque::new();
                for i in 0..n { if lab[k * n + i] == main as u32 { src[i] = i; q.push_back(i); } }
                while let Some(i) = q.pop_front() { let (r, c) = ((i / w) as i64, (i % w) as i64);
                    for dr in -1..=1i64 { for dc in -1..=1i64 { let (rr, cc) = (r + dr, c + dc); if rr < 0 || cc < 0 || rr >= h as i64 || cc >= w as i64 { continue; } let j = rr as usize * w + cc as usize; if src[j] == usize::MAX { src[j] = src[i]; q.push_back(j); } } } }
                let main_root = find(&mut par, main);
                let mut others = Vec::new();
                for &p in &pieces[1..] {
                    let (mut best, mut pair) = (f64::MAX, (0, 0));
                    for i in 0..n { if lab[k * n + i] == p as u32 && src[i] != usize::MAX { let (a, z) = (at(src[i]), at(i)); let g = (a[0] - z[0]).hypot(a[1] - z[1]); if g < best { best = g; pair = (src[i], i); } } }
                    let joined = find(&mut par, p) == main_root;
                    others.push((p, best, pair, joined));
                }
                others.sort_by(|a, z| a.1.partial_cmp(&z.1).unwrap());
                let isolated = others.iter().filter(|o| !o.3).count();
                rec["joinedThroughVias"] = json!(others.len() - isolated); rec["isolated"] = json!(isolated);
                let (p, gap, (ia, ib), joined) = others[0];
                let (a, z) = (at(ia), at(ib)); let m = [(a[0] + z[0]) / 2.0, (a[1] + z[1]) / 2.0];
                // what sits in the gap: another net's pad on this layer near its middle, else the other net's copper across it
                let on_layer = |pl: &[String]| pl.iter().any(|l| l == LAYERS[k] || l == "*.Cu");
                let pad = b.pads.iter().filter(|q| q.net != *net && on_layer(&q.layers)).map(|q| (q, (q.x - m[0]).hypot(q.y - m[1]))).filter(|(_, dd)| *dd <= gap / 2.0 + 1.5).min_by(|x, y| x.1.partial_cmp(&y.1).unwrap()).map(|(q, _)| q.clone());
                let mut across: std::collections::BTreeMap<u16, usize> = Default::default();
                let steps = ((gap / d).ceil() as usize).max(1) * 2;
                for s in 0..=steps { let t = s as f64 / steps as f64; if let Some(i) = cell(a[0] + (z[0] - a[0]) * t, a[1] + (z[1] - a[1]) * t) { let o = f.net_ids[k][i]; if o != id && o != u16::MAX { *across.entry(o).or_default() += 1; } } }
                let blocking_net = across.iter().max_by_key(|x| x.1).map(|(o, _)| f.net_names[*o as usize].clone());
                let gap_info = json!({"from": [r2(a[0]), r2(a[1])], "to": [r2(z[0]), r2(z[1])], "gapMm": r2(gap), "pieceAreaMm2": r2(size[p] as f64 * d * d), "joinedThroughVias": joined, "blockingPad": pad.as_ref().map(|q| format!("{}.{}", q.reference, q.pad)), "blockingNet": pad.as_ref().map(|q| q.net.clone()).or(blocking_net.clone())});
                rec["nearestGap"] = gap_info.clone();
                if important {
                    let sev = if isolated > 0 { "medium" } else { "low" };
                    let how = if joined { "joined only through the net's vias on other layers" } else { "not joined to the main piece on any layer" };
                    let (fix, action) = match &pad {
                        Some(q) => (format!("Rotate {} 90° or move it about {:.1} mm so its {} pad ({}) leaves the gap and {}'s pour on {} closes; re-pour and re-run.", q.reference, gap + 0.3, q.pad, q.net, net, LAYERS[k]),
                            json!({"type": "move", "reference": q.reference, "pad": q.pad, "orRotateDeg": 90, "byMm": r2(gap + 0.3), "net": net, "layer": LAYERS[k], "gap": gap_info, "why": "join a split pour"})),
                        None => (format!("Reroute {} out of the {:.1} mm gap at ({:.1}, {:.1}) on {} so {}'s pour closes; re-pour and re-run.", blocking_net.clone().unwrap_or_else(|| "the copper".into()), gap, m[0], m[1], LAYERS[k], net),
                            json!({"type": "reroute", "net": blocking_net, "at": [r2(m[0]), r2(m[1])], "layer": LAYERS[k], "gap": gap_info, "why": "join a split pour"})),
                    };
                    issues.push(Issue { severity: sev.into(), kind: "pour-fragmented".into(), what: format!("{net}: its pour on {} is in {} pieces ({} joined through vias, {} isolated); the nearest one ({:.1} mm², {how}) is {:.1} mm from the main piece", LAYERS[k], pieces.len(), others.len() - isolated, isolated, size[p] as f64 * d * d, gap), at: Some([r2(m[0]), r2(m[1])]), layer: Some(LAYERS[k].into()), net: Some(net.clone()), reference: pad.as_ref().map(|q| q.reference.clone()), fix, action: Some(action) });
                }
            }
            out.push(rec);
        }
    }
    json!(out)
}