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
2.29.0: scaled icon stroke; source designator positions when safe
6 files changed
+86−12
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.28.1"+version = "2.29.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.28.1"+version = "2.29.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.28.1+2.29.0
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.28.1",+ "version": "2.29.0", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad 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.", "dependencies": {},
page.json+2−2@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.28.1",+ "version": "2.29.0", "type": "app", "description": "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.", "dependencies": {},@@ -88,4 +88,4 @@ "label": "Browse parts" } ]-}+}
src/main.rs+80−6@@ -384,6 +384,11 @@ struct ParsedSymbol { package_name: Option<String>, /// A human line for the package, e.g. "SOIC-16, 3.9 x 9.9 mm body, 1.27 mm pitch". package_description: Option<String>,+ /// Where the source .kicad_sym puts its Reference and Value text, if it says. When no+ /// outline contract exists this is the next authority, so a symbol and its chip-outline+ /// twin agree instead of one taking the source's placement and the other mine.+ ref_pos: Option<(f64, f64)>,+ val_pos: Option<(f64, f64)>, /// The EAGLE `<deviceset>` name. None means "use `name`". A deviceset is the ORDERABLE /// part, so it wants the MPN; the symbol underneath it can keep a class name like "L", /// which is shared between every inductor. A user who opens a library and finds a@@ -479,6 +484,19 @@ pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Res } } + // Reference and Value carry their own placement in the source. Captured so a symbol+ // without an outline contract can still follow adom-symbol's intent rather than my+ // arithmetic, which is what makes a symbol and its -CHIP twin agree.+ let prop_at = |key: &str| -> Option<(f64, f64)> {+ let re = regex_lite::Regex::new(+ &format!(r#"(?s)\(property "{}" "[^"]*"\s*\(at\s+([-\d.]+)\s+([-\d.]+)"#, key)+ ).ok()?;+ let c = re.captures(content)?;+ Some((c[1].parse().ok()?, c[2].parse().ok()?))+ };+ let ref_pos = prop_at("Reference");+ let val_pos = prop_at("Value");+ let mut group_labels = Vec::new(); // Capture the caption's ANGLE and JUSTIFY too, not just its position. adom-symbol // places these deliberately (1.02 mm inside the body edge, rotated 90 on the top and@@ -530,6 +548,8 @@ pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Res pins, body, group_labels,+ ref_pos,+ val_pos, graphics, }) }@@ -652,6 +672,8 @@ fn parse_eagle_lbr(content: &str) -> Result<ParsedSymbol, String> { Ok(ParsedSymbol { package_name: None, package_description: None,+ ref_pos: None,+ val_pos: None, deviceset_name: None, value: name.clone(), name,@@ -1019,6 +1041,8 @@ fn synth_symbol_from_mod(km: &str, name: &str) -> ParsedSymbol { ParsedSymbol { package_name: None, package_description: None,+ ref_pos: None,+ val_pos: None, deviceset_name: None, name: name.to_string(), reference: "U".into(),@@ -1311,6 +1335,8 @@ struct ContractLayout { /// Where the icon was fitted, already clear of the inside designators. When present it /// is honoured as given: re-centring it here would undo that fitting. icon_rect: Option<(f64, f64, f64, f64)>,+ /// Stroke width adom-symbol suggests for the icon, in mm. None means it did not say.+ stroke_mm: Option<f64>, /// Text CENTRES for >NAME and >VALUE. None means adom-symbol hid that designator. reference: Option<(f64, f64)>, value: Option<(f64, f64)>,@@ -1349,6 +1375,7 @@ fn resolve_symbol_art(path: &std::path::Path) -> Result<SymbolArt, String> { }; let layout = ContractLayout { icon_rect: arr4("icon_rect"),+ stroke_mm: v.get("stroke_mm").and_then(|n| n.as_f64()), reference: pt("reference"), value: pt("value"), inside_body: v.get("designators").and_then(|d| d.get("inside_body"))@@ -1372,7 +1399,7 @@ fn resolve_symbol_art(path: &std::path::Path) -> Result<SymbolArt, String> { /// ended up jammed against the top edge of the taller symbols, overlapping the group /// labels, with the middle of the symbol left empty. So the art keeps its SIZE and shape /// from the contract and is translated as a whole onto the body's centre.-fn contract_wires(mm_svg: &str, dx: f64, svg_cy: f64, e_cy: f64, body: Option<&KicadBody>) -> (String, usize) {+fn contract_wires(mm_svg: &str, dx: f64, svg_cy: f64, e_cy: f64, body: Option<&KicadBody>, stroke_mm: Option<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(mm_svg) {@@ -1399,12 +1426,24 @@ fn contract_wires(mm_svg: &str, dx: f64, svg_cy: f64, e_cy: f64, body: Option<&K } } + // STROKE SCALES WITH THE ICON. A fixed 0.1016 mm is fine on a large icon and turns a+ // small one into a solid blob: measured on CH340G-2C-CHIP, the fitted icon is 3.7 x 2.4+ // mm with 343 segments whose median length is 0.090 mm, so 89% of them are SHORTER than+ // the line is wide and the drawing fills in. adom-symbol's suggested stroke_mm wins when+ // the contract carries one; otherwise the same rule of thumb from the icon's own width.+ // EAGLE treats 0 as a hairline, so that is the floor.+ let icon_w = {+ let (mut x0, mut x1) = (f64::MAX, f64::MIN);+ for p in &polys { for &(x, _) in p { x0 = x0.min(x); x1 = x1.max(x); } }+ if x0 <= x1 { x1 - x0 } else { 0.0 }+ };+ let stroke = stroke_mm.unwrap_or_else(|| (icon_w / 120.0).max(0.0508)).max(0.0); let mut out = String::new(); let mut segs = 0usize; for pts in &polys { 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 + ox, w[0].1 + oy, w[1].0 + ox, w[1].1 + oy));+ out.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"{:.4}\" layer=\"94\"/>\n",+ w[0].0 + ox, w[0].1 + oy, w[1].0 + ox, w[1].1 + oy, stroke)); segs += 1; } }@@ -1445,6 +1484,9 @@ fn symbol_art_wires(svg: &str, body: Option<&KicadBody>, scale_frac: f64) -> (St 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);+ // Stroke scales with the drawn size, same rule as the contract path: a fixed width on a+ // small icon draws segments wider than they are long and the whole thing fills in.+ let stroke = ((sw * scale) / 120.0).max(0.0508); let mut out = String::new(); let mut segs = 0usize; for poly in &polys {@@ -1458,7 +1500,7 @@ fn symbol_art_wires(svg: &str, body: Option<&KicadBody>, scale_frac: f64) -> (St } } 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));+ out.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"{:.4}\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1, stroke)); segs += 1; } }@@ -1472,7 +1514,7 @@ fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) // A contract that states where the icon goes has already fitted it clear of the // inside designators. Re-centring it on the body would undo exactly that. let recentre = if layout.icon_rect.is_some() { None } else { body };- contract_wires(mm_svg, *dx, *svg_cy, *e_cy, recentre)+ contract_wires(mm_svg, *dx, *svg_cy, *e_cy, recentre, layout.stroke_mm) } SymbolArt::Legacy(svg) => symbol_art_wires(svg, body, scale_frac), }@@ -1765,6 +1807,33 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S // Note the two tools measure text differently (adom-symbol 1.5 mm per character at the // 1.27 mm font, adom-lbr 0.6 x height), so the rects they compute differ; that is // precisely why one of them has to own the number rather than both approximating it.+ const GLYPH_ADVANCE: f64 = 1.18;++ // SECOND AUTHORITY: the source's own Reference/Value positions, used when there is no+ // contract, so a symbol and its -CHIP twin do not give two different answers side by+ // side in the tree (John, comparing CH340G-SQ with CH340G-SQ-CHIP).+ //+ // VALIDATED, NOT OBEYED. A position is honoured only if a wire cannot run through it.+ // Older adom-symbol builds and KiCad's stock libraries put Reference above the body and+ // Value below it, which on a four-sided symbol is exactly the lane the top and bottom+ // nets occupy: CH340G-SQ's own source puts Reference at y = 16.64, past the top pin ends+ // at 10.54, so obeying it blindly would put the label back where this was just fixed.+ let in_a_wire_lane = |pt: (f64, f64), label_w: f64| -> bool {+ let half = label_w / 2.0;+ sym.pins.iter().any(|p| match p.angle {+ 270 => (pt.0 - p.x).abs() < half + 0.5 && pt.1 >= p.y,+ 90 => (pt.0 - p.x).abs() < half + 0.5 && pt.1 <= p.y,+ 0 => (pt.1 - p.y).abs() < 1.27 && pt.0 <= p.x,+ _ => (pt.1 - p.y).abs() < 1.27 && pt.0 >= p.x,+ })+ };+ let source_designators = match (sym.ref_pos, sym.val_pos) {+ (Some(r), Some(v))+ if !in_a_wire_lane(r, 5.0 * GLYPH_ADVANCE * 1.27)+ && !in_a_wire_lane(v, 6.0 * GLYPH_ADVANCE * 1.27) => Some((r, v)),+ _ => None,+ };+ let contract_designators = match symbol_art { Some(SymbolArt::Contract { layout, .. }) if layout.inside_body && (layout.reference.is_some() || layout.value.is_some()) => Some(layout),@@ -1778,6 +1847,10 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S xml.push_str(&format!(" <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"96\" align=\"center\">>VALUE</text>\n", x, y)); } println!("HINT: >NAME/>VALUE placed from the outline contract (adom-symbol's own designator points), so the KiCad and Fusion renders match.");+ } else if let Some((r, v)) = source_designators {+ xml.push_str(&format!(" <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"95\" align=\"center\">>NAME</text>\n", r.0, r.1));+ xml.push_str(&format!(" <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"96\" align=\"center\">>VALUE</text>\n", v.0, v.1));+ println!("HINT: >NAME/>VALUE placed from the source .kicad_sym's own Reference/Value positions, so this symbol matches its chip-outline twin."); } else { let has_vertical_pins = sym.pins.iter().any(|p| p.angle == 90 || p.angle == 270); // GLYPH ADVANCE, taken from adom-symbol rather than from my own estimate. I used@@ -1788,7 +1861,6 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S // margins read correctly in Fusion. One model, and it is the one that has been checked // against the renderer. Over-reserving is the safe direction: it costs interior space, // never a collision.- const GLYPH_ADVANCE: f64 = 1.18; let pin_name_w = |p: &KicadPin| dedup_pin_names(std::slice::from_ref(p))[0].chars().count() as f64 * GLYPH_ADVANCE * 1.778; match (&sym.body, has_vertical_pins) { (Some(b), true) => {@@ -3581,6 +3653,8 @@ mod lint_tests { let mut sym = ParsedSymbol { package_name: None, package_description: None,+ ref_pos: None,+ val_pos: None, deviceset_name: None, name: "U1".into(), reference: "U".into(), value: String::new(), footprint: String::new(), description: String::new(),