main
John Lauer 0.1.37: every open gap from the buck molecule run closed in the binary: intake, sourcing check, evidence, netlist check… d49513e
//! The layer policy for 2-layer power boards (spec "planeUnder"): a region of one copper layer that
//! belongs to one net's plane, typically B.Cu under the hot loop kept as unbroken ground. The router
//! treats every region as a keepout for every other net on that layer (and for other nets' vias,
//! which pierce both layers); `issues` finds other-net copper already inside a region, for the gate.
use aiflow_board::{geom, Board, Pt, B_CU, F_CU, LAYER_NAMES};

use crate::rules::PlaneUnder;

/// A resolved region: layer index, the net that owns it, the polygon, and a label for messages.
#[derive(Clone, Debug)]
pub struct Region {
    pub layer: usize,
    pub net: String,
    pub poly: Vec<Pt>,
    pub label: String,
}

fn pad_box(board: &Board, keys: &[String]) -> Result<(f64, f64, f64, f64), String> {
    let mut pts = Vec::new();
    for k in keys {
        let found: Vec<Pt> = match k.split_once('.') {
            Some((r, n)) if board.pad_by_key(k).is_some() => board.pads.iter().filter(|p| p.reference == r && p.name == n).flat_map(|p| p.polys.iter().flatten().copied()).collect(),
            _ => board.pads.iter().filter(|p| p.reference == *k).flat_map(|p| p.polys.iter().flatten().copied()).collect(),
        };
        if found.is_empty() {
            return Err(format!("{k} is not a part or pad on the board"));
        }
        pts.extend(found);
    }
    Ok(geom::bbox(&pts))
}

/// Every planeUnder entry as a polygon on its layer. An entry naming an unknown layer, part or
/// shape is an error, so a typo never silently disables the policy.
pub fn regions(board: &Board, entries: &[PlaneUnder]) -> Result<Vec<Region>, String> {
    let mut out = Vec::new();
    for (k, e) in entries.iter().enumerate() {
        let layer = match e.layer.as_str() { "F.Cu" => F_CU, "B.Cu" => B_CU, other => return Err(format!("planeUnder[{k}]: layer {other:?} is not F.Cu or B.Cu")) };
        if e.net.is_empty() {
            return Err(format!("planeUnder[{k}]: needs the net that owns the region (\"net\": \"GND\")"));
        }
        let poly: Vec<Pt> = if e.polygon.len() >= 3 {
            e.polygon.clone()
        } else if !e.around.is_empty() {
            let (x0, y0, x1, y1) = pad_box(board, &e.around).map_err(|m| format!("planeUnder[{k}]: {m}"))?;
            let m = e.margin.unwrap_or(1.0);
            let (bx0, by0, bx1, by1) = board.bbox;
            let r4 = |v: f64| (v * 10000.0).round() / 10000.0;
            let (a, b, c, d) = (r4((x0 - m).max(bx0)), r4((y0 - m).max(by0)), r4((x1 + m).min(bx1)), r4((y1 + m).min(by1)));
            vec![(a, b), (c, b), (c, d), (a, d)]
        } else {
            return Err(format!("planeUnder[{k}]: needs \"around\" (parts or pads) or a \"polygon\" of 3 or more points"));
        };
        let label = e.name.clone().unwrap_or_else(|| if e.around.is_empty() { format!("{} {} polygon", e.net, e.layer) } else { format!("{} {} under {}", e.net, e.layer, e.around.join(",")) });
        out.push(Region { layer, net: e.net.clone(), poly, label });
    }
    Ok(out)
}

/// How much of a copper segment of half width hw touches the region (0 when it stays out), sampled
/// every 0.05 mm; a touch that is shorter than one sample still counts as one sample.
fn segment_inside(poly: &[Pt], a: Pt, b: Pt, hw: f64) -> Option<f64> {
    let l = (b.0 - a.0).hypot(b.1 - a.1);
    let n = ((l / 0.05).ceil() as usize).max(1);
    let hits = (0..=n).filter(|&k| {
        let t = k as f64 / n as f64;
        geom::point_poly_distance(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t, poly) < hw
    }).count();
    if hits == 0 { None } else { Some((l * hits as f64 / (n + 1) as f64).max(0.05)) }
}

fn segment_touches(poly: &[Pt], a: Pt, b: Pt, hw: f64) -> bool {
    segment_inside(poly, a, b, hw).is_some()
}

