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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
//! What a pour becomes when KiCad refills it. The router and the rest of the checker read a zone as
//! the shape saved in the file, one piece of copper; a refill cuts it around every track of another
//! net, and an island with something on it stays as a separate piece (benchmark B: KiCad's DRC
//! reported /USB2_VBUS unconnected to itself while every join here read closed).
//!
//! The model: per net and layer, rasterise the zones and close each cell within clearance plus half
//! `min_thickness` of foreign copper. What stays open is where the centre of a `min_thickness` brush
//! can reach, so a neck too thin to survive the fill's own erosion reads as cut. Copper of the net
//! joins whatever open cells lie within reach, and islands nothing touches are dropped, as KiCad's
//! island removal drops them. KiCad's DRC stays the judge; this exists to see splits while routing.
use crate::board::*;
use crate::conn::{touches, Piece, Uf};
use crate::geom::*;
use crate::index::Owner;
use crate::rules::Rules;
use crate::search::World;
use std::collections::{BTreeMap, BTreeSet};
/// Grid pitch. Half a cell of slack on the clearance, so a lane exactly one brush wide stays open.
const STEP: f64 = 0.05 * NM;
const SLACK: f64 = STEP * 0.75;
pub struct Split {
pub net: NetId,
pub layer: usize,
pub at: V,
/// Pieces beyond what plain connectivity already counts as open.
pub extra: usize,
/// Nets whose copper, gone, would rejoin the pieces.
pub cutters: Vec<NetId>,
}
struct Raster {
layer: usize,
x0: f64,
y0: f64,
w: usize,
h: usize,
inside: Vec<bool>,
blocked: Vec<u16>,
/// The cells each foreign net closes, once per obstacle.
by_net: BTreeMap<NetId, Vec<u32>>,
/// Item index and the cells it reaches.
reach: Vec<(usize, Vec<u32>)>,
}
impl Raster {
fn at(&self, i: usize) -> V {
V(self.x0 + (i % self.w) as f64 * STEP, self.y0 + (i / self.w) as f64 * STEP)
}
/// The cell nearest a point, if the point lies on the raster.
fn index(&self, p: V) -> Option<usize> {
let (cx, cy) = (((p.0 - self.x0) / STEP).round(), ((p.1 - self.y0) / STEP).round());
(cx >= 0.0 && cy >= 0.0 && (cx as usize) < self.w && (cy as usize) < self.h).then(|| cy as usize * self.w + cx as usize)
}
fn fill_poly(&mut self, pts: &[V]) {
let n = pts.len();
for row in 0..self.h {
let y = self.y0 + row as f64 * STEP;
let mut xs: Vec<f64> = Vec::new();
for k in 0..n {
let (a, b) = (pts[k], pts[(k + 1) % n]);
if (a.1 <= y) != (b.1 <= y) {
xs.push(a.0 + (y - a.1) / (b.1 - a.1) * (b.0 - a.0));
}
}
xs.sort_by(|p, q| p.total_cmp(q));
for c in xs.chunks(2) {
if c.len() < 2 {
continue;
}
let first = ((c[0] - self.x0) / STEP).ceil().max(0.0) as usize;
let last = ((c[1] - self.x0) / STEP).floor().min(self.w as f64 - 1.0);
if last < 0.0 {
continue;
}
for col in first..=last as usize {
self.inside[row * self.w + col] = true;
}
}
}
}
/// Cells of the bounding box `bb` grown by `need`, in order.
fn cells(&self, bb: (f64, f64, f64, f64), need: f64) -> Vec<usize> {
let span = |lo: f64, hi: f64, o: f64, n: usize| -> Option<(usize, usize)> {
let a = ((lo - need - o) / STEP).floor().max(0.0) as usize;
let b = ((hi + need - o) / STEP).ceil();
if b < 0.0 || a >= n {
return None;
}
Some((a, (b as usize).min(n - 1)))
};
let (Some((c0, c1)), Some((r0, r1))) = (span(bb.0, bb.2, self.x0, self.w), span(bb.1, bb.3, self.y0, self.h)) else { return Vec::new() };
let mut out = Vec::with_capacity((c1 - c0 + 1) * (r1 - r0 + 1));
for r in r0..=r1 {
for c in c0..=c1 {
out.push(r * self.w + c);
}
}
out
}
fn close(&mut self, shape: &Shape, need: f64, net: NetId) {
let mut hit = Vec::new();
for i in self.cells(shape.bbox(), need) {
if self.inside[i] && shape.dist_pt(self.at(i)) < need {
hit.push(i as u32);
}
}
for &i in &hit {
self.blocked[i as usize] = self.blocked[i as usize].saturating_add(1);
}
self.by_net.entry(net).or_default().extend(hit);
}
/// Labels the open cells, ignoring what `without` closes. 0: closed.
fn label(&self, without: Option<NetId>) -> (Vec<u32>, u32) {
let mut blocked = self.blocked.clone();
if let Some(cells) = without.and_then(|n| self.by_net.get(&n)) {
for &i in cells {
blocked[i as usize] -= 1;
}
}
let mut lab = vec![0u32; self.inside.len()];
let mut next = 0;
let mut stack = Vec::new();
for s in 0..lab.len() {
if lab[s] != 0 || !self.inside[s] || blocked[s] != 0 {
continue;
}
next += 1;
lab[s] = next;
stack.push(s);
while let Some(i) = stack.pop() {
let (c, r) = (i % self.w, i / self.w);
let mut go = |j: usize, stack: &mut Vec<usize>| {
if lab[j] == 0 && self.inside[j] && blocked[j] == 0 {
lab[j] = next;
stack.push(j);
}
};
if c > 0 { go(i - 1, &mut stack); }
if c + 1 < self.w { go(i + 1, &mut stack); }
if r > 0 { go(i - self.w, &mut stack); }
if r + 1 < self.h { go(i + self.w, &mut stack); }
}
}
(lab, next)
}
}
/// How many pieces the net is in after a refill: anything holding a pad or a kept island.
/// Returns the count and, for the most pieces' smallest kept island, where it lies.
fn pieces(rasters: &[Raster], base: &Uf, is_pad: &[bool], without: Option<NetId>) -> (usize, Option<(usize, V)>) {
let n = is_pad.len();
let labelled: Vec<(Vec<u32>, u32)> = rasters.iter().map(|r| r.label(without)).collect();
let total: u32 = labelled.iter().map(|l| l.1).sum();
// Items first, then every island of every layer.
let mut uf = Uf::new(n + total as usize);
for i in 0..n {
let root = base.clone().find(i as u32);
uf.union(i as u32, root);
}
let mut kept: BTreeSet<u32> = BTreeSet::new();
let mut offset = n as u32;
let mut where_is: Vec<(usize, usize)> = Vec::new();
for (ri, (r, (lab, count))) in rasters.iter().zip(&labelled).enumerate() {
for (item, cells) in &r.reach {
for &c in cells {
let l = lab[c as usize];
if l != 0 {
let id = offset + l - 1;
uf.union(*item as u32, id);
kept.insert(id);
}
}
}
for _ in 0..*count {
where_is.push((ri, 0));
}
offset += count;
}
let mut roots: BTreeSet<u32> = BTreeSet::new();
for (i, pad) in is_pad.iter().enumerate() {
if *pad {
roots.insert(uf.find(i as u32));
}
}
for &id in &kept {
roots.insert(uf.find(id));
}
// A cell of the smallest kept island, for the report.
let mut smallest: Option<(usize, usize, usize)> = None;
let mut offset = n as u32;
for (ri, (lab, count)) in labelled.iter().enumerate() {
let mut sizes = vec![0usize; *count as usize];
let mut first = vec![0usize; *count as usize];
for (i, &l) in lab.iter().enumerate() {
if l != 0 {
if sizes[l as usize - 1] == 0 {
first[l as usize - 1] = i;
}
sizes[l as usize - 1] += 1;
}
}
for k in 0..*count as usize {
if kept.contains(&(offset + k as u32)) && smallest.map(|s| sizes[k] < s.0).unwrap_or(true) {
smallest = Some((sizes[k], ri, first[k]));
}
}
offset += count;
}
(roots.len(), smallest.map(|(_, ri, i)| (rasters[ri].layer, rasters[ri].at(i))))
}
/// The kept islands of a net's pours that a refill leaves apart from the net's largest piece, each
/// with cell centres to try a stitching via at (spread over the island, the most central first).
/// A kept island is one the net's own copper touches: KiCad keeps it, and reports it unconnected.
pub fn loose_islands(board: &Board, rules: &Rules, world: &World, net: NetId) -> Vec<(usize, Vec<V>, Vec<usize>)> {
let (rasters, base, is_pad, _) = model(board, rules, world, net);
let n = is_pad.len();
let labelled: Vec<(Vec<u32>, u32)> = rasters.iter().map(|r| r.label(None)).collect();
let total: u32 = labelled.iter().map(|l| l.1).sum();
let mut uf = Uf::new(n + total as usize);
for i in 0..n {
let root = base.clone().find(i as u32);
uf.union(i as u32, root);
}
let mut kept: BTreeSet<u32> = BTreeSet::new();
let mut offset = n as u32;
let mut starts: Vec<u32> = Vec::new();
// The net's pads, in the order the model lists its items (pads first), and the pads on each island.
let pad_ids: Vec<usize> = board.pads.iter().enumerate().filter(|(_, p)| p.net == net && !p.layers.is_empty()).map(|(i, _)| i).collect();
let mut pads_on: BTreeMap<u32, BTreeSet<usize>> = BTreeMap::new();
for (r, (lab, count)) in rasters.iter().zip(&labelled) {
starts.push(offset);
for (item, cells) in &r.reach {
for &c in cells {
let l = lab[c as usize];
if l != 0 {
uf.union(*item as u32, offset + l - 1);
kept.insert(offset + l - 1);
if let Some(&p) = pad_ids.get(*item) {
pads_on.entry(offset + l - 1).or_default().insert(p);
}
}
}
}
offset += count;
}
// Cells per connected piece of the net; the largest is the net's body.
let mut size: BTreeMap<u32, usize> = BTreeMap::new();
let mut cells_of: BTreeMap<u32, Vec<(usize, usize)>> = BTreeMap::new();
for (ri, (lab, _)) in labelled.iter().enumerate() {
for (i, &l) in lab.iter().enumerate() {
if l != 0 && kept.contains(&(starts[ri] + l - 1)) {
*size.entry(uf.find(starts[ri] + l - 1)).or_insert(0) += 1;
cells_of.entry(starts[ri] + l - 1).or_default().push((ri, i));
}
}
}
let Some((&body, _)) = size.iter().max_by_key(|(k, v)| (**v, std::cmp::Reverse(**k))) else {
// No pad reaches any piece of the pour (every thermal spoke is starved, or the fill is cut away
// round each pad): the pour joins nothing, so every group of pads that copper does not join
// to the largest group is loose. On an I/O carrier board: small pours over pin-header pads
// left 16 joins open in KiCad, and this returned nothing to mend.
let mut groups: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
for (k, &pad) in pad_ids.iter().enumerate() {
groups.entry(uf.find(k as u32)).or_default().push(pad);
}
let big = groups.iter().max_by_key(|(k, v)| (v.len(), std::cmp::Reverse(**k))).map(|(k, _)| *k);
return groups.into_iter().filter(|(k, _)| Some(*k) != big).map(|(_, pads)| (board.pads[pads[0]].layers.first().copied().unwrap_or(0), Vec::new(), pads)).collect();
};
let mut out = Vec::new();
// Pads of the net that no spoke or copper joins to the body and that sit on no island of their
// own: each is a piece by itself (cells empty), to be given a via beside it.
let on_island: BTreeSet<usize> = pads_on.values().flat_map(|s| s.iter().copied()).collect();
for (k, &pad) in pad_ids.iter().enumerate() {
if uf.find(k as u32) != body && !on_island.contains(&pad) {
let layer = board.pads[pad].layers.first().copied().unwrap_or(0);
out.push((layer, Vec::new(), vec![pad]));
}
}
for (id, cells) in cells_of {
if uf.find(id) == body || cells.is_empty() {
continue;
}
let r = &rasters[cells[0].0];
let pts: Vec<V> = cells.iter().map(|&(_, i)| r.at(i)).collect();
let c = V(pts.iter().map(|p| p.0).sum::<f64>() / pts.len() as f64, pts.iter().map(|p| p.1).sum::<f64>() / pts.len() as f64);
let mut pts = pts;
pts.sort_by(|a, b| hypot(sub(*a, c)).total_cmp(&hypot(sub(*b, c))));
let step = (pts.len() / 60).max(1);
out.push((r.layer, pts.into_iter().step_by(step).collect(), pads_on.get(&id).map(|p| p.iter().copied().collect()).unwrap_or_default()));
}
out
}
pub fn splits(board: &Board, rules: &Rules, world: &World) -> Vec<Split> {
let nets: BTreeSet<NetId> = board.zones.iter().filter(|z| z.net != NO_NET && !z.keepout_tracks && z.outline.len() >= 3).map(|z| z.net).collect();
let mut out = Vec::new();
for net in nets {
if let Some(s) = split_of(board, rules, world, net) {
out.push(s);
}
}
out
}
/// The refill model of one net: its pours rasterised per layer and cut by foreign copper, the net's
/// own items joined without any fill, and how many pieces the net is in without its pours.
fn model(board: &Board, rules: &Rules, world: &World, net: NetId) -> (Vec<Raster>, Uf, Vec<bool>, usize) {
// Items of this net, and what joins them without any fill.
let mut items: Vec<Piece> = Vec::new();
let mut shapes: Vec<(Option<usize>, Shape, bool)> = Vec::new();
for (i, p) in board.pads.iter().enumerate().filter(|(_, p)| p.net == net && !p.layers.is_empty()) {
items.push(Piece::Pad(i));
shapes.push((None, p.shape.clone(), true));
}
for t in board.tracks.iter().filter(|t| t.net == net) {
items.push(Piece::Seg { layer: t.layer, a: t.a.into(), b: t.b.into(), r: t.width as f64 / 2.0 });
shapes.push((Some(t.layer), Shape::Capsule { a: t.a.into(), b: t.b.into(), r: t.width as f64 / 2.0 }, false));
}
for v in board.vias.iter().filter(|v| v.net == net) {
items.push(Piece::Via { at: v.at.into(), r: v.size as f64 / 2.0 });
shapes.push((None, Shape::Circle { c: v.at.into(), r: v.size as f64 / 2.0 }, false));
}
let is_pad: Vec<bool> = items.iter().map(|p| matches!(p, Piece::Pad(_))).collect();
let mut base = Uf::new(items.len());
for i in 0..items.len() {
for j in i + 1..items.len() {
if base.find(i as u32) != base.find(j as u32) && touches(&items[i], &items[j], board) {
base.union(i as u32, j as u32);
}
}
}
// Without any fill the net is in this many pieces; plain open joins are counted elsewhere,
// so compare with what the saved shapes join.
let plain = crate::conn::net_groups(board, net, &[]).len().max(1);
let layers: BTreeSet<usize> = board.zones.iter().filter(|z| z.net == net && !z.keepout_tracks).flat_map(|z| z.layers.iter().copied()).collect();
let mut rasters = Vec::new();
for layer in layers {
let own: Vec<&Zone> = board.zones.iter().filter(|z| z.net == net && !z.keepout_tracks && z.outline.len() >= 3 && z.layers.contains(&layer)).collect();
if own.is_empty() {
continue;
}
let bb = own.iter().flat_map(|z| z.outline.iter()).fold((f64::MAX, f64::MAX, f64::MIN, f64::MIN), |m, v| (m.0.min(v.0), m.1.min(v.1), m.2.max(v.0), m.3.max(v.1)));
let (x0, y0) = ((bb.0 / STEP).floor() * STEP, (bb.1 / STEP).floor() * STEP);
let (w, h) = (((bb.2 - x0) / STEP).ceil() as usize + 1, ((bb.3 - y0) / STEP).ceil() as usize + 1);
let mut r = Raster { layer, x0, y0, w, h, inside: vec![false; w * h], blocked: vec![0; w * h], by_net: BTreeMap::new(), reach: Vec::new() };
for z in &own {
r.fill_poly(&z.outline);
}
let thick = own.iter().map(|z| z.min_thickness).fold(0.0, f64::max);
let brush = thick / 2.0 - SLACK;
let zone_clr = own.iter().map(|z| z.clearance).fold(0.0, f64::max);
let top = own.iter().map(|z| z.priority).max().unwrap_or(0);
let window = (bb.0, bb.1, bb.2, bb.3);
let mut found: Vec<(Shape, f64, NetId)> = Vec::new();
world.layers[layer].visit(window, |o| {
if o.net != net && matches!(o.owner, Owner::Pad(_) | Owner::Conn(_) | Owner::Fixed) {
let local = if let Owner::Pad(i) = o.owner { world.pad_clearance[i as usize] } else { 0.0 };
found.push((o.shape.clone(), zone_clr.max(rules.clearance_for(net, o.net) as f64).max(local) + brush, o.net));
}
true
});
world.holes.visit(window, |o| {
if o.net != net && !matches!(o.shape, Shape::Poly { .. }) {
found.push((o.shape.clone(), rules.hole_clearance() as f64 + brush, o.net));
}
true
});
world.edge.visit(window, |o| {
found.push((o.shape.clone(), rules.edge as f64 + brush, NO_NET));
true
});
for (s, need, n) in &found {
r.close(s, *need, *n);
}
// Keep-outs, and zones of other nets that fill first. Their outlines stand in for their
// fills: fills of higher priority sit inside them.
for z in board.zones.iter().filter(|z| z.layers.contains(&layer) && z.outline.len() >= 3) {
let above = z.net != net && z.net != NO_NET && !z.keepout_tracks && z.priority > top;
if !(above || z.keepout_pour) {
continue;
}
let extra = if z.keepout_pour { brush } else { zone_clr.max(rules.clearance_for(net, z.net) as f64) + brush };
r.close(&Shape::Poly { pts: z.outline.clone(), r: 0.0 }, extra, z.net);
}
// What of the net's own copper reaches which cells.
let joins = own.iter().any(|z| z.joins_pads);
let touch = thick / 2.0 + SLACK;
let mut reach = Vec::new();
for (k, (only, shape, pad)) in shapes.iter().enumerate() {
if only.map(|l| l != layer).unwrap_or(false) || (*pad && !joins) {
continue;
}
if let Piece::Pad(i) = items[k] {
if !board.pads[i].layers.contains(&layer) {
continue;
}
}
// A pad joins a pour with thermal reliefs only through its spokes: four of them along the
// pad's axes, each across the relief gap. A spoke stands where its far end is open copper
// and nothing closes the way to it. KiCad wants two (min_resolved_spokes); with one the
// pad is joined but DRC reports it starved, with none it is not joined at all. Read as
// touching, pads that tracks had boxed in counted as joined: a motor-controller board had
// five such ground pads open in KiCad while every join read closed here (2026-11-10).
if let Piece::Pad(i) = items[k] {
let pd = &board.pads[i];
let relief = own.iter().find_map(|z| z.thermal.filter(|_| !z.thermal_tht_only || pd.drill.is_some()));
let relief = match pd.zone_connect {
Some(2) => None,
Some(0) => { continue; }
Some(3) if pd.drill.is_none() => None,
_ => relief,
};
if let Some((gap, _width)) = relief {
let c: V = pd.center.into();
let mut cells: Vec<u32> = Vec::new();
for q in 0..4 {
let a = (pd.angle + 90.0 * q as f64).to_radians();
let u = V(a.cos(), -a.sin());
let half = if q % 2 == 0 { pd.size.0 } else { pd.size.1 } / 2.0;
let far = half + gap + thick / 2.0 + 2.0 * STEP;
let steps = ((far - half) / STEP).ceil() as usize;
let open = |d: f64| -> Option<usize> { r.index(V(c.0 + u.0 * d, c.1 + u.1 * d)).filter(|&i| r.inside[i] && r.blocked[i] == 0) };
if (0..=steps).all(|s| open(half + (far - half) * s as f64 / steps.max(1) as f64).is_some()) {
if let Some(i) = open(far) {
cells.push(i as u32);
}
}
}
reach.push((k, cells));
continue;
}
}
let cells: Vec<u32> = r.cells(shape.bbox(), touch).into_iter().filter(|&i| r.inside[i] && shape.dist_pt(r.at(i)) <= touch).map(|i| i as u32).collect();
reach.push((k, cells));
}
r.reach = reach;
rasters.push(r);
}
(rasters, base, is_pad, plain)
}
fn split_of(board: &Board, rules: &Rules, world: &World, net: NetId) -> Option<Split> {
let (rasters, base, is_pad, plain) = model(board, rules, world, net);
let (now, smallest) = pieces(&rasters, &base, &is_pad, None);
if now <= plain {
return None;
}
let (layer, at) = smallest.unwrap_or((0, V(0.0, 0.0)));
let foreign: BTreeSet<NetId> = rasters.iter().flat_map(|r| r.by_net.keys().copied()).filter(|&n| n != NO_NET).collect();
let mut cutters = Vec::new();
for f in foreign {
if pieces(&rasters, &base, &is_pad, Some(f)).0 < now {
cutters.push(f);
}
}
Some(Split { net, layer, at, extra: now - plain, cutters })
}