← Commit history

eagle.rs: place RefDes/Value clear of full footprint Y extent (match adom-footprint fp_gen); altium.rs tidy; README license section

John Lauer ·49b31d0c77 ·3mo ago ·parent f5267c7
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();