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.39.0: KiCad round trip keeps fill styles and source metadata (wiki #24)
11 files changed
+111−27
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" } ]-}+}