← Commit history

2.29.0: scaled icon stroke; source designator positions when safe

John Lauer ·f8d3162bd8 ·1mo ago ·parent 195844b
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"     }   ]-}\ No newline at end of file+}
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\">&gt;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\">&gt;NAME</text>\n", r.0, r.1));+        xml.push_str(&format!("  <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"96\" align=\"center\">&gt;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(),