/// Other-net copper inside a region: tracks and zone outlines on its layer, and vias. One line each,
/// with the length or position, for the gate's report.
pub fn issues(board: &Board, regions: &[Region]) -> Vec<String> {
    let mut out = Vec::new();
    if regions.is_empty() {
        return out;
    }
    let tracks = board.tracks();
    let vias = board.vias();
    let zones = board.zones();
    for r in regions {
        let mut by_net: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
        for t in tracks.iter().filter(|t| t.layer == r.layer && t.net != r.net) {
            if let Some(mm) = segment_inside(&r.poly, t.a, t.b, t.width / 2.0) {
                *by_net.entry(t.net.clone()).or_insert(0.0) += mm;
            }
        }
        for (net, mm) in by_net {
            out.push(format!("{}: {net} track on {} inside the region (about {mm:.1} mm)", r.label, LAYER_NAMES[r.layer]));
        }
        for v in vias.iter().filter(|v| v.net != r.net) {
            if geom::point_poly_distance(v.x, v.y, &r.poly) < v.size / 2.0 {
                out.push(format!("{}: {} via at ({:.2}, {:.2}) inside the region", r.label, v.net, v.x, v.y));
            }
        }
        for z in zones.iter().filter(|z| z.net != r.net && z.layers.contains(&r.layer)) {
            let islands: Vec<&Vec<Pt>> = z.fills.iter().filter(|(l, _)| *l == r.layer).map(|(_, p)| p).collect();
            let shapes: Vec<&Vec<Pt>> = if islands.is_empty() { vec![&z.outline] } else { islands };
            let hit = shapes.iter().any(|s| s.len() >= 3 && ((0..s.len()).any(|k| segment_touches(&r.poly, s[k], s[(k + 1) % s.len()], 1e-6)) ||r.poly.iter().any(|&(x, y)| geom::point_in_poly(x, y, s))));
            if hit {
                out.push(format!("{}: {} zone on {} reaches into the region", r.label, if z.net.is_empty() { "an unassigned" } else { z.net.as_str() }, LAYER_NAMES[r.layer]));
            }
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board_copper::tests::{board, seg};
    use crate::{Router, Rules};

    fn entry(json: serde_json::Value) -> Vec<PlaneUnder> {
        crate::rules::PlaneUnder::from_spec(&serde_json::json!({ "planeUnder": json }))
    }

    #[test]
    fn around_resolves_to_the_pad_box_grown_by_the_margin() {
        let b = board(&[("U1", "GND", 10.0, 5.0, false), ("J1", "A", 2.0, 5.0, true)], "");
        let r = regions(&b, &entry(serde_json::json!([{"layer": "B.Cu", "net": "GND", "around": ["U1"], "margin": 1.0}]))).unwrap();
        assert_eq!(r.len(), 1);
        assert_eq!(r[0].layer, B_CU);
        let (x0, y0, x1, y1) = geom::bbox(&r[0].poly);
        assert!((x0 - 8.5).abs() < 1e-6 && (x1 - 11.5).abs() < 1e-6 && (y0 - 3.5).abs() < 1e-6 && (y1 - 6.5).abs() < 1e-6);
        assert!(regions(&b, &entry(serde_json::json!([{"layer": "B.Cu", "net": "GND", "around": ["U9"]}]))).is_err());
        assert!(regions(&b, &entry(serde_json::json!([{"layer": "In1.Cu", "net": "GND", "around": ["U1"]}]))).is_err());
    }

    #[test]
    fn issues_name_other_net_copper_inside_a_region() {
        let via = "\t(via\n\t\t(at 10 6)\n\t\t(size 0.6)\n\t\t(drill 0.3)\n\t\t(layers \"F.Cu\" \"B.Cu\")\n\t\t(net \"A\")\n\t)\n";
        let extra = seg("A", "B.Cu", (2.0, 5.0), (18.0, 5.0)) + &seg("GND", "B.Cu", (9.0, 5.0), (11.0, 5.0)) + &seg("A", "F.Cu", (2.0, 5.0), (18.0, 5.0)) + via;
        let b = board(&[("J1", "A", 2.0, 5.0, true), ("J2", "A", 18.0, 5.0, true), ("J3", "GND", 10.0, 1.0, true), ("J4", "GND", 10.0, 9.0, true)], &extra);
        let r = regions(&b, &entry(serde_json::json!({"layer": "B.Cu", "net": "GND", "polygon": [[8, 3], [12, 3], [12, 7], [8, 7]]}))).unwrap();
        let found = issues(&b, &r);
        assert_eq!(found.len(), 2, "{found:?}");
        assert!(found[0].contains("A track on B.Cu") && found[0].contains("about 4.2 mm"), "{found:?}");
        assert!(found[1].contains("A via at (10.00, 6.00)"), "{found:?}");
    }

    #[test]
    fn the_router_keeps_other_nets_out_of_a_region() {
        // A runs left to right; a GND region fills the middle of F.Cu top to bottom, so A must dive to B.Cu
        let pads = [("J1", "A", 2.0, 5.0, true), ("J2", "A", 18.0, 5.0, true), ("J3", "GND", 10.0, 1.0, true), ("J4", "GND", 10.0, 9.0, true)];
        let mut rules = Rules::default();
        rules.plane_under = entry(serde_json::json!([{"layer": "F.Cu", "net": "GND", "polygon": [[8, 0], [12, 0], [12, 10], [8, 10]]}]));
        let mut r = Router::new(board(&pads, ""), rules, false);
        let (_, _, failed) = crate::drive::route_board(&mut r, 2, 4, 50);
        assert_eq!(failed, 0);
        let a = r.net_ids["A"];
        let g = r.grid.clone();
        let mut on_b = false;
        for route in &r.nets[a].routes {
            for c in &route.cells {
                let (x, _) = g.to_xy(c.i as i64, c.j as i64);
                if c.layer() == F_CU {
                    assert!(!(8.0..=12.0).contains(&x), "A on F.Cu inside the GND region at x {x}");
                } else if (8.0..=12.0).contains(&x) {
                    on_b = true;
                }
            }
        }
        assert!(on_b, "A should cross the region on B.Cu");
        // GND itself may use its own region
        assert!(r.nets[r.net_ids["GND"]].failed.is_empty());
    }
}