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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
//! The designer's routing standards as numbers (PCB_Design_Process.md Part 2.2, 4.2, 4.4). These are the
//! defaults; a spec file may override them, with the same key names the reference board specs already use.
use crate::board::{Board, NetId};
use crate::geom::*;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug)]
pub struct Pair {
pub p: String,
pub n: String,
pub width: i64,
pub gap: i64,
}
/// One net class as written in the project. A field left out inherits from the Default class.
#[derive(Clone, Debug, Default)]
pub struct NetClass {
pub name: String,
/// Lower number wins when a net matches several classes; Default is always last.
pub priority: i64,
pub clearance: Option<i64>,
pub track: Option<i64>,
pub via_dia: Option<i64>,
pub via_drill: Option<i64>,
pub pair_width: Option<i64>,
pub pair_gap: Option<i64>,
}
/// A net's effective class after merging, every field filled.
#[derive(Clone, Debug)]
pub struct Resolved {
pub name: String,
pub clearance: i64,
pub track: i64,
pub via_dia: i64,
pub via_drill: i64,
pub pair_width: i64,
pub pair_gap: i64,
}
/// Project-wide minimums (.kicad_pro board/design_settings/rules and .kicad_dru). Zero: not set.
#[derive(Clone, Debug, Default)]
pub struct Minimums {
pub clearance: i64,
pub track: i64,
pub hole_clearance: i64,
pub hole_to_hole: i64,
pub via_dia: i64,
pub through_drill: i64,
pub annular: i64,
/// `solder_mask_to_copper_clearance`: what KiCad adds to a pad's mask margin when it tests
/// copper of another net against that opening.
pub mask_to_copper: i64,
}
/// Settings the user typed on the command line. The project file governs otherwise, and a spec's
/// numbers can only raise it: a spec is often an old run's, and must not lower his rule unasked. A
/// named setting wins, and the output board's project is updated to match, so DRC and the route agree
/// (reference rule: "if the user initially specifies a default the settings should be updated to match").
#[derive(Clone, Copy, Debug, Default)]
pub struct Explicit {
pub clearance: Option<i64>,
pub track: Option<i64>,
pub neck: Option<i64>,
pub via: Option<i64>,
pub drill: Option<i64>,
}
impl Explicit {
pub fn any(&self) -> bool {
self.clearance.is_some() || self.track.is_some() || self.neck.is_some() || self.via.is_some() || self.drill.is_some()
}
}
/// A clearance lowered for one net at one pad, in a .kicad_dru: the recorded exception for a pad no
/// track can leave under its class (reference decision on benchmark B U3.12: a local rule at the pad,
/// for review, rather than a lower class or a broken one).
#[derive(Clone, Debug, PartialEq)]
pub struct PadRule {
pub net: String,
pub reference: String,
pub pad: String,
pub min: i64,
}
impl PadRule {
/// The condition KiCad's DRC honours. Measured on KiCad 10 on a copy of benchmark B: this form
/// clears exactly the pad's two findings; `B.Parent.Reference` matches nothing and says nothing.
pub fn condition(&self) -> String {
format!("A.NetName == '{}' && B.memberOfFootprint('{}') && B.Pad_Number == '{}'", self.net, self.reference, self.pad)
}
/// Reads a condition `condition` wrote; any other condition is not one of these rules.
pub fn parse(cond: &str, min: i64) -> Option<PadRule> {
let rest = cond.strip_prefix("A.NetName == '")?;
let (net, rest) = rest.split_once("' && B.memberOfFootprint('")?;
let (reference, rest) = rest.split_once("') && B.Pad_Number == '")?;
let pad = rest.strip_suffix('\'')?;
Some(PadRule { net: net.to_string(), reference: reference.to_string(), pad: pad.to_string(), min })
}
}
/// One rail's current, as approved by the designer: spec `rails`. Part references, and amps.
#[derive(Clone, Debug, Default)]
pub struct RailSpec {
/// Where the current enters this net.
pub sources: Vec<String>,
/// The most the rail can carry: its worst-case sum, or what its source limits it to.
pub amps: f64,
/// What each part draws from this net. A part may list up to `amps`; a segment carries the sum
/// of what lies past it, never more than `amps`.
pub loads: std::collections::BTreeMap<String, f64>,
/// The rail's voltage, for the drop limit. None: the spec doesn't say, and no drop is judged
/// (a voltage is never guessed from a net's name here).
pub volts: Option<f64>,
}
#[derive(Clone, Debug)]
pub struct Rules {
pub explicit: Explicit,
/// In name order, and each rail's loads too: a hash map's order changes on every start of the
/// program, and sizing the rails in a different order gave different widths (clock.rs's test,
/// Benchmark C, 2026-11-09).
pub rails: std::collections::BTreeMap<String, RailSpec>,
/// Board under 3 x 3 in, set by `bind`: where 4.2's 1.25 x margin and floor apply.
pub small_board: bool,
/// Outer copper thickness, um, set by `bind`.
pub copper_um: f64,
pub pad_rules: Vec<PadRule>,
/// `pad_rules` on the bound board: (net, pad index) to clearance.
pub pad_exit: HashMap<(NetId, u32), i64>,
/// Minimum clearance on every net, fine-pitch escapes included (2.2).
pub clearance: i64,
/// Data line width on open runs, 0.2-0.3 mm (4.2).
pub track: i64,
/// Width leaving a fine-pitch part; necks up for long runs (4.2).
pub neck: i64,
pub via: i64,
pub drill: i64,
pub edge: i64,
/// Hole to hole, any net (2.2).
pub hole_hole: i64,
/// A pad's own via sits this far clear of the pad (4.4).
pub pad_via_gap: i64,
/// How far around a fine-pitch part counts as its escape.
pub escape_margin: f64,
/// Main-run floor for power on boards under 3 x 3 in (4.2).
pub power_floor: i64,
/// Explicit power widths per net: main run, then branches (spec wideNets).
pub wide: HashMap<String, Vec<i64>>,
/// Spec `loads` in amps per net and `maxRiseC`: what 4.2 says to size a rail from. Reported beside
/// the drawn widths; see `Router::rail_notes` for why they don't set a width yet.
pub loads: HashMap<String, f64>,
pub max_rise: Option<f64>,
pub pairs: Vec<Pair>,
pub kelvin: Vec<String>,
pub priority: Vec<String>,
pub plane_nets: Vec<String>,
pub skip_refs: Vec<String>,
/// Search cost weights in nm of equivalent track length. Bends are weighed against distance.
pub cost_bend45: f64,
pub cost_bend90: f64,
pub cost_via: f64,
pub cost_bottom: f64,
pub cost_tight: f64,
pub node_limit: usize,
pub passes: usize,
/// Whole-run budget in seconds; past it, the best result so far is returned (4.6).
pub time_limit: f64,
/// The standing minimum clearance before the project raises `clearance`: never go under it (2.2).
pub floor: i64,
pub classes: Vec<NetClass>,
/// netclass_patterns: (wildcard, class name).
pub patterns: Vec<(String, String)>,
/// netclass_assignments: net name to class names.
pub assignments: HashMap<String, Vec<String>>,
pub min: Minimums,
/// spec placement.innerFills: (footprint, net) that must be filled under the part (4.7).
pub inner_fills: Vec<(String, String)>,
/// rule_severities from the project (error, warning, ignore).
pub severities: HashMap<String, String>,
/// Filled by `bind`: effective classes, the class of each net id, and coupled pairs.
resolved: Vec<Resolved>,
net_class: Vec<u32>,
coupled: HashSet<(NetId, NetId)>,
max_clearance: i64,
/// When the search must stop, so the time limit holds even inside one long search.
pub deadline: Option<crate::clock::Instant>,
/// A* heuristic inflation; a soft search may run greedier than a first attempt.
pub search_weight: f64,
}
impl Default for Rules {
fn default() -> Self {
Rules {
explicit: Explicit::default(),
rails: Default::default(),
small_board: false,
copper_um: 35.0,
pad_rules: Vec::new(),
pad_exit: HashMap::new(),
clearance: mm(0.15),
track: mm(0.2),
neck: mm(0.15),
via: mm(0.6),
drill: mm(0.3),
edge: mm(0.3),
hole_hole: mm(0.25),
pad_via_gap: mm(0.1),
escape_margin: 1.0 * NM,
power_floor: mm(0.4),
wide: HashMap::new(),
loads: HashMap::new(),
max_rise: None,
pairs: Vec::new(),
kelvin: Vec::new(),
priority: Vec::new(),
plane_nets: Vec::new(),
skip_refs: Vec::new(),
cost_bend45: 0.3 * NM,
cost_bend90: 1.2 * NM,
cost_via: 3.0 * NM,
cost_bottom: 1.05,
cost_tight: 1.5,
node_limit: 800_000,
passes: 6,
time_limit: 240.0,
floor: mm(0.15),
classes: Vec::new(),
patterns: Vec::new(),
assignments: HashMap::new(),
min: Minimums::default(),
inner_fills: Vec::new(),
severities: HashMap::new(),
resolved: Vec::new(),
net_class: Vec::new(),
coupled: HashSet::new(),
max_clearance: 0,
deadline: None,
search_weight: 1.2,
}
}
}
impl Rules {
/// Reads a spec. Unknown keys are ignored, so a board's existing spec file works as it is.
pub fn load(path: &str) -> Result<Rules, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{path}: {e}"))?;
let mut r = Rules::default();
let num = |k: &str| v.get(k).and_then(|x| x.as_f64());
if let Some(x) = num("clearance") { r.clearance = mm(x); r.floor = mm(x); }
if let Some(x) = num("track") { r.track = mm(x); }
if let Some(x) = num("stub") { r.neck = mm(x); }
if let Some(x) = num("viaSignal") { r.via = mm(x); }
if let Some(x) = num("drillSignal") { r.drill = mm(x); }
if let Some(x) = num("edgeClearance") { r.edge = mm(x); }
if let Some(x) = v.pointer("/current/minPowerWidthMm").and_then(|x| x.as_f64()) { r.power_floor = mm(x); }
if let Some(o) = v.get("wideNets").and_then(|x| x.as_object()) {
for (k, a) in o {
let w: Vec<i64> = a.as_array().map(|a| a.iter().filter_map(|x| x.as_f64()).map(mm).collect()).unwrap_or_default();
r.wide.insert(k.clone(), w);
}
}
if let Some(o) = v.get("impedanceTargets").and_then(|x| x.as_object()) {
for t in o.values() {
let (Some(p), Some(n)) = (t.pointer("/pair/0").and_then(|x| x.as_str()), t.pointer("/pair/1").and_then(|x| x.as_str())) else { continue };
// Zero: the spec doesn't say; find_pairs takes it from the net class, or flags a default.
let width = t.get("widthMm").and_then(|x| x.as_f64()).map(mm).unwrap_or(0);
let gap = t.get("gapMm").and_then(|x| x.as_f64()).map(mm).unwrap_or(0);
r.pairs.push(Pair { p: p.to_string(), n: n.to_string(), width, gap });
}
}
if let Some(o) = v.get("rails").and_then(|x| x.as_object()) {
for (net, x) in o {
let list = |k: &str| x.get(k).and_then(|l| l.as_array()).map(|l| l.iter().filter_map(|s| s.as_str().map(String::from)).collect()).unwrap_or_default();
let loads = x.get("loads").and_then(|l| l.as_object()).map(|m| m.iter().filter_map(|(k, a)| a.as_f64().map(|a| (k.clone(), a))).collect()).unwrap_or_default();
r.rails.insert(net.clone(), RailSpec { sources: list("sources"), amps: x.get("amps").and_then(|a| a.as_f64()).unwrap_or(0.0), loads, volts: x.get("volts").and_then(|a| a.as_f64()).filter(|v| *v > 0.0) });
}
}
if let Some(o) = v.get("loads").and_then(|x| x.as_object()) {
for (k, l) in o {
if let Some(a) = l.get("amps").and_then(|x| x.as_f64()) {
r.loads.insert(k.clone(), a);
}
}
}
r.max_rise = num("maxRiseC");
if let Some(a) = v.get("routePriority").and_then(|x| x.as_array()) {
r.priority = a.iter().filter_map(|x| x.as_str().map(String::from)).collect();
}
if let Some(o) = v.get("kelvin").and_then(|x| x.as_object()) {
r.kelvin = o.keys().cloned().collect();
}
if let Some(o) = v.get("planes").and_then(|x| x.as_object()) {
r.plane_nets = o.keys().cloned().collect();
}
// Parked parts are written as a list or as a map of reference to reason.
match v.pointer("/placement/parked") {
Some(Value::Array(a)) => r.skip_refs = a.iter().filter_map(|x| x.as_str().map(String::from)).collect(),
Some(Value::Object(o)) => r.skip_refs = o.keys().cloned().collect(),
_ => {}
}
if let Some(o) = v.pointer("/placement/innerFills").and_then(|x| x.as_object()) {
for (fp, nets) in o {
for n in nets.as_array().map(|a| a.as_slice()).unwrap_or(&[]) {
if let Some(n) = n.as_str() {
r.inner_fills.push((fp.clone(), n.to_string()));
}
}
}
}
Ok(r)
}
/// The spec's rail for this net name, whatever its case (+3v3, +3V3).
pub fn rail(&self, net: &str) -> Option<&RailSpec> {
self.rails.get(net).or_else(|| self.rails.iter().find(|(k, _)| k.eq_ignore_ascii_case(net)).map(|(_, v)| v))
}
/// Width 4.2 asks for at this current: the conservative chart at the design's rise, 20 C by
/// default. On a board under 3 x 3 in a main run also gets 1.25 x and the floor.
pub fn rail_width(&self, amps: f64, main: bool) -> i64 {
let w = crate::current::conservative_mm(amps, self.max_rise.unwrap_or(20.0), self.copper_um);
let w = if main && self.small_board { (w * 1.25).max(to_mm(self.power_floor)) } else { w };
mm((w * 100.0).ceil() / 100.0)
}
/// Width that holds the copper's voltage drop over `len` (nm) at `amps` to the limit he set
/// (current.rs DROP_LIMIT of the rail's `volts`). Zero when any of them is missing.
pub fn drop_width(&self, amps: f64, len: f64, volts: f64) -> i64 {
if amps <= 0.0 || len <= 0.0 || volts <= 0.0 || self.copper_um <= 0.0 {
return 0;
}
let w = crate::current::RHO_CU * (len * 1e-9) * amps / (self.copper_um * 1e-6 * volts * crate::current::DROP_LIMIT);
mm((w * 1000.0 * 100.0).ceil() / 100.0)
}
/// Whether the spec records a current for this net.
pub fn has_load(&self, net: &str) -> bool {
self.loads.keys().any(|l| l.eq_ignore_ascii_case(net))
}
/// Works out every net's effective class on this board. Net ids belong to one parsed file, so
/// call this again for another board; without it the `*_for` calls answer with the single
/// strictest number in `clearance`, which is safe but pessimistic.
pub fn bind(&mut self, board: &Board) {
let (bw, bh) = (board.bbox.2 - board.bbox.0, board.bbox.3 - board.bbox.1);
self.small_board = bw > 0.0 && bw < 76.2 * NM && bh < 76.2 * NM;
self.copper_um = crate::current::outer_copper_um(&board.text);
self.pad_exit.clear();
let lowest = self.floor.max(self.min.clearance);
for rule in &self.pad_rules {
let Some(&net) = board.net_ids.get(&rule.net) else { continue };
for (i, p) in board.pads.iter().enumerate() {
if p.number == rule.pad && board.footprints[p.fp].reference == rule.reference {
self.pad_exit.insert((net, i as u32), rule.min.max(lowest));
}
}
}
self.resolved.clear();
self.net_class = vec![0; board.nets.len()];
self.coupled.clear();
self.max_clearance = self.clearance.max(self.floor);
if self.classes.is_empty() {
return;
}
let mut seen: HashMap<String, u32> = HashMap::new();
let names: Vec<String> = board.nets.clone();
for (id, net) in names.iter().enumerate() {
let matched = self.classes_of(net);
let key = matched.join("\u{1}");
let idx = match seen.get(&key) {
Some(i) => *i,
None => {
let i = self.resolved.len() as u32;
let res = self.resolve(&matched);
self.max_clearance = self.max_clearance.max(res.clearance);
self.resolved.push(res);
seen.insert(key, i);
i
}
};
self.net_class[id] = idx;
}
for (i, n) in names.iter().enumerate() {
if let Some(m) = pair_partner(n) {
if let Some(&j) = board.net_ids.get(&m) {
let (a, b) = (i as NetId, j);
self.coupled.insert((a.min(b), a.max(b)));
}
}
}
}
/// The class names a net belongs to, by explicit assignment and by pattern, best priority first.
pub fn classes_of(&self, net: &str) -> Vec<String> {
let mut names: Vec<&str> = Vec::new();
if let Some(list) = self.assignments.get(net) {
names.extend(list.iter().map(|s| s.as_str()));
}
names.extend(self.patterns.iter().filter(|(p, _)| wildcard(p, net)).map(|(_, c)| c.as_str()));
let mut v: Vec<&NetClass> = self.classes.iter().filter(|c| names.contains(&c.name.as_str())).collect();
v.sort_by_key(|c| c.priority);
v.into_iter().map(|c| c.name.clone()).collect()
}
fn resolve(&self, matched: &[String]) -> Resolved {
let def = self.classes.iter().find(|c| c.name == "Default");
let mut chain: Vec<&NetClass> = matched.iter().filter_map(|m| self.classes.iter().find(|c| &c.name == m)).collect();
chain.extend(def);
let pick = |f: fn(&NetClass) -> Option<i64>, fallback: i64| chain.iter().find_map(|c| f(c)).unwrap_or(fallback);
Resolved {
name: chain.first().map(|c| c.name.clone()).unwrap_or_else(|| "Default".into()),
clearance: pick(|c| c.clearance, self.floor),
track: pick(|c| c.track, self.track),
via_dia: pick(|c| c.via_dia, self.via),
via_drill: pick(|c| c.via_drill, self.drill),
pair_width: pick(|c| c.pair_width, 0),
pair_gap: pick(|c| c.pair_gap, 0),
}
}
pub fn class_of(&self, net: NetId) -> Option<&Resolved> {
self.net_class.get(net as usize).and_then(|&i| self.resolved.get(i as usize))
}
/// Clearance between two nets, the way KiCad's DRC decides it: the larger of the two classes;
/// a coupled differential pair may instead use its class's gap when that is smaller; and never
/// under the board minimum or the designer's standing minimum (2.2).
pub fn clearance_for(&self, a: NetId, b: NetId) -> i64 {
let (Some(ca), Some(cb)) = (self.class_of(a), self.class_of(b)) else { return self.clearance };
let mut c = ca.clearance.max(cb.clearance);
if a != b && self.coupled.contains(&(a.min(b), a.max(b))) {
let gap = ca.pair_gap.max(cb.pair_gap);
if gap > 0 {
c = c.min(gap);
}
}
c.max(self.floor).max(self.min.clearance)
}
/// `clearance_for`, unless a pad rule lowers it for this net at this pad.
pub fn clearance_at(&self, net: NetId, other: NetId, pad: Option<u32>) -> i64 {
pad.and_then(|p| self.pad_exit.get(&(net, p)).copied()).unwrap_or_else(|| self.clearance_for(net, other))
}
/// The largest value `clearance_for` can return on the bound board: the reach for an index query.
pub fn max_clearance(&self) -> i64 {
self.max_clearance.max(self.clearance).max(self.floor)
}
/// The class's track width. A rail class may hold a current width (10 mm on the benchmark B): it
/// is a default for drawing, not a limit.
pub fn track_for(&self, net: NetId) -> i64 {
self.class_of(net).map(|c| c.track).unwrap_or(self.track)
}
/// (diameter, drill) of the class's via.
pub fn via_for(&self, net: NetId) -> (i64, i64) {
self.class_of(net).map(|c| (c.via_dia, c.via_drill)).unwrap_or((self.via, self.drill))
}
/// (width, gap) of the class's differential pair, if it sets one.
pub fn pair_for(&self, net: NetId) -> Option<(i64, i64)> {
self.class_of(net).filter(|c| c.pair_width > 0 && c.pair_gap > 0).map(|c| (c.pair_width, c.pair_gap))
}
/// Copper of another net to a hole, plated or not. A project's min_hole_clearance when it has one.
/// His document has no number for this: 2.2's 0.25 is hole to hole, a different rule. The 0.25
/// here is the value KiCad projects carry untouched (134 of 176 of his project files, and the
/// Benchmark C and benchmark B; v1.3 sets 0.2), so a board with no project is judged as KiCad would.
pub fn hole_clearance(&self) -> i64 {
if self.min.hole_clearance > 0 { self.min.hole_clearance } else { mm(0.25) }
}
/// Hole to hole, any net. The project's value is the one confirmed for this design; 2.2's 0.25
/// is a starting value to confirm.
pub fn hole_to_hole(&self) -> i64 {
if self.min.hole_to_hole > 0 { self.min.hole_to_hole } else { self.hole_hole }
}
/// Finding severity for a KiCad rule name as this project sets it. A rule the project ignores
/// stays visible as a warning: hiding it is not this tool's call.
pub fn severity(&self, rule: &str) -> &'static str {
match self.severities.get(rule).map(|s| s.as_str()) {
Some("warning") | Some("ignore") | Some("exclusion") => "warn",
_ => "error",
}
}
}
/// KiCad's pair naming: the same name ending in + and -, or P and N.
pub fn pair_partner(net: &str) -> Option<String> {
let (stem, last) = net.split_at(net.len().checked_sub(1)?);
let other = match last {
"+" => "-",
"-" => "+",
"P" => "N",
"N" => "P",
_ => return None,
};
Some(format!("{stem}{other}"))
}
/// Anchored wildcard match with * and ?, as a netclass pattern is matched. Regular-expression
/// patterns (/.../) are not honoured; project.rs reports them.
pub fn wildcard(pat: &str, s: &str) -> bool {
let (p, t): (Vec<char>, Vec<char>) = (pat.chars().collect(), s.chars().collect());
let (mut i, mut j, mut star, mut mark) = (0usize, 0usize, None, 0usize);
while j < t.len() {
if i < p.len() && (p[i] == '?' || p[i] == t[j]) {
i += 1;
j += 1;
} else if i < p.len() && p[i] == '*' {
star = Some(i);
mark = j;
i += 1;
} else if let Some(st) = star {
i = st + 1;
mark += 1;
j = mark;
} else {
return false;
}
}
while i < p.len() && p[i] == '*' {
i += 1;
}
i == p.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wildcard_is_anchored() {
assert!(wildcard("*USB2_D*", "/MCU_USB2_D+"));
assert!(wildcard("+PS1_SENS", "+PS1_SENS"));
assert!(!wildcard("-PS1", "-PS10"));
assert!(wildcard("D?", "D1"));
assert!(!wildcard("D?", "D12"));
}
#[test]
fn pair_names() {
assert_eq!(pair_partner("/MCU_USB2_D+").as_deref(), Some("/MCU_USB2_D-"));
assert_eq!(pair_partner("USB.D_P").as_deref(), Some("USB.D_N"));
assert_eq!(pair_partner("GND"), None);
}
fn board_with(nets: &[&str]) -> Board {
let mut text = String::from("(kicad_pcb (layers (0 \"F.Cu\" signal) (2 \"B.Cu\" signal))");
for (i, n) in nets.iter().enumerate() {
text.push_str(&format!(" (segment (start 0 {i}) (end 1 {i}) (width 0.2) (layer \"F.Cu\") (net \"{n}\"))"));
}
text.push(')');
Board::parse(text).unwrap()
}
#[test]
fn clearance_is_pairwise_and_keeps_the_floor() {
// Benchmark B's classes: Default 0.2, USB 0.4 with a 0.12 pair gap, a rail at 0.5.
let b = board_with(&["A", "B", "/USB_D+", "/USB_D-", "RAIL"]);
let mut r = Rules::default();
let cl = |name: &str, p: i64, c: f64, gap: Option<f64>| NetClass { name: name.into(), priority: p, clearance: Some(mm(c)), pair_gap: gap.map(mm), pair_width: gap.map(|_| mm(0.2)), ..Default::default() };
r.classes = vec![cl("Default", i64::MAX, 0.2, Some(0.25)), cl("USB", 7, 0.4, Some(0.12)), cl("RAIL", 0, 0.5, None)];
r.patterns = vec![("*USB_D*".into(), "USB".into()), ("RAIL".into(), "RAIL".into())];
r.bind(&b);
let id = |n: &str| b.net_ids[n];
assert_eq!(r.clearance_for(id("A"), id("B")), mm(0.2));
assert_eq!(r.clearance_for(id("A"), id("/USB_D+")), mm(0.4));
assert_eq!(r.clearance_for(id("RAIL"), id("/USB_D+")), mm(0.5));
// The coupled pair uses its gap, but 0.12 is under the standing 0.15, so 0.15.
assert_eq!(r.clearance_for(id("/USB_D+"), id("/USB_D-")), mm(0.15));
// Fields a class leaves out come from Default.
assert_eq!(r.class_of(id("RAIL")).unwrap().pair_gap, mm(0.25));
assert_eq!(r.max_clearance(), mm(0.5));
}
#[test]
fn unbound_rules_answer_with_the_single_number() {
let r = Rules { clearance: mm(0.17), ..Default::default() };
assert_eq!(r.clearance_for(1, 2), mm(0.17));
}
}