app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
Colby Knox
1.0.59: EDA Bridge Standard board model (routing_state model:standard), layer_stack, pour_state, bodies, netlist, seven…
a0caf33
15d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
//! The standard board model on the boards this page ships, checked against the parser the routing
//! verbs already use (`pcb::pads`, `segments`, `vias`): same pads at the same place, same copper.
use std::path::PathBuf;
use kicad_core::board_model::{build, revision_of_text, Opts};
use kicad_core::pcb;
fn repo(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)
}
const BOARDS: &[&str] = &[
"demo/routing/esc/esc-g431-fable-placed-planes.kicad_pcb",
"demo/placement/esc-g431-unrouted.kicad_pcb",
"tour-pack-rp2040/template/rp2040-breakout.kicad_pcb",
"docs/autorouting/live-routing.kicad_pcb",
];
#[test]
fn pads_and_copper_agree_with_the_routing_parser() {
for rel in BOARDS {
let path = repo(rel);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{rel}: {e}"));
let legacy = pcb::parse_pcb_text(&text, &path.to_string_lossy()).unwrap();
let k = build(&text, None, &Opts { y_up: false }).unwrap();
let s = build(&text, None, &Opts::default()).unwrap();
assert_eq!(k["revision"], s["revision"], "{rel}: the revision does not depend on the frame");
let old = pcb::pads(&legacy);
let new = k["pads"].as_array().unwrap();
let mut checked = 0;
for p in &old {
if p.layers.iter().all(|l| !l.ends_with(".Cu")) {
continue;
}
let hit = new.iter().find(|n| n["ref"] == p.reference.as_str() && n["name"] == p.pad.as_str()
&& (n["x"].as_f64().unwrap() - p.x).abs() < 1e-5 && (n["y"].as_f64().unwrap() - p.y).abs() < 1e-5);
let hit = hit.or_else(|| new.iter().find(|n| n["ref"] == p.reference.as_str() && n["name"] == p.pad.as_str() && n.get("anchor").is_some()
&& (n["anchor"]["x"].as_f64().unwrap() - p.x).abs() < 1e-5 && (n["anchor"]["y"].as_f64().unwrap() - p.y).abs() < 1e-5));
let hit = hit.unwrap_or_else(|| panic!("{rel}: pad {} at ({}, {}) not in the model", p.name, p.x, p.y));
assert_eq!(hit["net"], p.net_name.as_str(), "{rel}: {}", p.name);
// y-up is the same pad mirrored
let i = new.iter().position(|n| std::ptr::eq(n, hit)).unwrap();
let up = &s["pads"][i];
assert!((up["y"].as_f64().unwrap() + hit["y"].as_f64().unwrap()).abs() < 1e-9);
checked += 1;
}
assert!(checked > 0, "{rel}");
assert_eq!(k["counts"]["tracks"].as_u64().unwrap() as usize, legacy.segments.len(), "{rel}");
assert_eq!(k["counts"]["vias"].as_u64().unwrap() as usize, legacy.vias.len(), "{rel}");
assert_eq!(k["counts"]["components"].as_u64().unwrap() as usize, legacy.footprints.len(), "{rel}");
assert!(k["outlineEdges"].as_array().unwrap().len() >= 4, "{rel}: a closed outline");
assert!(k.get("outlineOpenEdges").is_none(), "{rel}");
assert!(k["pads"].as_array().unwrap().iter().all(|p| p["sizeX"].as_f64().unwrap() > 0.0 && p["sizeY"].as_f64().unwrap() > 0.0), "{rel}: no 0 x 0 pad");
}
}
#[test]
fn revision_ignores_silkscreen_and_follows_copper() {
let text = std::fs::read_to_string(repo("docs/autorouting/live-routing.kicad_pcb")).unwrap();
let r0 = revision_of_text(&text).unwrap();
let at = text.rfind(')').unwrap();
let silk = format!("{}(gr_text \"note\" (at 1 1) (layer \"F.SilkS\") (effects (font (size 1 1))))\n)", &text[..at]);
assert_eq!(revision_of_text(&silk).unwrap(), r0);
let copper = format!("{}(segment (start 1 1) (end 2 1) (width 0.2) (layer \"F.Cu\") (net 0))\n)", &text[..at]);
assert_ne!(revision_of_text(&copper).unwrap(), r0);
}