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.1: geometry-first package description
7 files changed
+93−33
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.22.0"+version = "2.22.1" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.22.0"+version = "2.22.1" 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.22.0+2.22.1
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.22.0",+ "version": "2.22.1", "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.22.0",+ "version": "2.22.1", "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+87−27@@ -1411,29 +1411,31 @@ fn strip_catalog_boilerplate(text: &str) -> 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.+/// A human line for the PACKAGE, GEOMETRY FIRST.+///+/// Fusion's Content Manager shows about 20 characters of this column before it truncates+/// with an ellipsis (the full text is on hover), measured by the fusion-bridge thread. So+/// the first words have to be the ones that identify the package. A manufacturer-first+/// `(descr …)` truncates to "Walsin Technology C…", which identifies nothing, while the+/// geometry derived from the footprint NAME truncates to "SOIC-16, 3.9 x 9.9 m…", which+/// still tells you the family, the pin count and the size.+///+/// The `.kicad_mod`'s own `(descr …)` is appended after it when it adds something. It is+/// skipped when it merely restates the geometry: "SOIC, 8 Pin" beside+/// "SOIC-8, 3.9 x 4.9 mm body, 1.27 mm pitch" is noise, not detail. 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 descr = fp_content.and_then(|c| {+ let re = regex_lite::Regex::new(r#"\(descr\s+"([^"]*)""#).ok()?;+ let raw = re.captures(c)?.get(1)?.as_str();+ let mut t = raw.split("http").next().unwrap_or(raw).trim().to_string();+ // Cutting a URL can leave the bracket that introduced it, e.g. "(JEDEC MS-012AA, ".+ if t.matches('(').count() > t.matches(')').count() {+ if let Some(i) = t.rfind('(') { t.truncate(i); } }- }+ let t = t.trim().trim_end_matches(&[',', ';', '\u{b7}', '-'][..]).trim().to_string();+ if t.is_empty() { None } else { Some(t) }+ });+ let mut family = String::new(); let mut body = String::new(); let mut pitch = String::new();@@ -1441,14 +1443,72 @@ fn package_description(fp_content: Option<&str>, package_name: &str) -> String { 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) = dims.captures(tok) { body = format!("{} x {} mm", &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(", ")+ // Geometry only counts when the NAME actually encodes it. "0603N101J500CT" is a part+ // number, not a package description. Rather than fall back to the manufacturer-first+ // descr (which truncates to "Walsin Technology Co...", identifying nothing) or to the+ // package name (which just repeats the Name column beside it, the thing John objected+ // to), MEASURE the footprint: the pad count and the outline are always available and+ // they are what actually distinguishes one package from another.+ let geometry = if body.is_empty() && pitch.is_empty() {+ match fp_content {+ Some(km) => {+ let pads = parse_mod_pads(km);+ if pads.is_empty() {+ String::new()+ } else {+ // Measure the COPPER, not the silkscreen. package_outline() prefers the+ // F.SilkS box, which on a two-terminal chip part is a pair of short+ // ticks beside the pads: it measured an 0603 as "0.6 x 0.0 mm", a+ // number that is confidently wrong. The pad extent is always defined.+ let (mut x1, mut y1, mut x2, mut y2) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ for pd in &pads {+ x1 = x1.min(pd.x - pd.dx / 2.0); y1 = y1.min(pd.y - pd.dy / 2.0);+ x2 = x2.max(pd.x + pd.dx / 2.0); y2 = y2.max(pd.y + pd.dy / 2.0);+ }+ let (w, h) = ((x2 - x1).abs(), (y2 - y1).abs());+ // Refuse to state a measurement that cannot be right rather than print+ // a tidy-looking zero.+ if w < 0.2 || h < 0.2 {+ format!("{} pads", pads.len())+ } else {+ format!("{} pads, {:.1} x {:.1} mm", pads.len(), w, h)+ }+ }+ }+ None => String::new(),+ }+ } else {+ let mut parts = vec![family];+ if !body.is_empty() { parts.push(body); }+ if !pitch.is_empty() { parts.push(pitch); }+ parts.join(", ")+ };++ match (geometry.is_empty(), descr) {+ (true, Some(d)) => d,+ (true, None) => package_name.to_string(),+ (false, None) => geometry,+ (false, Some(d)) => {+ // Skip a descr that just restates the geometry: compare on alphanumeric tokens+ // rather than on the exact string, so "SOIC, 8 Pin" is recognised as a restating+ // of "SOIC-8, ...".+ let norm = |t: &str| -> Vec<String> {+ t.to_lowercase()+ .split(|c: char| !c.is_ascii_alphanumeric() && c != '.')+ .filter(|w| !w.is_empty() && *w != "pin" && *w != "pins" && *w != "mm")+ .map(|w| w.to_string())+ .collect()+ };+ let g = norm(&geometry);+ let dt = norm(&d);+ let covered = dt.iter().filter(|w| g.iter().any(|x| x == *w)).count();+ let redundant = !dt.is_empty() && covered * 100 / dt.len() >= 60;+ if redundant { geometry } else { format!("{}. {}", geometry, d) }+ }+ } } fn page_brief(path: &std::path::Path) -> Option<String> {