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
Colby Knox
AI Flow 0.2.73: neck-down, length tuning, grounded coplanar (#52)
2a6bec0
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
//! adom-aiflow's routing engine: a grid maze router for any KiCad 10 board.
//!
//! 0.1 mm grid over the outline, two signal layers (F.Cu, B.Cu), through vias between them
//! (0.6/0.3 mm for signals, 0.8/0.4 on the wide power nets), the board's rules plus every per-pad
//! clearance override. A* per connection (octilinear, +cost per via and per turn, F.Cu preferred),
//! obstacles per layer for the net being routed (every other net's copper dilated by clearance
//! plus half the track width), narrow pins entered on their axis through a reserved escape stub,
//! same-net overlapping pads clustered, each net routed as a minimum spanning tree onto its own
//! copper, shortest nets first, plane nets stubbed to a via, rip-up with history cost when a
//! connection fails, multi-pass with the failed nets first, and Kelvin sense taps routed first as
//! dedicated traces that the rest of their net treats as foreign copper.
//!
//!
//! Copper already on the board (an adopted or partly routed board) is fixed: its tracks and vias are
//! obstacles for every other net, and connections it already makes are not planned again. A spec's
//! planeUnder regions are keepouts for every net but their own on their layer.
//!
//! Output: a plan, a list of `kicad_route_net` calls ({net, width, viaSize, viaDrill, paths}).
pub mod board_copper;
pub mod copper_path;
pub mod decoupling;
pub mod signal_priority;
pub mod neckdown;
pub mod drive;
pub mod impedance;
pub mod plane_under;
pub mod rules;
pub mod simplify;
pub mod variants;
use std::collections::{HashMap, HashSet};
use aiflow_board::{geom, Board, Pad, Pt, B_CU, F_CU};
use aiflow_grid::{Grid, Mask, RES};
pub use rules::Rules;
pub const COST_ORTH: i64 = 10;
pub const COST_DIAG: i64 = 14;
pub const COST_VIA: i64 = 250;
pub const COST_TURN: i64 = 4;
/// What a change of direction adds to a step (issue #127). A 90 degree corner costs more than the two
/// 45 degree turns that replace it, and a sharper one more again, so the search rounds its corners.
/// With `flat` every turn costs the same, which gives up some corners to fit a crowded board.
/// `from` and `to` index DIRS.
pub fn turn_cost(from: usize, to: usize, flat: bool) -> i64 {
let eighths = |d: usize| match DIRS[d] { (1, 0) => 0, (1, 1) => 1, (0, 1) => 2, (-1, 1) => 3, (-1, 0) => 4, (-1, -1) => 5, (0, -1) => 6, _ => 7 };
let diff = (eighths(from) - eighths(to) as i32).rem_euclid(8);
match diff.min(8 - diff) { 0 => 0, 1 => COST_TURN, _ if flat => COST_TURN, 2 => 3 * COST_TURN, 3 => 5 * COST_TURN, _ => 7 * COST_TURN }
}
pub const LAYER_MULT: [f64; 2] = [1.0, 1.1];
/// Issue #183, single-face boards with F.Cu jumpers: the milled face (B.Cu) is the cheaper layer, and
/// entering the jumper layer (a wire on the component side) costs as much as a 30 mm detour.
pub const JUMPER_LAYER_MULT: [f64; 2] = [1.2, 1.0];
pub const JUMPER_COST: i64 = 3000;
pub const SOFT_EXTRA: i64 = 80;
pub const ZONE_COST: i64 = 20;
pub const VIA_WALL: i64 = 300;
pub const HIST_COST: i64 = 12;
/// Extra cost per step for the second net of an impedance pair off its partner's coupling line.
pub const PAIR_OFF: i64 = 10;
pub const DIRS: [(i32, i32); 8] = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)];
pub const STEP: [i64; 8] = [10, 10, 10, 10, 14, 14, 14, 14];
pub const INF: i64 = 1 << 40;
/// Half a cell diagonal: pads and stubs are off-grid. (Adding a margin here crosses a cell
/// boundary in the dilation radius and chokes the 0.5 mm-pitch escapes; a net that lands a hair
/// under the rule gets a per-net clearance from the spec instead: netClearance.)
pub fn slop() -> f64 {
0.05 * std::f64::consts::SQRT_2 + 1e-6
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Term {
pub l: u8,
pub i: i32,
pub j: i32,
}
impl Term {
pub fn new(l: usize, i: i64, j: i64) -> Term {
Term { l: l as u8, i: i as i32, j: j as i32 }
}
pub fn layer(&self) -> usize {
self.l as usize
}
}
#[derive(Clone, Debug)]
pub struct Cluster {
pub idx: usize,
pub pads: Vec<usize>,
pub primary: usize,
pub terminals: Vec<Term>,
pub escapes: HashMap<Term, Pt>,
pub corridors: HashMap<Term, Pt>,
pub connected: bool,
pub allow: f64,
pub released: HashSet<Term>,
}
impl Cluster {
pub fn key<'a>(&self, board: &'a Board) -> &'a str {
&board.pads[self.primary].key
}
pub fn xy(&self, board: &Board) -> Pt {
(board.pads[self.primary].x, board.pads[self.primary].y)
}
}
#[derive(Clone, Debug)]
pub enum End {
Pad(usize, Term),
Escape(usize, Term),
Copper,
Via,
}
impl End {
pub fn cluster(&self) -> Option<usize> {
match self {
End::Pad(c, _) | End::Escape(c, _) => Some(*c),
_ => None,
}
}
pub fn term(&self) -> Option<Term> {
match self {
End::Pad(_, t) | End::Escape(_, t) => Some(*t),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct Route {
pub cells: Vec<Term>,
pub width: f64,
pub start: End,
pub end: End,
pub breaks: HashSet<usize>,
pub via_end: bool,
pub via_end_layer: usize,
pub kelvin: bool,
/// The spec decoupling pair (index into `Router::pairs`) this route is the direct connection of:
/// routed before the rail and kept through rip-up (decoupling.rs).
pub pair: Option<usize>,
}
impl Route {
pub fn new(cells: Vec<Term>, width: f64, start: End, end: End) -> Route {
Route { cells, width, start, end, breaks: HashSet::new(), via_end: false, via_end_layer: B_CU, kelvin: false, pair: None }
}
}
#[derive(Clone, Debug)]
pub struct NetState {
pub idx: usize,
pub name: String,
pub pads: Vec<usize>,
pub clusters: Vec<Cluster>,
pub routes: Vec<Route>,
pub widths: Vec<f64>,
pub plane: Option<String>,
pub mst_len: f64,
pub used_escapes: HashSet<(usize, Term)>,
pub failed: Vec<(String, String)>,
pub rips: usize,
pub routed_order: Option<usize>,
pub via_in_pad: Vec<String>,
pub last_soft: Vec<Term>,
pub via: f64,
pub drill: f64,
/// Every cluster of the net is already joined by the board's own copper: nothing to route.
pub done_by_board: bool,
/// Issue #140: pads of this net the router cannot reach because they lie off the board (their part
/// is outside the outline, or the pad is off the routing grid), with the reason; never routed,
/// always reported as unrouted.
pub outside: Vec<(String, String)>,
}
#[derive(Clone)]
pub struct Snapshot {
pub nets: Vec<(Vec<Route>, HashSet<(usize, Term)>, Vec<(String, String)>, Vec<String>, Option<usize>)>,
}
pub struct Router {
pub board: Board,
pub grid: Grid,
pub rules: Rules,
pub verbose: bool,
pub nets: Vec<NetState>,
pub net_ids: HashMap<String, usize>,
pub pad_raw: [Vec<i16>; 2],
pub pad_owner: [Vec<i16>; 2],
pub route_raw: [Vec<i16>; 2],
pub big_clr: [Mask; 2],
pub big_clr_val: f64,
pub smd_pad_cells: HashMap<usize, Mask>,
pub thru_cells: Mask,
pub history: [Vec<i16>; 2],
pub chan_count: Vec<i16>,
pub chan_lines: HashMap<(usize, usize, Term), Vec<usize>>,
pub vias: Vec<(i32, i32, usize, f64, f64)>,
pub fp_index: HashMap<String, usize>,
/// cells of each pad, by its index in board.pads (a key may name two pads: issue #93)
pub pad_cells: HashMap<usize, Vec<usize>>,
pub inside: Mask,
pub edge_dist: Vec<f32>,
pub inside_w: HashMap<i64, Mask>,
pub rip_total: usize,
/// Every turn costs the same in the search: the later passes of a board that will not close (issue #127).
pub flat_turns: bool,
pub stats: HashMap<String, i64>,
pub kelvin_owner: HashMap<usize, usize>,
pub kelvin_clusters: HashSet<(usize, usize)>,
pub body_owner: Vec<i16>,
pub ic_zone: Mask,
pub ic_nets: HashMap<usize, HashSet<usize>>,
pub log_lines: Vec<String>,
/// The board's own tracks and vias, per layer: net id, -2 for copper on no known net, -1 empty.
pub fixed_raw: [Vec<i16>; 2],
pub fixed_tracks: Vec<aiflow_board::Track>,
pub fixed_vias: Vec<aiflow_board::Via>,
/// Per pad: the group of pads the board's own copper already joins (its own index when none).
pub pad_comp: Vec<usize>,
pub comp_copper: HashMap<usize, board_copper::CompCopper>,
/// Per net: landing cells on its fixed copper, with the group each belongs to.
pub fixed_targets: HashMap<usize, Vec<(usize, Term)>>,
/// planeUnder regions and, per layer, the region index owning each cell (-1 none).
pub keep: Vec<plane_under::Region>,
pub keep_raw: [Vec<i16>; 2],
/// Spec decoupling pairs resolved on this board (decoupling.rs).
pub pairs: Vec<decoupling::PairState>,
/// The signal-net order (`route --variants`, variants.rs); the default is shortest first.
pub order: variants::NetOrder,
}
impl Router {
pub fn log(&mut self, s: String) {
if self.verbose {
eprintln!("{s}");
}
self.log_lines.push(s);
}
pub fn stat(&mut self, k: &str, d: i64) {
*self.stats.entry(k.to_string()).or_insert(0) += d;
}
pub fn new(board: Board, rules: Rules, verbose: bool) -> Router {
let grid = Grid::new(board.bbox);
let n = grid.w * grid.h;
let mut names: Vec<&String> = board.nets.keys().collect();
names.sort();
let mut nets = Vec::new();
let mut net_ids = HashMap::new();
for (k, name) in names.iter().enumerate() {
net_ids.insert((*name).clone(), k);
nets.push(NetState {
idx: k, name: (*name).clone(), pads: board.nets[*name].clone(), clusters: Vec::new(), routes: Vec::new(),
widths: vec![rules.track], plane: rules.planes.get(*name).cloned(), mst_len: 0.0, used_escapes: HashSet::new(), failed: Vec::new(),
rips: 0, routed_order: None, via_in_pad: Vec::new(), last_soft: Vec::new(), via: rules.via_small, drill: rules.drill_small, done_by_board: false, outside: Vec::new(),
});
}
let mut refs: Vec<&String> = board.footprints.keys().collect();
refs.sort();
let fp_index: HashMap<String, usize> = refs.iter().enumerate().map(|(i, r)| ((*r).clone(), i)).collect();
let (mut inside, mut edge_dist) = grid.outline_distance(&board.outline);
// Edge.Cuts cutouts (a bore, a slot) are outside the board, and their edges keep the same
// edge clearance as the outline (issue #77)
for cut in &board.outline_cutouts {
let (in_cut, d_cut) = grid.outline_distance(cut);
for j in 0..grid.h {
for i in 0..grid.w {
if in_cut.get(i, j) { inside.set(i, j, false); }
let k = j * grid.w + i;
if d_cut[k] < edge_dist[k] { edge_dist[k] = d_cut[k]; }
}
}
}
let mut r = Router {
grid: grid.clone(), rules, verbose, nets, net_ids,
pad_raw: [vec![-1; n], vec![-1; n]], pad_owner: [vec![-1; n], vec![-1; n]], route_raw: [vec![-1; n], vec![-1; n]],
big_clr: [grid.mask(), grid.mask()], big_clr_val: 0.0, smd_pad_cells: HashMap::new(), thru_cells: grid.mask(),
history: [vec![0; n], vec![0; n]], chan_count: vec![0; n], chan_lines: HashMap::new(), vias: Vec::new(), fp_index,
pad_cells: HashMap::new(), inside, edge_dist, inside_w: HashMap::new(), rip_total: 0, flat_turns: false, stats: HashMap::new(), kelvin_owner: HashMap::new(), kelvin_clusters: HashSet::new(),
body_owner: vec![-1; n], ic_zone: grid.mask(), ic_nets: HashMap::new(), log_lines: Vec::new(),
fixed_raw: [vec![-1; n], vec![-1; n]], fixed_tracks: Vec::new(), fixed_vias: Vec::new(), pad_comp: Vec::new(), comp_copper: HashMap::new(), fixed_targets: HashMap::new(),
keep: Vec::new(), keep_raw: [vec![-1; n], vec![-1; n]], pairs: Vec::new(), order: variants::NetOrder::default(), board,
};
r.big_clr_val = r.rules.clearance;
// issue #183: a board with no vias has no plane to stitch down to; its plane nets are routed as
// tracks like any other net (a pour can still be added after routing)
if !r.rules.vias_allowed() {
let planes: Vec<String> = r.nets.iter_mut().filter_map(|n| n.plane.take().map(|_| n.name.clone())).collect();
let layers: Vec<&str> = (0..2).filter(|&l| r.rules.layer_allowed(l)).map(|l| aiflow_board::LAYER_NAMES[l]).collect();
r.log(format!("router: single-face mode: layers {}, no vias (a layer change only at a through-hole pad of the same net){}", layers.join(" + "),
if planes.is_empty() { String::new() } else { format!("; plane net(s) {} routed as tracks (no via can reach a plane)", planes.join(", ")) }));
}
let t0 = std::time::Instant::now();
r.set_aside_outside_pads();
r.stamp_pads();
r.stamp_holes();
r.stamp_board_copper();
r.stamp_plane_under();
r.build_clusters();
r.build_ic_zones();
r.init_decoupling();
let count = r.nets.iter().filter(|n| (n.clusters.len() >= 2 || n.plane.is_some()) && !n.done_by_board).count();
r.log(format!("router: {} x {} cells, {} pads stamped, {} nets to route, init {:.1}s", r.grid.w, r.grid.h, r.board.pads.len(), count, t0.elapsed().as_secs_f64()));
r
}
/// Issue #140: a part left outside the outline (pack found it no spot) has pads off the routing grid,
/// and a window built from them indexed past the grid (a panic in obstacle_maps). Such pads never
/// join a cluster: each is recorded on its net's `outside` list, with the part named, and reported
/// as an unrouted connection. A part that only overhangs the edge keeps its on-grid pads.
fn set_aside_outside_pads(&mut self) {
let off_board = self.board.parts_outside();
for net in 0..self.nets.len() {
let routable = self.nets[net].pads.len() >= 2 || self.nets[net].plane.is_some();
let pads = std::mem::take(&mut self.nets[net].pads);
let mut keep = Vec::with_capacity(pads.len());
for pi in pads {
let p = &self.board.pads[pi];
let (i, j) = self.grid.to_cell(p.x, p.y);
let why = if off_board.contains(&p.reference) {
Some(format!("{} is outside the board outline; move it inside (did `place pack` place one part fewer than asked?), then route again", p.reference))
} else if !self.grid.in_bounds(i, j) {
Some(format!("pad {} of {} lies outside the board outline (off the routing grid); move {} inside, then route again", p.key, p.reference, p.reference))
} else { None };
match why {
Some(w) => if routable { self.nets[net].outside.push((p.key.clone(), w)); },
None => keep.push(pi),
}
}
self.nets[net].pads = keep;
}
let n: usize = self.nets.iter().map(|n| n.outside.len()).sum();
if n > 0 {
let parts: Vec<String> = off_board.clone();
self.log(format!("router: {n} pad(s) set aside off the board{}", if parts.is_empty() { String::new() } else { format!(" (outside the outline: {})", parts.join(", ")) }));
}
}
// -- stamping --------------------------------------------------------------------
fn stamp_pads(&mut self) {
let g = self.grid.clone();
let pads: Vec<Pad> = self.board.pads.clone();
for (pi, p) in pads.iter().enumerate() {
let nid = self.net_ids.get(&p.net).map(|&k| k as i16).unwrap_or(-2); // -1 is empty; -2 is physical, unassigned copper.
let mut m = g.mask();
for poly in &p.polys {
g.poly_mask(poly, &mut m);
}
if let Some(d) = p.drill {
for c in g.disk_cells((p.x, p.y), d / 2.0, None) {
m.bits[c] = true;
}
}
let cells: Vec<usize> = m.bits.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect();
self.pad_cells.insert(pi, cells.clone());
let layers: Vec<usize> = if p.thru { vec![F_CU, B_CU] } else if p.on_f { vec![F_CU] } else { vec![B_CU] };
let fpi = self.fp_index[&p.reference] as i16;
for &l in &layers {
for &c in &cells {
self.pad_raw[l][c] = nid;
self.pad_owner[l][c] = fpi;
if p.clearance > self.rules.clearance {
self.big_clr[l].bits[c] = true;
}
}
if p.clearance > self.rules.clearance {
self.big_clr_val = self.big_clr_val.max(p.clearance);
}
}
if !p.thru && nid >= 0 {
let sm = self.smd_pad_cells.entry(nid as usize).or_insert_with(|| g.mask());
for &c in &cells {
sm.bits[c] = true;
}
}
if p.thru {
for &c in &cells {
self.thru_cells.bits[c] = true;
}
}
}
}
/// Drilled holes with no copper (mask-only NPTH): physical obstacles on both layers, like an
/// unnumbered copper hole, so tracks keep clearance and vias keep hole clearance. They are not
/// pads, so they never join a net or a cluster.
fn stamp_holes(&mut self) {
let g = self.grid.clone();
let holes = self.board.holes.clone();
for h in &holes {
let mut m = g.mask();
g.poly_mask(&h.poly, &mut m);
for c in g.disk_cells((h.x, h.y), h.size.0.min(h.size.1) / 2.0, None) {
m.bits[c] = true;
}
let fpi = self.fp_index.get(&h.reference).map(|&k| k as i16).unwrap_or(-1);
for c in m.bits.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i) {
for l in [F_CU, B_CU] {
if self.pad_raw[l][c] == -1 {
self.pad_raw[l][c] = -2;
self.pad_owner[l][c] = fpi;
}
}
self.thru_cells.bits[c] = true;
}
}
}
/// Resolve the planeUnder regions and mark their cells. A region the board cannot resolve is
/// logged and skipped here; `route` refuses such a spec before it builds a router.
fn stamp_plane_under(&mut self) {
let regions = match plane_under::regions(&self.board, &self.rules.plane_under) {
Ok(r) => r,
Err(e) => {
self.log(format!("planeUnder: {e}; no region applied"));
return;
}
};
let g = self.grid.clone();
for (k, reg) in regions.iter().enumerate() {
let mut m = g.mask();
g.poly_mask(®.poly, &mut m);
let mut cells = 0;
for (c, b) in m.bits.iter().enumerate() {
if *b {
self.keep_raw[reg.layer][c] = k as i16;
cells += 1;
}
}
self.log(format!(" planeUnder {}: {:.1} mm2 of {} kept for {} (a keepout for every other net)", reg.label, cells as f64 * RES * RES, aiflow_board::LAYER_NAMES[reg.layer], reg.net));
}
self.keep = regions;
}
/// Is this cell of the layer inside a planeUnder region that belongs to another net?
pub fn kept_from(&self, net_name: &str, l: usize, c: usize) -> bool {
let k = self.keep_raw[l][c];
k >= 0 && self.keep[k as usize].net != net_name
}
pub fn inside_for(&mut self, w: f64) -> &Mask {
let key = (w * 1000.0).round() as i64;
if !self.inside_w.contains_key(&key) {
let mut m = self.inside.clone();
let need = (self.rules.edge + w / 2.0 + slop()) as f32;
for (b, d) in m.bits.iter_mut().zip(&self.edge_dist) {
*b = *b && *d >= need;
}
self.inside_w.insert(key, m);
}
&self.inside_w[&key]
}
pub fn stamp_route(&mut self, net: usize, route: &Route, sign: i32) {
let g = self.grid.clone();
let nid = if sign > 0 { self.nets[net].idx as i16 } else { -1 };
let cells = &route.cells;
let mut k = 0;
while k + 1 < cells.len() {
let (l, i0, j0) = (cells[k].layer(), cells[k].i, cells[k].j);
let mut m = k + 1;
let (di, dj) = (cells[m].i - i0, cells[m].j - j0);
while m + 1 < cells.len() && cells[m + 1].layer() == l && cells[m].layer() == l && (cells[m + 1].i - cells[m].i, cells[m + 1].j - cells[m].j) == (di, dj) {
m += 1;
}
if cells[m].layer() == l {
let a = g.to_xy(i0 as i64, j0 as i64);
let b = g.to_xy(cells[m].i as i64, cells[m].j as i64);
for c in g.segment_cells(a, b, route.width / 2.0, Some(1e-6)) {
self.route_raw[l][c] = nid;
}
}
k = m;
}
// issue #183: with vias off a layer change is a through-hole pad of the net, never a via
let vias = self.rules.vias_allowed();
for k in 1..cells.len() {
if vias && cells[k].layer() != cells[k - 1].layer() && (cells[k].i, cells[k].j) == (cells[k - 1].i, cells[k - 1].j) {
self.stamp_via(cells[k].i, cells[k].j, net, sign);
}
}
if route.via_end {
let t = *cells.last().unwrap();
self.stamp_via(t.i, t.j, net, sign);
}
}
fn stamp_via(&mut self, i: i32, j: i32, net: usize, sign: i32) {
let g = self.grid.clone();
let (via, drill, nid) = (self.nets[net].via, self.nets[net].drill, self.nets[net].idx);
let xy = g.to_xy(i as i64, j as i64);
for c in g.disk_cells(xy, via / 2.0, Some(1e-6)) {
for l in 0..2 {
self.route_raw[l][c] = if sign > 0 { nid as i16 } else { -1 };
}
}
if sign > 0 {
self.vias.push((i, j, nid, via, drill));
} else {
self.vias.retain(|v| !(v.0 == i && v.1 == j && v.2 == nid));
}
}
/// Centre-to-centre distance this net's via must keep from a via of (nid, dia, drill).
pub fn via_gap(&self, net: usize, nid: usize, dia: f64, drill: f64) -> f64 {
let n = &self.nets[net];
if nid == n.idx {
(n.drill + drill) / 2.0 + self.rules.hole_to_hole + slop()
} else {
n.via / 2.0 + dia / 2.0 + self.rules.clearance + slop()
}
}
pub fn stamp_stub(&mut self, net: usize, cluster: usize, term: Term, sign: i32) {
let g = self.grid.clone();
let l = term.layer();
let cl = self.nets[net].clusters[cluster].clone();
let p = self.board.pads[cl.primary].clone();
let a = p.polys_centre;
let b = g.to_xy(term.i as i64, term.j as i64);
let nid = if sign > 0 { self.nets[net].idx as i16 } else { -1 };
let fpi = self.fp_index[&p.reference] as i16;
for c in g.segment_cells(a, b, self.stub_for(net) / 2.0, Some(1e-6)) {
self.pad_raw[l][c] = nid;
if sign > 0 {
self.pad_owner[l][c] = fpi;
}
}
if let Some(kend) = cl.corridors.get(&term) {
for c in g.segment_cells(b, *kend, self.rules.track / 2.0, Some(1e-6)) {
self.pad_raw[l][c] = nid;
if sign > 0 {
self.pad_owner[l][c] = fpi;
}
}
}
self.stamp_channel(net, cluster, term, sign);
if sign < 0 {
let cells = self.pad_cells[&cl.primary].clone();
let real = self.nets[net].idx as i16;
for c in cells {
self.pad_raw[l][c] = real;
self.pad_owner[l][c] = fpi;
}
}
}
fn stamp_channel(&mut self, net: usize, cluster: usize, term: Term, sign: i32) {
let key = (self.nets[net].idx, cluster, term);
if sign > 0 && self.chan_lines.contains_key(&key) {
return;
}
if sign < 0 {
if let Some(cells) = self.chan_lines.remove(&key) {
for c in cells {
self.chan_count[c] -= 1;
}
}
return;
}
let g = self.grid.clone();
let cl = &self.nets[net].clusters[cluster];
let p = &self.board.pads[cl.primary];
let b = g.to_xy(term.i as i64, term.j as i64);
let (ax, ay) = p.long_axis;
let (cx, cy) = p.polys_centre;
let sgn = if (b.0 - cx) * ax + (b.1 - cy) * ay > 0.0 { 1.0 } else { -1.0 };
let rax = (ax * 1e6).round() / 1e6;
let ray = (ay * 1e6).round() / 1e6;
let cend = (b.0 + sgn * rax * self.rules.channel, b.1 + sgn * ray * self.rules.channel);
let r = self.nets[net].via / 2.0 + self.rules.clearance + self.rules.track / 2.0 + slop();
let via = self.nets[net].via;
let cells: Vec<usize> = g.segment_cells(b, cend, r, Some(0.0));
let inside = self.inside_for(via).clone();
let cells: Vec<usize> = cells.into_iter().filter(|&c| inside.bits[c]).collect();
for &c in &cells {
self.chan_count[c] += 1;
}
self.chan_lines.insert(key, cells);
}
// -- clusters and terminals ----------------------------------------------------
fn build_clusters(&mut self) {
let nnets = self.nets.len();
for net in 0..nnets {
if self.nets[net].pads.len() < 2 && self.nets[net].plane.is_none() {
continue;
}
let mut by_fp: Vec<(String, Vec<usize>)> = Vec::new();
for &pi in &self.nets[net].pads.clone() {
let r = self.board.pads[pi].reference.clone();
if let Some(e) = by_fp.iter_mut().find(|(k, _)| *k == r) {
e.1.push(pi);
} else {
by_fp.push((r, vec![pi]));
}
}
let mut clusters: Vec<Cluster> = Vec::new();
for (_, pads) in by_fp {
let mut groups: Vec<(Vec<usize>, HashSet<usize>)> = Vec::new();
for pi in pads {
let mask: HashSet<usize> = self.pad_cells[&pi].iter().copied().collect();
let mut merged: Option<usize> = None;
let mut gi = 0;
while gi < groups.len() {
if !groups[gi].1.is_disjoint(&mask) {
if let Some(m) = merged {
let (gp, gc) = groups.remove(gi);
groups[m].0.extend(gp);
groups[m].1.extend(gc);
continue;
} else {
groups[gi].0.push(pi);
groups[gi].1.extend(mask.iter().copied());
merged = Some(gi);
}
}
gi += 1;
}
if merged.is_none() {
groups.push((vec![pi], mask));
}
}
for (gpads, _) in groups {
let primary = *gpads.iter().max_by(|&&a, &&b| {
let pa = &self.board.pads[a];
let pb = &self.board.pads[b];
(pa.half_long * pa.half_short).partial_cmp(&(pb.half_long * pb.half_short)).unwrap()
}).unwrap();
clusters.push(Cluster { idx: clusters.len(), pads: gpads, primary, terminals: Vec::new(), escapes: HashMap::new(), corridors: HashMap::new(), connected: false, allow: 0.25, released: HashSet::new() });
}
}
self.nets[net].clusters = clusters;
let name = self.nets[net].name.clone();
if let Some(w) = self.rules.wide.get(&name) {
self.nets[net].widths = w.clone();
self.nets[net].via = self.rules.via;
self.nets[net].drill = self.rules.drill;
} else if let Some(w) = self.rules.mid.get(&name) {
self.nets[net].widths = w.clone();
}
if let Some(ir) = self.rules.impedance.get(&name) {
self.nets[net].widths = vec![ir.width];
// issue #52: pins narrower than the target width: also try the rule width (a pair keeps its
// solved pitch); neckdown::widen_plan widens it back wherever it clears
let narrowest = self.nets[net].pads.iter().map(|&p| 2.0 * self.board.pads[p].half_short).fold(f64::INFINITY, f64::min);
if narrowest + 0.1 < ir.width && self.rules.track < ir.width { self.nets[net].widths.push(self.rules.track); }
}
let count = self.nets[net].clusters.len();
if !self.comp_copper.is_empty() {
let plane = self.nets[net].plane.is_some();
let first = self.comp_of(net, 0);
let done = if plane { (0..count).all(|c| self.joined_by_board(net, c) || self.on_plane(net, c)) } else { count >= 2 && (0..count).all(|c| self.comp_of(net, c) == first) };
if done {
self.nets[net].done_by_board = true;
for c in 0..count {
self.nets[net].clusters[c].connected = true;
}
continue;
}
}
for c in 0..count {
self.terminals_for(net, c);
}
}
// MOSFET drain and source nets get the wide treatment too
for p in self.board.pads.clone() {
if p.reference.starts_with('Q') && matches!(p.name.as_str(), "1" | "2" | "3" | "5") {
if let Some(&n) = self.net_ids.get(&p.net) {
if self.nets[n].plane.is_none() && !self.rules.wide.contains_key(&p.net) {
self.nets[n].widths = vec![1.0, 0.5, 0.25];
self.nets[n].via = self.rules.via;
self.nets[n].drill = self.rules.drill;
}
}
}
}
}
fn build_ic_zones(&mut self) {
let g = self.grid.clone();
let mut by_fp: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
for (n, net) in self.nets.iter().enumerate() {
for (c, cl) in net.clusters.iter().enumerate() {
if !cl.escapes.is_empty() {
by_fp.entry(self.board.pads[cl.primary].reference.clone()).or_default().push((n, c));
}
}
}
let mut refs: Vec<String> = by_fp.keys().cloned().collect();
refs.sort();
for r in refs {
let clusters = &by_fp[&r];
if clusters.len() < 2 {
continue;
}
let fp = &self.board.footprints[&r];
let mut inner = Vec::new();
for &(n, c) in clusters {
let p = &self.board.pads[self.nets[n].clusters[c].primary];
let (ax, ay) = p.long_axis;
let (cx, cy) = p.polys_centre;
let sgn = if (fp.x - cx) * ax + (fp.y - cy) * ay > 0.0 { 1.0 } else { -1.0 };
inner.push((cx + sgn * ax * p.half_long, cy + sgn * ay * p.half_long));
}
let (x0, y0, x1, y1) = geom::bbox(&inner);
if x1 - x0 < 1.0 || y1 - y0 < 1.0 {
continue;
}
let (i0, j0) = g.to_cell(x0, y0);
let (i1, j1) = g.to_cell(x1, y1);
let idx = self.fp_index[&r] as i16;
for j in j0.max(0)..=j1.min(g.h as i64 - 1) {
for i in i0.max(0)..=i1.min(g.w as i64 - 1) {
self.body_owner[j as usize * g.w + i as usize] = idx;
}
}
let pads: Vec<&Pad> = self.board.pads.iter().filter(|q| q.reference == r).collect();
let nets: HashSet<usize> = pads.iter().filter_map(|q| self.net_ids.get(&q.net).copied()).collect();
let m = (self.rules.ic_zone / RES).round() as i64;
let (px0, py0, px1, py1) = geom::bbox(&pads.iter().map(|q| (q.x, q.y)).collect::<Vec<_>>());
let (a0, b0) = g.to_cell(px0, py0);
let (a1, b1) = g.to_cell(px1, py1);
for j in (b0 - m).max(0)..=(b1 + m).min(g.h as i64 - 1) {
for i in (a0 - m).max(0)..=(a1 + m).min(g.w as i64 - 1) {
self.ic_zone.bits[j as usize * g.w + i as usize] = true;
}
}
let foreign = self.nets.len() - nets.len();
self.ic_nets.insert(idx as usize, nets);
self.log(format!(" ic {}: body keepout {:.1} x {:.1} mm for {} foreign nets, escape band +{}/cell", r, (i1 - i0) as f64 * RES, (j1 - j0) as f64 * RES, foreign, ZONE_COST));
}
}
fn terminals_for(&mut self, net: usize, c: usize) {
let g = self.grid.clone();
let primary = self.nets[net].clusters[c].primary;
let p = self.board.pads[primary].clone();
let md = p.min_dim();
self.nets[net].clusters[c].allow = if md >= 1.0 { 1.0 } else if md >= 0.5 { 0.5 } else { 0.25 };
if p.thru {
let (i, j) = g.to_cell(p.x, p.y);
self.nets[net].clusters[c].terminals = vec![Term::new(F_CU, i, j), Term::new(B_CU, i, j)];
return;
}
let l = if p.on_f { F_CU } else { B_CU };
let narrow = md < self.rules.narrow && p.half_long > p.half_short * 1.4;
// a pad the board's own copper already reaches is joined where it is: no escape stub of its own
if !narrow || self.joined_by_board(net, c) {
let (i, j) = g.to_cell(p.x, p.y);
self.nets[net].clusters[c].terminals = vec![Term::new(l, i, j)];
return;
}
self.nets[net].clusters[c].allow = 0.25;
let (cx, cy) = p.polys_centre;
let (ax, ay) = p.long_axis;
let rax = (ax * 1e6).round() / 1e6;
let ray = (ay * 1e6).round() / 1e6;
let mut found: Vec<(Term, Option<Pt>)> = Vec::new();
for sgn in [1.0f64, -1.0] {
let mut hit: Option<(Term, Option<Pt>)> = None;
for corridor in [self.rules.corridor, self.rules.corridor / 2.0, 0.0] {
let mut d = self.rules.escape;
while d <= 1.6 && hit.is_none() {
let ex = cx + sgn * ax * (p.half_long + d);
let ey = cy + sgn * ay * (p.half_long + d);
let (i, j) = g.to_cell(ex, ey);
let (bx, by) = g.to_xy(i, j);
let kend = (bx + sgn * rax * corridor, by + sgn * ray * corridor);
if g.in_bounds(i, j) && self.escape_ok(net, &p, l, i, j, kend) {
hit = Some((Term::new(l, i, j), if corridor > 0.0 { Some(kend) } else { None }));
}
d += 0.1;
}
if hit.is_some() {
break;
}
}
if let Some(h) = hit {
found.push(h);
}
}
if found.is_empty() {
let (i, j) = g.to_cell(p.x, p.y);
self.nets[net].clusters[c].terminals = vec![Term::new(l, i, j)];
return;
}
self.nets[net].clusters[c].terminals = found.iter().map(|(t, _)| *t).collect();
for (t, kend) in found {
let xy = g.to_xy(t.i as i64, t.j as i64);
self.nets[net].clusters[c].escapes.insert(t, xy);
if let Some(k) = kend {
self.nets[net].clusters[c].corridors.insert(t, k);
}
self.stamp_stub(net, c, t, 1);
}
}
fn escape_ok(&mut self, net: usize, p: &Pad, l: usize, i: i64, j: i64, corridor_end: Pt) -> bool {
let g = self.grid.clone();
let track = self.rules.track;
if !self.inside_for(track).get(i as usize, j as usize) {
return false;
}
let b = g.to_xy(i, j);
let a = p.polys_centre;
let mut checks = vec![(a, b, self.stub_for(net) / 2.0)];
if (b.0 - corridor_end.0).hypot(b.1 - corridor_end.1) > 1e-6 {
let (ki, kj) = g.to_cell(corridor_end.0, corridor_end.1);
if !g.in_bounds(ki, kj) || !self.inside_for(track).get(ki as usize, kj as usize) {
return false;
}
checks.push((b, corridor_end, track / 2.0));
}
let mut samples: Vec<(f64, f64, f64)> = Vec::new();
for (s0, s1, hw) in checks {
let n = (((s1.0 - s0.0).hypot(s1.1 - s0.1)) / 0.05).ceil().max(1.0) as usize;
for k in 0..=n {
let t = k as f64 / n as f64;
samples.push((s0.0 + (s1.0 - s0.0) * t, s0.1 + (s1.1 - s0.1) * t, hw));
}
}
samples.push((b.0, b.1, track / 2.0));
let reach = 2.5;
let net_name = self.nets[net].name.clone();
if !self.fixed_tracks.is_empty() || !self.fixed_vias.is_empty() || !self.keep.is_empty() {
for &(x, y, hw) in &samples {
if self.fixed_blocks(&net_name, Some(l), x, y, hw + self.rules.clearance) {
return false;
}
let (ci, cj) = g.to_cell(x, y);
if g.in_bounds(ci, cj) && self.kept_from(&net_name, l, cj as usize * g.w + ci as usize) {
return false;
}
}
}
for q in &self.board.pads {
if q.reference == p.reference || q.net == net_name {
continue;
}
if (q.x - b.0).abs() > reach + q.half_long || (q.y - b.1).abs() > reach + q.half_long {
continue;
}
// only copper on the escape's own layer is in its way (a bottom pad's escape passes over top pads)
if !q.thru && !q.on_layer(l) {
continue;
}
let clr = self.rules.clearance.max(q.clearance);
for poly in &q.polys {
for &(x, y, hw) in &samples {
if geom::point_poly_distance(x, y, poly) < hw + clr {
return false;
}
}
}
}
// a drilled hole with no copper blocks escapes of its own footprint too (a USB-C shell peg)
for h in &self.board.holes {
if (h.x - b.0).abs() > reach + h.diameter() || (h.y - b.1).abs() > reach + h.diameter() {
continue;
}
for &(x, y, hw) in &samples {
if geom::point_poly_distance(x, y, &h.poly) < hw + self.rules.clearance {
return false;
}
}
}
true
}
// -- obstacle maps ---------------------------------------------------------------
/// (obs_o, obs_d, soft, via_ok) for the window (i0, j0, i1, j1 inclusive), each a Mask of window size.
pub fn obstacle_maps(&mut self, net: usize, width: f64, win: (i64, i64, i64, i64), soft: bool) -> ([Mask; 2], [Mask; 2], [Option<Mask>; 2], Mask) {
let g = self.grid.clone();
// a window always lies inside the grid (window_for clamps it); a hand-made one is clamped here too
let (wx, hy) = (g.w as i64 - 1, g.h as i64 - 1);
let (i0, j0) = (win.0.clamp(0, wx), win.1.clamp(0, hy));
let (i1, j1) = (win.2.clamp(i0, wx), win.3.clamp(j0, hy));
let clr = self.rules.clearance.max(self.rules.net_clearance.get(&self.nets[net].name).copied().unwrap_or(0.0));
let r_o = (clr + width / 2.0 + slop()) / RES;
let r_d = r_o + 0.0707 / RES;
let r_big = (self.big_clr_val + width / 2.0 + slop()) / RES;
let pad_r = (r_d.max(r_big)).ceil() as i64 + 1;
let (pi0, pj0) = ((i0 - pad_r).max(0), (j0 - pad_r).max(0));
let (pi1, pj1) = ((i1 + pad_r).min(g.w as i64 - 1), (j1 + pad_r).min(g.h as i64 - 1));
let ww = (pi1 - pi0 + 1) as usize;
let hh = (pj1 - pj0 + 1) as usize;
let sub = Grid { res: g.res, x0: 0.0, y0: 0.0, w: ww, h: hh };
let nid = self.nets[net].idx as i16;
let via = self.nets[net].via;
let mut via_block = Mask::new(ww, hh);
let mut obs_o: [Mask; 2] = [Mask::new(1, 1), Mask::new(1, 1)];
let mut obs_d: [Mask; 2] = [Mask::new(1, 1), Mask::new(1, 1)];
let mut softm: [Option<Mask>; 2] = [None, None];
let inside_w = self.inside_for(width).clone();
let inside_via = self.inside_for(via).clone();
let net_name = self.nets[net].name.clone();
let keep_foreign: Vec<bool> = self.keep.iter().map(|k| k.net != net_name).collect();
for l in 0..2 {
let mut other_pads = Mask::new(ww, hh);
let mut other_routes = Mask::new(ww, hh);
let mut big = Mask::new(ww, hh);
for jj in 0..hh {
let base = (pj0 as usize + jj) * g.w + pi0 as usize;
for ii in 0..ww {
let c = base + ii;
let pr = self.pad_raw[l][c];
let rr = self.route_raw[l][c];
let fr = self.fixed_raw[l][c];
let kr = self.keep_raw[l][c];
let op = (pr != -1 && pr != nid) || (fr != -1 && fr != nid) || (kr >= 0 && keep_foreign[kr as usize]);
let orr = rr != -1 && rr != nid;
other_pads.bits[jj * ww + ii] = op;
other_routes.bits[jj * ww + ii] = orr;
big.bits[jj * ww + ii] = self.big_clr[l].bits[c] && op;
}
}
let mut hard = other_pads.clone();
if !soft {
hard.or_with(&other_routes);
}
let mut o = sub.dilate(&hard, r_o);
o.or_with(&sub.dilate(&big, r_big));
let mut d = sub.dilate(&hard, r_d);
d.or_with(&sub.dilate(&big, r_big + 0.0707 / RES));
for jj in 0..hh {
for ii in 0..ww {
let c = (pj0 as usize + jj) * g.w + pi0 as usize + ii;
if !inside_w.bits[c] {
o.bits[jj * ww + ii] = true;
}
}
}
if l == F_CU && !self.ic_nets.is_empty() {
let foreign: Vec<i16> = self.ic_nets.iter().filter(|(_, nets)| !nets.contains(&net)).map(|(k, _)| *k as i16).collect();
if !foreign.is_empty() {
for jj in 0..hh {
for ii in 0..ww {
let c = (pj0 as usize + jj) * g.w + pi0 as usize + ii;
let bo = self.body_owner[c];
if bo != -1 && foreign.contains(&bo) {
o.bits[jj * ww + ii] = true;
d.bits[jj * ww + ii] = true;
}
}
}
}
}
let s = if soft { Some(sub.dilate(&other_routes, r_d)) } else { None };
obs_o[l] = crop(&o, ww, i0 - pi0, j0 - pj0, i1 - pi0, j1 - pj0);
obs_d[l] = crop(&d, ww, i0 - pi0, j0 - pj0, i1 - pi0, j1 - pj0);
softm[l] = s.map(|m| crop(&m, ww, i0 - pi0, j0 - pj0, i1 - pi0, j1 - pj0));
let mut hr = other_pads.clone();
hr.or_with(&other_routes);
// the net's own clearance (spec netClearance, which the gate's hint tells the AI to raise) and
// the same diagonal grid margin as tracks: with the rule clearance alone a via landed 0.186 mm
// from another net's track under a 0.2 mm rule, and raising netClearance could not move it
via_block.or_with(&sub.dilate(&hr, (clr + via / 2.0 + slop() + 0.0707) / RES));
via_block.or_with(&sub.dilate(&big, (self.big_clr_val + via / 2.0 + slop()) / RES));
}
if let Some(own) = self.smd_pad_cells.get(&(nid as usize)) {
let mut m = Mask::new(ww, hh);
for jj in 0..hh {
for ii in 0..ww {
m.bits[jj * ww + ii] = own.bits[(pj0 as usize + jj) * g.w + pi0 as usize + ii];
}
}
via_block.or_with(&sub.dilate(&m, (via / 2.0 + slop()) / RES));
}
{
let mut m = Mask::new(ww, hh);
for jj in 0..hh {
for ii in 0..ww {
m.bits[jj * ww + ii] = self.thru_cells.bits[(pj0 as usize + jj) * g.w + pi0 as usize + ii];
}
}
via_block.or_with(&sub.dilate(&m, (via / 2.0 + slop()) / RES));
}
// other vias: annular ring plus clearance for another net, hole to hole for this net
let mut groups: HashMap<i64, Vec<(usize, usize)>> = HashMap::new();
for &(vi, vj, vnid, dia, drill) in &self.vias {
if vi as i64 >= pi0 && vi as i64 <= pi1 && vj as i64 >= pj0 && vj as i64 <= pj1 {
let gap = self.via_gap(net, vnid, dia, drill);
groups.entry((gap * 10000.0).round() as i64).or_default().push(((vj as i64 - pj0) as usize, (vi as i64 - pi0) as usize));
}
}
for (gapk, cells) in groups {
let mut vm = Mask::new(ww, hh);
for (a, b) in cells {
vm.bits[a * ww + b] = true;
}
via_block.or_with(&sub.dilate(&vm, gapk as f64 / 10000.0 / RES - 1e-6));
}
for jj in 0..hh {
for ii in 0..ww {
let c = (pj0 as usize + jj) * g.w + pi0 as usize + ii;
if !inside_via.bits[c] {
via_block.bits[jj * ww + ii] = true;
}
}
}
let mut via_ok = crop(&via_block, ww, i0 - pi0, j0 - pj0, i1 - pi0, j1 - pj0);
for b in via_ok.bits.iter_mut() {
*b = !*b;
}
// an impedance-controlled net stays on the one layer its width was solved for: no vias
if let Some(l) = self.impedance_layer(net) {
for b in obs_o[1 - l].bits.iter_mut() {
*b = true;
}
for b in via_ok.bits.iter_mut() {
*b = false;
}
}
// issue #183: a layer the spec (or the project's .kicad_dru) leaves out is a wall, and with vias
// off the only layer change is on a through-hole pad of this net (the lead joins both faces)
if !self.rules.default_copper_mode() {
for l in 0..2 {
if !self.rules.layer_allowed(l) {
for b in obs_o[l].bits.iter_mut() { *b = true; }
}
}
if !self.rules.vias_allowed() {
let (ww, hw) = ((i1 - i0 + 1) as usize, (j1 - j0 + 1) as usize);
for jj in 0..hw {
for ii in 0..ww {
let c = (j0 as usize + jj) * g.w + i0 as usize + ii;
via_ok.bits[jj * ww + ii] = self.own_pth_cell(nid, c);
}
}
}
}
(obs_o, obs_d, softm, via_ok)
}
/// Issue #183: is grid cell `c` a through-hole pad of net id `nid` (copper of that net on both layers)?
pub fn own_pth_cell(&self, nid: i16, c: usize) -> bool {
self.thru_cells.bits[c] && self.pad_raw[F_CU][c] == nid && self.pad_raw[B_CU][c] == nid
}
/// Issue #183: the cluster of this net whose through-hole pad covers cell (i, j), if any.
pub fn pth_cluster_at(&self, net: usize, i: i32, j: i32) -> Option<usize> {
let c = j as usize * self.grid.w + i as usize;
self.nets[net].clusters.iter().position(|cl| cl.pads.iter().any(|p| self.board.pads[*p].thru && self.pad_cells.get(p).is_some_and(|cells| cells.contains(&c))))
}
/// The width of a net's escape stubs: the rule stub, or an impedance-controlled net's own width
/// when every one of its pads is at least that wide (so a pair leaves its pads already at the
/// solved width and gap; a narrower pad keeps the rule stub and its breakout stays uncontrolled).
pub fn stub_for(&self, net: usize) -> f64 {
match self.rules.impedance.get(&self.nets[net].name) {
Some(ir) if ir.width > self.rules.stub && self.nets[net].pads.iter().all(|&p| 2.0 * self.board.pads[p].half_short + 1e-9 >= ir.width) => ir.width,
_ => self.rules.stub,
}
}
/// The layer an impedance-controlled net is held to (impedance::table chose it).
pub fn impedance_layer(&self, net: usize) -> Option<usize> {
self.rules.impedance.get(&self.nets[net].name).and_then(|ir| match ir.layer.as_str() { "F.Cu" => Some(F_CU), "B.Cu" => Some(B_CU), _ => None })
}
/// For the second net of an impedance pair: the cells of the search window whose distance from
/// the partner's routed centre line is the pair's centre pitch (within 0.045 mm, so one 45 degree offset line qualifies).
/// The search charges PAIR_OFF for every step off this line, so the net follows its partner at
/// the solved gap wherever the obstacles allow. Returns (layer, window mask).
pub fn pair_guide(&self, net: usize, win: (i64, i64, i64, i64)) -> Option<(usize, Vec<bool>)> {
let ir = self.rules.impedance.get(&self.nets[net].name)?;
let (partner, pitch) = (ir.partner.as_ref()?, ir.pitch?);
let layer = self.impedance_layer(net)?;
let &pid = self.net_ids.get(partner)?;
let cells: Vec<Term> = self.nets[pid].routes.iter().flat_map(|r| r.cells.iter().copied()).filter(|t| t.layer() == layer).collect();
if cells.is_empty() {
return None;
}
let (i0, j0, i1, j1) = win;
let ww = (i1 - i0 + 1) as usize;
let hw = (j1 - j0 + 1) as usize;
let band = 0.045;
let reach = ((pitch + band) / RES).ceil() as i64;
let mut mind = vec![f64::INFINITY; ww * hw];
for t in &cells {
let (ci, cj) = (t.i as i64, t.j as i64);
if ci < i0 - reach || ci > i1 + reach || cj < j0 - reach || cj > j1 + reach {
continue;
}
for dj in -reach..=reach {
let j = cj + dj;
if j < j0 || j > j1 {
continue;
}
for di in -reach..=reach {
let i = ci + di;
if i < i0 || i > i1 {
continue;
}
let d = ((di * di + dj * dj) as f64).sqrt() * RES;
let k = (j - j0) as usize * ww + (i - i0) as usize;
if d < mind[k] {
mind[k] = d;
}
}
}
}
Some((layer, mind.iter().map(|d| (d - pitch).abs() <= band).collect()))
}
}
/// Crop a window-sized mask (row stride ww) to the inclusive sub-window.
fn crop(m: &Mask, ww: usize, a: i64, b: i64, c: i64, d: i64) -> Mask {
let w = (c - a + 1) as usize;
let h = (d - b + 1) as usize;
let mut out = Mask::new(w, h);
for jj in 0..h {
for ii in 0..w {
out.bits[jj * w + ii] = m.bits[(b as usize + jj) * ww + a as usize + ii];
}
}
out
}