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.25.0: centre the thumbnail; hide pin names on glyph symbols
7 files changed
+70−29
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.24.0"+version = "2.25.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.24.0"+version = "2.25.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.24.0+2.25.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.24.0",+ "version": "2.25.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.24.0",+ "version": "2.25.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+64−23@@ -1305,10 +1305,16 @@ fn resolve_symbol_art(path: &std::path::Path) -> Result<SymbolArt, String> { /// 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) {+/// CENTRED ON THE BODY. The contract places art in exact symbol mm-space, which is right+/// when the symbol is drawn around the origin and wrong the moment it is not: measured on+/// the CH340G variant set, the body centres sit at y = 0, -4.61, -7.79, -20.05 and -27.86+/// depending on the layout, while the art stayed at the origin every time. The thumbnail+/// 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) { let poly_re = regex_lite::Regex::new(r#"<(polyline|polygon)\b[^>]*\bpoints="([^"]+)""#).unwrap();- let mut out = String::new();- let mut segs = 0usize;+ let mut polys: Vec<Vec<(f64, f64)>> = Vec::new(); 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| {@@ -1318,8 +1324,27 @@ fn contract_wires(mm_svg: &str, dx: f64, svg_cy: f64, e_cy: f64) -> (String, usi Some((x - dx, e_cy - (y - svg_cy))) }).collect(); if closed { if let Some(&first) = pts.first() { pts.push(first); } }+ if pts.len() >= 2 { polys.push(pts); }+ }++ // Translate the whole drawing so its bounding box sits on the body's centre. Shape and+ // size are untouched; only where it sits changes.+ let (mut ox, mut oy) = (0.0f64, 0.0f64);+ if let Some(b) = body {+ let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ for p in &polys { for &(x, y) in p { x0 = x0.min(x); y0 = y0.min(y); x1 = x1.max(x); y1 = y1.max(y); } }+ if x0 <= x1 {+ ox = (b.left + b.right) / 2.0 - (x0 + x1) / 2.0;+ oy = (b.top + b.bottom) / 2.0 - (y0 + y1) / 2.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, w[0].1, w[1].0, w[1].1));+ 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)); segs += 1; } }@@ -1383,27 +1408,11 @@ fn symbol_art_wires(svg: &str, body: Option<&KicadBody>, scale_frac: f64) -> (St /// 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::Contract { mm_svg, dx, svg_cy, e_cy } => contract_wires(mm_svg, *dx, *svg_cy, *e_cy, body), SymbolArt::Legacy(svg) => symbol_art_wires(svg, body, scale_frac), } } -/// The attributes a BOM and the fusion-bridge linter read off a part.-///-/// Accepts a wiki component `page.json` (fields under `component`) or a bare JSON-/// object with the same keys. Distributor part numbers may be written either flat-/// (`"mouser_pn": "584-..."`) or under `distributor_links` as a string or as an-/// object carrying `pn` / `part_number`, because both shapes are already in use.-/// An absent or unreadable file is not an error - a part with no metadata still-/// generates, it just generates without attributes, exactly as before.-/// The wiki page's one-line `brief`, used when the `.kicad_sym` carries no Description.-/// Several published sources have none at all (adom/s10b-ph-sm4-tb, ray/stm32l431rct6tr,-/// ray/xl7026e1), so their devicesets reached Fusion with an empty Description column and-/// nothing to search on. The page brief is the right text and it is already sitting beside-/// the symbol, so the library carries it rather than the importer having to guess.-/// Catalogue boilerplate that belongs on a wiki PAGE, not in a part's description. A page-/// brief is written for a reader browsing the library; the same sentence repeated in every-/// Fusion Description cell is noise that pushes the real text out of a narrow column. /// KiCad's stock symbol libraries name a symbol after its CLASS ("R", "C", "L", "D", /// "LED", "Crystal_GND24", "Conn_01x02"), because they describe a kind of part rather than /// a specific one. That name is right for a symbol and wrong for a DEVICESET: a user who@@ -1440,6 +1449,9 @@ fn is_generic_description(text: &str) -> bool { || (t.contains("crystal") && t.contains("pin") && t.len() < 60) } +/// Catalogue boilerplate that belongs on a wiki PAGE, not in a part's description. A page+/// brief is written for a reader browsing the library; the same sentence repeated in every+/// Fusion Description cell is noise that pushes the real text out of a narrow column. fn strip_catalog_boilerplate(text: &str) -> String { // LONGEST FIRST: "Datasheet-verified pinout" has to go before "Datasheet-verified", // or the short form eats its prefix and strands the word "pinout" on its own.@@ -1569,6 +1581,11 @@ fn package_description(fp_content: Option<&str>, package_name: &str) -> String { } } +/// The wiki page's one-line `brief`, used when the `.kicad_sym` carries no Description.+/// Several published sources have none at all (adom/s10b-ph-sm4-tb, ray/stm32l431rct6tr,+/// ray/xl7026e1), so their devicesets reached Fusion with an empty Description column and+/// nothing to search on. The page brief is the right text and it is already sitting beside+/// the symbol, so the library carries it rather than the importer having to guess. fn page_brief(path: &std::path::Path) -> Option<String> { let raw = fs::read_to_string(path).ok()?; let doc = serde_json::from_str::<serde_json::Value>(&raw).ok()?;@@ -1582,6 +1599,14 @@ fn page_brief(path: &std::path::Path) -> Option<String> { None } +/// The attributes a BOM and the fusion-bridge linter read off a part.+///+/// Accepts a wiki component `page.json` (fields under `component`) or a bare JSON+/// object with the same keys. Distributor part numbers may be written either flat+/// (`"mouser_pn": "584-..."`) or under `distributor_links` as a string or as an+/// object carrying `pn` / `part_number`, because both shapes are already in use.+/// An absent or unreadable file is not an error - a part with no metadata still+/// generates, it just generates without attributes, exactly as before. fn part_attributes(path: &std::path::Path) -> Vec<(String, String)> { let Ok(raw) = fs::read_to_string(path) else { return Vec::new() }; let Ok(doc) = serde_json::from_str::<serde_json::Value>(&raw) else { return Vec::new() };@@ -1667,13 +1692,29 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"96\" align=\"top-center\">>VALUE</text>\n", value_y)); let deduped_names = dedup_pin_names(&sym.pins);+ // A GLYPH SYMBOL HIDES ITS PIN NAMES. EAGLE draws a pin's name at its INNER end, which+ // is inside the body for an IC and inside the DRAWING for a two-terminal part that has+ // no body box: measured on the published LED, both names land on the triangle, and on+ // the crystal all four land on the plates ("G@2" drawn straight through the box). No+ // engineer labels a diode's pins on a schematic anyway, so show the pad NUMBER and hide+ // the name, which is what EAGLE's own passive libraries do. Tightly scoped: only a+ // symbol with NO body box, its own artwork, and at most four pins, so no IC is touched.+ // NOT keyed on "has no body box": a crystal's first rectangle IS its can, so it parses+ // as a body while being part of the drawing, and the four pin names printed straight+ // through it. The test that holds for both is artwork plus a small pin count: an IC+ // with a body and no artwork of its own keeps its names.+ let glyph_only = !sym.graphics.is_empty() && sym.pins.len() <= 4;+ if glyph_only {+ println!("HINT: glyph symbol ({} pins, no body box): pin NAMES hidden and pad numbers shown, so the names do not print on top of the drawing.", sym.pins.len());+ } for (i, pin) in sym.pins.iter().enumerate() { xml.push_str(&format!(- " <pin name=\"{}\" x=\"{}\" y=\"{}\" length=\"{}\" direction=\"{}\"{}/>\n",+ " <pin name=\"{}\" x=\"{}\" y=\"{}\" length=\"{}\" direction=\"{}\"{}{}/>\n", esc_xml(&deduped_names[i]), pin.x, pin.y, pin_length_keyword(pin.length), pin_direction(&pin.pin_type),- pin_rotation(pin.angle)+ pin_rotation(pin.angle),+ if glyph_only { " visible=\"pad\"" } else { "" } )); }