← Commit history

Restore generate --symbol-art (override kicad_sym drawing with an outline-contract .mm.svg; composes with --fp) — ported from v1.8.4, keeps all 2.x native-gen fixes (2.4.0)

John Lauer ·0cbc22df00 ·3mo ago ·parent 2bc97ba
3 files changed +156−17
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.3.2"+version = "2.4.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.3.2+2.4.0
src/main.rs+154−15
@@ -37,6 +37,16 @@ enum Commands {         /// Symbol name override         #[arg(long)]         name: 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+        /// symbol mm-space. Else a legacy flattened outline SVG. Composes with --fp.+        #[arg(long)]+        symbol_art: Option<PathBuf>,+        /// Size of LEGACY symbol art as a fraction of the body's smaller side.+        /// Ignored for the `.mm.svg` contract (placement is exact).+        #[arg(long, default_value_t = 0.38)]+        symbol_art_scale: f64,     },     /// Lint an existing .lbr file for Fusion 360 compatibility     Lint {@@ -846,7 +856,125 @@ fn package_graphics(km: &str) -> String {     out } -fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>) -> String {+/// Resolved `--symbol-art` input. PREFERRED: an adom-symbol OUTLINE CONTRACT+/// (`<mpn>-symbol-outline.mm.svg` + `<mpn>-symbol-outline.json` sidecar) whose+/// polylines are ALREADY placed in the symbol's mm space; the sidecar gives the+/// exact Y-up transform, so it's transcribed 1:1 with zero guessing. Anything+/// else is a `Legacy` raw outline we centre + scale by eye.+enum SymbolArt {+    Contract { mm_svg: String, dx: f64, svg_cy: f64, e_cy: f64 },+    Legacy(String),+}++/// Resolve a `--symbol-art` path. A `*.mm.svg` with a readable `*.json` sidecar+/// is a contract; otherwise legacy.+fn resolve_symbol_art(path: &std::path::Path) -> Result<SymbolArt, String> {+    let content = fs::read_to_string(path).map_err(|e| format!("Cannot read symbol art {}: {}", path.display(), e))?;+    let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");+    if name.ends_with(".mm.svg") {+        let json_path = path.with_file_name(name.replace(".mm.svg", ".json"));+        if let Ok(js) = fs::read_to_string(&json_path) {+            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&js) {+                let g = |a: &str, b: &str| v.get(a).and_then(|o| o.get(b)).and_then(|n| n.as_f64());+                if let (Some(svg_cx), Some(svg_cy), Some(e_cx), Some(e_cy)) = (+                    g("placement_svg_mm", "center_x"), g("placement_svg_mm", "center_y"),+                    g("placement_eagle_mm", "center_x"), g("placement_eagle_mm", "center_y"),+                ) {+                    return Ok(SymbolArt::Contract { mm_svg: content, dx: svg_cx - e_cx, svg_cy, e_cy });+                }+            }+        }+        eprintln!("WARN: {name} looks like an outline contract but its .json sidecar is missing/unreadable — falling back to centre+scale guessing.");+    }+    Ok(SymbolArt::Legacy(content))+}++/// Transcribe a contract `.mm.svg` (symbol mm-space, Y-down) to EAGLE `<wire>`s+/// (Y-up), 1:1: `eagle_x = svg_x - dx`, `eagle_y = e_cy - (svg_y - svg_cy)`.+fn contract_wires(mm_svg: &str, dx: f64, svg_cy: f64, e_cy: f64) -> (String, usize) {+    let poly_re = regex_lite::Regex::new(r#"<(polyline|polygon)\b[^>]*\bpoints="([^"]+)""#).unwrap();+    let mut out = String::new();+    let mut segs = 0usize;+    for c in poly_re.captures_iter(mm_svg) {+        let closed = &c[1] == "polygon";+        let mut pts: Vec<(f64, f64)> = c[2].split_whitespace().filter_map(|p| {+            let mut it = p.split(',');+            let x: f64 = it.next()?.trim().parse().ok()?;+            let y: f64 = it.next()?.trim().parse().ok()?;+            Some((x - dx, e_cy - (y - svg_cy)))+        }).collect();+        if closed { if let Some(&first) = pts.first() { pts.push(first); } }+        for w in pts.windows(2) {+            out.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1016\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+            segs += 1;+        }+    }+    (out, segs)+}++/// Legacy raw outline SVG → wires: centre + scale to ~`scale_frac` of the body's+/// smaller side, decimate in output space so dense sampling collapses.+fn symbol_art_wires(svg: &str, body: Option<&KicadBody>, scale_frac: f64) -> (String, usize) {+    let poly_re = regex_lite::Regex::new(r#"<(polyline|polygon)\b[^>]*\bpoints="([^"]+)""#).unwrap();+    let mut polys: Vec<Vec<(f64, f64)>> = Vec::new();+    for c in poly_re.captures_iter(svg) {+        let closed = &c[1] == "polygon";+        let mut pts: Vec<(f64, f64)> = c[2].split_whitespace().filter_map(|p| {+            let mut it = p.split(',');+            let x: f64 = it.next()?.trim().parse().ok()?;+            let y: f64 = it.next()?.trim().parse().ok()?;+            Some((x, y))+        }).collect();+        if closed { if let Some(&first) = pts.first() { pts.push(first); } }+        polys.push(pts);+    }+    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+    for poly in &polys { for &(x, y) in poly {+        minx = minx.min(x); miny = miny.min(y); maxx = maxx.max(x); maxy = maxy.max(y);+    }}+    let (sw, sh) = (maxx - minx, maxy - miny);+    if !(sw > 0.0 && sh > 0.0) { return (String::new(), 0); }+    let (target, cx, cy) = match body {+        Some(b) => {+            let bw = (b.right - b.left).abs();+            let bh = (b.top - b.bottom).abs();+            (bw.min(bh) * scale_frac, (b.left + b.right) / 2.0, (b.top + b.bottom) / 2.0)+        }+        None => (26.0 * scale_frac, 0.0, 0.0),+    };+    let scale = target / sw.max(sh);+    let tol_mm = 0.12;+    let (smx, smy) = ((minx + maxx) / 2.0, (miny + maxy) / 2.0);+    let map = |x: f64, y: f64| (cx + (x - smx) * scale, cy - (y - smy) * scale);+    let mut out = String::new();+    let mut segs = 0usize;+    for poly in &polys {+        let mut kept: Vec<(f64, f64)> = Vec::new();+        let n = poly.len();+        for (i, &(px, py)) in poly.iter().enumerate() {+            let pp = map(px, py);+            if i == 0 || i == n - 1 { kept.push(pp); }+            else if let Some(&last) = kept.last() {+                if ((pp.0 - last.0).powi(2) + (pp.1 - last.1).powi(2)).sqrt() >= tol_mm { kept.push(pp); }+            }+        }+        for w in kept.windows(2) {+            out.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1016\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+            segs += 1;+        }+    }+    (out, segs)+}++/// Wire block for a resolved symbol-art input.+fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) -> (String, usize) {+    match art {+        SymbolArt::Contract { mm_svg, dx, svg_cy, e_cy } => contract_wires(mm_svg, *dx, *svg_cy, *e_cy),+        SymbolArt::Legacy(svg) => symbol_art_wires(svg, body, scale_frac),+    }+}++fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64) -> String {     let mut xml = String::with_capacity(8192);      xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -966,19 +1094,25 @@ fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>) -> String {         xml.push_str(&format!("  <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.bottom, body.left, body.top));     } -    // The kicad_sym's own drawing (polylines/arcs) as layer-94 wires — direct-    // coords, no Y-flip (KiCad symbol space is already Y-up like EAGLE).-    for g in &sym.graphics {-        match g {-            SymGraphic::Poly(pts) => {-                for w in pts.windows(2) {-                    xml.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+    // The chip drawing. A `--symbol-art` SVG is an explicit OVERRIDE; otherwise+    // transcribe the `.kicad_sym`'s OWN (polyline)/(arc) graphics as raw layer-94+    // wires (KiCad symbol space is Y-up, same as the pins → carried through 1:1).+    if let Some(art) = symbol_art {+        let (wires, _) = symbol_art_block(art, sym.body.as_ref(), symbol_art_scale);+        xml.push_str(&wires);+    } else {+        for g in &sym.graphics {+            match g {+                SymGraphic::Poly(pts) => {+                    for w in pts.windows(2) {+                        xml.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+                    }                 }-            }-            SymGraphic::Arc { start, mid, end } => {-                let pts = flatten_arc(*start, *mid, *end, 12);-                for w in pts.windows(2) {-                    xml.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+                SymGraphic::Arc { start, mid, end } => {+                    let pts = flatten_arc(*start, *mid, *end, 12);+                    for w in pts.windows(2) {+                        xml.push_str(&format!("  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+                    }                 }             }         }@@ -1285,7 +1419,7 @@ fn main() {     let cli = Cli::parse();      match cli.command {-        Commands::Generate { sym, fp, output, name } => {+        Commands::Generate { sym, fp, output, name, symbol_art, symbol_art_scale } => {             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);@@ -1320,7 +1454,12 @@ fn main() {             };              let out_default = sym.as_ref().or(fp.as_ref()).cloned().unwrap_or_else(|| PathBuf::from(&parsed.name));-            let lbr = generate_lbr(&parsed, fp_content.as_deref());+            let art = match symbol_art.as_ref().map(|p| resolve_symbol_art(p)) {+                Some(Ok(a)) => Some(a),+                Some(Err(e)) => { eprintln!("ERROR: {e}"); process::exit(1); }+                None => None,+            };+            let lbr = generate_lbr(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale);              // Lint before writing             let lint = lint_lbr(&lbr);