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.22.0: descriptions on symbol, deviceset and package rows
8 files changed
+136−9
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.21.0"+version = "2.22.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.21.0"+version = "2.22.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."
SKILL.md+28@@ -157,6 +157,34 @@ adom-lbr generate --sym CH340G.kicad_sym --fp CH340G.kicad_mod --part-metadata p --alt-sym CH340G-2CG.kicad_sym --alt-sym CH340G-SQG.kicad_sym -o CH340G.lbr ``` +### Naming and descriptions (2.22.0)++`--name` names the SYMBOL and the DEVICESET only. The PACKAGE takes the footprint's own+`(footprint "NAME")`, so N symbol variants of one chip share ONE package and `assemble`+dedupes it to a single footprint (and the bridge to a single 3D model). `--package-name`+overrides that when you want the MPN, or any other name, in Fusion's package column.++Descriptions matter in Fusion: the tree shows a Description column for every row and the+ADD dialog searches the deviceset's text. adom-lbr fills all three:++- SYMBOL and DEVICESET take, in order: `--description`, else the `.kicad_sym`'s own+ Description property, else the wiki page's brief from `--part-metadata`.+- PACKAGE takes the `.kicad_mod`'s `(descr …)`, else a line derived from the footprint+ name: `SOIC-8_3.9x4.9mm_P1.27mm` reads as `SOIC-8, 3.9 x 4.9 mm body, 1.27 mm pitch`.+ It no longer repeats the package name, which told a reader nothing.+- Catalogue boilerplate ("Datasheet-verified pinout", "KiCad symbol/footprint/3D") is+ stripped from a page brief before it becomes a description. It is page prose.++For a VARIANT SET, give each `generate` its own `--description` saying which look it is.+Ten devicesets carrying one identical sentence tell a reader nothing about which to pick:++```+adom-lbr generate --sym CH340G-SQ.kicad_sym --fp CH340G.kicad_mod --part-metadata page.json \+ --name CH340G-SQ \+ --description "CH340G: USB to serial converter, UART. Square 4-side symbol, pins grouped by function" \+ -o CH340G-SQ.lbr+```+ Duplicate PAD names (KiCad same-net arrays: thermal vias, ground paddles, shield tabs) are resolved to the EAGLE `@`-suffix convention (`17`, `17@2`, `17@3`, ...) with one space-separated multi-pad `<connect>` binding the whole
VERSION+1−1@@ -1 +1 @@-2.21.0+2.22.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.21.0",+ "version": "2.22.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.21.0",+ "version": "2.22.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+102−3@@ -68,6 +68,12 @@ enum Commands { /// falling back to the symbol name when the footprint does not carry one. #[arg(long)] package_name: Option<String>,+ /// Description for the symbol and deviceset, overriding the .kicad_sym's own+ /// Description property and the page brief. Use it to say which LOOK a variant is+ /// ("4-side square symbol, pins grouped by function"), because a set of variants+ /// that all carry the same sentence tells a reader nothing about which to pick.+ #[arg(long)]+ description: Option<String>, /// Chip outline drawn on the schematic symbol as <wire>s, OVERRIDING the /// kicad_sym's own drawing. PREFERRED: adom-symbol's OUTLINE CONTRACT /// `<mpn>-symbol-outline.mm.svg` (+ its `.json` sidecar) — placed 1:1 in@@ -370,6 +376,8 @@ struct ParsedSymbol { /// and a Fusion library gained a duplicate footprint (and a duplicate 3D model) per /// variant. A package IS the footprint, so it takes the footprint's name. 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>, } pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedSymbol, String> {@@ -482,6 +490,7 @@ pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Res Ok(ParsedSymbol { package_name: None,+ package_description: None, name, reference: prop("Reference"), value: prop("Value"),@@ -611,6 +620,7 @@ fn parse_eagle_lbr(content: &str) -> Result<ParsedSymbol, String> { Ok(ParsedSymbol { package_name: None,+ package_description: None, value: name.clone(), name, reference,@@ -966,6 +976,7 @@ fn synth_symbol_from_mod(km: &str, name: &str) -> ParsedSymbol { }).collect(); ParsedSymbol { package_name: None,+ package_description: None, name: name.to_string(), reference: "U".into(), value: name.to_string(),@@ -1372,6 +1383,74 @@ fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) /// ray/xl7026e1), so their devicesets reached Fusion with an empty Description column and /// nothing to search on. The page brief is the right text and it is already sitting beside /// the symbol, so the library carries it rather than the importer having to guess.+/// 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.+fn strip_catalog_boilerplate(text: &str) -> String {+ const NOISE: [&str; 5] = [+ "Datasheet-verified pinout",+ "KiCad symbol/footprint/3D",+ "Symbol, footprint and 3D model",+ "Generated by adom",+ "Verified pinout",+ ];+ // 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+ // reference": a part number or a voltage is full of periods that are not sentence ends.+ let mut out = text.to_string();+ for n in NOISE {+ while let Some(i) = out.find(n) {+ // take the phrase plus any trailing separator it brought with it+ let mut j = i + n.len();+ let bytes = out.as_bytes();+ while j < bytes.len() && matches!(bytes[j], b'.' | b',' | b';' | b' ' | b'\n') { j += 1; }+ out.replace_range(i..j, "");+ }+ }+ let out = out.trim().trim_end_matches(&['.', ',', ';', ' '][..]).trim().to_string();+ if out.is_empty() { text.trim().to_string() } else { out }+}++/// A human line for the PACKAGE. The `.kicad_mod`'s own `(descr …)` when it has one,+/// otherwise the geometry encoded in the footprint NAME turned into words:+/// `SOIC-16_3.9x9.9mm_P1.27mm` reads as "SOIC-16, 3.9 x 9.9 mm body, 1.27 mm pitch".+/// It used to repeat the package name, which tells a reader nothing they cannot already+/// see in the Name column beside it.+fn package_description(fp_content: Option<&str>, package_name: &str) -> String {+ if let Some(c) = fp_content {+ if let Some(re) = regex_lite::Regex::new(r#"\(descr\s+"([^"]*)""#).ok() {+ if let Some(m) = re.captures(c).and_then(|c| c.get(1)) {+ // drop a trailing URL: useful in a file, useless in a narrow Fusion column+ let t = m.as_str();+ let mut t = t.split("http").next().unwrap_or(t).trim().to_string();+ // Cutting the URL can leave the bracket that introduced it, e.g.+ // "SOIC, 8 Pin (JEDEC MS-012AA, " from "(JEDEC MS-012AA, https://...)".+ // Drop from an unmatched '(' rather than shipping half a parenthetical.+ if t.matches('(').count() > t.matches(')').count() {+ if let Some(i) = t.rfind('(') { t.truncate(i); }+ }+ let t = t.trim().trim_end_matches(&[',', ';', '·', '-'][..]).trim().to_string();+ if !t.is_empty() { return t; }+ }+ }+ }+ let mut family = String::new();+ let mut body = String::new();+ let mut pitch = String::new();+ let dims = regex_lite::Regex::new(r"^([\d.]+)x([\d.]+)mm$").unwrap();+ let pit = regex_lite::Regex::new(r"^P([\d.]+)mm$").unwrap();+ for (i, tok) in package_name.split('_').enumerate() {+ if i == 0 { family = tok.to_string(); continue; }+ if let Some(c) = dims.captures(tok) { body = format!("{} x {} mm body", &c[1], &c[2]); continue; }+ if let Some(c) = pit.captures(tok) { pitch = format!("{} mm pitch", &c[1]); continue; }+ }+ let mut parts: Vec<String> = vec![family];+ if !body.is_empty() { parts.push(body); }+ if !pitch.is_empty() { parts.push(pitch); }+ parts.retain(|p| !p.is_empty());+ parts.join(", ")+}+ fn page_brief(path: &std::path::Path) -> Option<String> { let raw = fs::read_to_string(path).ok()?; let doc = serde_json::from_str::<serde_json::Value>(&raw).ok()?;@@ -1420,6 +1499,12 @@ pub(crate) struct AltGate { /// Returns the pin names as emitted (deduplicated); the connects must reuse them. fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64) -> Vec<String> { xml.push_str(&format!("<symbol name=\"{}\">\n", esc_xml(&sym.name)));+ // Fusion shows a Description column for every row in the tree, and the SYMBOL rows were+ // blank for every part we ship. The deviceset text is the right text for the symbol too:+ // it is what a person is looking for when they are reading the list.+ if !sym.description.trim().is_empty() {+ xml.push_str(&format!(" <description>{}</description>\n", esc_xml(&sym.description)));+ } if let Some(body) = &sym.body { xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.top, body.right, body.top));@@ -1527,7 +1612,7 @@ pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, s // byte-identical except for this line, and assemble's content dedup could not see // that they were the same footprint: the merged library gained a second package // (and the bridge a second 3D model) per variant.- xml.push_str(&format!(" <description>{}</description>\n", esc_xml(sym.package_name.as_deref().unwrap_or(&sym.name))));+ xml.push_str(&format!(" <description>{}</description>\n", esc_xml(sym.package_description.as_deref().unwrap_or(sym.package_name.as_deref().unwrap_or(&sym.name))))); let pad_names: Vec<String> = mod_pads.iter().map(|p| p.name.clone()).collect(); let (out_names, groups) = dedup_pad_names(&pad_names); pad_groups = groups;@@ -2037,7 +2122,7 @@ fn main() { let cli = Cli::parse(); match cli.command {- Commands::Generate { sym, fp, output, name, package_name, symbol_art, symbol_art_scale, pin_map, part_metadata, symbol_layout, alt_sym } => {+ Commands::Generate { sym, fp, output, name, package_name, description, symbol_art, symbol_art_scale, pin_map, part_metadata, symbol_layout, alt_sym } => { let fp_content = fp.as_ref().map(|p| { fs::read_to_string(p).unwrap_or_else(|e| { eprintln!("ERROR: Cannot read footprint {}: {}", p.display(), e);@@ -2122,12 +2207,25 @@ fn main() { sym.as_ref().and_then(|p| p.parent()).map(|d| d.join("page.json")).filter(|p| p.exists()) }); let part_attrs = pm_path.as_ref().map(|p| part_attributes(p)).unwrap_or_default();- if parsed.description.trim().is_empty() {+ // Description precedence: an explicit --description, then the .kicad_sym's own+ // Description property, then the wiki page's brief. Whatever wins has catalogue+ // boilerplate stripped: a page brief is written for someone browsing the wiki,+ // and repeating "Datasheet-verified pinout, KiCad symbol/footprint/3D" in every+ // 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() { 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."); parsed.description = brief; } }+ parsed.description = strip_catalog_boilerplate(&parsed.description);+ if parsed.description.trim().is_empty() {+ println!("HINT: this part has NO description: the deviceset, the symbol and the ADD dialog will all read blank in Fusion. Pass --description, or give the source a Description property.");+ }+ parsed.package_description = Some(package_description(fp_content.as_deref(),+ parsed.package_name.as_deref().unwrap_or(&parsed.name))); if part_attrs.is_empty() { println!("HINT: no part attributes (MPN/MANUFACTURER/MOUSER_PN/DIGIKEY_PN/LCSC_PN). \ The geometry is fine but a BOM cannot order this part - pass --part-metadata or put a page.json beside the symbol.");@@ -2911,6 +3009,7 @@ mod lint_tests { }; let mut sym = ParsedSymbol { package_name: None,+ package_description: None, name: "U1".into(), reference: "U".into(), value: String::new(), footprint: String::new(), description: String::new(), pins: vec![