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
Merge PR #59: generate: emit fp_poly as <polygon> (pin-1 marker) and part attributes on <technology>
5 files changed
+98−9
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.16.0"+version = "2.18.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.17.1+2.18.0
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.16.0",+ "version": "2.18.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": {},
src/main.rs+94−5@@ -81,6 +81,15 @@ enum Commands { /// `pinmap.json` beside the symbol when that file exists. #[arg(long)] pin_map: Option<PathBuf>,+ /// Part metadata for the EAGLE/Fusion attributes that make a part+ /// ORDERABLE — MPN, MANUFACTURER, MOUSER_PN, DIGIKEY_PN, LCSC_PN — read+ /// from a wiki component `page.json` (or any JSON with the same+ /// `component` block). Defaults to `page.json` beside the symbol, so a+ /// component repo just works. Geometry and 3D bindings alone do not tell+ /// a BOM what to buy, and the bridge downstream cannot invent part+ /// numbers, so they have to be carried here.+ #[arg(long)]+ part_metadata: Option<PathBuf>, /// Schematic-symbol pin layout policy. `auto` (default): a symbol that /// mirrors the physical PACKAGE (pins on all four sides, rotated names) /// is re-laid into EE-convention two columns — inputs left, outputs and@@ -1089,7 +1098,8 @@ fn kicad_layer_to_eagle(klayer: &str) -> Option<u32> { } /// Transcribe the footprint's REAL silkscreen/fab graphics — `fp_circle`,-/// `fp_rect`, `fp_line`, `fp_arc` — into EAGLE `<circle>`/`<wire>` on the mapped+/// `fp_rect`, `fp_line`, `fp_arc`, `fp_poly` — into EAGLE `<circle>`/`<wire>`/+/// `<polygon>` on the mapped /// layer (21 tPlace / 51 tDocu / …), Y negated (KiCad Y-down → EAGLE Y-up). /// Returns "" when the footprint carries no silk/fab graphics, so the caller can /// fall back to a bbox rectangle.@@ -1100,6 +1110,7 @@ fn package_graphics(km: &str) -> String { let mid_re = regex_lite::Regex::new(r#"\(mid\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap(); let end_re = regex_lite::Regex::new(r#"\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap(); let width_re = regex_lite::Regex::new(r#"\(width\s+(-?[\d.]+)\)"#).unwrap();+ let xy_re = regex_lite::Regex::new(r#"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap(); let el_of = |b: &str| layer_re.captures(b).and_then(|c| kicad_layer_to_eagle(&c[1])); let w_of = |b: &str| width_re.captures(b).and_then(|c| c[1].parse::<f64>().ok()).unwrap_or(0.127); let f = |re: ®ex_lite::Regex, b: &str| re.captures(b).map(|c| (c[1].parse::<f64>().unwrap_or(0.0), c[2].parse::<f64>().unwrap_or(0.0)));@@ -1110,6 +1121,24 @@ fn package_graphics(km: &str) -> String { let radius = ((ex - cx).powi(2) + (ey - cy).powi(2)).sqrt(); out.push_str(&format!(" <circle x=\"{:.4}\" y=\"{:.4}\" radius=\"{:.4}\" width=\"{:.4}\" layer=\"{}\"/>\n", cx, -cy, radius, w_of(b), el)); }+ // fp_poly was silently skipped, and it is not a rare shape: KiCad draws the+ // pin-1 marker as a small FILLED polygon on F.SilkS, and the gullwing generator+ // draws the whole F.Fab body outline as one. Across 2,540 published parts, 97%+ // mark pin 1 that way — so every one of them reached Fusion with no pin-1+ // indicator at all while its geometry looked perfect (adom/adom-lbr#933).+ for b in sexpr_blocks(km, "fp_poly") {+ let Some(el) = el_of(b) else { continue };+ let verts: Vec<(f64, f64)> = xy_re+ .captures_iter(b)+ .map(|c| (c[1].parse::<f64>().unwrap_or(0.0), c[2].parse::<f64>().unwrap_or(0.0)))+ .collect();+ if verts.len() < 3 { continue }+ out.push_str(&format!(" <polygon width=\"{:.4}\" layer=\"{}\">\n", w_of(b), el));+ for (x, y) in verts {+ out.push_str(&format!(" <vertex x=\"{:.4}\" y=\"{:.4}\"/>\n", x, -y));+ }+ out.push_str(" </polygon>\n");+ } for b in sexpr_blocks(km, "fp_rect") { let (Some(el), Some((x1, y1r)), Some((x2, y2r))) = (el_of(b), f(&start_re, b), f(&end_re, b)) else { continue }; let (y1, y2, w) = (-y1r, -y2r, w_of(b));@@ -1249,7 +1278,39 @@ fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) } } -pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>) -> String {+/// The attributes a BOM and the fusion-bridge linter read off a part.+///+/// Accepts a wiki component `page.json` (fields under `component`) or a bare JSON+/// object with the same keys. Distributor part numbers may be written either flat+/// (`"mouser_pn": "584-..."`) or under `distributor_links` as a string or as an+/// object carrying `pn` / `part_number`, because both shapes are already in use.+/// An absent or unreadable file is not an error - a part with no metadata still+/// generates, it just generates without attributes, exactly as before.+fn part_attributes(path: &std::path::Path) -> Vec<(String, String)> {+ let Ok(raw) = fs::read_to_string(path) else { return Vec::new() };+ let Ok(doc) = serde_json::from_str::<serde_json::Value>(&raw) else { return Vec::new() };+ let c = doc.get("component").unwrap_or(&doc);+ let clean = |v: Option<&serde_json::Value>| -> Option<String> {+ v.and_then(|x| x.as_str()).map(|x| x.trim().to_string()).filter(|x| !x.is_empty())+ };+ let mut out = Vec::new();+ if let Some(v) = clean(c.get("mpn")) { out.push(("MPN".to_string(), v)); }+ if let Some(v) = clean(c.get("manufacturer")) { out.push(("MANUFACTURER".to_string(), v)); }+ let links = c.get("distributor_links");+ for (key, attr) in [("mouser", "MOUSER_PN"), ("digikey", "DIGIKEY_PN"), ("lcsc", "LCSC_PN")] {+ let flat = clean(c.get(&format!("{key}_pn")));+ let nested = links.and_then(|d| d.get(key)).and_then(|e| match e {+ serde_json::Value::String(t) => Some(serde_json::Value::String(t.clone())),+ other => other.get("pn").or_else(|| other.get("part_number")).cloned(),+ });+ if let Some(v) = flat.or_else(|| clean(nested.as_ref())) {+ out.push((attr.to_string(), v));+ }+ }+ out+}++pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)]) -> String { let mut xml = String::with_capacity(8192); xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -1468,7 +1529,21 @@ pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_ } xml.push_str(" </connects>\n");- xml.push_str(" <technologies><technology name=\"\"/></technologies>\n");+ // Attributes ride on <technology>, which is where Fusion's BOM export and the+ // fusion-bridge partMetadata linter look for them. An empty technology - what+ // this always emitted - produces a part with perfect geometry that nothing can+ // order.+ if part_attrs.is_empty() {+ xml.push_str(" <technologies><technology name=\"\"/></technologies>\n");+ } else {+ xml.push_str(" <technologies>\n <technology name=\"\">\n");+ for (k, v) in part_attrs {+ xml.push_str(&format!(+ " <attribute name=\"{}\" value=\"{}\" constant=\"no\"/>\n",+ esc_xml(k), esc_xml(v)));+ }+ xml.push_str(" </technology>\n </technologies>\n");+ } xml.push_str(" </device>\n"); xml.push_str(" </devices>\n"); xml.push_str("</deviceset>\n");@@ -1812,7 +1887,7 @@ fn main() { let cli = Cli::parse(); match cli.command {- Commands::Generate { sym, fp, output, name, symbol_art, symbol_art_scale, pin_map, symbol_layout } => {+ Commands::Generate { sym, fp, output, name, symbol_art, symbol_art_scale, pin_map, part_metadata, symbol_layout } => { 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);@@ -1878,6 +1953,20 @@ fn main() { pin_map_path.as_ref().map(|p| p.display().to_string()).unwrap_or_default()); } + // Part metadata: an explicit --part-metadata wins, else page.json beside+ // the symbol, mirroring how pinmap.json is picked up.+ let pm_path = part_metadata.clone().or_else(|| {+ 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 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.");+ } else {+ println!("HINT: part attributes: {}", part_attrs.iter()+ .map(|(k, _)| k.as_str()).collect::<Vec<_>>().join(", "));+ }+ match symbol_layout.as_str() { "keep" => {} "auto" | "two-column" => {@@ -1897,7 +1986,7 @@ fn main() { } } - let lbr = generate_lbr(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map);+ let lbr = generate_lbr(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map, &part_attrs); // Lint before writing let lint = lint_lbr(&lbr);
src/manage.rs+1−1@@ -1244,7 +1244,7 @@ fn export_part(slug: &str) -> Value { let lbr = match crate::parse_kicad_sym(&sym_txt, Some(&mpn)) { Ok(mut parsed) => { crate::relayout_two_columns(&mut parsed, false);- Some(crate::generate_lbr(&parsed, Some(&fp_txt), None, 0.38, &pin_map))+ Some(crate::generate_lbr(&parsed, Some(&fp_txt), None, 0.38, &pin_map, &[])) } Err(_) => None, };