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 real-footprint .lbr generation + fix round pad / silk circle / symbol outline (2.1.1)
1 file changed
+328−1
src/main.rs+328−1@@ -205,6 +205,7 @@ struct ParsedSymbol { pins: Vec<KicadPin>, body: Option<KicadBody>, group_labels: Vec<GroupLabel>,+ graphics: Vec<SymGraphic>, } fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedSymbol, String> {@@ -250,6 +251,28 @@ fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedS }) }; + // The symbol's own drawing: (polyline (pts (xy …))) and (arc (start)(mid)(end)).+ let mut graphics: Vec<SymGraphic> = Vec::new();+ let xy_re = regex_lite::Regex::new(r#"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap();+ for b in sexpr_blocks(content, "polyline") {+ let pts: Vec<(f64, f64)> = xy_re.captures_iter(b)+ .map(|c| (c[1].parse().unwrap_or(0.0), c[2].parse().unwrap_or(0.0)))+ .collect();+ if pts.len() >= 2 { graphics.push(SymGraphic::Poly(pts)); }+ }+ let arc_re = regex_lite::Regex::new(+ r#"(?s)\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\).*?\(mid\s+(-?[\d.]+)\s+(-?[\d.]+)\).*?\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#+ ).unwrap();+ for b in sexpr_blocks(content, "arc") {+ if let Some(c) = arc_re.captures(b) {+ graphics.push(SymGraphic::Arc {+ start: (c[1].parse().unwrap_or(0.0), c[2].parse().unwrap_or(0.0)),+ mid: (c[3].parse().unwrap_or(0.0), c[4].parse().unwrap_or(0.0)),+ end: (c[5].parse().unwrap_or(0.0), c[6].parse().unwrap_or(0.0)),+ });+ }+ }+ let mut group_labels = Vec::new(); let text_re = regex_lite::Regex::new( r#"\(text "([^"]+)"\s+\(at\s+([-\d.]+)\s+([-\d.]+)"#@@ -279,6 +302,7 @@ fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedS pins, body, group_labels,+ graphics, }) } @@ -406,6 +430,7 @@ fn parse_eagle_lbr(content: &str) -> Result<ParsedSymbol, String> { pins, body, group_labels: Vec::new(),+ graphics: Vec::new(), }) } @@ -568,10 +593,251 @@ fn synth_symbol_from_mod(km: &str, name: &str) -> ParsedSymbol { pins, body: Some(KicadBody { left: -7.62, top: body_h / 2.0, right: 7.62, bottom: -body_h / 2.0 }), group_labels: Vec::new(),+ graphics: Vec::new(),+ }+}++/// A drawing primitive from the `.kicad_sym` symbol body (KiCad symbol space,+/// Y-up — same as the pins, so it carries through to EAGLE without a Y-flip).+#[derive(Debug, Serialize, Deserialize)]+enum SymGraphic {+ Poly(Vec<(f64, f64)>),+ Arc { start: (f64, f64), mid: (f64, f64), end: (f64, f64) },+}++/// One SMD/thru-hole pad parsed from a `.kicad_mod`, in KiCad coordinates+/// (millimetres, Y-down). `drill` is `Some` only for thru-hole pads.+struct ModPad {+ name: String,+ smd: bool,+ x: f64,+ y: f64,+ dx: f64,+ dy: f64,+ angle: f64,+ drill: Option<f64>,+ /// EAGLE pad shape mapped from the KiCad shape token: "round" | "square" |+ /// "long" | "octagon".+ shape: String,+}++/// Split a `.kicad_mod` into the raw text of each `(<tag> …)` s-expression,+/// balancing parens so multi-line definitions are captured whole. `tag` is the+/// opening token without its paren, e.g. `"pad"` or `"fp_line"`.+fn sexpr_blocks<'a>(km: &'a str, tag: &str) -> Vec<&'a str> {+ let open = format!("({tag}");+ let mut blocks = Vec::new();+ let mut i = 0;+ while let Some(rel) = km[i..].find(&open) {+ let start = i + rel;+ // Require whitespace after the token so "(pads"/"(fp_lines" don't match.+ let after = &km[start + open.len()..];+ if !after.starts_with(|c: char| c.is_whitespace()) {+ i = start + open.len();+ continue;+ }+ let mut depth = 0i32;+ let mut end = start;+ for (k, c) in km[start..].char_indices() {+ match c {+ '(' => depth += 1,+ ')' => {+ depth -= 1;+ if depth == 0 {+ end = start + k + 1;+ break;+ }+ }+ _ => {}+ }+ }+ if end > start {+ blocks.push(&km[start..end]);+ i = end;+ } else {+ break;+ } }+ blocks } -fn generate_lbr(sym: &ParsedSymbol, _fp_content: Option<&str>) -> String {+/// Parse every real (named) pad out of a `.kicad_mod`, preserving the true+/// per-side positions, sizes, and rotations. Mechanical pads (empty name) and+/// pads missing position/size are skipped.+fn parse_mod_pads(km: &str) -> Vec<ModPad> {+ // Groups: 1/2 = pad name (quoted/bare), 3 = type (smd/thru_hole), 4 = SHAPE+ // (circle/oval/rect/roundrect/…) — the shape word we were previously dropping.+ let name_re = regex_lite::Regex::new(r#"^\(pad\s+(?:"([^"]*)"|(\S+))\s+(\S+)\s+(\S+)"#).unwrap();+ let at_re = regex_lite::Regex::new(r#"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\s*\)"#).unwrap();+ let size_re = regex_lite::Regex::new(r#"\(size\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\)"#).unwrap();+ let drill_re = regex_lite::Regex::new(r#"\(drill\s+(-?[\d.]+)"#).unwrap();++ let mut pads = Vec::new();+ for block in sexpr_blocks(km, "pad") {+ let Some(nc) = name_re.captures(block) else { continue };+ let name = nc.get(1).or_else(|| nc.get(2)).map(|m| m.as_str().to_string()).unwrap_or_default();+ if name.is_empty() {+ continue;+ }+ let smd = nc.get(3).map(|m| m.as_str()).unwrap_or("") == "smd";+ let Some(at) = at_re.captures(block) else { continue };+ let Some(size) = size_re.captures(block) else { continue };+ let x: f64 = at[1].parse().unwrap_or(0.0);+ let y: f64 = at[2].parse().unwrap_or(0.0);+ let angle: f64 = at.get(3).and_then(|m| m.as_str().parse().ok()).unwrap_or(0.0);+ let dx: f64 = size[1].parse().unwrap_or(0.0);+ let dy: f64 = size[2].parse().unwrap_or(0.0);+ let drill = drill_re.captures(block).and_then(|c| c[1].parse().ok());+ // KiCad pad shape → EAGLE pad shape.+ let shape = match nc.get(4).map(|m| m.as_str()).unwrap_or("") {+ "circle" => "round",+ "rect" => "square",+ "oval" => "long",+ "roundrect" | "custom" => "round",+ _ => "round",+ }.to_string();+ pads.push(ModPad { name, smd, x, y, dx, dy, angle, drill, shape });+ }+ pads+}++/// Map a KiCad pad rotation (degrees) to an EAGLE `rot=` attribute, matching+/// Fusion 360's own library export convention: the KiCad angle maps directly+/// (KiCad 90 → `R90`), so a non-square pad on a QFP/SOIC top/bottom side is+/// oriented along the lead. Rect pads are 180°-symmetric so the Y-flip sense+/// is moot; the direct mapping is what Fusion writes (verified by the Fusion+/// bridge against a round-tripped `.flbr`).+fn smd_rotation(angle: f64) -> &'static str {+ let a = ((angle.round() as i64) % 360 + 360) % 360;+ match a {+ 90 => " rot=\"R90\"",+ 180 => " rot=\"R180\"",+ 270 => " rot=\"R270\"",+ _ => "",+ }+}++/// Bounding box (min_x, min_y, max_x, max_y) of `fp_line` endpoints on the+/// given layer, in KiCad coordinates. Each `fp_line` is extracted as a balanced+/// block and matched against its OWN `(layer …)`, so coordinates never leak+/// across layers. Returns `None` if the layer has no lines.+fn fp_line_bbox(km: &str, layer: &str) -> Option<(f64, f64, f64, f64)> {+ let pt_re = regex_lite::Regex::new(r#"\((?:start|end)\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap();+ let layer_re = regex_lite::Regex::new(r#"\(layer\s+"([^"]+)"\)"#).unwrap();+ let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ let mut found = false;+ for block in sexpr_blocks(km, "fp_line") {+ if layer_re.captures(block).map(|c| c[1].to_string()).as_deref() != Some(layer) {+ continue;+ }+ for c in pt_re.captures_iter(block) {+ let x: f64 = c[1].parse().unwrap_or(0.0);+ let y: f64 = c[2].parse().unwrap_or(0.0);+ minx = minx.min(x); miny = miny.min(y);+ maxx = maxx.max(x); maxy = maxy.max(y);+ found = true;+ }+ }+ found.then_some((minx, miny, maxx, maxy))+}++/// Derive the package silkscreen outline bbox from the `.kicad_mod` body.+/// Prefers the silkscreen layer, falls back to fab, then to the pad extents.+fn package_outline(km: &str, pads: &[ModPad]) -> (f64, f64, f64, f64) {+ fp_line_bbox(km, "F.SilkS")+ .or_else(|| fp_line_bbox(km, "F.Fab"))+ .unwrap_or_else(|| {+ let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ for p in pads {+ minx = minx.min(p.x - p.dx); miny = miny.min(p.y - p.dy);+ maxx = maxx.max(p.x + p.dx); maxy = maxy.max(p.y + p.dy);+ }+ (minx, miny, maxx, maxy)+ })+}++/// Flatten a 3-point (start → mid → end) circular arc into a polyline of `segs`+/// segments. Falls back to the three literal points when they're collinear.+fn flatten_arc(start: (f64, f64), mid: (f64, f64), end: (f64, f64), segs: usize) -> Vec<(f64, f64)> {+ use std::f64::consts::PI;+ let ((ax, ay), (bx, by), (cx, cy)) = (start, mid, end);+ let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));+ if d.abs() < 1e-9 {+ return vec![start, mid, end];+ }+ let ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;+ let uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;+ let r = ((ax - ux).powi(2) + (ay - uy).powi(2)).sqrt();+ let a0 = (ay - uy).atan2(ax - ux);+ let a1 = (cy - uy).atan2(cx - ux);+ let am = (by - uy).atan2(bx - ux);+ let norm = |mut t: f64| { while t < 0.0 { t += 2.0 * PI; } while t >= 2.0 * PI { t -= 2.0 * PI; } t };+ let ccw_end = norm(a1 - a0); // CCW sweep from start→end+ let ccw_mid = norm(am - a0); // CCW offset of the mid point+ // If mid lies within the CCW sweep, go CCW; else go CW the other way round.+ let (dir, sweep) = if ccw_mid <= ccw_end { (1.0, ccw_end) } else { (-1.0, 2.0 * PI - ccw_end) };+ (0..=segs).map(|i| {+ let a = a0 + dir * sweep * (i as f64 / segs as f64);+ (ux + r * a.cos(), uy + r * a.sin())+ }).collect()+}++/// Map a KiCad graphics layer to its EAGLE package-layer number. Only silk/fab+/// are drawn in the package outline; copper/mask/paste/courtyard are skipped.+fn kicad_layer_to_eagle(klayer: &str) -> Option<u32> {+ match klayer {+ "F.SilkS" => Some(21),+ "B.SilkS" => Some(22),+ "F.Fab" => Some(51),+ "B.Fab" => Some(52),+ _ => None,+ }+}++/// Transcribe the footprint's REAL silkscreen/fab graphics — `fp_circle`,+/// `fp_rect`, `fp_line`, `fp_arc` — into EAGLE `<circle>`/`<wire>` 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.+fn package_graphics(km: &str) -> String {+ let layer_re = regex_lite::Regex::new(r#"\(layer\s+"([^"]+)"\)"#).unwrap();+ let center_re = regex_lite::Regex::new(r#"\(center\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap();+ let start_re = regex_lite::Regex::new(r#"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)"#).unwrap();+ 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 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: ®ex_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)));++ let mut out = String::new();+ for b in sexpr_blocks(km, "fp_circle") {+ let (Some(el), Some((cx, cy)), Some((ex, ey))) = (el_of(b), f(¢er_re, b), f(&end_re, b)) else { continue };+ 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));+ }+ 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));+ for (ax, ay, bx, by) in [(x1, y1, x2, y1), (x2, y1, x2, y2), (x2, y2, x1, y2), (x1, y2, x1, y1)] {+ out.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"{:.4}\" layer=\"{}\"/>\n", ax, ay, bx, by, w, el));+ }+ }+ for b in sexpr_blocks(km, "fp_line") {+ let (Some(el), Some((sx, sy)), Some((ex, ey))) = (el_of(b), f(&start_re, b), f(&end_re, b)) else { continue };+ out.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"{:.4}\" layer=\"{}\"/>\n", sx, -sy, ex, -ey, w_of(b), el));+ }+ for b in sexpr_blocks(km, "fp_arc") {+ let (Some(el), Some(s), Some(m), Some(e)) = (el_of(b), f(&start_re, b), f(&mid_re, b), f(&end_re, b)) else { continue };+ let w = w_of(b);+ for win in flatten_arc(s, m, e, 12).windows(2) {+ out.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"{:.4}\" layer=\"{}\"/>\n", win[0].0, -win[0].1, win[1].0, -win[1].1, w, el));+ }+ }+ out+}++fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>) -> String { let mut xml = String::with_capacity(8192); xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -589,6 +855,48 @@ fn generate_lbr(sym: &ParsedSymbol, _fp_content: Option<&str>) -> String { // Packages section — generate a placeholder SMD package from pin positions xml.push_str("<packages>\n"); xml.push_str(&format!("<package name=\"{}\">\n", esc_xml(&sym.name)));++ // Real footprint from the .kicad_mod when given; else a placeholder.+ let mod_pads = fp_content.map(parse_mod_pads).unwrap_or_default();+ if !mod_pads.is_empty() {+ let km = fp_content.unwrap();+ xml.push_str(&format!(" <description>{}</description>\n", esc_xml(&sym.name)));+ for p in &mod_pads {+ if p.smd {+ xml.push_str(&format!(+ " <smd name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" dx=\"{:.4}\" dy=\"{:.4}\" layer=\"1\"{}/>\n",+ esc_xml(&p.name), p.x, -p.y, p.dx, p.dy, smd_rotation(p.angle)));+ } else {+ // Round thru-hole pads: real drill + circular diameter (not oblong).+ let drill = p.drill.unwrap_or_else(|| p.dx.min(p.dy) * 0.6);+ let diameter = if p.shape == "round" { p.dx } else { p.dx.max(p.dy) };+ xml.push_str(&format!(+ " <pad name=\"{}\" x=\"{:.4}\" y=\"{:.4}\" drill=\"{:.4}\" diameter=\"{:.4}\" shape=\"{}\"{}/>\n",+ esc_xml(&p.name), p.x, -p.y, drill, diameter, p.shape, smd_rotation(p.angle)));+ }+ }+ // Real silk/fab graphics (circles/lines/arcs); bbox rectangle only as fallback.+ let (minx, miny, maxx, maxy) = package_outline(km, &mod_pads);+ let (sx1, sy1, sx2, sy2) = (minx, -maxy, maxx, -miny);+ let silk = package_graphics(km);+ if silk.is_empty() {+ for (x1, y1, x2, y2) in [(sx1, sy1, sx2, sy1), (sx2, sy1, sx2, sy2), (sx2, sy2, sx1, sy2), (sx1, sy2, sx1, sy1)] {+ xml.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.127\" layer=\"21\"/>\n", x1, y1, x2, y2));+ }+ } else {+ xml.push_str(&silk);+ }+ let mut top = sy2;+ let mut bot = sy1;+ for p in &mod_pads {+ let py = -p.y;+ let r = p.dx.max(p.dy) / 2.0;+ top = top.max(py + r);+ bot = bot.min(py - r);+ }+ xml.push_str(&format!(" <text x=\"0\" y=\"{:.3}\" size=\"1.27\" layer=\"25\" align=\"bottom-center\">>NAME</text>\n", top + 0.6));+ xml.push_str(&format!(" <text x=\"0\" y=\"{:.3}\" size=\"1.27\" layer=\"27\" align=\"top-center\">>VALUE</text>\n", bot - 0.6));+ } else { xml.push_str(&format!(" <description>{} placeholder footprint</description>\n", esc_xml(&sym.name))); // Generate SMD pads from pin numbers@@ -633,6 +941,7 @@ fn generate_lbr(sym: &ParsedSymbol, _fp_content: Option<&str>) -> String { xml.push_str(&format!(" <text x=\"0\" y=\"{:.3}\" size=\"1.27\" layer=\"25\" align=\"bottom-center\">>NAME</text>\n", body_h / 2.0 + 0.5)); xml.push_str(&format!(" <text x=\"0\" y=\"{:.3}\" size=\"1.27\" layer=\"27\" align=\"top-center\">>VALUE</text>\n", -body_h / 2.0 - 0.5));+ } xml.push_str("</package>\n"); xml.push_str("</packages>\n");@@ -648,6 +957,24 @@ 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));+ }+ }+ 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));+ }+ }+ }+ }+ let name_y = sym.body.as_ref().map(|b| b.top + 2.54).unwrap_or(10.0); let value_y = sym.body.as_ref().map(|b| b.bottom - 2.54).unwrap_or(-10.0); xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"95\" align=\"bottom-center\">>NAME</text>\n", name_y));