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.
← Commit history
1.0.18: NATIVE (Rust) build. 1.0.18: inner plane layers go into the Freerouting DSN as (type power), vias only, so signals stop crossing the planes (dsn.stats.powerLayers); the reply reports routed.widened and perNet[].widened (wiki #93).
4 files changed
+79−8
rust/Cargo.lock+3−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.17"+version = "1.0.18" dependencies = [ "kicad-core", "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.17"+version = "1.0.18" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.17"+version = "1.0.18" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.17"+version = "1.0.18" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_routing.rs+6−2@@ -990,10 +990,14 @@ fn freerouting_execute(state: &mut State, prep: Prepared, phase: &Phase, own_gui let segments: usize = copper.iter().map(|c| c.segments.len()).sum(); let vias: usize = copper.iter().map(|c| c.vias.len()).sum(); let skipped: usize = copper.iter().map(|c| c.skipped_existing).sum();+ // Wiki #93: Freerouting can hand back wires a hair under the net class width (0.1874 mm+ // against 0.2); they land at the class width and are counted here, so the reply says how+ // much of the router's answer was corrected instead of hiding it.+ let widened: usize = copper.iter().map(|c| c.widened).sum(); let routed = json!({ "nets": routed_names, "netCount": copper.len(), "segments": segments, "vias": vias,- "skippedExisting": skipped, "unroutedNets": unrouted,- "perNet": copper.iter().map(|c| json!({"net": c.net, "segments": c.segments.len(), "vias": c.vias.len()})).collect::<Vec<_>>(),+ "skippedExisting": skipped, "unroutedNets": unrouted, "widened": widened,+ "perNet": copper.iter().map(|c| json!({"net": c.net, "segments": c.segments.len(), "vias": c.vias.len(), "widened": c.widened})).collect::<Vec<_>>(), }); let dsn_info = json!({"stats": out.stats, "rules": rules, "viaPadstack": out.via_padstack}); let mut response = json!({
rust/crates/kicad-core/src/dsn.rs+69−2@@ -437,6 +437,9 @@ pub struct DsnStats { pub approximated_pads: Vec<String>, pub planes: usize, pub keepouts: usize,+ /// Inner layers written as `(type power)`: the router only drops vias into them.+ #[serde(rename = "powerLayers")]+ pub power_layers: Vec<String>, pub notes: Vec<String>, } @@ -714,6 +717,8 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt // ---- zones: copper zones as planes, rule areas as keepouts let mut planes = String::new();+ // Largest copper-plane polygon per layer, in mm^2, to decide which inner layers are planes.+ let mut plane_area: BTreeMap<String, f64> = BTreeMap::new(); for z in tree.find_all("zone") { let mut layers: Vec<String> = z.find("layers").and_then(Sx::as_list).map(|l| l.iter().skip(1).filter_map(Sx::as_atom).map(str::to_string).collect()).unwrap_or_default(); if let Some(l) = z.find("layer").and_then(Sx::value) {@@ -724,7 +729,8 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt continue; } let Some(pts) = z.find("polygon").and_then(|p| p.find("pts")) else { continue };- let points: Vec<String> = pts.find_all("xy").filter_map(|p| Some(pt(atom_f64(p, 1)?, atom_f64(p, 2)?))).collect();+ let coords: Vec<(f64, f64)> = pts.find_all("xy").filter_map(|p| Some((atom_f64(p, 1)?, atom_f64(p, 2)?))).collect();+ let points: Vec<String> = coords.iter().map(|(x, y)| pt(*x, *y)).collect(); if points.len() < 3 { continue; }@@ -737,6 +743,9 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt } else if !net.is_empty() { planes.push_str(&format!(" (plane {} (polygon {} 0 {}))\n", q(&net), layer, points.join(" "))); stats.planes += 1;+ let a = polygon_area(&coords);+ let e = plane_area.entry(layer.clone()).or_insert(0.0);+ *e = e.max(a); } } }@@ -772,6 +781,26 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt if source != "Edge.Cuts" { stats.notes.push(format!("boundary: {source}")); }+ // Wiki #93: an inner layer that IS a plane must not carry signal traces. Freerouting+ // honours `(type power)` by only dropping vias into it; written as `signal`, it happily+ // routed /nRST and /HSE_OUT across the +3V3 plane. A layer counts as a plane when KiCad+ // declares it `power`, or when one copper zone on it covers at least half the board.+ // Outer layers never do: parts sit there, and a full GND pour on B.Cu is still routable.+ let board_area = polygon_area(&boundary);+ let outer = [copper.first().cloned(), copper.last().cloned()];+ for layer in &copper {+ if outer.iter().any(|o| o.as_deref() == Some(layer.as_str())) {+ continue;+ }+ let declared = board.layers.iter().any(|l| &l.name == layer && l.kind == "power");+ let covered = board_area > 0.0 && plane_area.get(layer).map(|a| *a >= 0.5 * board_area).unwrap_or(false);+ if declared || covered {+ stats.power_layers.push(layer.clone());+ }+ }+ if !stats.power_layers.is_empty() {+ stats.notes.push(format!("plane layers reserved (type power, vias only): {}", stats.power_layers.join(", ")));+ } let mut boundary_pts: Vec<String> = boundary.iter().map(|(x, y)| pt(*x, *y)).collect(); boundary_pts.push(boundary_pts[0].clone()); @@ -821,7 +850,8 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt out.push_str(" (resolution um 10)\n (unit um)\n"); out.push_str(" (structure\n"); for (i, layer) in copper.iter().enumerate() {- out.push_str(&format!(" (layer {}\n (type signal)\n (property\n (index {i})\n )\n )\n", layer));+ let kind = if stats.power_layers.contains(layer) { "power" } else { "signal" };+ out.push_str(&format!(" (layer {}\n (type {kind})\n (property\n (index {i})\n )\n )\n", layer)); } out.push_str(&format!(" (boundary\n (path pcb 0 {})\n )\n", boundary_pts.join(" "))); out.push_str(&planes);@@ -1244,6 +1274,43 @@ mod tests { assert!(t.contains("(place U1 20000 -10000 front 0)")); } + // Wiki #93: Freerouting routed /nRST and /HSE_OUT across the ESC's +3V3 plane because+ // every layer was declared `signal`. A plane layer is now `power` (vias only).+ #[test]+ fn inner_plane_layers_are_reserved_as_power_and_outer_pours_are_not() {+ let text = r#"(kicad_pcb (version 20260206) (generator "pcbnew")+ (layers (0 "F.Cu" signal) (4 "In1.Cu" signal) (6 "In2.Cu" signal) (8 "In3.Cu" power) (2 "B.Cu" signal) (25 "Edge.Cuts" user))+ (gr_rect (start 0 0) (end 40 20) (layer "Edge.Cuts"))+ (footprint "X:R1" (layer "F.Cu") (at 10 10) (property "Reference" "R1")+ (pad "1" smd rect (at -1 0) (size 1 1) (layers "F.Cu") (net "GND"))+ (pad "2" smd rect (at 1 0) (size 1 1) (layers "F.Cu") (net "SIG")))+ (footprint "X:R2" (layer "F.Cu") (at 30 10) (property "Reference" "R2")+ (pad "1" smd rect (at -1 0) (size 1 1) (layers "F.Cu") (net "GND"))+ (pad "2" smd rect (at 1 0) (size 1 1) (layers "F.Cu") (net "SIG")))+ (zone (net "GND") (net_name "GND") (layer "In1.Cu") (polygon (pts (xy 0 0) (xy 40 0) (xy 40 20) (xy 0 20))))+ (zone (net "GND") (net_name "GND") (layer "In2.Cu") (polygon (pts (xy 0 0) (xy 10 0) (xy 10 10) (xy 0 10))))+ (zone (net "GND") (net_name "GND") (layer "B.Cu") (polygon (pts (xy 0 0) (xy 40 0) (xy 40 20) (xy 0 20))))+)"#;+ let board = pcb::parse_pcb_text(text, "x").unwrap();+ let dsn = write_dsn(&board, text, "x", &Rules::default(), None).unwrap();+ let t = &dsn.text;+ let kind_of = |layer: &str| {+ let at = t.find(&format!("(layer {layer}\n")).unwrap_or_else(|| panic!("{layer} missing: {t}"));+ t[at..].lines().nth(1).unwrap().trim().to_string()+ };+ // Full-board zone on an inner layer: a plane, so vias only.+ assert_eq!(kind_of("In1.Cu"), "(type power)");+ // A quarter-board zone is a local pour, not a plane: still routable.+ assert_eq!(kind_of("In2.Cu"), "(type signal)");+ // KiCad itself declares In3 a power layer.+ assert_eq!(kind_of("In3.Cu"), "(type power)");+ // A full GND pour on an OUTER layer never reserves it: parts sit there.+ assert_eq!(kind_of("B.Cu"), "(type signal)");+ assert_eq!(kind_of("F.Cu"), "(type signal)");+ assert_eq!(dsn.stats.power_layers, vec!["In1.Cu", "In3.Cu"]);+ assert!(dsn.stats.notes.iter().any(|n| n.contains("In1.Cu, In3.Cu")), "{:?}", dsn.stats.notes);+ }+ #[test] fn no_outline_falls_back_to_pads_with_a_note() { let text = r#"(kicad_pcb (version 20260206) (layers (0 "F.Cu" signal) (2 "B.Cu" signal))