app
Adom Library
Public Made by Adomby adom
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.
← Commit history
2.47.5: generate refuses a supplied zero-pad footprint instead of inventing copper (#32, John's patch); GH 5230534
11 files changed
+82−10
CHANGELOG.md+3@@ -1,5 +1,8 @@ # Changelog +## 2.47.5 (2026-10-12)+- `generate` refuses a supplied footprint that has no pads (`ERROR: [ZERO_PAD_FOOTPRINT]`, exit 2, nothing written) instead of inventing copper. A mechanical-only footprint for an off-board part (a flying-lead sensor whose leads land on a separate connector) used to come out with two touching placeholder SMD pads and only a lint warning. A zero-pad package variant is refused the same way, and the Manager returns a structured error. Generating with no `--fp` still makes the documented placeholder. (John)+ ## 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)
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.47.4"+version = "2.47.5" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.47.4"+version = "2.47.5" 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.4+2.47.5
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.47.4",+ "version": "2.47.5", "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.4",+ "version": "2.47.5", "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+51−4@@ -2338,11 +2338,21 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S deduped_names } -pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)]) -> String {+pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)]) -> Result<String, String> { generate_lbr_gates(sym, fp_content, symbol_art, symbol_art_scale, pin_map, part_attrs, &[], &[]) } -pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)], alts: &[AltGate], variants: &[PackageVariant]) -> String {+pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)], alts: &[AltGate], variants: &[PackageVariant]) -> Result<String, String> {+ // An explicitly supplied zero-pad footprint is not an absent footprint.+ // Reject this unsupported off-board export before inventing copper or writing output.+ if fp_content.is_some_and(|km| parse_mod_pads(km).is_empty()) {+ return Err("[ZERO_PAD_FOOTPRINT] Supplied footprint has no parsed electrical pads; off-board/mechanical export is unsupported. No placeholder copper was generated. Keep the schematic-only component separate from its actual PCB connector.".into());+ }+ for variant in variants {+ if parse_mod_pads(&variant.km).is_empty() {+ return Err(format!("[ZERO_PAD_FOOTPRINT] Package variant {} has no parsed electrical pads; export refused without inventing copper.", variant.suffix));+ }+ } let mut xml = String::with_capacity(8192); xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -2627,7 +2637,7 @@ Place them within the pad they serve, or they will import as unconnected copper. xml.push_str("</devicesets>\n"); xml.push_str("</library>\n</drawing>\n</eagle>");- xml+ Ok(xml) } // ── Linter ──@@ -3536,7 +3546,10 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or println!("HINT: {} alternate symbol(s) as extra gates of {}: {}", alts.len(), parsed.name, alts.iter().map(|a| format!("{} ({})", a.gate, a.sym.name)).collect::<Vec<_>>().join(", ")); }- let lbr = generate_lbr_gates(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map, &part_attrs, &alts, &pkg_variants);+ let lbr = generate_lbr_gates(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map, &part_attrs, &alts, &pkg_variants).unwrap_or_else(|e| {+ eprintln!("ERROR: {e}");+ process::exit(2);+ }); // Losses are decided here, once, before anything is linted or // written: the parser only collected the margin pads.@@ -5142,3 +5155,37 @@ mod lint_tests { assert!(r.errors.iter().any(|e| e.code == "empty-packages"), "{:?}", r.errors); } }++#[cfg(test)]+mod zero_pad_export_tests {+ use super::*;+ const SENSOR: &str = include_str!("../tests/fixtures/zero-pad/sensor.kicad_sym");+ const MECHANICAL: &str = include_str!("../tests/fixtures/zero-pad/sensor.kicad_mod");+ const ELECTRICAL: &str = r#"(footprint "two-pin"+ (pad "1" smd rect (at -2 0) (size 1 1) (layers "F.Cu"))+ (pad "2" smd rect (at 2 0) (size 1 1) (layers "F.Cu")))"#;+ #[test]+ fn explicit_zero_pad_footprint_is_refused_without_copper() {+ let sym = parse_kicad_sym(SENSOR, None).unwrap();+ let err = generate_lbr(&sym, Some(MECHANICAL), None, 0.38, &Default::default(), &[]).unwrap_err();+ assert!(err.contains("ZERO_PAD_FOOTPRINT"));+ }+ #[test]+ fn zero_pad_variant_is_refused_as_well() {+ let sym = parse_kicad_sym(SENSOR, None).unwrap();+ let variants = [PackageVariant { suffix: "-offboard".into(), km: MECHANICAL.into(), description: None }];+ let err = generate_lbr_gates(&sym, Some(ELECTRICAL), None, 0.38, &Default::default(), &[], &[], &variants).unwrap_err();+ assert!(err.contains("ZERO_PAD_FOOTPRINT"));+ assert!(err.contains("-offboard"));+ }+ #[test]+ fn real_pads_remain_exact_and_absent_footprint_stays_distinct() {+ let sym = parse_kicad_sym(SENSOR, None).unwrap();+ let xml = generate_lbr(&sym, Some(ELECTRICAL), None, 0.38, &Default::default(), &[]).unwrap();+ assert_eq!(xml.matches("<smd ").count(), 2);+ assert!(xml.contains("x=\"-2.0000\""));+ assert!(xml.contains("x=\"2.0000\""));+ let placeholder = generate_lbr(&sym, None, None, 0.38, &Default::default(), &[]).unwrap();+ assert!(placeholder.contains("placeholder footprint"));+ }+}
src/manage.rs+4−1@@ -1302,7 +1302,10 @@ fn export_part(slug: &str) -> Value { let lbr = match crate::parse_kicad_sym(&sym_txt, Some(&mpn)) { Ok(mut parsed) => { crate::relayout_two_columns(&mut parsed, false);- Some(crate::generate_lbr(&parsed, Some(&fp_txt), None, 0.38, &pin_map, &[]))+ match crate::generate_lbr(&parsed, Some(&fp_txt), None, 0.38, &pin_map, &[]) {+ Ok(lbr) => Some(lbr),+ Err(error) => return json!({"status":"error","error":error}),+ } } Err(_) => None, };
tests/fixtures/zero-pad/sensor.kicad_modadded+7@@ -0,0 +1,7 @@+(footprint "NTCLE428E3103H301A_MECHANICAL_ONLY" (version 20240108) (generator "adom") (layer "F.Cu")+(attr board_only exclude_from_pos_files exclude_from_bom)+(descr "Off-board300mm straight sensor/cable envelope. Zero copper pads; terminate the flying leads using a separately selected connector.")+(fp_rect (start 0 -1.5) (end 10 1.5) (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))+(fp_line (start 10 -.425) (end 300 -.425) (stroke (width .1) (type default)) (layer "F.Fab"))+(fp_line (start 10 .425) (end 300 .425) (stroke (width .1) (type default)) (layer "F.Fab"))+(model "${KIPRJMOD}/ntcle428e3103h301a.step" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0))))
tests/fixtures/zero-pad/sensor.kicad_symadded+12@@ -0,0 +1,12 @@+(kicad_symbol_lib (version 20231120) (generator "adom")+(symbol "NTCLE428E3103H301A" (in_bom yes) (on_board no)+(property "Reference" "TH" (at 0 5.08 0) (effects (font (size 1.27 1.27))))+(property "Value" "NTCLE428E3103H301A" (at 0 -5.08 0) (effects (font (size 1.27 1.27))))+(property "MPN" "NTCLE428E3103H301A" (at 0 0 0) (hide yes) (effects (font (size 1.27 1.27))))+(property "Datasheet" "https://www.vishay.com/docs/29186/ntcle428.pdf" (at 0 0 0) (hide yes) (effects (font (size 1.27 1.27))))+(symbol "NTCLE428E3103H301A_0_1"+(rectangle (start -2.54 1.27) (end 2.54 -1.27) (stroke (width 0) (type default)) (fill (type background)))+(polyline (pts (xy -3.175 -2.54) (xy 3.175 2.54) (xy 4.445 2.54)) (stroke (width 0) (type default)) (fill (type none))))+(symbol "NTCLE428E3103H301A_1_1"+(pin passive line (at -5.08 0 0) (length 2.54) (name "Lead1" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))+(pin passive line (at 5.08 0 180) (length 2.54) (name "Lead2" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1))))))))