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.24.0: deviceset takes the MPN; class-word descriptions lose to the page brief
7 files changed
+91−11
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.23.1"+version = "2.24.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.23.1"+version = "2.24.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.23.1+2.24.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.23.1",+ "version": "2.24.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+2−2@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.23.1",+ "version": "2.24.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.", "dependencies": {},@@ -88,4 +88,4 @@ "label": "Browse parts" } ]-}+}
src/main.rs+85−5@@ -378,6 +378,11 @@ struct ParsedSymbol { package_name: Option<String>, /// A human line for the package, e.g. "SOIC-16, 3.9 x 9.9 mm body, 1.27 mm pitch". package_description: Option<String>,+ /// The EAGLE `<deviceset>` name. None means "use `name`". A deviceset is the ORDERABLE+ /// part, so it wants the MPN; the symbol underneath it can keep a class name like "L",+ /// which is shared between every inductor. A user who opens a library and finds a+ /// deviceset called "L" cannot tell which part it is.+ deviceset_name: Option<String>, } pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedSymbol, String> {@@ -491,6 +496,7 @@ pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Res Ok(ParsedSymbol { package_name: None, package_description: None,+ deviceset_name: None, name, reference: prop("Reference"), value: prop("Value"),@@ -621,6 +627,7 @@ fn parse_eagle_lbr(content: &str) -> Result<ParsedSymbol, String> { Ok(ParsedSymbol { package_name: None, package_description: None,+ deviceset_name: None, value: name.clone(), name, reference,@@ -987,6 +994,7 @@ fn synth_symbol_from_mod(km: &str, name: &str) -> ParsedSymbol { ParsedSymbol { package_name: None, package_description: None,+ deviceset_name: None, name: name.to_string(), reference: "U".into(), value: name.to_string(),@@ -1396,13 +1404,53 @@ fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) /// Catalogue boilerplate that belongs on a wiki PAGE, not in a part's description. A page /// brief is written for a reader browsing the library; the same sentence repeated in every /// Fusion Description cell is noise that pushes the real text out of a narrow column.+/// KiCad's stock symbol libraries name a symbol after its CLASS ("R", "C", "L", "D",+/// "LED", "Crystal_GND24", "Conn_01x02"), because they describe a kind of part rather than+/// a specific one. That name is right for a symbol and wrong for a DEVICESET: a user who+/// opens a library and sees "L" cannot tell which inductor it is.+fn is_generic_part_name(name: &str) -> bool {+ let n = name.trim();+ const EXACT: [&str; 14] = ["R", "C", "L", "D", "Q", "Y", "U", "J", "K", "LED", "FB",+ "Fuse", "Crystal", "Jumper"];+ if EXACT.iter().any(|e| e.eq_ignore_ascii_case(n)) { return true; }+ const PREFIX: [&str; 12] = ["Conn_", "Crystal", "R_", "C_", "L_", "D_", "LED_", "Q_",+ "Fuse_", "Ferrite", "Jumper_", "TestPoint"];+ PREFIX.iter().any(|p| n.len() > p.len() && n.to_lowercase().starts_with(&p.to_lowercase()))+}++/// A description that names a CLASS of part rather than the part. KiCad's stock libraries+/// ship these, so a converted library shows "Inductor" or "Unpolarized capacitor" where the+/// wiki page's brief says which inductor and which capacitor. When both exist, the brief is+/// the one worth carrying into Fusion.+fn is_generic_description(text: &str) -> bool {+ let t = text.trim().to_lowercase();+ if t.is_empty() { return true; }+ if t.contains("script generated") || t.contains("kicad-library-utils") { return true; }+ const CLASSES: [&str; 22] = [+ "resistor", "capacitor", "unpolarized capacitor", "polarized capacitor",+ "inductor", "ferrite bead", "diode", "light emitting diode", "led",+ "zener diode", "schottky diode", "crystal", "oscillator", "transistor",+ "npn transistor", "pnp transistor", "mosfet", "jfet", "fuse", "jumper",+ "test point", "connector",+ ];+ if CLASSES.iter().any(|c| t == *c) { return true; }+ // "Four pin crystal, GND on pins 2 and 4" and "Generic connector, single row, 01x02"+ t.starts_with("generic connector")+ || t.starts_with("generic ")+ || (t.contains("crystal") && t.contains("pin") && t.len() < 60)+}+ fn strip_catalog_boilerplate(text: &str) -> String {- const NOISE: [&str; 5] = [+ // LONGEST FIRST: "Datasheet-verified pinout" has to go before "Datasheet-verified",+ // or the short form eats its prefix and strands the word "pinout" on its own.+ const NOISE: [&str; 7] = [ "Datasheet-verified pinout", "KiCad symbol/footprint/3D", "Symbol, footprint and 3D model", "Generated by adom", "Verified pinout",+ "Datasheet-verified",+ "Datasheet verified", ]; // Remove the boilerplate PHRASES in place. An earlier version split the text on every // '.' and rejoined it, which turned "2.5V precision reference" into "2. 5V precision@@ -1785,7 +1833,7 @@ pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, s // Deviceset with COMPLETE device (package + connects) let ref_prefix = if sym.reference.is_empty() { "U" } else { &sym.reference }; xml.push_str("<devicesets>\n");- xml.push_str(&format!("<deviceset name=\"{}\" prefix=\"{}\">\n", esc_xml(&sym.name), esc_xml(ref_prefix)));+ xml.push_str(&format!("<deviceset name=\"{}\" prefix=\"{}\">\n", esc_xml(sym.deviceset_name.as_deref().unwrap_or(&sym.name)), esc_xml(ref_prefix))); xml.push_str(&format!(" <description>{}</description>\n", esc_xml(&sym.description))); xml.push_str(" <gates>\n"); xml.push_str(&format!(" <gate name=\"G$1\" symbol=\"{}\" x=\"0\" y=\"0\"{}/>\n", esc_xml(&sym.name),@@ -2284,9 +2332,17 @@ fn main() { // Fusion Description cell crowds out the words that identify the part. if let Some(d) = description.clone() { parsed.description = d;- } else if parsed.description.trim().is_empty() {+ } else if is_generic_description(&parsed.description) {+ // The source Description is empty, or it names a CLASS rather than a part+ // ("Inductor", "Unpolarized capacitor", "Generic connector, single row").+ // The page brief says WHICH inductor, so it wins. An explicit --description+ // still beats both. if let Some(brief) = pm_path.as_ref().and_then(|p| page_brief(p)) {- println!("HINT: symbol had no Description; using the page brief so the deviceset is not blank in Fusion.");+ if parsed.description.trim().is_empty() {+ println!("HINT: symbol had no Description; using the page brief so the deviceset is not blank in Fusion.");+ } else {+ println!("HINT: the source Description is a class word (\"{}\"); using the page brief instead, which names the actual part.", parsed.description.trim());+ } parsed.description = brief; } }@@ -2297,6 +2353,22 @@ fn main() { parsed.package_description = Some(package_description(fp_content.as_deref(), parsed.package_name.as_deref().unwrap_or(&parsed.name))); + // A DEVICESET IS THE ORDERABLE PART, so when the symbol carries a KiCad class+ // name ("L", "Crystal_GND24", "Conn_01x02") and the page knows the MPN, the+ // deviceset takes the MPN. The SYMBOL keeps its class name: it is the drawing,+ // it is shared between every part of that class, and assemble dedupes symbols+ // by content anyway. Without this a user opening the library sees a deviceset+ // called "L" and cannot tell which inductor it is.+ if name.is_none() && is_generic_part_name(&parsed.name) {+ if let Some(mpn) = part_attrs.iter().find(|(k, _)| k == "MPN").map(|(_, v)| v.clone()) {+ let mpn = eagle_safe_name(&mpn);+ if mpn != parsed.name {+ println!("HINT: deviceset named \"{}\" from the part's MPN; the symbol keeps the class name \"{}\". --name overrides.", mpn, parsed.name);+ parsed.deviceset_name = Some(mpn);+ }+ }+ }+ // A SEMANTIC PART WITH NO ARTWORK IS AN UNFINISHED SYMBOL, and it is worth // saying so at generate time. A diode, LED, crystal or inductor that reaches a // schematic as two bare pin stubs reads as a placeholder to any engineer (wiki@@ -3144,6 +3216,13 @@ mod lint_tests { assert_eq!(strip_catalog_boilerplate("STM32F103. 72 MHz Cortex-M3"), "STM32F103. 72 MHz Cortex-M3"); // nothing but boilerplate: keep the original rather than emit an empty description assert!(!strip_catalog_boilerplate("Datasheet-verified pinout").is_empty());+ // the real ray/srn6045-100m brief ends with a BARE "Datasheet-verified.", which the+ // longer phrase did not match, so it survived on the end of the description+ assert_eq!(+ strip_catalog_boilerplate("Bourns 10 uH semi-shielded SMD power inductor, 6.0x6.0 mm. 3.0 A Isat, 58.6 mOhm DCR. Datasheet-verified."),+ "Bourns 10 uH semi-shielded SMD power inductor, 6.0x6.0 mm. 3.0 A Isat, 58.6 mOhm DCR");+ // and the longer form must not strand the word "pinout"+ assert!(!strip_catalog_boilerplate("ADR4525 reference. Datasheet-verified pinout.").contains("pinout")); } #[test]@@ -3198,7 +3277,7 @@ mod lint_tests { footprint: String::new(), description: String::new(), pins: vec![pin("1", 270, 3.81), pin("2", 90, -3.81)], body: None, group_labels: vec![], graphics,- package_name: None, package_description: None,+ package_name: None, package_description: None, deviceset_name: None, }; // with its own drawing (the capacitor's two plates) it must be LEFT ALONE let mut drawn = mk(vec![SymGraphic::Poly(vec![(-2.032, 0.762), (2.032, 0.762)])]);@@ -3222,6 +3301,7 @@ mod lint_tests { let mut sym = ParsedSymbol { package_name: None, package_description: None,+ deviceset_name: None, name: "U1".into(), reference: "U".into(), value: String::new(), footprint: String::new(), description: String::new(), pins: vec![