app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
//! 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());
}
}