← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.38.0"+version = "2.39.0" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.38.0"+version = "2.39.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.38.0+2.39.0
bin/adom-lbr
⋯ 1 unchanged line ⋯
crates/altium-codec/src/altium_decode.rs+1
@@ -361,6 +361,7 @@ fn decode_sym_graphic(txt: &str, rec: &str) -> Option<Graphic> {             y2: coord("Corner.Y"),             width,             filled: txt.contains("|IsSolid=T"),+            fill_style: None,         }),         "11" => Some(Graphic::Circle {             cx: coord("Location.X"),
crates/altium-codec/src/kicad.rs+13−1
@@ -184,7 +184,18 @@ fn import_symbol(text: &str, wanted: &str) -> Result<Symbol> {         graphics: import_symbol_graphics(drawing),         designator_prefix: Some(prefix),         footprint: None,-        parameters: Vec::new(),+        // Source properties become parameters with their visibility kept, so+        // Manufacturer/Datasheet/etc survive the hub instead of being+        // discarded (wiki #24). Reference and Footprint stay structural.+        parameters: props.iter().filter_map(|p| {+            let name = p.atom_at(1)?;+            if matches!(name, "Reference" | "Footprint") { return None; }+            let hidden = p.child("effects").map(|e| {+                e.child("hide").and_then(|h| h.atom_at(1)) == Some("yes")+                    || e.items().iter().any(|i| matches!(i, Sx::Atom(a) if a == "hide"))+            }).unwrap_or(false);+            Some(crate::model::Parameter { name: name.into(), value: p.atom_at(2)?.into(), visible: !hidden })+        }).collect(),         pin_numbers_hidden: Some(has_hide("pin_numbers")),         pin_names_hidden: Some(has_hide("pin_names")),     })@@ -242,6 +253,7 @@ fn import_symbol_graphics(root: &Sx) -> Vec<crate::model::Graphic> {             out.push(Graphic::Rect {                 x1: to_mil(s[0]), y1: to_mil(s[1]), x2: to_mil(e[0]), y2: to_mil(e[1]),                 width: width_of(g), filled: filled_of(g),+                fill_style: g.child("fill").and_then(|f| f.child("type")).and_then(|t| t.atom_at(1)).map(str::to_owned),             });         }     }
crates/altium-codec/src/kicad_export.rs+25−13
@@ -161,13 +161,19 @@ pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String {     };     s.push_str(&prop("Reference", &reference, cx, top + 1.27, false));     s.push_str(&prop("Value", &value, cx, bottom - 1.27, false));-    s.push_str(&prop("Footprint", &footprint, 0.0, 0.0, true));-    // "" not "~": since format 20250318 a tilde is a literal tilde.-    s.push_str(&prop("Datasheet", "", 0.0, 0.0, true));-    // any extra parameters become hidden properties (at origin)+    // Hidden metadata sits in spaced rows BELOW the body, never piled at the+    // origin: native library editors draw hidden fields, and a stack of them+    // at (0,0) obscured the symbol centre (wiki #24).+    s.push_str(&prop("Footprint", &footprint, cx, bottom - 3.81, true));+    // "" not "~": since format 20250318 a tilde is a literal tilde. The source+    // Datasheet value survives when the import captured one.+    let datasheet = sym.parameters.iter().find(|p| p.name.eq_ignore_ascii_case("Datasheet"));+    s.push_str(&prop("Datasheet", datasheet.map(|p| p.value.as_str()).unwrap_or(""), cx, bottom - 6.35, true));+    let mut metadata_y = bottom - 8.89;     for p in &sym.parameters {-        if !p.name.eq_ignore_ascii_case("Value") {-            s.push_str(&prop(&p.name, &p.value, 0.0, 0.0, !p.visible));+        if !["Value", "Reference", "Footprint", "Datasheet"].iter().any(|k| p.name.eq_ignore_ascii_case(k)) {+            s.push_str(&prop(&p.name, &p.value, cx, metadata_y, !p.visible));+            metadata_y -= 2.54;         }     } @@ -201,27 +207,33 @@ pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String {  /// One symbol body graphic → KiCad s-expr (mils→mm via `m`). fn sym_graphic(g: &Graphic, m: &dyn Fn(f64) -> String) -> String {-    let stroke_fill = |w: f64, filled: bool| {-        let fill = if filled { "outline" } else { "none" };+    let stroke_fill = |w: f64, fill: &str| {         format!(             "\t\t\t\t(stroke (width {}) (type default))\n\t\t\t\t(fill (type {fill}))\n",             n((w * MM_PER_MIL).max(0.0))         )     };     match g {-        Graphic::Rect { x1, y1, x2, y2, width, filled } => format!(+        Graphic::Rect { x1, y1, x2, y2, width, filled, fill_style } => format!(             "\t\t\t(rectangle\n\t\t\t\t(start {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n",-            m(*x1), m(*y1), m(*x2), m(*y2), stroke_fill(*width, *filled)+            m(*x1), m(*y1), m(*x2), m(*y2), stroke_fill(*width, match fill_style.as_deref() {+                // Preserve the source's fill kind verbatim; the legacy boolean+                // (older JSON without the field) keeps its established mapping.+                Some("background") => "background",+                Some("none") => "none",+                Some(_) => "outline",+                None => if *filled { "outline" } else { "none" },+            })         ),         Graphic::Circle { cx, cy, r, width } => format!(             "\t\t\t(circle\n\t\t\t\t(center {} {})\n\t\t\t\t(radius {})\n{}\t\t\t)\n",-            m(*cx), m(*cy), m(*r), stroke_fill(*width, false)+            m(*cx), m(*cy), m(*r), stroke_fill(*width, "none")         ),         Graphic::Arc { cx, cy, r, start, end, width } => {             let (s, mid, e) = arc_points(*cx, *cy, *r, *start, *end);             format!(                 "\t\t\t(arc\n\t\t\t\t(start {} {})\n\t\t\t\t(mid {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n",-                m(s.0), m(s.1), m(mid.0), m(mid.1), m(e.0), m(e.1), stroke_fill(*width, false)+                m(s.0), m(s.1), m(mid.0), m(mid.1), m(e.0), m(e.1), stroke_fill(*width, "none")             )         }         Graphic::Polyline { points, width } => {@@ -231,7 +243,7 @@ fn sym_graphic(g: &Graphic, m: &dyn Fn(f64) -> String) -> String {             }             format!(                 "\t\t\t(polyline\n\t\t\t\t(pts\n{pts}\t\t\t\t)\n{}\t\t\t)\n",-                stroke_fill(*width, false)+                stroke_fill(*width, "none")             )         }     }
crates/altium-codec/src/lib.rs+54
@@ -47,6 +47,60 @@ mod tests {         }     } +    #[test]+    fn background_and_outline_fill_remain_distinct_through_canonical_json() {+        // wiki #24: (fill (type background)) came back as (fill (type outline)),+        // which paints the whole body solid and obscures the symbol graphics.+        use crate::model::Graphic;+        for style in ["background", "outline", "none"] {+            let source = format!(r#"(kicad_symbol_lib (version 20231120) (generator "test")+              (symbol "FILL" (property "Reference" "U" (at 0 0 0))+                (symbol "FILL_0_1"+                  (rectangle (start -2.54 2.54) (end 2.54 -2.54)+                    (stroke (width 0.254) (type default)) (fill (type {style}))))+                (symbol "FILL_1_1"+                  (pin passive line (at -5.08 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 = import_kicad(&source, r#"(footprint "FILL" (layer "F.Cu") (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu")))"#, "FILL").unwrap().symbol;+            let json = serde_json::to_string(&sym).unwrap();+            let canonical: Symbol = serde_json::from_str(&json).unwrap();+            let out = export_kicad_symbol(&canonical);+            assert!(out.contains(&format!("(fill (type {style}))")), "style {style} lost: {out}");+        }+        // Older JSON carries only the boolean; its established rendering stands.+        let legacy: Graphic = serde_json::from_str(+            r#"{"kind":"rect","x1":0.0,"y1":0.0,"x2":100.0,"y2":100.0,"filled":true}"#).unwrap();+        let mut sym = two_pin_symbol();+        sym.graphics.push(legacy);+        assert!(export_kicad_symbol(&sym).contains("(fill (type outline))"));+    }++    #[test]+    fn datasheet_and_metadata_survive_kicad_without_duplicate_properties() {+        // wiki #24 second half: source properties were discarded, so Datasheet+        // exported empty; and hidden fields piled at the origin, which native+        // editors draw on top of the symbol centre.+        let source = r#"(kicad_symbol_lib (version 20231120) (generator "test")+          (symbol "META" (property "Reference" "U" (at 0 0 0))+            (property "Datasheet" "https://manufacturer.example/datasheet.pdf" (at 0 0 0) (effects (font (size 1 1)) hide))+            (property "Manufacturer" "Maker" (at 0 0 0) (effects (font (size 1 1)) (hide yes)))+            (property "VisibleNote" "Read me" (at 0 0 0) (effects (font (size 1 1))))+            (symbol "META_1_1"+              (pin passive line (at -5.08 0 0) (length 2.54) (name "A" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))))))"#;+        let fpm = r#"(footprint "META" (layer "F.Cu") (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu")))"#;+        let sym = import_kicad(source, fpm, "META").unwrap().symbol;+        assert_eq!(sym.parameters.len(), 3, "{:?}", sym.parameters);+        assert!(!sym.parameters.iter().find(|p| p.name == "Datasheet").unwrap().visible);+        assert!(!sym.parameters.iter().find(|p| p.name == "Manufacturer").unwrap().visible);+        assert!(sym.parameters.iter().find(|p| p.name == "VisibleNote").unwrap().visible);+        let out = export_kicad_symbol(&sym);+        assert_eq!(out.matches("(property \"Datasheet\"").count(), 1, "one Datasheet property, never two");+        assert!(out.contains("https://manufacturer.example/datasheet.pdf"));+        assert!(!out.contains("(at 0 0 0)\n\t\t\t(effects (font (size 1.27 1.27)) hide"), "hidden metadata must not sit at the origin");+        let back = import_kicad(&out, fpm, "META").unwrap().symbol;+        assert_eq!(back.parameters.iter().find(|p| p.name == "Manufacturer").unwrap().value, "Maker");+        assert!(back.parameters.iter().find(|p| p.name == "VisibleNote").unwrap().visible);+    }+     #[test]     fn silk_fp_poly_rides_the_hub_json_and_round_trips_to_kicad() {         // wiki #16: the pin-1 marker is a FILLED F.SilkS polygon in 97 percent
crates/altium-codec/src/model.rs+6−1
@@ -85,7 +85,12 @@ pub enum Graphic {     /// Connected line segments through `points` (mils).     Polyline { points: Vec<[f64; 2]>, #[serde(default)] width: f64 },     /// Axis-aligned rectangle (mils).-    Rect { x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64, #[serde(default)] filled: bool },+    Rect { x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64, #[serde(default)] filled: bool,+        /// KiCad fill type ("background" | "outline" | "none"), preserved+        /// verbatim so a round trip cannot turn a background fill into an+        /// outline fill that paints the body solid (wiki #24). Optional with+        /// a legacy-boolean fallback, so older JSON keeps its rendering.+        #[serde(default, skip_serializing_if = "Option::is_none")] fill_style: Option<String> },     /// Circle (mils).     Circle { cx: f64, cy: f64, r: f64, #[serde(default)] width: f64 },     /// Arc, angles in degrees (mils).
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.38.0",+  "version": "2.39.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+8−8
@@ -1,8 +1,8 @@ {   "slug": "adom-lbr",-  "version": "2.38.0",+  "version": "2.39.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.",+  "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": {},   "tags": [     "eda",@@ -61,15 +61,15 @@     "screenshots/**",     "dockbar.json"   ],-  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+  "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",   "sample_prompts": [     {       "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",-      "label": "KiCad → Altium"+      "label": "KiCad \u2192 Altium"     },     {       "prompt": "Import this Altium footprint into the adom-lbr JSON",-      "label": "Altium → JSON"+      "label": "Altium \u2192 JSON"     },     {       "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +77,15 @@     },     {       "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",-      "label": "Altium → KiCad"+      "label": "Altium \u2192 KiCad"     },     {       "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",-      "label": "JSON → KiCad"+      "label": "JSON \u2192 KiCad"     },     {       "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup",       "label": "Browse parts"     }   ]-}\ No newline at end of file+}