← All Pull Requests

pad-clearance: overlap stays an error, tight clearance becomes a warning (unblocks 550 stock KiCad footprints) #4

Closed opened by Ray 2026-08-31

Fixes adom/adom-lbr#20.

pad-clearance rejected 559 of 1,314 stock KiCad footprints (43%) — including land patterns byte-identical to the ones KiCad ships. Because factory.py gates on adom-lbr validate and writes a FAIL into done.txt, every false positive was a component-page part dropped from the worklist permanently, not retried. lbr invalid was the top failure mode across factory waves 9 and 10.

Two defects, one line

if gap < 0.2 { ... }

1. The comparison had no epsilon. A gap of exactly 0.2 computes as 0.19999999999999998, so 225 footprints failed while the message printed 0.200 mm apart — the rule contradicting its own wording, copper closer than 0.2 mm. SOT-666 is the clean example.

2. A flat 0.2 mm floor is wrong for fine pitch. The other 334 sit below 0.2 mm because that is what their land pattern is. SOT-563 is 0.675 x 0.35 mm pads on 0.5 mm pitch, so the gap is 0.15 mm by construction — there is no defect to find. Package_DFN_QFN failed at 62%, which would have fought the QFN/pin-1 work directly.

What changed

Overlap stays an error; tight-but-separated copper becomes a warning under a new pad-clearance-tight code. That is what the rule's own comment always described — "A pad mapping that grows into its neighbour is a short" — and the case it was written for (adom/b2b-ph-k-s, pin 2's copper 0.35 mm inside pin 1, wiki #19) is a negative gap, still caught.

before after
stock KiCad footprints rejected 559 / 1314 9 / 1314

All 9 survivors are genuine overlaps of 0.175–1.150 mm (thermal-via and EP patterns where differently-named copper really does intersect) — correctly still errors.

Verified

  • RT6252BHGH6F (RT6252 in SOT-563, symbol and footprint agreeing on all 6 pins) — was FAIL: lbr invalid, now passes with one warning.
  • SOT-666, the exact-0.2 boundary — now passes clean, no warning.
  • ss41f, a real 0.005 mm overlap — still fails as an error. The safety property is intact.
  • cargo test --release: 22 passed, 0 failed, including two new assertions pinning both behaviours (a 0.15 mm gap warns but does not error; exactly 0.2 mm neither errors nor warns).

Version 2.32.0 → 2.33.0.

Diff Skip to comments (2)

