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
Fusion-reject fixes: empty pin/connect name -> pad number; per-symbol pin-uniqueness (merged libs); lint ERRORs for empty pin/connect + unresolved connect (2.2.1)
1 file changed
+91−26
src/main.rs+91−26@@ -527,23 +527,32 @@ fn pin_length_keyword(len: f64) -> &'static str { } fn dedup_pin_names(pins: &[KicadPin]) -> Vec<String> {- let mut counts: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();- for pin in pins {- *counts.entry(&pin.name).or_insert(0) += 1;+ // Effective EAGLE pin name: when the KiCad pin name is empty (common on+ // mechanical / 1-pin parts: `(number "1")(name "")`), fall back to the pad+ // NUMBER. Fusion REJECTS `<pin name="">` / `<connect pin="">`, so a name is+ // mandatory. This is the single source of truth for both the <pin> and the+ // <connect> emission, so the fallback fixes both at once.+ let eff: Vec<String> = pins.iter().enumerate().map(|(i, p)| {+ let n = p.name.trim();+ if !n.is_empty() {+ n.to_string()+ } else {+ let num = p.number.trim();+ if !num.is_empty() { num.to_string() } else { format!("P{}", i + 1) }+ }+ }).collect();+ let mut counts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+ for n in &eff {+ *counts.entry(n.clone()).or_insert(0) += 1; }- let mut seen: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();- pins.iter().map(|pin| {- let total = counts[pin.name.as_str()];- if total == 1 {- pin.name.clone()+ let mut seen: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+ eff.iter().map(|name| {+ if counts[name] == 1 {+ name.clone() } else {- let n = seen.entry(&pin.name).or_insert(0);- *n += 1;- if *n == 1 {- pin.name.clone()- } else {- format!("{}@{}", pin.name, n)- }+ let c = seen.entry(name.clone()).or_insert(0);+ *c += 1;+ if *c == 1 { name.clone() } else { format!("{}@{}", name, c) } } }).collect() }@@ -1084,18 +1093,74 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult { errors.push(LintError { code: "device-no-package".into(), message: "Device has no package attribute — Fusion needs a package to show the component".into() }); } - // Check for duplicate pin names (EAGLE requires unique)- let pin_re = regex_lite::Regex::new(r#"<pin name="([^"]+)""#).unwrap();- let mut pin_names = std::collections::HashMap::new();- for cap in pin_re.captures_iter(content) {- *pin_names.entry(cap[1].to_string()).or_insert(0u32) += 1;+ // Per-symbol pin analysis. Pin names must be NON-EMPTY (Fusion rejects+ // <pin name="">) and UNIQUE WITHIN a symbol. Across symbols, a repeat is+ // LEGAL — e.g. a merged multi-part lib where four 1-pin symbols each have a+ // pin "1" — so the uniqueness check is scoped to each <symbol> block, not+ // the whole file.+ let sym_block_re = regex_lite::Regex::new(r#"(?s)<symbol name="([^"]*)">(.*?)</symbol>"#).unwrap();+ let pin_name_re = regex_lite::Regex::new(r#"<pin name="([^"]*)""#).unwrap();+ let mut sym_pins: std::collections::HashMap<String, std::collections::HashSet<String>> =+ std::collections::HashMap::new();+ for sc in sym_block_re.captures_iter(content) {+ let sname = sc[1].to_string();+ let body = &sc[2];+ let mut within: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+ let mut set = std::collections::HashSet::new();+ for pc in pin_name_re.captures_iter(body) {+ let pname = pc[1].to_string();+ if pname.trim().is_empty() {+ errors.push(LintError {+ code: "empty-pin-name".into(),+ message: format!("Symbol '{}' has a <pin name=\"\"> — Fusion rejects empty pin names (fall back to the pad number)", sname),+ });+ continue;+ }+ *within.entry(pname.clone()).or_insert(0) += 1;+ set.insert(pname);+ }+ for (name, count) in &within {+ if *count > 1 {+ errors.push(LintError {+ code: "duplicate-pin-name".into(),+ message: format!("Pin name '{}' appears {} times in symbol '{}' — EAGLE requires unique pin names within a symbol (use @suffix)", name, count, sname),+ });+ }+ }+ sym_pins.insert(sname, set); }- for (name, count) in &pin_names {- if *count > 1 {- errors.push(LintError {- code: "duplicate-pin-name".into(),- message: format!("Pin name '{}' appears {} times — EAGLE requires unique pin names (use @suffix for duplicates)", name, count),- });++ // Connect integrity: no empty pin=, and every connect's pin must resolve to a+ // named <pin> in its gate's symbol. Resolve per-deviceset (gate names like+ // G$1 repeat across devicesets, so a global map would cross-wire them).+ let deviceset_re = regex_lite::Regex::new(r#"(?s)<deviceset\b.*?</deviceset>"#).unwrap();+ let gate_re = regex_lite::Regex::new(r#"<gate name="([^"]*)" symbol="([^"]*)""#).unwrap();+ let connect_full_re = regex_lite::Regex::new(r#"<connect gate="([^"]*)" pin="([^"]*)" pad="([^"]*)"/>"#).unwrap();+ for ds in deviceset_re.captures_iter(content) {+ let block = &ds[0];+ let mut gate_sym: std::collections::HashMap<String, String> = std::collections::HashMap::new();+ for g in gate_re.captures_iter(block) {+ gate_sym.insert(g[1].to_string(), g[2].to_string());+ }+ for c in connect_full_re.captures_iter(block) {+ let (gate, pin) = (c[1].to_string(), c[2].to_string());+ if pin.trim().is_empty() {+ errors.push(LintError {+ code: "empty-connect-pin".into(),+ message: "A <connect> has pin=\"\" — Fusion rejects empty connect pins (map to the gate's pin name)".into(),+ });+ continue;+ }+ if let Some(sym) = gate_sym.get(&gate) {+ if let Some(names) = sym_pins.get(sym) {+ if !names.contains(&pin) {+ errors.push(LintError {+ code: "connect-pin-unresolved".into(),+ message: format!("Connect pin '{}' (gate '{}') matches no <pin name> in symbol '{}' — Fusion cannot bind it", pin, gate, sym),+ });+ }+ }+ } } }