← Commit history

2.47.4: lint treats overlapping pads on the same pin as one net (Ray, GH PR #3), order-safe across devices; GH ba5f9d1

Colby Knox ·41a1148dcd ·11d ago ·parent 9e5922f
8 files changed +69−5
CHANGELOG.md+3
@@ -1,5 +1,8 @@ # Changelog +## 2.47.4 (2026-10-08)+- Lint (`pad-clearance`, so `validate` too): overlapping pads wired to the same pin are one net, not a short. KiCad's USB-C receptacles stack paired pads on purpose (A1 over B12, both GND; A4 over B9, both VBUS), and EAGLE names the paired pins `GND@1`, `GND@2`; those footprints failed validation as four shorts. Overlapping pads on different pins still fail, and a pair counts as one net only if every device that uses the package wires it that way. (Ray)+ ## 2.47.3 (2026-10-07) - A round KiCad SMD pad (`circle`) is round in Fusion: `roundness="100"`. It used to be written with no roundness, so EAGLE drew it square; a 0.5 mm probe test point came out as a square pad. - An SMD pad with no paste layer gets `cream="no"`, and one with no mask layer gets `stop="no"`, so a test point or a fiducial is not printed with solder paste in Fusion.
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.47.3"+version = "2.47.4" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.47.3"+version = "2.47.4" 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.47.3+2.47.4
bin/adom-lbr
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.47.3",+  "version": "2.47.4",   "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+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.47.3",+  "version": "2.47.4",   "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": {},
src/main.rs+61
@@ -2678,6 +2678,28 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {         // had just been used to fix.         let smd_re = regex_lite::Regex::new(r#"<smd [^>]*/>"#).unwrap();         let pad_re = regex_lite::Regex::new(r#"<pad [^>]*/>"#).unwrap();+        // Which pin each pad wires to, per package, from every device that uses it. Two pads that wire to the SAME+        // pin name are one net, so their copper may overlap by design: KiCad's USB-C receptacles stack the paired+        // A1/B12 (GND) and A4/B9 (VBUS) pads, and EAGLE names the duplicate pins GND@1, GND@2. Without this the+        // HRO TYPE-C-31-M-12 footprint failed as a short (adom/type-c-31-m-12, 2026-10-08).+        let pin_base = |n: &str| n.split('@').next().unwrap_or(n).to_string();+        let dev_re = regex_lite::Regex::new(r#"(?s)<device [^>]*package="([^"]*)"[^>]*>(.*?)</device>"#).unwrap();+        let con_re = regex_lite::Regex::new(r#"<connect [^>]*/>"#).unwrap();+        // Every pin a pad wires to across the devices that use the package: two devices may wire the same package+        // differently, and the pair is one net only if it is one net in all of them.+        let mut pad_net: std::collections::HashMap<(String, String), std::collections::BTreeSet<String>> = std::collections::HashMap::new();+        for d in dev_re.captures_iter(content) {+            for c in con_re.find_iter(&d[2]) {+                let el = c.as_str();+                let get = |k: &str| regex_lite::Regex::new(&format!(r#"\b{}="([^"]*)""#, k)).ok()+                    .and_then(|re| re.captures(el).map(|m| m[1].to_string()));+                if let (Some(pin), Some(pads)) = (get("pin"), get("pad")) {+                    for pad in pads.split_whitespace() {+                        pad_net.entry((d[1].to_string(), pad.to_string())).or_default().insert(pin_base(&pin));+                    }+                }+            }+        }         let attr = |el: &str, k: &str| -> Option<String> {             regex_lite::Regex::new(&format!(r#"{}="([^"]*)""#, k)).ok()?                 .captures(el).map(|c| c[1].to_string())@@ -2716,6 +2738,10 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {                     // NAME, NAME@2, NAME@3. Comparing those would fail every such footprint.                     let base = |n: &str| n.split('@').next().unwrap_or(n).to_string();                     if base(&a.name) == base(&b.name) { continue; }+                    // pads wired to the same pin (one net): overlapping copper is a merged pad, not a short+                    if let (Some(na), Some(nb)) = (pad_net.get(&(pkg.clone(), a.name.clone())), pad_net.get(&(pkg.clone(), b.name.clone()))) {+                        if na.len() == 1 && na == nb { continue; }+                    }                     let gx = (a.x - b.x).abs() - (a.hx + b.hx);                     let gy = (a.y - b.y).abs() - (a.hy + b.hy);                     let gap = if a.round && b.round {@@ -4806,6 +4832,41 @@ mod lint_tests {         assert!(!lint_lbr(&vias).errors.iter().any(|e| e.code == "pad-clearance"));     } +    #[test]+    fn overlapping_pads_on_the_same_pin_are_one_net_but_different_pins_still_short() {+        // KiCad's USB_C_Receptacle_HRO_TYPE-C-31-M-12 stacks A1 over B12 (both GND): EAGLE pins GND@1 and GND@2.+        use super::lint_lbr;+        let lib = |pins: (&str, &str)| format!(r#"<eagle><drawing><layers></layers><library>+            <packages><package name="P"><smd name="A1" x="0" y="0" dx="0.6" dy="1.2"/><smd name="B12" x="0.3" y="0" dx="0.6" dy="1.2"/></package></packages>+            <symbols><symbol name="S"><pin name="{a}" x="0" y="0" length="short"/><pin name="{b}" x="0" y="2" 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="A1"/><connect gate="G$1" pin="{b}" pad="B12"/></connects>+            <technologies><technology name=""/></technologies></device></devices></deviceset></devicesets>+            </library></drawing></eagle>"#, a = pins.0, b = pins.1);+        let same = lint_lbr(&lib(("GND@1", "GND@2")));+        assert!(!same.errors.iter().any(|e| e.code == "pad-clearance"), "same net must pass: {:?}", same.errors);+        let diff = lint_lbr(&lib(("GND", "VBUS")));+        assert!(diff.errors.iter().any(|e| e.code == "pad-clearance"), "different nets must still fail: {:?}", diff.errors);+    }++    #[test]+    fn a_short_in_one_device_is_not_hidden_by_another_device_on_the_same_package() {+        // Two devices share package P: one wires A1/B12 to GND@1/GND@2 (one net), the other to GND/VBUS (a short).+        // The short must be reported whichever device comes last.+        use super::lint_lbr;+        let dev = |name: &str, b: &str| format!(r#"<device name="{name}" package="P"><connects><connect gate="G$1" pin="GND@1" pad="A1"/><connect gate="G$1" pin="{b}" pad="B12"/></connects><technologies><technology name=""/></technologies></device>"#);+        let lib = |first: String, second: String| format!(r#"<eagle><drawing><layers></layers><library>+            <packages><package name="P"><smd name="A1" x="0" y="0" dx="0.6" dy="1.2"/><smd name="B12" x="0.3" y="0" dx="0.6" dy="1.2"/></package></packages>+            <symbols><symbol name="S"><pin name="GND@1" x="0" y="0" length="short"/><pin name="GND@2" x="0" y="2" length="short"/><pin name="VBUS" x="0" y="4" length="short"/></symbol></symbols>+            <devicesets><deviceset name="D"><gates><gate name="G$1" symbol="S" x="0" y="0"/></gates>+            <devices>{first}{second}</devices></deviceset></devicesets>+            </library></drawing></eagle>"#);+        for (a, b) in [(dev("A", "GND@2"), dev("B", "VBUS")), (dev("B", "VBUS"), dev("A", "GND@2"))] {+            let r = lint_lbr(&lib(a, b));+            assert!(r.errors.iter().any(|e| e.code == "pad-clearance"), "the GND/VBUS short must fail in either order: {:?}", r.errors);+        }+    }+     #[test]     fn a_round_pad_off_a_square_pads_corner_is_measured_as_copper_not_boxes() {         // KiCad's stock Package_TO_SOT_THT:TO-92 (staggered): square pad 1 at (0,0), round pad