skill
Adom SF Convert
Public Made by Adomby adom
Native Rust converter: EAGLE/Fusion .lbr + Altium .SchLib/.PcbLib → KiCad, rendering each format's own geometry.
← Commit history
eagle.rs: place RefDes/Value clear of full footprint Y extent (match adom-footprint fp_gen); altium.rs tidy; README license section
3 files changed
+81−18
package.json+4−3@@ -1,6 +1,6 @@ { "slug": "adom-sfconvert",- "version": "1.0.6",+ "version": "1.0.7", "type": "skill", "description": "Native Rust converter: EAGLE/Fusion .lbr + Altium .SchLib/.PcbLib \u2192 KiCad symbol+footprint, rendering each format's OWN geometry. No KiCad install, no service round-trip.", "dependencies": {@@ -23,7 +23,8 @@ "symbol" ], "hero": {- "path": "adom-sfconvert.png"+ "type": "image",+ "path": "adom-sfconvert-hero-v4.png" }, "discovery_triggers": [ "convert eagle to kicad",@@ -37,7 +38,7 @@ "license": "MIT", "files": [ "skills/adom-sfconvert/SKILL.md",- "adom-sfconvert.png",+ "adom-sfconvert-hero-v4.png", "README.md", "SKILL.md", "install.sh",
src/altium.rs+3−1@@ -46,7 +46,9 @@ pub fn convert(file: &Path, out_dir: &Path, stem: &str, ext: &str, svg: bool) -> if want_fp { match altium_pcblib::parse_altium_pcblib_path(file) { Ok(fp) => {- let km = altium_pcblib::to_kicad_mod(&fp, stem);+ // altium_pcblib hardcodes RefDes/Value at (0, ±3) — re-place it+ // clear of the whole footprint, the same rule adom-footprint uses.+ let km = crate::eagle::reposition_fp_text(&altium_pcblib::to_kicad_mod(&fp, stem)); let p = out_dir.join(format!("{stem}.kicad_mod")); std::fs::write(&p, km)?; files.push(p.display().to_string());
src/eagle.rs+74−14@@ -173,12 +173,17 @@ pub fn render_symbol_svg(lbr: &str, name: &str) -> String { /// EAGLE `<package>` → KiCad `.kicad_mod` (SMD + THT pads + silk lines). fn package_to_mod(pkg: &str, name: &str) -> String {- let mut out = format!(- "(footprint \"{name}\" (version 20221018) (generator adom-sfconvert) (layer \"F.Cu\")\n \- (attr smd)\n \- (fp_text reference \"REF**\" (at 0 -3) (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n \- (fp_text value \"{name}\" (at 0 3) (layer \"F.Fab\") (effects (font (size 1 1) (thickness 0.15))))\n"- );+ // Collect pads + silk/fab wires FIRST, tracking the footprint's Y extent in+ // KiCad space, so RefDes/Value land CLEAR of the whole footprint — matching+ // adom-footprint's fp_gen rule (RefDes ~1 mm above the top, Value ~1 mm below+ // the bottom). The old hardcoded `(at 0 ±3)` sat inside the pad ring of larger+ // parts (e.g. an LQFP-64 whose pads reach ±5.9 mm), smearing text over copper.+ let mut geom = String::new();+ let (mut miny, mut maxy) = (f64::MAX, f64::MIN);+ let mut track = |ya: f64, yb: f64| {+ miny = miny.min(ya).min(yb);+ maxy = maxy.max(ya).max(yb);+ }; // SMD pads for m in Regex::new(r"<smd\b[^>]*/?>").unwrap().find_iter(pkg) { let t = m.as_str();@@ -189,8 +194,12 @@ fn package_to_mod(pkg: &str, name: &str) -> String { let (shape, extra) = if round > 0.0 { ("roundrect".to_string(), format!(" (roundrect_rratio {:.3})", (round / 2.0).min(0.5))) } else { ("rect".to_string(), String::new()) };- let at = if r != 0.0 { format!("{x:.4} {:.4} {r}", -y) } else { format!("{x:.4} {:.4}", -y) };- out += &format!(+ let ky = -y;+ // Rotation-agnostic conservative half-extent (a 90°-rotated pad swaps dx/dy).+ let half = dx.max(dy) / 2.0;+ track(ky - half, ky + half);+ let at = if r != 0.0 { format!("{x:.4} {ky:.4} {r}") } else { format!("{x:.4} {ky:.4}") };+ geom += &format!( " (pad \"{n}\" smd {shape} (at {at}) (size {dx:.4} {dy:.4}) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"){extra})\n" ); }@@ -200,9 +209,10 @@ fn package_to_mod(pkg: &str, name: &str) -> String { let (Some(n), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue }; let drill = fattr(t, "drill").unwrap_or(0.6); let dia = fattr(t, "diameter").unwrap_or(drill + 0.5);- out += &format!(- " (pad \"{n}\" thru_hole circle (at {x:.4} {:.4}) (size {dia:.4} {dia:.4}) (drill {drill:.4}) (layers \"*.Cu\" \"*.Mask\"))\n",- -y+ let ky = -y;+ track(ky - dia / 2.0, ky + dia / 2.0);+ geom += &format!(+ " (pad \"{n}\" thru_hole circle (at {x:.4} {ky:.4}) (size {dia:.4} {dia:.4}) (drill {drill:.4}) (layers \"*.Cu\" \"*.Mask\"))\n" ); } // Silk / fab wires (layer 21 = tPlace silk, 51 = tDocu fab).@@ -213,15 +223,65 @@ fn package_to_mod(pkg: &str, name: &str) -> String { let layer = attr(t, "layer").unwrap_or_default(); let kl = match layer.as_str() { "21" | "22" => "F.SilkS", "51" | "52" => "F.Fab", _ => continue }; let w = fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.12);- out += &format!(- " (fp_line (start {x1:.4} {:.4}) (end {x2:.4} {:.4}) (stroke (width {w:.3}) (type solid)) (layer \"{kl}\"))\n",- -y1, -y2+ let (ky1, ky2) = (-y1, -y2);+ track(ky1, ky2);+ geom += &format!(+ " (fp_line (start {x1:.4} {ky1:.4}) (end {x2:.4} {ky2:.4}) (stroke (width {w:.3}) (type solid)) (layer \"{kl}\"))\n" ); }+ // RefDes/Value clear of the full Y extent (+1 mm); fall back to ±3 if empty.+ let r2 = |v: f64| (v * 100.0).round() / 100.0;+ let (ref_y, val_y) = if miny <= maxy { (r2(miny - 1.0), r2(maxy + 1.0)) } else { (-3.0, 3.0) };+ let mut out = format!(+ "(footprint \"{name}\" (version 20221018) (generator adom-sfconvert) (layer \"F.Cu\")\n \+ (attr smd)\n \+ (fp_text reference \"REF**\" (at 0 {ref_y}) (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n \+ (fp_text value \"{name}\" (at 0 {val_y}) (layer \"F.Fab\") (effects (font (size 1 1) (thickness 0.15))))\n"+ );+ out += &geom; out += ")\n"; out } +/// Re-place a `.kicad_mod`'s RefDes/Value text CLEAR of the whole footprint —+/// applies the SAME one placement rule as adom-footprint's fp_gen (RefDes ~1 mm+/// above the top extent, Value ~1 mm below) to a `.kicad_mod` we didn't generate+/// ourselves, e.g. the `altium_pcblib` crate's output, which hardcodes `(at 0 ±3)`+/// (smeared over the pad ring of larger parts). Rewrites only the FIRST+/// `(fp_text reference …)` / `(fp_text value …)` `(at …)`; any other text is left+/// alone. No-op when the mod has no pad/line geometry to measure.+pub fn reposition_fp_text(km: &str) -> String {+ let pad = Regex::new(r"\(pad\b[^()]*\(at -?[\d.]+ (-?[\d.]+)(?: -?[\d.]+)?\) \(size -?[\d.]+ (-?[\d.]+)\)").unwrap();+ let line = Regex::new(r"\(start -?[\d.]+ (-?[\d.]+)\) \(end -?[\d.]+ (-?[\d.]+)\)").unwrap();+ let (mut miny, mut maxy) = (f64::MAX, f64::MIN);+ for c in pad.captures_iter(km) {+ let y: f64 = c[1].parse().unwrap_or(0.0);+ let dy: f64 = c[2].parse().unwrap_or(0.0);+ miny = miny.min(y - dy / 2.0);+ maxy = maxy.max(y + dy / 2.0);+ }+ for c in line.captures_iter(km) {+ let y1: f64 = c[1].parse().unwrap_or(0.0);+ let y2: f64 = c[2].parse().unwrap_or(0.0);+ miny = miny.min(y1).min(y2);+ maxy = maxy.max(y1).max(y2);+ }+ if miny > maxy {+ return km.to_string();+ }+ let r2 = |v: f64| (v * 100.0).round() / 100.0;+ let (ref_y, val_y) = (r2(miny - 1.0), r2(maxy + 1.0));+ let mut out = km.to_string();+ for (kind, y) in [("reference", ref_y), ("value", val_y)] {+ let re = Regex::new(&format!(+ r"(\(fp_text {kind}\b[^()]*\(at )-?[\d.]+ -?[\d.]+(?: -?[\d.]+)?(\))"+ )).unwrap();+ let rep = format!("${{1}}0 {y}${{2}}");+ out = re.replace(&out, rep.as_str()).into_owned();+ }+ out+}+ /// EAGLE `<symbol>` → KiCad `.kicad_sym` (pins + the body rectangle). fn symbol_to_sym(sym: &str, name: &str, connects: &HashMap<String, String>) -> String { let mut pins = String::new();