← Commit history

2.25.0: centre the thumbnail; hide pin names on glyph symbols

John Lauer ·e266e5ff29 ·1mo ago ·parent 147587b
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"     }   ]-}\ No newline at end of file+}
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\">&gt;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 { "" }         ));     }