← 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

John Lauer ·5f9858492b ·1mo ago ·parent 535ff58
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));