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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
//! Writes copper into a copy of the board. Nothing else in the file changes. The input file is
//! never overwritten (4.6: never overwrite a board someone has reviewed).
use crate::board::*;
use crate::geom::*;
use crate::router::*;
use std::fmt::Write;
fn uuid(seed: u64) -> String {
// A fixed mix: stable across runs for the same order, unique within a file.
let mut x = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0xA5A5_5A5A_DEAD_BEEF;
let mut next = || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
let (a, b) = (next(), next());
format!("{:08x}-{:04x}-4{:03x}-8{:03x}-{:012x}", (a >> 32) as u32, (a >> 16) as u16, a & 0xfff, (b >> 48) & 0xfff, b & 0xffff_ffff_ffff)
}
/// The board file's own number for each net name, from its `(net N "name")` table. A file that
/// numbers its nets (KiCad 9 and earlier) has to get those numbers back on new copper: the parser
/// numbers nets in the order it meets them, which is not the file's order. Written with the
/// parser's number, every track on a small sensor board carried another net's number
/// (2026-11-10); KiCad re-derives a track's net from the pads it touches when it loads a board, so
/// its DRC said nothing, and only this program's own check of the written file showed it.
fn file_numbers(board: &Board) -> std::collections::HashMap<String, String> {
let mut out = std::collections::HashMap::new();
for line in board.text.lines() {
let l = line.trim();
let Some(rest) = l.strip_prefix("(net ") else { continue };
let Some((num, name)) = rest.split_once(' ') else { continue };
if !num.chars().all(|c| c.is_ascii_digit()) || !name.starts_with('"') || !name.ends_with("\")") {
continue;
}
let name = &name[1..name.len() - 2];
out.entry(name.replace("\\\"", "\"").replace("\\\\", "\\")).or_insert_with(|| num.to_string());
}
out
}
fn net_field(board: &Board, net: NetId, numbers: Option<&std::collections::HashMap<String, String>>) -> String {
match numbers {
Some(table) => table.get(board.net_name(net)).cloned().unwrap_or_else(|| "0".to_string()),
None => format!("\"{}\"", board.net_name(net).replace('\\', "\\\\").replace('"', "\\\"")),
}
}
fn n(v: i64) -> String {
let s = format!("{:.6}", v as f64 / NM);
let t = s.trim_end_matches('0').trim_end_matches('.');
t.to_string()
}
pub fn render(board: &Board, routes: &[&Route], rules: &crate::rules::Rules) -> String {
let numeric = board.text.contains("\n\t(net 0 ") || board.text.contains("\n (net 0 ");
let numbers = numeric.then(|| file_numbers(board));
let mut out = String::new();
let mut seed = 1u64;
for r in routes {
let net = net_field(board, r.task.net, numbers.as_ref());
for s in &r.segs {
seed += 1;
let _ = write!(out, "\t(segment\n\t\t(start {} {})\n\t\t(end {} {})\n\t\t(width {})\n\t\t(layer \"{}\")\n\t\t(net {})\n\t\t(uuid \"{}\")\n\t)\n", n(s.a.0), n(s.a.1), n(s.b.0), n(s.b.1), n(s.width), board.layers[s.layer].name, net, uuid(seed));
}
for v in &r.vias {
seed += 1;
// A via with no track of its own (a stitching via, a return via, a via in a pad) is marked
// free. KiCad gives any other via the net of whatever copper it lands in when the board
// loads, and the file still holds the OLD plane fills: on a large Ethernet switch board a
// GND via through the +3.3V plane came back as a +3.3V via, 0.11 mm from a GND pad.
// Checked in KiCad 10: with the flag the via keeps its net and the finding is gone.
let free = if r.segs.is_empty() { "\n\t\t(free yes)" } else { "" };
let _ = write!(out, "\t(via\n\t\t(at {} {})\n\t\t(size {})\n\t\t(drill {})\n\t\t(layers \"F.Cu\" \"B.Cu\"){free}\n\t\t(net {})\n\t\t(uuid \"{}\")\n\t)\n", n(v.0), n(v.1), n(rules.via_for(r.task.net).0), n(rules.via_for(r.task.net).1), net, uuid(seed));
}
}
let end = board.text.rfind(')').unwrap_or(board.text.len());
let mut text = String::with_capacity(board.text.len() + out.len());
text.push_str(&board.text[..end]);
text.push_str(&out);
text.push_str(&board.text[end..]);
text
}
/// A copy of the file with every track and via removed, for measuring against a reference.
pub fn strip(board: &Board) -> String {
let mut spans: Vec<(usize, usize)> = board.tracks.iter().map(|t| t.span).chain(board.vias.iter().map(|v| v.span)).chain(board.teardrop_spans.iter().copied()).collect();
spans.sort();
spans.dedup();
let mut text = String::with_capacity(board.text.len());
let mut at = 0;
for (a, b) in spans {
if a < at {
continue;
}
text.push_str(&board.text[at..a]);
at = b;
}
text.push_str(&board.text[at..]);
text
}