← Commit history

2.26.0: EAGLE alignment vocabulary for group captions; invalid-align lint

John Lauer ·0d1eacb046 ·1mo ago ·parent 78ddb2e
7 files changed +108−15
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.25.0"+version = "2.26.0" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.25.0"+version = "2.26.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.25.0+2.26.0
bin/adom-lbr
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.25.0",+  "version": "2.26.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.25.0",+  "version": "2.26.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+102−9
@@ -356,6 +356,12 @@ struct GroupLabel {     text: String,     x: f64,     y: f64,+    /// Rotation from the source `(at x y ANGLE)`. adom-symbol writes 0 for the left and+    /// right captions and 90 for the top and bottom ones.+    angle: u32,+    /// KiCad `(justify left|right)`. It is the anchored END of the text, so it decides+    /// which EAGLE align value is faithful; KiCad's vertical default is centre.+    justify_right: bool, }  #[derive(Debug, Serialize, Deserialize)]@@ -474,14 +480,33 @@ pub(crate) fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Res     }      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+    // bottom edges) and states which end is anchored; re-deriving the side from x alone+    // threw that away and got the right-hand captions wrong.     let text_re = regex_lite::Regex::new(-        r#"\(text "([^"]+)"\s+\(at\s+([-\d.]+)\s+([-\d.]+)"#+        r#"\(text "([^"]+)"\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)"#     ).unwrap();-    for cap in text_re.captures_iter(content) {+    let text_starts: Vec<usize> = text_re.find_iter(content).map(|m| m.start()).collect();+    for (i, cap) in text_re.captures_iter(content).enumerate() {+        // `justify` lives in the (effects (font ...) (justify ...)) block AFTER the (at ...),+        // so read the window from this element up to the next one rather than trying to+        // close the s-expression with a regex: a non-greedy match stops at the first ')',+        // which is the end of (size 0.762 0.762), long before the justify.+        let start = text_starts.get(i).copied().unwrap_or(0);+        let end = text_starts.get(i + 1).copied().unwrap_or(content.len());+        let window = &content[start..end];+        // TENTHS OF A DEGREE: KiCad writes a text angle as 900 for 90 degrees here, while+        // pin angles in the same file are plain degrees. Taking it as degrees turned every+        // rotated caption into 900 % 360 = 180 and lost the rotation entirely.+        let raw: f64 = cap[4].parse().unwrap_or(0.0);+        let deg = if raw.abs() > 360.0 { raw / 10.0 } else { raw };         group_labels.push(GroupLabel {             text: cap[1].to_string(),             x: cap[2].parse().unwrap_or(0.0),             y: cap[3].parse().unwrap_or(0.0),+            angle: ((deg.round() as i64 % 360 + 360) % 360) as u32,+            justify_right: window.contains("justify right"),         });     } @@ -1722,16 +1747,27 @@ fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&S     let body_left = sym.body.as_ref().map(|b| b.left).unwrap_or(-10.0);     let body_right = sym.body.as_ref().map(|b| b.right).unwrap_or(10.0);     for label in &sym.group_labels {-        let (lx, align) = if label.x > body_center_x {-            (body_right - 1.0, " align=\"right\"")-        } else {-            (body_left + 1.0, "")-        };+        // FAITHFUL MAPPING of adom-symbol's four caption forms, rather than re-deriving the+        // side from x. EAGLE's align vocabulary is the nine {bottom|center|top}-{left|+        // center|right} values; a bare "right" is not one of them, so Fusion logged a+        // warning per caption, ignored it, and fell back to LEFT anchoring, which made every+        // right-hand caption run outward across the body line (John saw it on 2CG and SQG).+        // KiCad's vertical default is centre, so center-* is the faithful vertical anchor.+        //   left   (at x y 0)   justify left  -> R0,  center-left+        //   right  (at x y 0)   justify right -> R0,  center-right+        //   top    (at x y 90)  justify right -> R90, center-right+        //   bottom (at x y 90)  justify left  -> R90, center-left+        let align = if label.justify_right { "center-right" } else { "center-left" };+        let rot = if label.angle == 90 || label.angle == 270 { " rot=\"R90\"" } else { "" };+        // The source already places captions 1.02 mm inside the edge. Use its coordinates+        // instead of recomputing them from the body, which is how the old code lost the+        // top and bottom captions entirely.         xml.push_str(&format!(-            "  <text x=\"{}\" y=\"{}\" size=\"0.762\" layer=\"94\" font=\"vector\" ratio=\"15\"{}>{}</text>\n",-            lx, label.y, align, esc_xml(&label.text)+            "  <text x=\"{:.4}\" y=\"{:.4}\" size=\"0.762\" layer=\"94\" font=\"vector\" ratio=\"15\"{} align=\"{}\">{}</text>\n",+            label.x, label.y, rot, align, esc_xml(&label.text)         ));     }+    let _ = (body_center_x, body_left, body_right);      xml.push_str("</symbol>\n");     deduped_names@@ -1976,6 +2012,30 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {         errors.push(LintError { code: "missing-devicesets".into(), message: "No <devicesets> section found".into() });     } +    // EAGLE's align vocabulary is exactly nine values. Anything else makes Fusion log a+    // warning per element on open and fall back to bottom-left, so text anchored to an+    // edge runs OUTWARD across the body line instead of inward. adom-lbr shipped+    // align="right" on right-side group captions until 2.26.0; this stops any align value+    // reaching a library without a name EAGLE knows.+    {+        const VALID: [&str; 9] = ["bottom-left", "bottom-center", "bottom-right",+                                  "center-left", "center", "center-right",+                                  "top-left", "top-center", "top-right"];+        let re = regex_lite::Regex::new(r#"align="([^"]*)""#).unwrap();+        let mut bad: Vec<String> = Vec::new();+        for c in re.captures_iter(content) {+            let v = c[1].to_string();+            if !VALID.contains(&v.as_str()) && !bad.contains(&v) { bad.push(v); }+        }+        if !bad.is_empty() {+            errors.push(LintError {+                code: "invalid-align".into(),+                message: format!("align={:?} is not an EAGLE alignment. Valid: {}. Fusion warns per element and falls back to bottom-left, so edge-anchored text runs across the body line.",+                                 bad.join(", "), VALID.join(", ")),+            });+        }+    }+     // Check for empty packages. Matching the literal "<packages></packages>" missed     // a library whose section held only whitespace or a comment, which shipped with     // zero packages while its .kicad_mod had two perfectly good SMD pads. Ask the@@ -3304,6 +3364,39 @@ mod lint_tests {         assert!(out.contains("2.5 x 0.5 mm"), "expected the copper extent: {out}");     } +    #[test]+    fn group_captions_map_to_eagle_alignment_not_a_bare_right() {+        use super::parse_kicad_sym;+        // adom-symbol's four caption forms, verbatim in shape: left and right unrotated,+        // top and bottom rotated, with the angle written in TENTHS of a degree (900 = 90).+        let src = r#"(kicad_symbol_lib (version 20211014) (generator adom)+          (symbol "T" (property "Reference" "U" (id 0) (at 0 0 0))+            (symbol "T_1_1"+              (text "Handshake" (at -11.68 0.00 0)+                (effects (font (size 0.762 0.762)) (justify left)))+              (text "UART" (at 11.68 0.00 0)+                (effects (font (size 0.762 0.762)) (justify right)))+              (text "Power" (at -7.72 7.08 900)+                (effects (font (size 0.762 0.762)) (justify right)))+              (text "Ground" (at -7.72 -22.65 900)+                (effects (font (size 0.762 0.762)) (justify left)))+              (pin passive line (at -15.24 0 0) (length 2.54)+                (name "A" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))+            )))"#;+        let sym = parse_kicad_sym(src, None).expect("parses");+        let by = |t: &str| sym.group_labels.iter().find(|g| g.text == t).expect(t).clone();+        // left: unrotated, anchored on its left end+        let l = by("Handshake"); assert_eq!(l.angle, 0); assert!(!l.justify_right);+        // right: unrotated, anchored on its RIGHT end. Emitting a bare align="right" here+        // is what made Fusion warn and fall back to left anchoring, so the caption ran out+        // across the body line.+        let r = by("UART"); assert_eq!(r.angle, 0); assert!(r.justify_right);+        // top and bottom: rotated. 900 is tenths of a degree; reading it as degrees gives+        // 900 % 360 = 180 and loses the rotation.+        let t = by("Power"); assert_eq!(t.angle, 90); assert!(t.justify_right);+        let b = by("Ground"); assert_eq!(b.angle, 90); assert!(!b.justify_right);+    }+     #[test]     fn a_symbol_that_draws_itself_is_not_relaid_out() {         use super::{relayout_two_columns, KicadPin, ParsedSymbol, SymGraphic};