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
1.0.6: 1.0.6: kicad_add_zone takes rule-area keepouts (type keepout, copperPour/tracks/vias/pads/footprints) for Kelvin sense taps and sensitive nodes; kicad_zone_state reports the zone type
1 file changed
+100−5
rust/crates/kicad-core/src/zones.rs+100−5@@ -34,10 +34,29 @@ pub struct ZoneSpec { pub thermal_gap: f64, pub thermal_bridge: f64, pub name: String,+ /// A rule area (keepout) instead of copper: what it keeps out.+ pub keepout: Option<Keepout>,+}++/// Rule-area flags (`kicad_add_zone {"type": "keepout", "keepout": {"copperPour": true, ...}}`).+#[derive(Clone, Debug, Default)]+pub struct Keepout {+ pub copper_pour: bool,+ pub tracks: bool,+ pub vias: bool,+ pub pads: bool,+ pub footprints: bool, } impl ZoneSpec { pub fn public(&self) -> Value {+ if let Some(k) = &self.keepout {+ return json!({+ "type": "keepout", "layers": self.layers, "points": self.polygon.len(), "name": self.name,+ "keepout": {"copperPour": k.copper_pour, "tracks": k.tracks, "vias": k.vias, "pads": k.pads, "footprints": k.footprints},+ "outlineAreaMm2": round2(polygon_area(&self.polygon).abs()),+ });+ } json!({ "net": self.net.name, "layers": self.layers, "points": self.polygon.len(), "priority": self.priority, "clearance": self.clearance, "minThickness": self.min_thickness,@@ -88,11 +107,27 @@ pub fn parse_specs(board: &Board, args: &Value) -> Result<Vec<ZoneSpec>, Routing let copper = pcb::copper_layers(board); let mut out = Vec::with_capacity(list.len()); for (k, z) in list.iter().enumerate() {- let net = pcb::resolve_net(board, z.get("net")).map_err(|resp| {+ let is_keepout = z.get("type").and_then(Value::as_str) == Some("keepout") || z.get("keepout").is_some();+ let keepout = if is_keepout {+ let flags = z.get("keepout").cloned().unwrap_or_else(|| json!({"copperPour": true}));+ let f = |name: &str| flags.get(name).and_then(Value::as_bool).unwrap_or(false);+ let ko = Keepout { copper_pour: f("copperPour"), tracks: f("tracks"), vias: f("vias"), pads: f("pads"), footprints: f("footprints") };+ if !(ko.copper_pour || ko.tracks || ko.vias || ko.pads || ko.footprints) {+ return Err(RoutingError::new("invalid_argument", format!("zone {}: a keepout needs at least one of copperPour, tracks, vias, pads, footprints", k + 1)));+ }+ Some(ko)+ } else {+ None+ };+ let net = if is_keepout {+ NetRef { format: board.net_format, number: None, name: String::new(), key: Value::Null }+ } else {+ pcb::resolve_net(board, z.get("net")).map_err(|resp| { RoutingError::new("unknown_net", format!("zone {}: {}", k + 1, resp.get("message").and_then(Value::as_str).unwrap_or("net not found on this board"))) .with("availableNets", json!(board.nets.iter().map(|n| n.name.clone()).filter(|n| !n.is_empty()).collect::<Vec<_>>()))- })?;- if net.name.is_empty() {+ })?+ };+ if net.name.is_empty() && !is_keepout { return Err(RoutingError::new("invalid_argument", format!("zone {}: a copper zone needs a net", k + 1))); } let mut layers: Vec<String> = match (z.get("layers"), z.get("layer")) {@@ -142,7 +177,7 @@ pub fn parse_specs(board: &Board, args: &Value) -> Result<Vec<ZoneSpec>, Routing None | Some(Value::Null) => 0, Some(v) => v.as_u64().filter(|p| *p <= 255).ok_or_else(|| RoutingError::new("invalid_argument", format!("zone {}: priority is an integer 0..255", k + 1)))? as u32, };- let name = z.get("name").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| format!("{} pour", net.name));+ let name = z.get("name").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| if is_keepout { "keepout".to_string() } else { format!("{} pour", net.name) }); out.push(ZoneSpec { net, layers,@@ -154,6 +189,7 @@ pub fn parse_specs(board: &Board, args: &Value) -> Result<Vec<ZoneSpec>, Routing thermal_gap: mm(z.get("thermalGap"), "thermalGap", 0.5)?, thermal_bridge: mm(z.get("thermalBridge"), "thermalBridge", 0.5)?, name,+ keepout, }); } Ok(out)@@ -163,6 +199,27 @@ pub fn parse_specs(board: &Board, args: &Value) -> Result<Vec<ZoneSpec>, Routing /// loads the snapshot; no filled polygons are written). pub fn zone_sexpr(z: &ZoneSpec) -> String { let mut s = String::from("\t(zone\n");+ if let Some(k) = &z.keepout {+ // a rule area: no net, the layers, the keepout flags, the outline+ if z.net.format == pcb::NetFormat::Number {+ s.push_str("\t\t(net 0)\n\t\t(net_name \"\")\n");+ }+ if z.layers.len() == 1 {+ s.push_str(&format!("\t\t(layer \"{}\")\n", z.layers[0]));+ } else {+ s.push_str(&format!("\t\t(layers {})\n", z.layers.iter().map(|l| format!("\"{l}\"")).collect::<Vec<_>>().join(" ")));+ }+ s.push_str(&format!("\t\t(uuid \"{}\")\n", new_uuid()));+ s.push_str(&format!("\t\t(name {})\n", serde_json::to_string(&z.name).unwrap_or_default()));+ s.push_str("\t\t(hatch edge 0.5)\n");+ let a = |b: bool| if b { "not_allowed" } else { "allowed" };+ s.push_str(&format!("\t\t(keepout\n\t\t\t(tracks {})\n\t\t\t(vias {})\n\t\t\t(pads {})\n\t\t\t(copperpour {})\n\t\t\t(footprints {})\n\t\t)\n", a(k.tracks), a(k.vias), a(k.pads), a(k.copper_pour), a(k.footprints)));+ s.push_str("\t\t(fill\n\t\t\t(thermal_gap 0.5)\n\t\t\t(thermal_bridge_width 0.5)\n\t\t)\n");+ let pts: Vec<String> = z.polygon.iter().map(|(x, y)| format!("(xy {} {})", pcb::num(*x), pcb::num(*y))).collect();+ s.push_str(&format!("\t\t(polygon\n\t\t\t(pts\n\t\t\t\t{}\n\t\t\t)\n\t\t)\n", pts.join(" ")));+ s.push_str("\t)");+ return s;+ } s.push_str(&pcb::net_sexpr(&z.net)); if z.net.format == pcb::NetFormat::Number { s.push_str(&format!("\t\t(net_name {})\n", serde_json::to_string(&z.net.name).unwrap_or_default()));@@ -216,6 +273,27 @@ fn polyset_bytes(polygon: &[(f64, f64)]) -> Vec<u8> { /// diagonal-edge border. Never filled here: the fill is KiCad's, on request. pub fn zone_bytes(z: &ZoneSpec, layer_ids: &[i32], net_code: i32) -> Vec<u8> { let mut out = Vec::new();+ if let Some(k) = &z.keepout {+ wire_varint(&mut out, 2, 3); // ZT_RULE_AREA+ for id in layer_ids {+ wire_varint(&mut out, 3, *id as u64);+ }+ wire_message(&mut out, 4, &polyset_bytes(&z.polygon));+ wire_message(&mut out, 5, z.name.as_bytes());+ let mut ra = Vec::new();+ for (n, b) in [(1u32, k.copper_pour), (2, k.vias), (3, k.tracks), (4, k.pads), (5, k.footprints)] {+ if b {+ wire_varint(&mut ra, n, 1);+ }+ }+ wire_message(&mut out, 7, &ra);+ let mut border = Vec::new();+ wire_varint(&mut border, 1, 3);+ wire_message(&mut border, 2, &distance_bytes(500_000));+ wire_message(&mut out, 11, &border);+ wire_varint(&mut out, 12, 1);+ return out;+ } wire_varint(&mut out, 2, 1); // ZT_COPPER for id in layer_ids { wire_varint(&mut out, 3, *id as u64);@@ -275,6 +353,7 @@ pub struct ZoneRead { pub layers: Vec<i32>, pub priority: u32, pub filled: bool,+ pub rule_area: bool, pub outline_area_mm2: f64, /// (layer id, polygon count, filled area in mm2) per filled layer. pub fills: Vec<(i32, usize, f64)>,@@ -365,6 +444,8 @@ pub fn decode_zone(b: &[u8]) -> Result<ZoneRead, String> { } } }+ (2, 0) => z.rule_area = varint_value(f.data)? == 3,+ (7, 2) => z.rule_area = true, (3, 0) => z.layers.push(varint_value(f.data)? as i32), (3, 2) => { // packed repeated enum@@ -433,6 +514,7 @@ pub fn state_json(zones: &[ZoneRead], board: &Board) -> Value { .collect(); list.push(json!({ "id": z.id, "name": z.name, "net": z.net, "layers": z.layers.iter().map(|l| copper_layer_name(*l)).collect::<Vec<_>>(),+ "type": if z.rule_area { "keepout" } else { "copper" }, "priority": z.priority, "filled": z.filled, "outlineAreaMm2": round2(z.outline_area_mm2), "fills": fills, })); }@@ -480,6 +562,19 @@ mod tests { assert!(back.id.is_empty() && !back.filled); } + #[test]+ fn keepout_zone_round_trips() {+ let b = board();+ let specs = parse_specs(&b, &json!({"type": "keepout", "layers": ["F.Cu", "B.Cu"], "polygon": [[1, 1], [5, 1], [5, 3], [1, 3]], "keepout": {"copperPour": true}, "name": "kelvin"})).unwrap();+ let z = &specs[0];+ assert!(z.keepout.as_ref().unwrap().copper_pour && z.net.name.is_empty());+ let s = zone_sexpr(z);+ assert!(s.contains("(copperpour not_allowed)") && s.contains("(tracks allowed)") && !s.contains("(net "));+ let back = decode_zone(&zone_bytes(z, &[3, 34], 0)).unwrap();+ assert!(back.rule_area && back.net.is_empty() && back.layers == vec![3, 34]);+ assert_eq!(z.public()["type"], json!("keepout"));+ }+ #[test] fn refuses_bad_zones() { let b = board();@@ -491,7 +586,7 @@ mod tests { #[test] fn state_sums_fill_area_per_layer() { let b = board();- let z = ZoneRead { id: "a".into(), name: "GND pour".into(), net: "GND".into(), layers: vec![3], priority: 0, filled: true, outline_area_mm2: 100.0, fills: vec![(3, 2, 80.0)] };+ let z = ZoneRead { id: "a".into(), name: "GND pour".into(), net: "GND".into(), layers: vec![3], priority: 0, filled: true, rule_area: false, outline_area_mm2: 100.0, fills: vec![(3, 2, 80.0)] }; let v = state_json(&[z], &b); assert_eq!(v["filledAreaByLayerMm2"]["F.Cu"], json!(80.0)); assert_eq!(v["filledCoverageByLayerPercent"]["F.Cu"], json!(4.0));