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
kicad_remove_route honours dryRun (wiki #134): lists what would go and KiCad's DRC without it, removes nothing
2 files changed
+87−2
rust/crates/kicad-bridge/src/verbs_routing.rs+1−1@@ -80,7 +80,7 @@ pub static VERBS: &[Verb] = &[ name: "kicad_remove_route", summary: "Remove specific unlocked copper item IDs live as one Undo step.", mechanism: Mechanism::Ipc, risk: "write", timeout_sec: 130,- input: "{\"filePath\", \"expectedRevision\", \"itemIds\": [\"<uuid>\", ...]}",+ input: "{\"filePath\", \"expectedRevision\", \"itemIds\": [\"<uuid>\", ...], \"dryRun\"?: true (lists what would go and KiCad's DRC without it, removes nothing), \"validate\"?: false}", example: "kicad_remove_route {\"filePath\":\"C:/p/b.kicad_pcb\",\"expectedRevision\":\"<rev>\",\"itemIds\":[\"6f1c...\"]}", hint: "filePath, expectedRevision, itemIds from route_net. Refuses missing IDs, pads, footprints and locked copper.", related: &["kicad_route_net", "kicad_routing_state"],
rust/crates/kicad-core/src/ipc.rs+86−1@@ -899,7 +899,7 @@ pub fn commit_copper(ctx: &Ctx, args: &Value, plan: &Plan, message: &str) -> RRe /// `kicad_remove_route`: delete specific unlocked copper items as one Undo step. pub fn remove_route(ctx: &Ctx, args: &Value) -> RResult<Value> { let s = connect(ctx, args)?;- let (_, _, revision) = s.snapshot()?;+ let (text, data, revision) = s.snapshot()?; check_revision(args, &revision)?; let ids: Vec<String> = match args.get("itemIds").and_then(Value::as_array) { Some(a) if !a.is_empty() && a.len() <= 1024 && a.iter().all(Value::is_string) => a.iter().filter_map(Value::as_str).map(str::to_string).collect(),@@ -911,6 +911,9 @@ pub fn remove_route(ctx: &Ctx, args: &Value) -> RResult<Value> { if items.len() != wanted.len() || items.iter().any(|i| item_locked(i)) { return Err(RoutingError::new("invalid_item_ids", "Some IDs are missing, locked, or are not copper; nothing removed")); }+ if args.get("dryRun") == Some(&json!(true)) {+ return Ok(remove_preview(ctx, &s, &text, &data, &revision, &ids, args));+ } check_revision(args, &s.revision()?)?; let to_delete: Vec<String> = wanted.iter().cloned().collect(); transaction(&s, "Adom remove route", || {@@ -942,6 +945,79 @@ pub fn remove_route(ctx: &Ctx, args: &Value) -> RResult<Value> { Ok(result) } +/// `kicad_remove_route {dryRun:true}` (wiki #134): what would go and KiCad's DRC of the snapshot+/// without it. Nothing is written.+fn remove_preview(ctx: &Ctx, s: &Session, text: &str, data: &Board, revision: &str, ids: &[String], args: &Value) -> Value {+ let items: Vec<Value> = ids.iter().map(|id| {+ if let Some(t) = data.segments.iter().find(|x| x.uuid.as_deref() == Some(id.as_str())) {+ json!({"id": id, "kind": "track", "net": t.net_name, "layer": t.layer, "start": t.start, "end": t.end, "width": t.width})+ } else if let Some(v) = data.vias.iter().find(|x| x.uuid.as_deref() == Some(id.as_str())) {+ json!({"id": id, "kind": "via", "net": v.net_name, "position": v.position, "size": v.size, "drill": v.drill})+ } else {+ json!({"id": id, "kind": "arc"})+ }+ }).collect();+ let (without, cut) = strip_items(text, ids);+ let mut out = json!({"success": true, "dryRun": true, "mutated": false, "revision": revision, "wouldRemove": items,+ "_hint": "Nothing was removed. drc is KiCad's DRC of the board without these items: unconnectedAfter is what removing them leaves to route. Submit with dryRun:false and the same revision to remove them as one Undo step."});+ if args.get("validate") == Some(&json!(false)) {+ return out;+ }+ if cut != ids.len() {+ out["drc"] = json!({"available": false, "why": format!("{} of {} items were found in the board text", cut, ids.len())});+ return out;+ }+ let source = s.source_path();+ match (drc_snapshot(ctx.kicad_cli.as_deref(), &source, text), drc_snapshot(ctx.kicad_cli.as_deref(), &source, &without)) {+ (Ok(before), Ok(after)) => {+ out["unconnectedBefore"] = before["unconnected"].clone();+ out["unconnectedAfter"] = after["unconnected"].clone();+ out["newErrors"] = json!(new_errors(&before, &after).unwrap_or_default());+ out["drc"] = after;+ }+ (Err(e), _) | (_, Err(e)) => out["drc"] = json!({"available": false, "errorCode": e.code, "why": e.message}),+ }+ out+}++/// The board text with the top-level items carrying these UUIDs cut out, and how many were cut.+pub fn strip_items(text: &str, ids: &[String]) -> (String, usize) {+ // the spans of the root list's direct children, quotes respected+ let mut spans: Vec<(usize, usize)> = Vec::new();+ let (mut depth, mut quoted, mut escaped, mut start) = (0i32, false, false, 0usize);+ for (i, c) in text.char_indices() {+ if quoted {+ if escaped { escaped = false } else if c == '\\' { escaped = true } else if c == '"' { quoted = false }+ continue;+ }+ match c {+ '"' => quoted = true,+ '(' => {+ depth += 1;+ if depth == 2 { start = i; }+ }+ ')' => {+ if depth == 2 { spans.push((start, i + 1)); }+ depth -= 1;+ }+ _ => {}+ }+ }+ let mut out = String::with_capacity(text.len());+ let (mut last, mut cut) = (0, 0);+ for (a, b) in spans {+ let item = &text[a..b];+ let kind_ok = item.starts_with("(segment") || item.starts_with("(arc") || item.starts_with("(via");+ if kind_ok && ids.iter().any(|id| item.contains(&format!("(uuid \"{id}\")")) || item.contains(&format!("(tstamp \"{id}\")"))) {+ out.push_str(&text[last..a]);+ last = b;+ cut += 1;+ }+ }+ out.push_str(&text[last..]);+ (out, cut)+}+ /// `kicad_routing_validate`: KiCad DRC on the live, unsaved board snapshot. pub fn routing_validate(ctx: &Ctx, args: &Value) -> RResult<Value> { let s = connect(ctx, args)?;@@ -1569,6 +1645,15 @@ mod tests { use super::*; use std::cell::RefCell; + #[test]+ fn strip_items_cuts_only_the_named_copper() {+ let text = "(kicad_pcb (version 1)\n\t(segment (start 0 0) (end 1 0) (uuid \"a\"))\n\t(via (at 1 0) (uuid \"b\"))\n\t(gr_text \"a (uuid \\\"a\\\")\" (uuid \"c\"))\n\t(segment (start 2 0) (end 3 0) (uuid \"d\"))\n)";+ let (out, n) = strip_items(text, &["a".to_string(), "b".to_string()]);+ assert_eq!(n, 2);+ assert!(!out.contains("(uuid \"a\"))") && !out.contains("(uuid \"b\")"));+ assert!(out.contains("(uuid \"c\")") && out.contains("(uuid \"d\")"));+ }+ #[test] fn revision_is_sha256_of_the_board_text() { assert_eq!(revision_of("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");