← Commit history

1.0.20: NATIVE (Rust) build. 1.0.20: reservePlanes defaults to false (ESC fixture: reserved 30 unconnected, open 3), reply reports routed.segmentsOnPlaneLayers and planeLayers (wiki #93).

John Lauer ·6f8e15e650 ·1mo ago ·parent b414d5b
4 files changed +22−8
rust/Cargo.lock+3−3
@@ -244,7 +244,7 @@ dependencies = [  [[package]] name = "kicad-bridge"-version = "1.0.19"+version = "1.0.20" dependencies = [  "kicad-core",  "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [  [[package]] name = "kicad-core"-version = "1.0.19"+version = "1.0.20" dependencies = [  "kicad-ipc-rs",  "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [  [[package]] name = "kicad-platform"-version = "1.0.19"+version = "1.0.20" dependencies = [  "serde",  "serde_json",
rust/Cargo.toml+1−1
@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"]  [workspace.package]-version = "1.0.19"+version = "1.0.20" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_routing.rs+8−3
@@ -150,12 +150,12 @@ pub static VERBS: &[Verb] = &[         name: "kicad_autoroute",         summary: "Route a board with the engine the USER picked: \"ai\" (recommended) returns the plan request you drive with kicad_route_net; \"freerouting\" runs Freerouting headless (installed on demand) and lands its copper live as one undo step per net, or into the closed file.",         mechanism: Mechanism::Ipc, risk: "write", timeout_sec: 900,-        input: "{\"engine\": \"ai\" | \"freerouting\", \"filePath\": \"C:/.../x.kicad_pcb\", \"nets\"?: [\"GND\", ...], \"dryRun\"?: bool, \"passes\"?: 20, \"threads\"?: n, \"timeoutSec\"?: 600, \"apply\"?: \"file\" | \"ipc\", \"backup\"?: true, \"job\"?: true | \"<id>\", \"reservePlanes\"?: true}",+        input: "{\"engine\": \"ai\" | \"freerouting\", \"filePath\": \"C:/.../x.kicad_pcb\", \"nets\"?: [\"GND\", ...], \"dryRun\"?: bool, \"passes\"?: 20, \"threads\"?: n, \"timeoutSec\"?: 600, \"apply\"?: \"file\" | \"ipc\", \"backup\"?: true, \"job\"?: true | \"<id>\", \"reservePlanes\"?: false}",         example: "kicad_autoroute {\"engine\":\"ai\",\"filePath\":\"C:/Users/john/proj/board.kicad_pcb\"}",         hint: "ASK THE USER WHICH ENGINE THEY WANT BEFORE ROUTING and name both every time. ai (recommended): any capable model can drive kicad_route_net (GPT-6 Astra in Codex has routed a 94-footprint board to zero unconnected items this way, Claude Fable 5.1 can, newer models will); the routing verbs give it pads, nets, zones, a live revision, one native undo step per trace and DRC per step, so it honours the schematic's intent and can explain every trace. freerouting: deterministic, installed on demand (88 MB download, bundled Java, no elevation), kicad_freerouting {\"action\":\"status\"} says whether it is present; not installed answers freerouting_not_installed with the install call, and you ask the user before installing. Report which engine produced the copper. Over the relay use job:true and poll; the relay times out around 130 s while the bridge keeps working, and the result of a timed-out synchronous call is not retrievable: kicad_autoroute {\"engine\":\"freerouting\",\"job\":true} answers at once with a job id, kicad_autoroute {\"job\":\"<id>\"} reports running (stepLabel, percent), done (the full routing reply) or failed, kept for an hour. A direct caller may still wait synchronously (timeout 900 s).",         related: &["kicad_freerouting", "kicad_routing_state", "kicad_route_net", "kicad_routing_validate", "kicad_board_pads"],         pitfalls: &["Never pick the engine for the user; the hint makes you ask (same pattern as foregroundReason).",-            "freerouting reserves inner plane layers by default (dsn.stats.powerLayers, vias only), which keeps the planes whole but leaves a dense board with more unconnected items; reservePlanes:false lets signals cross the planes. Say which one you ran.", "engine ai mutates nothing by itself: the copper lands through kicad_route_net calls you make afterwards.", "engine freerouting never installs anything by itself: freerouting_not_installed means ask, then kicad_freerouting {\"action\":\"install\"}, then call again.", "engine freerouting applies live (apply ipc: the board open in the PCB editor with the IPC API on, one native undo step per net named Freerouting <net>) or to the closed file (apply file, .adom-bak beside it); auto picks ipc when the board is open, file otherwise.", "Oval, roundrect and custom pads reach the router as their bounding rect (conservative); dsn.approximatedPads lists them.", "Over the relay use job:true and poll; the relay times out around 130 s while the bridge keeps working, and the result of a timed-out synchronous call is not retrievable. A real board takes minutes (a 59-net four-layer board: 40 passes), so job:true is the default choice over ab.", "One autoroute job per board at a time: job_busy names the running id, poll it to completion (or failure) before starting another. The poll takes no engine; a finished job answers for an hour, then job_not_found."],+            "freerouting may run signals across inner plane layers by default (routed.segmentsOnPlaneLayers counts them; KiCad's refill carves the plane around each). reservePlanes:true keeps the planes whole, vias only, but on a dense board leaves far more unconnected (ESC fixture: 30 against 3). Say which one you ran and what it cost.", "engine ai mutates nothing by itself: the copper lands through kicad_route_net calls you make afterwards.", "engine freerouting never installs anything by itself: freerouting_not_installed means ask, then kicad_freerouting {\"action\":\"install\"}, then call again.", "engine freerouting applies live (apply ipc: the board open in the PCB editor with the IPC API on, one native undo step per net named Freerouting <net>) or to the closed file (apply file, .adom-bak beside it); auto picks ipc when the board is open, file otherwise.", "Oval, roundrect and custom pads reach the router as their bounding rect (conservative); dsn.approximatedPads lists them.", "Over the relay use job:true and poll; the relay times out around 130 s while the bridge keeps working, and the result of a timed-out synchronous call is not retrievable. A real board takes minutes (a 59-net four-layer board: 40 passes), so job:true is the default choice over ab.", "One autoroute job per board at a time: job_busy names the running id, poll it to completion (or failure) before starting another. The poll takes no engine; a finished job answers for an hour, then job_not_found."],     },     Verb {         name: "kicad_freerouting",@@ -863,7 +863,9 @@ fn freerouting_prepare(args: &Value, dry_run: bool) -> Result<Prepared, Value> {             live_args[k] = v.clone();         }     }-    let reserve_planes = args.get("reservePlanes").and_then(Value::as_bool).unwrap_or(true);+    // Default OPEN: on the ESC fixture reserving the planes left 30 connections unrouted+    // against 3 (wiki #93). The reply counts the segments that crossed a plane instead.+    let reserve_planes = args.get("reservePlanes").and_then(Value::as_bool).unwrap_or(false);     Ok(Prepared { exe, passes, threads, timeout_sec, wanted, loaded, use_ipc, live_args, backup: backup_wanted(args), dry_run, reserve_planes }) } @@ -997,9 +999,12 @@ fn freerouting_execute(state: &mut State, prep: Prepared, phase: &Phase, own_gui     // 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();+    // What leaving the planes open cost: segments the router ran across a plane layer.+    let on_planes: usize = copper.iter().flat_map(|c| c.segments.iter()).filter(|sg| out.stats.plane_layers.contains(&sg.layer)).count();     let routed = json!({         "nets": routed_names, "netCount": copper.len(), "segments": segments, "vias": vias,         "skippedExisting": skipped, "unroutedNets": unrouted, "widened": widened,+        "segmentsOnPlaneLayers": on_planes, "planeLayers": out.stats.plane_layers,         "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});
rust/crates/kicad-core/src/dsn.rs+10−1
@@ -440,6 +440,10 @@ pub struct DsnStats {     /// Inner layers written as `(type power)`: the router only drops vias into them.     #[serde(rename = "powerLayers")]     pub power_layers: Vec<String>,+    /// Inner layers that ARE planes (declared power, or half the board covered by one zone),+    /// whether or not they were reserved.+    #[serde(rename = "planeLayers")]+    pub plane_layers: Vec<String>,     pub notes: Vec<String>, } @@ -639,7 +643,9 @@ pub fn write_dsn(board: &Board, text: &str, name: &str, rules: &Rules, nets: Opt  /// `reserve_planes`: write plane layers as `(type power)` (wiki #93). False lets the router /// use them for signals, which completes more of a dense board at the cost of cutting-/// through the planes; the caller chooses, the reply says which it got.+/// through the planes; the caller chooses, the reply says which it got. Measured on the ESC+/// G431 fixture (2026-09-18): reserved left 30 connections unrouted, open left 3, with the+/// same six clearance errors, so kicad_autoroute defaults to open and reports the crossings. pub fn write_dsn_with(board: &Board, text: &str, name: &str, rules: &Rules, nets: Option<&[String]>, reserve_planes: bool) -> Result<Dsn, String> {     let tree = parse_sexpr(text).ok_or("empty board text")?;     let copper = pcb::copper_layers(board);@@ -918,6 +924,9 @@ pub fn write_dsn_with(board: &Board, text: &str, name: &str, rules: &Rules, nets         }         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.plane_layers.push(layer.clone());+        }         if reserve_planes && (declared || covered) {             stats.power_layers.push(layer.clone());         } else if declared || covered {