← Commit history

kicad_remove_route honours dryRun (wiki #134): lists what would go and KiCad's DRC without it, removes nothing

Colby Knox ·b0bde5cde9 ·15d ago ·parent a0caf33
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");