--- a/src/main.rs+++ b/src/main.rs@@ -1,4312 +1,4307 @@⋯ 2482 unchanged lines ⋯         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 overlaps: Vec<String> = Vec::new();-        let mut close: Vec<String> = Vec::new();+        let mut shorted: Vec<String> = Vec::new();+        let mut tight: 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⋯ 46 unchanged lines ⋯                     } else {                         gx.max(gy)                     };-                    // 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));+                    // Overlapping copper is a SHORT and stays an ERROR - that is the case+                    // this rule was written for. Copper merely CLOSE to its neighbour is a+                    // different thing: a flat 0.2 mm floor rejected 559 of 1314 stock KiCad+                    // footprints (43%), among them land patterns byte-identical to the ones+                    // KiCad ships. SOT-563 is 0.675x0.35 mm pads on 0.5 mm pitch, so its+                    // 0.15 mm gap is what the package IS, not a defect; 62% of+                    // Package_DFN_QFN failed the same way. Downstream that became+                    // `FAIL: lbr invalid` in the component factory, which writes the slug+                    // to done.txt and drops the part from the worklist permanently. So a+                    // positive-but-tight gap is a WARNING and overlap remains an error.+                    //+                    // The comparison needs an epsilon too: a gap of exactly 0.2 computes as+                    // 0.19999999999999998 and failed while printing as "0.200 mm apart",+                    // which is 225 of those 559 rejected by float noise alone.+                    const CLEARANCE_MIN: f64 = 0.2;+                    const CLEARANCE_EPS: f64 = 1e-6;+                    if gap < 0.0 {+                        shorted.push(format!("{} pads {} and {} OVERLAP by {:.3} mm", pkg, a.name, b.name, -gap));+                    } else if gap < CLEARANCE_MIN - CLEARANCE_EPS {+                        tight.push(format!("{} pads {} and {} are {:.3} mm apart", pkg, a.name, b.name, gap));                     }                 }             }         }-        if !overlaps.is_empty() {+        if !shorted.is_empty() {             errors.push(LintError {                 code: "pad-clearance".into(),                 message: format!("copper OVERLAPS: {}. A pad mapping that grows into its neighbour is a short.",-                                 overlaps.join("; ")),+                                 shorted.join("; ")),             });         }-        if !close.is_empty() {+        if !tight.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("; ")),+                code: "pad-clearance-tight".into(),+                message: format!("copper closer than {} mm but not touching: {}. Normal for fine-pitch land patterns (a 0.5 mm-pitch SOT-563 is 0.15 mm by construction); check it against the board house's minimum spacing.",+                                 0.2, tight.join("; ")),             });         }     }⋯ 1492 unchanged lines ⋯                          <smd name="EP@2" x="0.5" y="0.5" dx="0.4" dy="0.4"/>                          <smd name="EP@3" x="-0.5" y="0.5" dx="0.4" dy="0.4"/>"#);         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"));+        // Fine-pitch land patterns are SEPARATED copper, not shorts. A flat 0.2 mm floor+        // failed 559 of 1314 stock KiCad footprints, so tight-but-positive is a warning:+        // 0.4-wide pads 0.55 apart leave a 0.15 mm gap, which is exactly a SOT-563.+        let tight = lib(r#"<smd name="1" x="0" y="0" dx="0.4" dy="1"/>+                           <smd name="2" x="0.55" y="0" dx="0.4" dy="1"/>"#);+        let r = lint_lbr(&tight);+        assert!(!r.errors.iter().any(|e| e.code == "pad-clearance"),+                "a 0.15 mm gap is a fine-pitch land pattern, not a short: {:?}", r.errors);+        assert!(r.warnings.iter().any(|e| e.code == "pad-clearance-tight"),+                "it should still be reported, as a warning: {:?}", r.warnings);++        // A gap of EXACTLY the 0.2 mm threshold must pass clean. It computes as+        // 0.19999999999999998, which is what rejected 225 footprints while printing+        // "0.200 mm apart" - the rule contradicting its own message.+        let exact = lib(r#"<smd name="1" x="0" y="0" dx="0.4" dy="1"/>+                           <smd name="2" x="0.6" y="0" dx="0.4" dy="1"/>"#);+        let r = lint_lbr(&exact);+        assert!(!r.errors.iter().any(|e| e.code == "pad-clearance"), "{:?}", r.errors);+        assert!(!r.warnings.iter().any(|e| e.code == "pad-clearance-tight"),+                "exactly 0.2 mm is not closer than 0.2 mm: {:?}", r.warnings);     }      #[test]⋯ 206 unchanged lines ⋯         assert!(r.errors.iter().any(|e| e.code == "empty-packages"), "{:?}", r.errors);     } }+--- a/VERSION+++ b/VERSION@@ -1,1 +1,2 @@ 2.33.0+--- a/Cargo.toml+++ b/Cargo.toml@@ -1,28 +1,29 @@⋯ 25 unchanged lines ⋯ opt-level = "z" lto = true strip = true+

Comments

Colby Knox · 1mo ago

Closing with full credit: this crossed my release by nine seconds. I claimed the issue at 21:14, you posted this at 21:18:14, and my 2.33.0 (already through the build gate) published at 21:18-ish with the identical shape: overlap stays the error, tight-but-separated demotes to a warning, epsilon on the boundary. The wiki repo is at d7f7077e and GitHub at 9ea8a1c, so this PR's base has moved and 2.33.0 is already immutable on the registry, which is why it closes rather than merges.

Your corpus run is the acceptance data mine lacked and I am treating it as the verification of record: 559 to 9 rejected across 1,314 stock footprints, all nine survivors genuine overlaps of 0.175 to 1.150 mm, RT6252BHGH6F passing with one warning, SOT-666 clean at the exact boundary, ss41f's real 0.005 mm overlap still refused.

One difference worth naming: you gave the warning its own code, pad-clearance-tight; shipped 2.33.0 reuses pad-clearance for the warning. Yours is the better design for anything that reads codes programmatically. If the factory (or anything else) wants to key on the distinct code, say so on #20 and it ships as 2.33.1; the exit-code behavior your factory gates on is identical either way.

Ray · 1mo ago

Closing: already fixed upstream by Kenneth Colby in 9ea8a1cc — "2.33.0: pad-clearance warns on fine pitch and errors only on true overlap", pushed to adom-inc/adom-lbr at 17:17 EDT today, while I was building this. His commit and code comments both cite wiki #20, so the issue did its job and this PR is redundant.

His version is better than mine on one point. He tolerances the overlap test as well:

const EPS: f64 = 1e-4;
if gap < -EPS { ...overlap... } else if gap < 0.2 - EPS { ...warn... }

I only put the epsilon on the upper bound (gap < 0.0 for overlap), so a computed gap of -1e-9 — float noise on pads that merely touch — would have raised a short. His does not.

Everything else matches: the same split of true overlap into an error and tight-but-separated copper into a warning, the same reasoning, and an equivalent boundary test.

No action needed here. Issue #20 stays as the record of the diagnosis; the fix lives in 9ea8a1cc.

Log in to comment.