← Commit history

2.31.0: roundrect pad radius survives as EAGLE roundness

John Lauer ·e1b0fdf7cc ·1mo ago ·parent d0edf35
7 files changed +47−11
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.30.1"+version = "2.31.0" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.30.1"+version = "2.31.0" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time." 
VERSION+1−1
@@ -1 +1 @@-2.30.1+2.31.0
bin/adom-lbr
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.30.1",+  "version": "2.31.0",   "type": "app",   "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},
page.json+2−2
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.30.1",+  "version": "2.31.0",   "type": "app",   "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",   "dependencies": {},@@ -88,4 +88,4 @@       "label": "Browse parts"     }   ]-}\ No newline at end of file+}
src/main.rs+41−5
@@ -1087,6 +1087,10 @@ struct ModPad {     /// EAGLE pad shape mapped from the KiCad shape token: "round" | "square" |     /// "long" | "octagon".     shape: String,+    /// EAGLE `roundness`, 0..100 as a percent of the pad's SMALLER side, from KiCad's+    /// `roundrect_rratio`. None for a pad that is not a roundrect. Altium rounds pads by+    /// default to reduce copper lift-off; dropping this made every such pad import square.+    roundness: Option<u32>, }  /// Split a `.kicad_mod` into the raw text of each `(<tag> …)` s-expression,@@ -1139,6 +1143,9 @@ fn parse_mod_pads(km: &str) -> Vec<ModPad> {     let at_re = regex_lite::Regex::new(r#"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\s*\)"#).unwrap();     let size_re = regex_lite::Regex::new(r#"\(size\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\)"#).unwrap();     let drill_re = regex_lite::Regex::new(r#"\(drill\s+(-?[\d.]+)"#).unwrap();+    // KiCad states the corner radius as a RATIO of the smaller side; EAGLE states it as a+    // percent of the same side, and a ratio of 0.5 is a fully rounded end. So N = r * 200.+    let rr_re = regex_lite::Regex::new(r#"\(roundrect_rratio\s+(-?[\d.]+)"#).unwrap();      let mut pads = Vec::new();     for block in sexpr_blocks(km, "pad") {@@ -1155,6 +1162,12 @@ fn parse_mod_pads(km: &str) -> Vec<ModPad> {         let angle: f64 = at.get(3).and_then(|m| m.as_str().parse().ok()).unwrap_or(0.0);         let dx: f64 = size[1].parse().unwrap_or(0.0);         let dy: f64 = size[2].parse().unwrap_or(0.0);+        let kshape = nc.get(4).map(|m| m.as_str()).unwrap_or("");+        let roundness = if kshape == "roundrect" || kshape == "oval" {+            let r: f64 = rr_re.captures(block).and_then(|c| c[1].parse().ok())+                .unwrap_or(if kshape == "oval" { 0.5 } else { 0.25 });+            Some(((r * 200.0).round() as i64).clamp(0, 100) as u32)+        } else { None };         let drill = drill_re.captures(block).and_then(|c| c[1].parse().ok());         // KiCad pad shape → EAGLE pad shape.         let shape = match nc.get(4).map(|m| m.as_str()).unwrap_or("") {@@ -1164,7 +1177,7 @@ fn parse_mod_pads(km: &str) -> Vec<ModPad> {             "roundrect" | "custom" => "round",             _ => "round",         }.to_string();-        pads.push(ModPad { name, smd, x, y, dx, dy, angle, drill, shape });+        pads.push(ModPad { name, smd, x, y, dx, dy, angle, drill, shape, roundness });     }     pads }@@ -1275,8 +1288,9 @@ fn emit_variant_package(xml: &mut String, km: &str, name: &str, description: &st     for (i, p) in pads.iter().enumerate() {         if p.smd {             xml.push_str(&format!(-                "  <smd name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" dx=\"{:.4}\" dy=\"{:.4}\" layer=\"1\"{}/>\n",-                esc_xml(&out_names[i]), p.x, -p.y, p.dx, p.dy, smd_rotation(p.angle)));+                "  <smd name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" dx=\"{:.4}\" dy=\"{:.4}\" layer=\"1\"{}{}/>\n",+                esc_xml(&out_names[i]), p.x, -p.y, p.dx, p.dy, smd_rotation(p.angle),+                    p.roundness.map(|r| format!(" roundness=\"{r}\"")).unwrap_or_default()));         } else {             let drill = p.drill.unwrap_or_else(|| p.dx.min(p.dy) * 0.6);             let diameter = if p.shape == "round" { p.dx } else { p.dx.max(p.dy) };@@ -2139,8 +2153,9 @@ pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, s         for (i, p) in mod_pads.iter().enumerate() {             if p.smd {                 xml.push_str(&format!(-                    "  <smd name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" dx=\"{:.4}\" dy=\"{:.4}\" layer=\"1\"{}/>\n",-                    esc_xml(&out_names[i]), p.x, -p.y, p.dx, p.dy, smd_rotation(p.angle)));+                    "  <smd name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" dx=\"{:.4}\" dy=\"{:.4}\" layer=\"1\"{}{}/>\n",+                    esc_xml(&out_names[i]), p.x, -p.y, p.dx, p.dy, smd_rotation(p.angle),+                    p.roundness.map(|r| format!(" roundness=\"{r}\"")).unwrap_or_default()));             } else {                 // Round thru-hole pads: real drill + circular diameter (not oblong).                 let drill = p.drill.unwrap_or_else(|| p.dx.min(p.dy) * 0.6);@@ -3846,6 +3861,27 @@ mod lint_tests {         assert!(!decls.contains("102"), "{decls}");     } +    #[test]+    fn roundrect_pads_carry_their_corner_radius_into_eagle() {+        use super::parse_mod_pads;+        let km = r#"(footprint "T"+            (pad "1" smd roundrect (at -2 0) (size 1.0 0.5) (roundrect_rratio 0.25) (layers "F.Cu"))+            (pad "2" smd roundrect (at 2 0) (size 1.0 0.5) (roundrect_rratio 0.5) (layers "F.Cu"))+            (pad "3" smd rect (at 0 2) (size 1.0 0.5) (layers "F.Cu"))+            (pad "4" smd oval (at 0 -2) (size 1.0 0.5) (layers "F.Cu")))"#;+        let pads = parse_mod_pads(km);+        let r = |n: &str| pads.iter().find(|p| p.name == n).unwrap().roundness;+        // KiCad states the radius as a RATIO of the smaller side, EAGLE as a PERCENT of it,+        // and a ratio of 0.5 is a fully rounded end, so N = r * 200.+        assert_eq!(r("1"), Some(50));+        assert_eq!(r("2"), Some(100));+        // a plain rect stays sharp: absent, not zero, so the attribute is simply not emitted+        assert_eq!(r("3"), None);+        // an OVAL pad is a stadium, which is a fully rounded roundrect. Not in the original+        // ask, but it is what oval MEANS, and these were importing square too.+        assert_eq!(r("4"), Some(100));+    }+     #[test]     fn a_heatsink_via_joins_the_pad_it_sits_inside() {         use super::{owning_pad, parse_mod_pads};