← 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).

John Lauer ·555e682736 ·1mo ago ·parent 231cdac
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))