← Commit history

1.0.4: 1.0.4: kicad_add_zone (copper pours as one Undo step with a filled DRC preflight) and kicad_zone_state (filled copper per layer, the ablation metric)

John Lauer ·472362a612 ·1mo ago ·parent df61ffd
6 files changed +142−5
rust/Cargo.lock+4−3
@@ -244,7 +244,7 @@ dependencies = [  [[package]] name = "kicad-bridge"-version = "1.0.3"+version = "1.0.4" dependencies = [  "kicad-core",  "kicad-platform",@@ -255,10 +255,11 @@ dependencies = [  [[package]] name = "kicad-core"-version = "1.0.3"+version = "1.0.4" dependencies = [  "kicad-ipc-rs",  "kicad-platform",+ "prost-types",  "quick-xml",  "serde",  "serde_json",@@ -280,7 +281,7 @@ dependencies = [  [[package]] name = "kicad-platform"-version = "1.0.3"+version = "1.0.4" dependencies = [  "serde",  "serde_json",
rust/Cargo.toml+1−1
@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"]  [workspace.package]-version = "1.0.3"+version = "1.0.4" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_routing.rs+26
@@ -47,6 +47,26 @@ pub static VERBS: &[Verb] = &[         related: &["kicad_route_net", "kicad_routing_state"],         pitfalls: &["Does not save the file or delete a whole net implicitly."],     },+    Verb {+        name: "kicad_add_zone",+        summary: "Add copper zones (pours) live as one Undo step: DRC preflight with the zone filled, then created and filled through the IPC API.",+        mechanism: Mechanism::Ipc, risk: "write", timeout_sec: 130,+        input: "{\"filePath\", \"expectedRevision\", \"net\", \"layers\": [\"F.Cu\"], \"polygon\": [[x, y], ...] mm, \"priority\"?: 0, \"clearance\"?: 0.2, \"minThickness\"?: 0.25, \"connection\"?: \"thermal\"|\"solid\", \"thermalGap\"?, \"thermalBridge\"?, \"name\"?, \"dryRun\"?, \"validate\"?, \"refill\"?, \"save\"?} or {\"zones\": [ {...}, ... ]}",+        example: "kicad_add_zone {\"filePath\":\"C:/p/b.kicad_pcb\",\"expectedRevision\":\"<rev>\",\"net\":\"+VBAT\",\"layers\":[\"F.Cu\",\"B.Cu\"],\"polygon\":[[130,60],[166,60],[166,90],[130,90]],\"priority\":2}",+        hint: "Pours for current, heat and copper retention (ablation): power and return nets first, higher priority for the smaller nets inside a bigger pour, thermal reliefs by default (solid for the power stage). The preflight fills the zone on a snapshot and refuses new DRC errors; kicad_zone_state reads the filled copper back.",+        related: &["kicad_zone_state", "kicad_route_net", "kicad_routing_validate", "kicad_routing_state"],+        pitfalls: &["A zone is copper on every listed layer inside the polygon that clears other nets; it does not connect two islands of its net by itself: check kicad_routing_validate for unconnected items after the fill.", "Do not pour switching nodes or sensitive analog nets wide; see the kicad-copper-pours skill for which nets get a pour and which do not.", "Coordinates are mm, +y down, board frame."],+    },+    Verb {+        name: "kicad_zone_state",+        summary: "Read every zone on the live board with KiCad's filled copper area per layer and the coverage.",+        mechanism: Mechanism::Ipc, risk: "read", timeout_sec: 130,+        input: "{\"filePath\": \"C:/.../x.kicad_pcb\", \"refill\"?: true}",+        example: "kicad_zone_state {\"filePath\":\"C:/p/b.kicad_pcb\",\"refill\":true}",+        hint: "refill:true refills every zone first (fills go stale after routing). filledAreaByLayerMm2 is what an ablation process leaves on the board.",+        related: &["kicad_add_zone", "kicad_routing_validate"],+        pitfalls: &["Coverage is against the outline's bounding box; a non-rectangular board reads low.", "Pads, tracks and vias outside zones are not counted."],+    },     Verb {         name: "kicad_routing_validate",         summary: "Run KiCad DRC against the current unsaved live board snapshot.",@@ -125,6 +145,8 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value>         "kicad_route_net" => live(state, args, Live::RouteNet),         "kicad_remove_route" => live(state, args, Live::RemoveRoute),         "kicad_routing_validate" => live(state, args, Live::Validate),+        "kicad_add_zone" => live(state, args, Live::AddZone),+        "kicad_zone_state" => live(state, args, Live::ZoneState),         "kicad_board_pads" => board_pads(args),         "kicad_add_track" => add_track(args),         "kicad_add_via" => add_via(args),@@ -145,6 +167,8 @@ enum Live {     RouteNet,     RemoveRoute,     Validate,+    AddZone,+    ZoneState, }  #[cfg(feature = "ipc")]@@ -160,6 +184,8 @@ fn live(state: &mut State, args: &Value, which: Live) -> Value {         Live::RouteNet => ipc::route_net(&ctx, args),         Live::RemoveRoute => ipc::remove_route(&ctx, args),         Live::Validate => ipc::routing_validate(&ctx, args),+        Live::AddZone => ipc::add_zone(&ctx, args),+        Live::ZoneState => ipc::zone_state(&ctx, args),     };     match result {         Ok(v) => v,
rust/crates/kicad-core/Cargo.toml+2−1
@@ -16,6 +16,7 @@ ureq = { version = "3", default-features = false, features = ["rustls", "gzip"], # KiCad IPC API client (protobuf over nng). Behind the ipc feature: nng-sys is a C build # (cmake), so a bare cross type-check can turn it off. kicad-ipc-rs = { version = "0.5", features = ["blocking"], optional = true }+prost-types = { version = "0.14", optional = true }  [features] # net: HTTPS fetches (wiki design rules, PCM, KiCad update check) through ureq + rustls.@@ -23,4 +24,4 @@ kicad-ipc-rs = { version = "0.5", features = ["blocking"], optional = true } # from a box without one runs with --no-default-features. default = ["net", "ipc"] net = ["dep:ureq"]-ipc = ["dep:kicad-ipc-rs"]+ipc = ["dep:kicad-ipc-rs", "dep:prost-types"]
rust/crates/kicad-core/src/ipc.rs+108
@@ -28,6 +28,7 @@ use kicad_ipc_rs::{BoardLayerInfo, BoardNet, DocumentSpecifier, DocumentType, Ed use crate::cli; use crate::pcb::{self, Board, Plan, RoutingError}; use crate::placement;+use crate::zones;  /// The ipc cargo feature is compiled in. The verb group asks this before promising live /// routing; a build with the feature off has no `ipc` module at all.@@ -767,6 +768,113 @@ pub fn routing_validate(ctx: &Ctx, args: &Value) -> RResult<Value> {     Ok(out) } +// ---------------------------------------------------------------------------+// Zones: kicad_add_zone, kicad_zone_state+// ---------------------------------------------------------------------------++/// `kicad_add_zone`: one or more copper zones (pours) as ONE native Undo step, with the+/// same revision guard and DRC preflight as `route_net`: the zones' s-expressions are+/// spliced into the editor's snapshot, KiCad's DRC (zones refilled) must add no error, or+/// nothing lands. After the commit the new zones are filled (`refill:false` skips that).+pub fn add_zone(ctx: &Ctx, args: &Value) -> RResult<Value> {+    let s = connect(ctx, args)?;+    let (text, data, revision) = s.snapshot()?;+    check_revision(args, &revision)?;+    let specs = zones::parse_specs(&data, args)?;+    let blocks: Vec<String> = specs.iter().map(zones::zone_sexpr).collect();+    let mut check = Value::Null;+    if args.get("validate") != Some(&json!(false)) {+        let source = s.source_path();+        let before = drc_snapshot(ctx.kicad_cli.as_deref(), &source, &text)?;+        let after = drc_snapshot(ctx.kicad_cli.as_deref(), &source, &pcb::splice(&text, &blocks))?;+        let added = new_errors(&before, &after)?;+        if !added.is_empty() {+            return Err(RoutingError::new("drc_rejected", "The zone adds DRC errors once filled; no live copper was changed").with("violations", json!(added)).with("mutated", json!(false)));+        }+        check = after;+    }+    let public: Vec<Value> = specs.iter().map(zones::ZoneSpec::public).collect();+    if args.get("dryRun") == Some(&json!(true)) {+        return Ok(json!({"success": true, "dryRun": true, "revision": revision, "drc": check, "zones": public,+            "_hint": "Zones validated (filled and DRC-checked on a snapshot) without editing. Submit with dryRun:false and the same revision."}));+    }+    check_revision(args, &s.revision()?)?;+    let nets = s.client.get_nets().map_err(map_err)?;+    let mut items = Vec::with_capacity(specs.len());+    for z in &specs {+        let Some(net) = nets.iter().find(|n| n.name == z.net.name) else {+            return Err(RoutingError::new("unknown_net", format!("The live board has no net named '{}'", z.net.name)).with("mutated", json!(false)));+        };+        let mut ids = Vec::with_capacity(z.layers.len());+        for l in &z.layers {+            ids.push(BoardLayerInfo::id_from_name(l).ok_or_else(|| RoutingError::new("unknown_layer", format!("KiCad has no layer named {l}")))?);+        }+        items.push(prost_types::Any { type_url: zones::ZONE_TYPE_URL.to_string(), value: zones::zone_bytes(z, &ids, net.code) });+    }+    let count = items.len();+    let label = format!("Adom zone {}", specs.iter().map(|z| z.net.name.clone()).collect::<Vec<_>>().join(", "));+    let created = transaction(&s, &label, || {+        let created = s.client.create_items(items, None)?;+        if created.len() != count {+            return Err(IpcFailure::from("KiCad did not create every requested zone".to_string()));+        }+        Ok(created)+    })?;+    let ids: Vec<String> = created.iter().filter_map(|a| zones::zone_id(&a.value)).collect();+    let mut result = json!({+        "success": true, "mutated": true, "source": "live-editor",+        "itemIds": ids, "undoSteps": 1, "zones": public, "drc": check, "saved": false, "refilled": false,+        "_hint": "Zones committed live as one KiCad Undo step and filled. kicad_zone_state reads the filled copper per layer; kicad_routing_validate is the DRC on the live board. The file is saved only with save:true.",+    });+    if ids.len() != count {+        result["postCommitError"] = json!(format!("KiCad answered with {} zone ids for {} zones", ids.len(), count));+    }+    if args.get("refill") != Some(&json!(false)) && !ids.is_empty() {+        match s.client.refill_zones(ids.clone()) {+            Ok(()) => result["refilled"] = json!(true),+            Err(e) => result["postCommitError"] = json!(format!("zones created but not filled: {e}")),+        }+    }+    match s.revision() {+        Ok(r) => {+            result["revision"] = json!(r);+            if args.get("save") == Some(&json!(true)) {+                match s.client.save_document() {+                    Ok(()) => result["saved"] = json!(true),+                    Err(e) => result["postCommitError"] = json!(e.to_string()),+                }+            }+        }+        Err(e) => result["postCommitError"] = json!(e.message),+    }+    Ok(result)+}++/// `kicad_zone_state`: every zone on the live board with its filled copper area per layer+/// (KiCad's own fill polygons), the sum per layer and the coverage against the outline+/// box. `refill:true` refills every zone first (the fill can be stale after routing).+pub fn zone_state(ctx: &Ctx, args: &Value) -> RResult<Value> {+    let s = connect(ctx, args)?;+    let mut refilled = false;+    if args.get("refill") == Some(&json!(true)) {+        s.client.refill_zones(Vec::new()).map_err(map_err)?;+        refilled = true;+    }+    let (_text, data, revision) = s.snapshot()?;+    let raws = s.client.get_items_raw_by_type_codes(vec![PcbObjectTypeCode::new_zone().code]).map_err(map_err)?;+    let mut read = Vec::with_capacity(raws.len());+    for a in &raws {+        read.push(zones::decode_zone(&a.value).map_err(|e| RoutingError::new("zone_undecodable", format!("KiCad returned a zone the bridge could not decode: {e}")))?);+    }+    let mut out = json!({+        "success": true, "boardPath": s.file_path, "revision": revision, "source": "live-editor",+        "kicadVersion": s.version.full_version, "socket": s.socket, "refilled": refilled,+        "_hint": "filledAreaByLayerMm2 is the copper KiCad's zones keep on each layer: for a copper-ablation process, what does not have to be milled away. Add pours with kicad_add_zone; kicad_routing_validate is the DRC.",+    });+    merge(&mut out, &zones::state_json(&read, &data));+    Ok(out)+}+ // --------------------------------------------------------------------------- // Placement: kicad_placement_state, kicad_move_footprint, kicad_placement_validate // ---------------------------------------------------------------------------
rust/crates/kicad-core/src/lib.rs+1
@@ -16,6 +16,7 @@ pub mod bridge_log; pub mod install; pub mod pcb; pub mod placement;+pub mod zones; #[cfg(feature = "ipc")] pub mod ipc; pub mod uninstall;