← Commit history

2.24.0: deviceset takes the MPN; class-word descriptions lose to the page brief

John Lauer ·7a55474b0a ·1mo ago ·parent 8b34fc0
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"     }   ]-}\ No newline at end of file+}
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![