← Commit history

2.33.0: pad-clearance warns on fine pitch and errors only on true overlap (wiki #20)

Colby Knox ·d7f7077eb4 ·1mo ago ·parent 65b77e2
7 files changed +71−22
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.32.0"+version = "2.33.0" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.32.0"+version = "2.33.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.32.0+2.33.0
bin/adom-lbr
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.32.0",+  "version": "2.33.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+8−8
@@ -1,8 +1,8 @@ {   "slug": "adom-lbr",-  "version": "2.32.0",+  "version": "2.33.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.",+  "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": {},   "tags": [     "eda",@@ -61,15 +61,15 @@     "screenshots/**",     "dockbar.json"   ],-  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",   "sample_prompts": [     {       "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",-      "label": "KiCad → Altium"+      "label": "KiCad \u2192 Altium"     },     {       "prompt": "Import this Altium footprint into the adom-lbr JSON",-      "label": "Altium → JSON"+      "label": "Altium \u2192 JSON"     },     {       "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +77,15 @@     },     {       "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",-      "label": "Altium → KiCad"+      "label": "Altium \u2192 KiCad"     },     {       "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",-      "label": "JSON → KiCad"+      "label": "JSON \u2192 KiCad"     },     {       "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup",       "label": "Browse parts"     }   ]-}\ No newline at end of file+}
src/main.rs+59−10
@@ -2483,7 +2483,8 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {         struct P { name: String, x: f64, y: f64, hx: f64, hy: f64, round: bool }         let num = |s: &str| s.parse::<f64>().unwrap_or(0.0);         let rot90 = |r: Option<&str>| matches!(r, Some("R90") | Some("R270"));-        let mut bad: Vec<String> = Vec::new();+        let mut overlaps: Vec<String> = Vec::new();+        let mut close: Vec<String> = Vec::new();         let pkg_re = regex_lite::Regex::new(r#"(?s)<package name="([^"]*)">(.*?)</package>"#).unwrap();         // Match the whole element, then read each attribute from it. An optional (?:rot=...)?         // group followed by a greedy [^>]* silently matches EMPTY and lets the tail eat the@@ -2536,21 +2537,37 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {                     } else {                         gx.max(gy)                     };-                    if gap < 0.2 {-                        bad.push(if gap < 0.0 {-                            format!("{} pads {} and {} OVERLAP by {:.3} mm", pkg, a.name, b.name, -gap)-                        } else {-                            format!("{} pads {} and {} are {:.3} mm apart", pkg, a.name, b.name, gap)-                        });+                    // True copper OVERLAP is a short and stays a hard error. A small+                    // POSITIVE gap is what fine-pitch land patterns look like by+                    // construction (a 0.5 mm-pitch SOT-563 has 0.150 mm between pads in+                    // KiCad's own library), so it warns instead of failing: a flat 0.2 mm+                    // ERROR rejected 559 of 1314 stock KiCad footprints (wiki #20), each+                    // one a part silently dropped from the factory worklist. The epsilon+                    // keeps a gap of exactly the threshold clean: 1.2 - 1.0 is+                    // 0.19999999999999996 in floats, and a message that says "closer than+                    // 0.2 mm" must not reject a pair that is "0.200 mm apart".+                    const EPS: f64 = 1e-4;+                    if gap < -EPS {+                        overlaps.push(format!("{} pads {} and {} OVERLAP by {:.3} mm", pkg, a.name, b.name, -gap));+                    } else if gap < 0.2 - EPS {+                        close.push(format!("{} pads {} and {} are {:.3} mm apart", pkg, a.name, b.name, gap));                     }                 }             }         }-        if !bad.is_empty() {+        if !overlaps.is_empty() {             errors.push(LintError {                 code: "pad-clearance".into(),-                message: format!("copper closer than 0.2 mm: {}. A pad mapping that grows into its neighbour is a short.",-                                 bad.join("; ")),+                message: format!("copper OVERLAPS: {}. A pad mapping that grows into its neighbour is a short.",+                                 overlaps.join("; ")),+            });+        }+        if !close.is_empty() {+            warnings.push(LintError {+                code: "pad-clearance".into(),+                message: format!("copper closer than 0.2 mm: {}. Fine-pitch land patterns sit here by construction; \+check the source footprint intends it rather than a mapping having grown a pad.",+                                 close.join("; ")),             });         }     }@@ -4051,6 +4068,38 @@ mod lint_tests {         assert!(!lint_lbr(&vias).errors.iter().any(|e| e.code == "pad-clearance"));     } +    #[test]+    fn fine_pitch_gaps_warn_instead_of_failing_and_the_exact_boundary_passes_clean() {+        // wiki #20: a flat 0.2 mm ERROR rejected 559 of 1314 stock KiCad footprints,+        // and 225 of them sat at exactly 0.200 mm because of float representation.+        use super::lint_lbr;+        let lib = |pads: &str| format!(r#"<eagle><drawing><layers></layers><library>+            <packages><package name="P">{pads}</package></packages>+            <symbols><symbol name="S"><pin name="A" x="0" y="0" length="short"/></symbol></symbols>+            <devicesets><deviceset name="D"><gates><gate name="G$1" symbol="S" x="0" y="0"/></gates>+            <devices><device name="" package="P"><connects><connect gate="G$1" pin="A" pad="1"/></connects>+            <technologies><technology name=""/></technologies></device></devices></deviceset></devicesets>+            </library></drawing></eagle>"#);+        // a 0.5 mm-pitch SOT-563 row: 0.150 mm apart by construction. WARNING, never error.+        let fine = lib(r#"<smd name="1" x="0" y="0" dx="0.675" dy="0.35"/>+                          <smd name="2" x="0" y="0.5" dx="0.675" dy="0.35"/>"#);+        let r = lint_lbr(&fine);+        assert!(r.errors.iter().all(|e| e.code != "pad-clearance"), "fine pitch must not FAIL: {:?}", r.errors);+        assert!(r.warnings.iter().any(|w| w.code == "pad-clearance"), "but it should still WARN: {:?}", r.warnings);+        // exactly 0.200 mm apart: 1.0 mm rect pads at x -0.6 and 0.6 compute a gap of+        // 0.19999999999999996 in floats. The epsilon keeps the rule's own wording true:+        // "closer than 0.2 mm" cannot reject "0.200 mm apart". Clean, not even a warning.+        let boundary = lib(r#"<smd name="1" x="-0.6" y="0" dx="1.0" dy="1.0"/>+                              <smd name="2" x="0.6" y="0" dx="1.0" dy="1.0"/>"#);+        let rb = lint_lbr(&boundary);+        assert!(rb.errors.iter().all(|e| e.code != "pad-clearance"), "{:?}", rb.errors);+        assert!(rb.warnings.iter().all(|w| w.code != "pad-clearance"), "{:?}", rb.warnings);+        // negative control: genuinely overlapping copper is still a hard error.+        let overlap = lib(r#"<smd name="1" x="0" y="0" dx="1.2" dy="1.2"/>+                             <smd name="2" x="1.0" y="0" dx="1.2" dy="1.2"/>"#);+        assert!(lint_lbr(&overlap).errors.iter().any(|e| e.code == "pad-clearance"));+    }+     #[test]     fn roundrect_pads_carry_their_corner_radius_into_eagle() {         use super::parse_mod_pads;