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
John Lauer
1.0.3: 1.0.3: placement. kicad_placement_state, kicad_move_footprint (a batch of moves as one native undo step, refused…
2569119
1mo ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
//! The placement demo fixture (`demo/placement/`, built by `tools/make_placement_fixture.py`
//! from the public Adom ESC G431 board) read back through the bridge's own parser: both
//! copies keep every footprint, carry zero segments and zero vias, and the unplaced copy
//! has every footprint parked to the right of the outline with no courtyard collision,
//! which is exactly what `kicad_placement_state` and `kicad_placement_validate` report.
use std::path::{Path, PathBuf};
use kicad_core::pcb;
use kicad_core::placement;
fn repo_file(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)
}
fn load(name: &str) -> pcb::Board {
let p = repo_file(&format!("demo/placement/{name}"));
let text = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()));
pcb::parse_pcb_text(&text, &p.to_string_lossy()).unwrap()
}
#[test]
fn unplaced_copy_is_parked_unrouted_and_collision_free() {
let b = load("esc-g431-unplaced.kicad_pcb");
assert_eq!(b.footprints.len(), 149);
assert_eq!(b.segments.len(), 0);
assert_eq!(b.vias.len(), 0);
assert_eq!(b.net_format, pcb::NetFormat::Name);
assert_eq!(pcb::copper_layers(&b), vec!["F.Cu", "In1.Cu", "In2.Cu", "B.Cu"]);
// Teardrop zones are gone with the copper; the user's pours stay.
assert_eq!(b.zones.len(), 49);
let outline = placement::outline(&b).expect("Edge.Cuts outline");
assert_eq!((outline.min_x, outline.min_y, outline.max_x, outline.max_y), (104.0, 58.0, 168.0, 132.0));
let places = placement::placements(&b);
assert!(places.iter().all(|p| p.rotation == 0.0), "parked footprints are at rotation 0");
assert!(places.iter().all(|p| p.inside_outline == Some(false)));
let parked = placement::parked(&places, Some(&outline));
assert_eq!(parked.len(), 149, "every footprint sits to the right of the outline");
assert!(places.iter().all(|p| p.courtyard.min_x >= 173.0 - 1e-6), "5 mm margin from the outline");
assert_eq!(placement::overlaps(&places), Vec::<placement::Overlap>::new(), "the parking grid has no courtyard collisions");
assert!(places.iter().all(|p| p.side == "F.Cu"), "the ESC is assembled on one side and the tool keeps every side as it was");
let with_courtyard = places.iter().filter(|p| p.courtyard_source == "courtyard").count();
assert!(with_courtyard > 140, "{with_courtyard} of 149 have a courtyard shape");
let s = placement::state(&b, false);
assert_eq!(s["footprintCount"], 149);
assert_eq!(s["padNetsOmitted"], false);
assert!(s["ratsnest"]["totalMm"].as_f64().unwrap() > 1000.0, "parked parts have a long ratsnest");
let v = placement::validate(&b);
assert_eq!(v["placed"], false);
assert_eq!(v["courtyardOverlapCount"], 0);
assert_eq!(v["parked"].as_array().unwrap().len(), 149);
}
#[test]
fn unrouted_copy_keeps_the_original_placement() {
let b = load("esc-g431-unrouted.kicad_pcb");
assert_eq!(b.footprints.len(), 149);
assert_eq!(b.segments.len(), 0);
assert_eq!(b.vias.len(), 0);
assert_eq!(b.zones.len(), 49);
let u2 = b.footprints.iter().find(|f| f.reference == "U2").unwrap();
let at = u2.position.as_ref().unwrap();
assert_eq!((at.x, at.y, at.rotation), (130.895002, 87.0, Some(-90.0)));
let places = placement::placements(&b);
let inside = places.iter().filter(|p| p.inside_outline == Some(true)).count();
assert!(inside >= 140, "{inside} of 149 inside the outline (the board's own placement)");
let unplaced = load("esc-g431-unplaced.kicad_pcb");
let (_, routed_total) = placement::nets(&b);
let (nets, parked_total) = placement::nets(&unplaced);
assert!(parked_total > routed_total * 2.0, "the ratsnest estimate falls when parts are placed: {parked_total} parked vs {routed_total} placed");
let gnd = nets.iter().find(|n| n.name == "GND").expect("GND net");
assert!(gnd.refs.len() > 20);
}