← Commit history

Merge PR #59: generate: emit fp_poly as <polygon> (pin-1 marker) and part attributes on <technology>

Ray ·2d093ce389 ·1mo ago ·parent 35f37dc
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: &regex_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,     };