app
AI Flow: Routing
Public Made by Adomby adom
The routing stage of AI Flow: routes a placed KiCad board connection by connection and reports what it made, what it could not and why. A guide, never a gate.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
//! The board as this router sees it: copper layers, nets, footprints and pads with their exact
//! outlines, tracks, vias, zones, the edge and every hole. Read straight from a `.kicad_pcb`.
use crate::geom::*;
use crate::sx::{self, Node};
use std::collections::{BTreeMap, HashMap};
pub type NetId = u32;
pub const NO_NET: NetId = 0;
#[derive(Clone, Debug)]
pub struct Layer {
pub name: String,
pub signal: bool,
}
#[derive(Clone, Debug)]
pub struct Pad {
pub fp: usize,
pub number: String,
pub net: NetId,
pub center: P,
pub shape: Shape,
/// Copper layers the pad exists on (indices into `Board::layers`).
pub layers: Vec<usize>,
pub drill: Option<f64>,
pub npth: bool,
/// The pad's own clearance override, else its footprint's, in nm (0 when it has none). KiCad's
/// DRC holds other copper to it even where the net class allows less.
pub local_clearance: f64,
/// The pad's own way of joining a pour (`zone_connect`: 0 none, 1 thermal relief, 2 solid, 3
/// thermal for through-hole only), its footprint's if it has none, else the zone decides.
pub zone_connect: Option<u8>,
pub size: (f64, f64),
pub angle: f64,
/// Mask opening beyond the copper, resolved the way KiCad does: the pad's own margin, else the
/// footprint's, else the board's pad_to_mask_clearance.
pub mask_margin: f64,
/// Opening in F.Mask and in B.Mask. A pad listing no mask layer has no opening at all.
pub mask: (bool, bool),
pub paste: bool,
/// The schematic's electrical type (power_in, input, ...), empty when the file has none.
pub pintype: String,
/// The schematic's pin name (VCC_5, IOVDD_1, ...), empty when the file has none.
pub pinfunction: String,
}
#[derive(Clone, Debug)]
pub struct Footprint {
pub reference: String,
pub value: String,
pub lib: String,
pub at: P,
pub bottom: bool,
pub pads: Vec<usize>,
/// Smallest centre-to-centre distance between two of its copper pads, nm.
pub min_pitch: f64,
pub dnp: bool,
}
#[derive(Clone, Debug)]
pub struct Track {
pub net: NetId,
pub layer: usize,
pub a: P,
pub b: P,
pub width: i64,
pub span: (usize, usize),
}
#[derive(Clone, Debug)]
pub struct Via {
pub net: NetId,
pub at: P,
pub size: i64,
pub drill: i64,
pub span: (usize, usize),
/// Mask over the via on the front and back (4.4: every via tented on both sides).
pub tent: (bool, bool),
}
/// A teardrop's filled copper, as KiCad wrote it. It is not an obstacle to routing but it does
/// count for solder mask, so the mask check reads it.
#[derive(Clone, Debug)]
pub struct Teardrop {
pub net: NetId,
pub layer: usize,
pub pts: Vec<V>,
}
/// Board-wide mask settings from `(setup ...)`.
#[derive(Clone, Debug)]
pub struct MaskSetup {
pub pad_to_mask: f64,
/// Smallest web of mask between two openings.
pub web: f64,
pub allow_bridges_in_footprints: bool,
pub tent: (bool, bool),
}
#[derive(Clone, Debug)]
pub struct Zone {
pub net: NetId,
pub layers: Vec<usize>,
pub outline: Vec<V>,
/// Filled copper per layer, as read from the file (empty when the zone is not filled).
pub fills: Vec<(usize, Vec<V>)>,
pub keepout_tracks: bool,
pub keepout_vias: bool,
pub priority: i64,
pub name: String,
/// `(keepout (copperpour not_allowed))`: no zone fills here.
pub keepout_pour: bool,
/// The zone's own clearance, nm (`connect_pads (clearance x)`), 0 when it sets none.
pub clearance: f64,
/// `min_thickness`, nm: copper narrower than this does not survive a refill.
pub min_thickness: f64,
/// `connect_pads no`: the fill keeps clear of pads of its own net.
pub joins_pads: bool,
/// Thermal reliefs (the default `connect_pads`): the gap round a pad and the width of the spokes
/// across it, nm. None: pads join the fill solid (`connect_pads yes`) or not at all.
pub thermal: Option<(f64, f64)>,
/// `connect_pads thru_hole_only`: reliefs for through-hole pads, solid for the rest.
pub thermal_tht_only: bool,
}
#[derive(Clone, Debug)]
pub struct Board {
pub text: String,
pub layers: Vec<Layer>,
pub nets: Vec<String>,
pub net_ids: HashMap<String, NetId>,
pub footprints: Vec<Footprint>,
pub pads: Vec<Pad>,
pub tracks: Vec<Track>,
pub vias: Vec<Via>,
pub zones: Vec<Zone>,
pub edges: Vec<(V, V)>,
pub bbox: (f64, f64, f64, f64),
/// Teardrops are stored as small zones. They are copper KiCad regenerates from the routing,
/// so they are not obstacles, and they go when the routing goes.
pub teardrop_spans: Vec<(usize, usize)>,
pub teardrops: Vec<Teardrop>,
pub mask: MaskSetup,
}
fn xy(n: &Node) -> Option<V> {
Some(V(n.num(1)? * NM, n.num(2)? * NM))
}
impl Board {
pub fn read(path: &str) -> Result<Board, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?;
Board::parse(text)
}
pub fn parse(text: String) -> Result<Board, String> {
let root = sx::parse(&text)?;
if root.head() != "kicad_pcb" {
return Err("not a kicad_pcb file".into());
}
let mut b = Board {
text: String::new(),
layers: Vec::new(),
nets: vec![String::new()],
net_ids: HashMap::new(),
footprints: Vec::new(),
pads: Vec::new(),
tracks: Vec::new(),
vias: Vec::new(),
zones: Vec::new(),
edges: Vec::new(),
bbox: (f64::MAX, f64::MAX, f64::MIN, f64::MIN),
teardrop_spans: Vec::new(),
teardrops: Vec::new(),
mask: MaskSetup { pad_to_mask: 0.0, web: 0.0, allow_bridges_in_footprints: false, tent: (true, true) },
};
if let Some(s) = root.child("setup") {
let num = |k: &str| s.child(k).and_then(|x| x.num(1)).unwrap_or(0.0) * NM;
b.mask.pad_to_mask = num("pad_to_mask_clearance");
b.mask.web = num("solder_mask_min_width");
b.mask.allow_bridges_in_footprints = s.child("allow_soldermask_bridges_in_footprints").and_then(|x| x.atom(1)) == Some("yes");
b.mask.tent = tent_of(s, (true, true));
}
b.net_ids.insert(String::new(), NO_NET);
// Copper layers in stack order: F.Cu, inner layers by number, B.Cu.
let mut inner: BTreeMap<u32, Layer> = BTreeMap::new();
let (mut top, mut bot) = (None, None);
if let Some(ls) = root.child("layers") {
for l in ls.items() {
let (Some(name), Some(kind)) = (l.atom(1), l.atom(2)) else { continue };
if !name.ends_with(".Cu") {
continue;
}
let layer = Layer { name: name.to_string(), signal: kind == "signal" || kind == "mixed" };
if name == "F.Cu" {
top = Some(layer);
} else if name == "B.Cu" {
bot = Some(layer);
} else if let Some(n) = name.strip_prefix("In").and_then(|s| s.strip_suffix(".Cu")).and_then(|s| s.parse().ok()) {
inner.insert(n, layer);
}
}
}
b.layers.extend(top);
b.layers.extend(inner.into_values());
b.layers.extend(bot);
if b.layers.len() < 2 {
return Err("board has fewer than two copper layers".into());
}
// Older files carry a net table; KiCad 10 writes the net by name on each item.
let mut table: HashMap<i64, String> = HashMap::new();
for n in root.children("net") {
if let (Some(id), Some(name)) = (n.num(1), n.atom(2)) {
table.insert(id as i64, name.to_string());
}
}
let net_of = |b: &mut Board, node: &Node| -> NetId {
let Some(n) = node.child("net") else { return NO_NET };
let name = match (n.atom(1), n.atom(2)) {
(_, Some(name)) => name.to_string(),
(Some(a), None) => {
if let Some(name) = a.parse::<i64>().ok().and_then(|id| table.get(&id)) {
name.clone()
} else {
a.to_string()
}
}
_ => String::new(),
};
b.intern(&name)
};
for n in root.items() {
match n.head() {
"footprint" | "module" => b.read_footprint(n, &net_of)?,
"segment" => {
let (Some(s), Some(e)) = (n.child("start").and_then(xy), n.child("end").and_then(xy)) else { continue };
let Some(layer) = n.child("layer").and_then(|l| l.atom(1)).and_then(|l| b.layer_index(l)) else { continue };
let width = mm(n.child("width").and_then(|w| w.num(1)).unwrap_or(0.0));
let net = net_of(&mut b, n);
b.tracks.push(Track { net, layer, a: to_p(s), b: to_p(e), width, span: n.span() });
}
"arc" => {
let (Some(s), Some(m), Some(e)) = (n.child("start").and_then(xy), n.child("mid").and_then(xy), n.child("end").and_then(xy)) else { continue };
let Some(layer) = n.child("layer").and_then(|l| l.atom(1)).and_then(|l| b.layer_index(l)) else { continue };
let width = mm(n.child("width").and_then(|w| w.num(1)).unwrap_or(0.0));
let net = net_of(&mut b, n);
let pts = arc_points(s, m, e, 50_000.0);
for w in pts.windows(2) {
b.tracks.push(Track { net, layer, a: to_p(w[0]), b: to_p(w[1]), width, span: n.span() });
}
}
"via" => {
let Some(at) = n.child("at").and_then(xy) else { continue };
let size = mm(n.child("size").and_then(|w| w.num(1)).unwrap_or(0.0));
let drill = mm(n.child("drill").and_then(|w| w.num(1)).unwrap_or(0.0));
let net = net_of(&mut b, n);
let tent = tent_of(n, b.mask.tent);
b.vias.push(Via { net, at: to_p(at), size, drill, span: n.span(), tent });
}
"zone" => b.read_zone(n, &net_of),
"gr_line" | "gr_arc" | "gr_rect" | "gr_circle" | "gr_poly" => b.read_edge(n, &|v| v),
_ => {}
}
}
for e in b.edges.clone() {
b.bbox = (b.bbox.0.min(e.0 .0).min(e.1 .0), b.bbox.1.min(e.0 .1).min(e.1 .1), b.bbox.2.max(e.0 .0).max(e.1 .0), b.bbox.3.max(e.0 .1).max(e.1 .1));
}
b.text = text;
Ok(b)
}
pub fn intern(&mut self, name: &str) -> NetId {
if let Some(&i) = self.net_ids.get(name) {
return i;
}
let i = self.nets.len() as NetId;
self.nets.push(name.to_string());
self.net_ids.insert(name.to_string(), i);
i
}
pub fn layer_index(&self, name: &str) -> Option<usize> {
self.layers.iter().position(|l| l.name == name)
}
pub fn net_name(&self, n: NetId) -> &str {
&self.nets[n as usize]
}
pub fn pad_name(&self, i: usize) -> String {
let p = &self.pads[i];
format!("{}.{}", self.footprints[p.fp].reference, p.number)
}
/// Layer indices named by a `(layers ...)` list, wildcards expanded.
fn expand_layers(&self, names: &[&str]) -> Vec<usize> {
let mut out = Vec::new();
for n in names {
match *n {
"*.Cu" => out.extend(0..self.layers.len()),
"F&B.Cu" => {
out.push(0);
out.push(self.layers.len() - 1);
}
other => out.extend(self.layer_index(other)),
}
}
out.sort_unstable();
out.dedup();
out
}
fn read_footprint(&mut self, n: &Node, net_of: &dyn Fn(&mut Board, &Node) -> NetId) -> Result<(), String> {
let at = n.child("at").ok_or("footprint without (at)")?;
let (fx, fy) = (at.num(1).unwrap_or(0.0) * NM, at.num(2).unwrap_or(0.0) * NM);
let frot = at.num(3).unwrap_or(0.0);
let bottom = n.child("layer").and_then(|l| l.atom(1)) == Some("B.Cu");
let mut reference = String::new();
let mut value = String::new();
for p in n.children("property") {
match p.atom(1) {
Some("Reference") => reference = p.atom(2).unwrap_or("").to_string(),
Some("Value") => value = p.atom(2).unwrap_or("").to_string(),
_ => {}
}
}
for t in n.children("fp_text") {
match t.atom(1) {
Some("reference") if reference.is_empty() => reference = t.atom(2).unwrap_or("").to_string(),
Some("value") if value.is_empty() => value = t.atom(2).unwrap_or("").to_string(),
_ => {}
}
}
let dnp = n.child("attr").map(|a| a.atoms().contains(&"dnp")).unwrap_or(false);
let fp_idx = self.footprints.len();
let lib = n.atom(1).unwrap_or("").to_string();
let fp_margin = n.child("solder_mask_margin").and_then(|m| m.num(1));
let mut fp = Footprint { reference, value, lib, at: (fx as i64, fy as i64), bottom, pads: Vec::new(), min_pitch: f64::INFINITY, dnp };
for p in n.children("pad") {
let kind = p.atom(2).unwrap_or("smd");
let shape_name = p.atom(3).unwrap_or("rect");
let pat = p.child("at").ok_or("pad without (at)")?;
let (lx, ly) = (pat.num(1).unwrap_or(0.0) * NM, pat.num(2).unwrap_or(0.0) * NM);
let pang = pat.num(3).unwrap_or(0.0);
let (rx, ry) = rot(lx, ly, frot);
let (cx, cy) = (fx + rx, fy + ry);
let size = p.child("size").map(|s| (s.num(1).unwrap_or(0.0) * NM, s.num(2).unwrap_or(0.0) * NM)).unwrap_or((0.0, 0.0));
let names = p.child("layers").map(|l| l.atoms().into_iter().skip(1).collect::<Vec<_>>()).unwrap_or_default();
let layers = self.expand_layers(&names);
let drill_node = p.child("drill");
let drill = drill_node.and_then(|d| d.atoms().iter().skip(1).find_map(|a| a.parse::<f64>().ok())).map(|d| d * NM);
let off = drill_node.and_then(|d| d.child("offset")).map(|o| (o.num(1).unwrap_or(0.0) * NM, o.num(2).unwrap_or(0.0) * NM)).unwrap_or((0.0, 0.0));
let (ox, oy) = rot(off.0, off.1, pang);
let center = V(cx + ox, cy + oy);
let rr = p.child("roundrect_rratio").and_then(|r| r.num(1)).unwrap_or(0.0);
let delta = p.child("rect_delta").map(|d| (d.num(1).unwrap_or(0.0) * NM, d.num(2).unwrap_or(0.0) * NM));
let shape = pad_shape(shape_name, center, size, pang, rr, delta, p, kind);
let net = net_of(self, p);
let npth = kind == "np_thru_hole";
let pi = self.pads.len();
let mask_margin = p.child("solder_mask_margin").and_then(|m| m.num(1)).or(fp_margin).map(|m| m * NM).unwrap_or(self.mask.pad_to_mask);
let has = |l: &str| names.iter().any(|n| *n == l || *n == "*.Mask" && l.ends_with(".Mask") || *n == "F&B.Mask" && l.ends_with(".Mask"));
let mask = (has("F.Mask"), has("B.Mask"));
let paste = names.iter().any(|n| n.ends_with(".Paste"));
self.pads.push(Pad { fp: fp_idx, number: p.atom(1).unwrap_or("").to_string(), net, center: to_p(V(cx, cy)), shape, layers: if npth { Vec::new() } else { layers }, drill, npth, local_clearance: p.child("clearance").or(n.child("clearance")).and_then(|c| c.num(1)).unwrap_or(0.0) * NM, zone_connect: p.child("zone_connect").or(n.child("zone_connect")).and_then(|c| c.num(1)).map(|v| v as u8), size, angle: pang, mask_margin, mask, paste, pintype: p.child("pintype").and_then(|t| t.atom(1)).unwrap_or("").to_string(), pinfunction: p.child("pinfunction").and_then(|t| t.atom(1)).unwrap_or("").to_string() });
fp.pads.push(pi);
}
for i in 0..fp.pads.len() {
for j in i + 1..fp.pads.len() {
let (a, b) = (&self.pads[fp.pads[i]], &self.pads[fp.pads[j]]);
if a.layers.is_empty() || b.layers.is_empty() {
continue;
}
let d = hypot(sub(a.center.into(), b.center.into()));
if d > 1.0 {
fp.min_pitch = fp.min_pitch.min(d);
}
}
}
self.footprints.push(fp);
for z in n.children("zone") {
self.read_zone(z, net_of);
}
let xf = |v: V| {
let (x, y) = rot(v.0, v.1, frot);
V(fx + x, fy + y)
};
for k in ["fp_line", "fp_arc", "fp_rect", "fp_circle", "fp_poly"] {
for g in n.children(k) {
self.read_edge(g, &xf);
}
}
Ok(())
}
fn read_edge(&mut self, n: &Node, xf: &dyn Fn(V) -> V) {
if n.child("layer").and_then(|l| l.atom(1)) != Some("Edge.Cuts") {
return;
}
let head = n.head();
if head.ends_with("_line") {
if let (Some(a), Some(b)) = (n.child("start").and_then(xy), n.child("end").and_then(xy)) {
self.edges.push((xf(a), xf(b)));
}
} else if head.ends_with("_arc") {
if let (Some(a), Some(m), Some(b)) = (n.child("start").and_then(xy), n.child("mid").and_then(xy), n.child("end").and_then(xy)) {
let pts = arc_points(xf(a), xf(m), xf(b), 100_000.0);
for w in pts.windows(2) {
self.edges.push((w[0], w[1]));
}
}
} else if head.ends_with("_rect") {
if let (Some(a), Some(b)) = (n.child("start").and_then(xy), n.child("end").and_then(xy)) {
let c = [a, V(b.0, a.1), b, V(a.0, b.1)];
for i in 0..4 {
self.edges.push((xf(c[i]), xf(c[(i + 1) % 4])));
}
}
} else if head.ends_with("_circle") {
if let (Some(c), Some(e)) = (n.child("center").and_then(xy), n.child("end").and_then(xy)) {
let r = hypot(sub(e, c));
let pts: Vec<V> = (0..=48).map(|i| {
let a = i as f64 / 48.0 * std::f64::consts::TAU;
xf(V(c.0 + r * a.cos(), c.1 + r * a.sin()))
}).collect();
for w in pts.windows(2) {
self.edges.push((w[0], w[1]));
}
}
} else if head.ends_with("_poly") {
if let Some(pts) = n.child("pts") {
let v: Vec<V> = pts.children("xy").filter_map(xy).map(xf).collect();
for i in 0..v.len() {
self.edges.push((v[i], v[(i + 1) % v.len()]));
}
}
}
}
fn read_zone(&mut self, n: &Node, net_of: &dyn Fn(&mut Board, &Node) -> NetId) {
if n.child("attr").and_then(|a| a.child("teardrop")).is_some() {
self.teardrop_spans.push(n.span());
let net = net_of(self, n);
for f in n.children("filled_polygon") {
let Some(layer) = f.child("layer").and_then(|l| l.atom(1)).and_then(|l| self.layer_index(l)) else { continue };
if let Some(p) = f.child("pts") {
self.teardrops.push(Teardrop { net, layer, pts: pts_of(p) });
}
}
return;
}
let net = net_of(self, n);
let mut names: Vec<&str> = Vec::new();
if let Some(l) = n.child("layer") {
names.extend(l.atoms().into_iter().skip(1));
}
if let Some(l) = n.child("layers") {
names.extend(l.atoms().into_iter().skip(1));
}
let layers = self.expand_layers(&names);
let outline: Vec<V> = n.child("polygon").and_then(|p| p.child("pts")).map(pts_of).unwrap_or_default();
let mut fills = Vec::new();
for f in n.children("filled_polygon") {
let Some(l) = f.child("layer").and_then(|l| l.atom(1)).and_then(|l| self.layer_index(l)) else { continue };
if let Some(p) = f.child("pts") {
fills.push((l, pts_of(p)));
}
}
let ko = n.child("keepout");
let na = |k: &str| ko.and_then(|o| o.child(k)).and_then(|c| c.atom(1)) == Some("not_allowed");
let priority = n.child("priority").and_then(|p| p.num(1)).unwrap_or(0.0) as i64;
let name = n.child("name").and_then(|p| p.atom(1)).unwrap_or("").to_string();
let pads = n.child("connect_pads");
let clearance = pads.and_then(|c| c.child("clearance")).and_then(|c| c.num(1)).unwrap_or(0.0) * NM;
let min_thickness = n.child("min_thickness").and_then(|c| c.num(1)).unwrap_or(0.25) * NM;
let joins_pads = pads.and_then(|c| c.atom(1)) != Some("no");
let mode = pads.and_then(|c| c.atom(1));
let fill = n.child("fill");
let relief = (fill.and_then(|f| f.child("thermal_gap")).and_then(|c| c.num(1)).unwrap_or(0.5) * NM, fill.and_then(|f| f.child("thermal_bridge_width")).and_then(|c| c.num(1)).unwrap_or(0.5) * NM);
let thermal = if mode.is_none() || mode == Some("thru_hole_only") { Some(relief) } else { None };
let thermal_tht_only = mode == Some("thru_hole_only");
self.zones.push(Zone { net, layers, outline, fills, keepout_tracks: na("tracks"), keepout_vias: na("vias"), priority, name, keepout_pour: na("copperpour"), clearance, min_thickness, joins_pads, thermal, thermal_tht_only });
}
/// Whether a point lies inside the board outline (holes in the outline count as outside).
pub fn inside(&self, p: V) -> bool {
let mut inside = false;
for (a, b) in &self.edges {
if (a.1 > p.1) != (b.1 > p.1) && p.0 < (b.0 - a.0) * (p.1 - a.1) / (b.1 - a.1) + a.0 {
inside = !inside;
}
}
inside
}
/// Inner layers given over to a reference plane: a zone of one net covers most of the board.
/// KiCad's layer type doesn't tell: his benchmark C and benchmark B mark GND planes "signal". Routing
/// through a plane cuts the return path (2.2, 4.2), so these never take a track.
pub fn plane_layers(&self) -> Vec<usize> {
let last = self.layers.len() - 1;
let board_area = (self.bbox.2 - self.bbox.0).max(0.0) * (self.bbox.3 - self.bbox.1).max(0.0);
let mut out = Vec::new();
for l in 1..last {
// Added up per net: benchmark B's GND plane is two zones, 48% and 13% of the board.
let mut per_net: HashMap<NetId, f64> = HashMap::new();
for z in self.zones.iter().filter(|z| z.net != NO_NET && !z.keepout_tracks && z.layers.contains(&l)) {
let n = z.outline.len();
let mut a = 0.0;
for i in 0..n {
let (p, q) = (z.outline[i], z.outline[(i + 1) % n]);
a += p.0 * q.1 - q.0 * p.1;
}
*per_net.entry(z.net).or_insert(0.0) += (a / 2.0).abs();
}
let covered = per_net.values().copied().fold(0.0, f64::max);
if board_area > 0.0 && covered >= 0.5 * board_area {
out.push(l);
}
}
out
}
/// Nets with a copper plane on an inner layer: zone net to its plane layers.
pub fn plane_nets(&self) -> HashMap<NetId, Vec<usize>> {
let mut out: HashMap<NetId, Vec<usize>> = HashMap::new();
let last = self.layers.len() - 1;
for z in &self.zones {
if z.net == NO_NET || z.keepout_tracks {
continue;
}
for &l in &z.layers {
if l != 0 && l != last {
out.entry(z.net).or_default().push(l);
}
}
}
out
}
}
fn pts_of(p: &Node) -> Vec<V> {
let mut out = Vec::new();
for c in p.items() {
match c.head() {
"xy" => out.extend(xy(c)),
"arc" => {
if let (Some(a), Some(m), Some(b)) = (c.child("start").and_then(xy), c.child("mid").and_then(xy), c.child("end").and_then(xy)) {
out.extend(arc_points(a, m, b, 100_000.0));
}
}
_ => {}
}
}
out
}
/// A `(tenting ...)` block on a via or in the setup, or the default when it has none.
fn tent_of(n: &Node, default: (bool, bool)) -> (bool, bool) {
let Some(t) = n.child("tenting") else { return default };
let side = |k: &str, d: bool| match t.child(k).and_then(|x| x.atom(1)) {
Some("yes") => true,
Some("no") => false,
_ => d,
};
(side("front", default.0), side("back", default.1))
}
pub fn to_p(v: V) -> P {
(v.0.round() as i64, v.1.round() as i64)
}
fn rect_pts(center: V, w: f64, h: f64, ang: f64, rr: f64) -> Vec<V> {
let (hw, hh) = (w / 2.0, h / 2.0);
let mut local: Vec<(f64, f64)> = Vec::new();
if rr <= 0.0 {
local.extend([(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)]);
} else {
// Corner arcs with enough facets that the polygon stays within a micron of the true outline.
let n = 12;
for (cx, cy, a0) in [(hw - rr, -hh + rr, -90.0f64), (hw - rr, hh - rr, 0.0), (-hw + rr, hh - rr, 90.0), (-hw + rr, -hh + rr, 180.0)] {
for i in 0..=n {
let a = (a0 + 90.0 * i as f64 / n as f64).to_radians();
local.push((cx + rr * a.cos(), cy + rr * a.sin()));
}
}
}
local
.into_iter()
.map(|(x, y)| {
let (rx, ry) = rot(x, y, ang);
V(center.0 + rx, center.1 + ry)
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn pad_shape(name: &str, center: V, size: (f64, f64), ang: f64, rr: f64, delta: Option<(f64, f64)>, p: &Node, kind: &str) -> Shape {
let (w, h) = size;
match name {
"circle" => Shape::Circle { c: center, r: w / 2.0 },
"oval" => {
if (w - h).abs() < 1.0 {
Shape::Circle { c: center, r: w / 2.0 }
} else if w > h {
let d = (w - h) / 2.0;
let (x, y) = rot(d, 0.0, ang);
Shape::Capsule { a: V(center.0 - x, center.1 - y), b: V(center.0 + x, center.1 + y), r: h / 2.0 }
} else {
let d = (h - w) / 2.0;
let (x, y) = rot(0.0, d, ang);
Shape::Capsule { a: V(center.0 - x, center.1 - y), b: V(center.0 + x, center.1 + y), r: w / 2.0 }
}
}
"roundrect" => Shape::Poly { pts: rect_pts(center, w, h, ang, rr * w.min(h)), r: 0.0 },
"trapezoid" => {
let (dx, dy) = delta.unwrap_or((0.0, 0.0));
let (hw, hh) = (w / 2.0, h / 2.0);
let local = [(-hw - dy / 2.0, -hh + dx / 2.0), (hw + dy / 2.0, -hh - dx / 2.0), (hw - dy / 2.0, hh + dx / 2.0), (-hw + dy / 2.0, hh - dx / 2.0)];
let pts = local.iter().map(|&(x, y)| {
let (rx, ry) = rot(x, y, ang);
V(center.0 + rx, center.1 + ry)
}).collect();
Shape::Poly { pts, r: 0.0 }
}
"custom" => {
// A custom pad is its anchor plus every primitive, all one piece of copper. KiCad's DRC
// and connectivity see the union, so all of it is kept, not only the largest piece.
let anchor_circle = p.child("options").and_then(|o| o.child("anchor")).and_then(|a| a.atom(1)) == Some("circle");
let mut parts = vec![if anchor_circle { Shape::Circle { c: center, r: w / 2.0 } } else { Shape::Poly { pts: rect_pts(center, w, h, ang, 0.0), r: 0.0 } }];
let xf = |v: V| {
let (rx, ry) = rot(v.0, v.1, ang);
V(center.0 + rx, center.1 + ry)
};
if let Some(prims) = p.child("primitives") {
for g in prims.items() {
let half = g.child("width").and_then(|w| w.num(1)).unwrap_or(0.0) * NM / 2.0;
let filled = g.child("fill").and_then(|f| f.atom(1)).map(|f| f != "no" && f != "none").unwrap_or(true);
match g.head() {
"gr_poly" => {
if let Some(pp) = g.child("pts") {
parts.push(Shape::Poly { pts: pts_of(pp).into_iter().map(xf).collect(), r: half });
}
}
"gr_line" => {
if let (Some(a), Some(e)) = (g.child("start").and_then(xy), g.child("end").and_then(xy)) {
parts.push(Shape::Capsule { a: xf(a), b: xf(e), r: half });
}
}
"gr_arc" => {
if let (Some(a), Some(m), Some(e)) = (g.child("start").and_then(xy), g.child("mid").and_then(xy), g.child("end").and_then(xy)) {
for s in arc_points(a, m, e, 50_000.0).windows(2) {
parts.push(Shape::Capsule { a: xf(s[0]), b: xf(s[1]), r: half });
}
}
}
"gr_rect" => {
if let (Some(a), Some(e)) = (g.child("start").and_then(xy), g.child("end").and_then(xy)) {
let pts: Vec<V> = [a, V(e.0, a.1), e, V(a.0, e.1)].into_iter().map(xf).collect();
parts.push(Shape::Poly { pts, r: half });
}
}
"gr_circle" => {
if let (Some(c), Some(e)) = (g.child("center").and_then(xy), g.child("end").and_then(xy)) {
let r = hypot(sub(e, c));
if filled {
parts.push(Shape::Circle { c: xf(c), r: r + half });
} else {
let ring: Vec<V> = (0..=48).map(|i| {
let a = i as f64 / 48.0 * std::f64::consts::TAU;
V(c.0 + r * a.cos(), c.1 + r * a.sin())
}).collect();
for s in ring.windows(2) {
parts.push(Shape::Capsule { a: xf(s[0]), b: xf(s[1]), r: half });
}
}
}
}
_ => {}
}
}
}
if parts.len() == 1 { parts.pop().unwrap() } else { Shape::Multi(parts) }
}
_ => {
let _ = kind;
Shape::Poly { pts: rect_pts(center, w, h, ang, 0.0), r: 0.0 }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A 100 x 100 board with rectangles on In1.Cu: (net, width, height) of each zone.
fn with_zones(zones: &[(&str, f64, f64)]) -> Board {
let mut t = String::from(r#"(kicad_pcb (layers (0 "F.Cu" signal) (4 "In1.Cu" signal) (6 "In2.Cu" signal) (2 "B.Cu" signal)) (gr_rect (start 0 0) (end 100 100) (layer "Edge.Cuts"))"#);
let mut y0 = 0.0;
for (net, w, h) in zones {
t.push_str(&format!(r#"(zone (net "{net}") (layer "In1.Cu") (polygon (pts (xy 0 {y0}) (xy {w} {y0}) (xy {w} {}) (xy 0 {}))))"#, y0 + h, y0 + h));
y0 += h;
}
t.push(')');
Board::parse(t).unwrap()
}
#[test]
fn a_plane_split_into_two_zones_counts() {
// Benchmark B's shape: neither zone is half the board, together they are 61%.
assert_eq!(with_zones(&[("GND", 100.0, 48.0), ("GND", 100.0, 13.0)]).plane_layers(), vec![1]);
assert_eq!(with_zones(&[("GND", 100.0, 60.0)]).plane_layers(), vec![1]);
}
#[test]
fn zones_of_two_nets_do_not_add_up_to_a_plane() {
assert!(with_zones(&[("GND", 100.0, 30.0), ("+3V3", 100.0, 30.0)]).plane_layers().is_empty());
assert!(with_zones(&[("GND", 100.0, 40.0)]).plane_layers().is_empty());
}
}