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
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)
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);