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.38.0: footprint polygons ride the hub JSON and the viewer (wiki #16)
12 files changed
+138−21
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.37.0"+version = "2.38.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.37.0"+version = "2.38.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.37.0+2.38.0
bin/adom-lbr⋯ 1 unchanged line ⋯
crates/altium-codec/src/altium_decode.rs+2−2@@ -46,7 +46,7 @@ pub fn decode_altium(schlib: Option<&[u8]>, pcblib: Option<&[u8]>, mpn: &str) -> let want_fp = symbol.footprint.clone(); let footprint = match pcblib { Some(b) => decode_pcblib_footprint(b, want_fp.as_deref())?,- None => Footprint { name: mpn.to_string(), pads: vec![], graphics: vec![] },+ None => Footprint { name: mpn.to_string(), pads: vec![], graphics: vec![], polys: vec![] }, }; build_part(symbol, footprint, mpn) }@@ -501,7 +501,7 @@ fn decode_pcblib_footprint(bytes: &[u8], want: Option<&str>) -> Result<Footprint } } - Ok(Footprint { name: fp_name, pads, graphics })+ Ok(Footprint { name: fp_name, pads, graphics, polys: vec![] }) } /// Altium layer byte → neutral role. Counterpart of `pcblib::layer_enc`.
crates/altium-codec/src/kicad.rs+38−2@@ -330,7 +330,7 @@ fn arc_from_3pts( // Footprint import // --------------------------------------------------------------------------- -fn import_footprint(text: &str) -> Result<Footprint> {+pub(crate) fn import_footprint(text: &str) -> Result<Footprint> { let root = parse_root(text)?; // (footprint "NAME" ...) — name is the first atom after head let name = root.atom_at(1).unwrap_or("FP").to_string();@@ -373,7 +373,7 @@ fn import_footprint(text: &str) -> Result<Footprint> { }); } - Ok(Footprint { name, pads, graphics: import_footprint_graphics(&root) })+ Ok(Footprint { name, pads, graphics: import_footprint_graphics(&root), polys: import_footprint_polys(&root) }) } /// Map a KiCad footprint layer name to a neutral [`FpLayer`] role. Returns@@ -463,6 +463,42 @@ fn import_footprint_graphics(root: &Sx) -> Vec<FpGraphic> { out } +/// Import a KiCad footprint's FILLED polygons (`fp_poly`) into [`FpPoly`]s+/// (mm, Y-down): the pin-1 marker shape (wiki #16). The whole `(pts …)` vertex+/// list rides across; stroke width when the source states one.+fn import_footprint_polys(root: &Sx) -> Vec<crate::model::FpPoly> {+ let mut out = Vec::new();+ let mut polys = Vec::new();+ root.collect("fp_poly", &mut polys);+ for g in polys {+ let layer = g.child("layer").and_then(|l| l.atom_at(1)).and_then(kicad_fp_layer);+ let Some(layer) = layer else { continue };+ let width = g.child("stroke")+ .and_then(|st| st.child("width"))+ .and_then(|w| w.nums().first().copied())+ .or_else(|| g.child("width").and_then(|w| w.nums().first().copied()))+ .unwrap_or(0.0);+ let mut points: Vec<[f64; 2]> = Vec::new();+ if let Some(pts) = g.child("pts") {+ let mut xys = Vec::new();+ pts.collect("xy", &mut xys);+ for xy in xys {+ let n = xy.nums();+ if n.len() >= 2 {+ points.push([n[0], n[1]]);+ }+ }+ }+ let filled = g.child("fill").and_then(|f| f.atom_at(1))+ .map(|v| v == "yes" || v == "solid")+ .unwrap_or(false);+ if points.len() >= 3 {+ out.push(crate::model::FpPoly { layer, points, width, filled });+ }+ }+ out+}+ // --------------------------------------------------------------------------- // Public: KiCad symbol + footprint → AdomLbrPart // ---------------------------------------------------------------------------
crates/altium-codec/src/kicad_export.rs+11@@ -268,6 +268,17 @@ pub fn export_kicad_footprint_for(fp: &Footprint, ver: KicadVersion) -> String { for g in &fp.graphics { s.push_str(&fp_graphic(g)); }+ // Filled polygons ride back out as fp_poly with the full vertex list, so a+ // KiCad -> JSON -> KiCad round trip keeps the pin-1 marker (wiki #16).+ for poly in &fp.polys {+ let pts: String = poly.points.iter()+ .map(|p| format!("\n\t\t\t(xy {} {})", p[0], p[1]))+ .collect();+ s.push_str(&format!(+ "\t(fp_poly\n\t\t(pts{pts}\n\t\t)\n\t\t(stroke (width {}) (type solid)) (fill {}) (layer \"{}\"))\n",+ poly.width, if poly.filled { "yes" } else { "no" }, kicad_fp_layer(&poly.layer)+ ));+ } // pads for pad in &fp.pads {
crates/altium-codec/src/lib.rs+35−5@@ -47,6 +47,34 @@ mod tests { } } + #[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+ // of the catalog. It rides a NEW optional field (not a new FpGraphic+ // variant) so already-installed binaries keep loading new JSON.+ let km = r#"(footprint "T" (layer "F.Cu")+ (pad "1" smd rect (at 0 0) (size 1.0 1.0) (layers "F.Cu"))+ (fp_poly (pts (xy -2.6 -2.47) (xy -2.84 -2.8) (xy -2.36 -2.8))+ (stroke (width 0.12) (type solid)) (fill yes) (layer "F.SilkS")))"#;+ let fp = crate::kicad::import_footprint(km).expect("import");+ assert_eq!(fp.polys.len(), 1, "the filled triangle must import");+ assert_eq!(fp.polys[0].points.len(), 3);+ assert!((fp.polys[0].points[0][0] - -2.6).abs() < 1e-9);+ assert!((fp.polys[0].width - 0.12).abs() < 1e-9);+ assert!(fp.polys[0].filled, "(fill yes) must import as filled");+ // JSON round trip keeps it; a JSON WITHOUT the field still parses.+ let j = serde_json::to_string(&fp).unwrap();+ assert!(j.contains("\"polys\""));+ let back: Footprint = serde_json::from_str(&j).unwrap();+ assert_eq!(back.polys.len(), 1);+ let legacy: Footprint = serde_json::from_str(+ r#"{"name":"T","pads":[],"graphics":[]}"#).unwrap();+ assert!(legacy.polys.is_empty(), "legacy JSON without the field must load");+ // KiCad export re-emits the fp_poly with the vertex list+ let out = export_kicad_footprint(&fp);+ assert!(out.contains("fp_poly") && out.contains("(xy -2.6 -2.47)"), "{out}");+ }+ #[test] fn model_checksum_matches_altium_and_the_vendor_fixture() { // Tiny hand-computed case: [73, 2, 3] -> 73*1 + 2*1 + 3*2 = 81.@@ -87,8 +115,8 @@ mod tests { use std::io::Read; let pad = Pad { number: "1".into(), x_mm: 0.0, y_mm: 0.0, w_mm: 1.0, h_mm: 1.0, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None };- let fp1 = Footprint { name: "WITHMODEL".into(), pads: vec![pad.clone()], graphics: vec![] };- let fp2 = Footprint { name: "BARE".into(), pads: vec![pad], graphics: vec![] };+ let fp1 = Footprint { name: "WITHMODEL".into(), pads: vec![pad.clone()], graphics: vec![], polys: vec![] };+ let fp2 = Footprint { name: "BARE".into(), pads: vec![pad], graphics: vec![], polys: vec![] }; let step = b"ISO-10303-21;\r\nHEADER;\r\nENDSEC;\r\nEND-ISO-10303-21;\r\n".to_vec(); let m = EmbeddedModel { name: "WITHMODEL.step".into(), step: step.clone(), rot_x: 0.0, rot_y: 0.0, rot_z: 0.0, dz_mil: 0.0, overall_height_mil: 0.0 };@@ -134,7 +162,7 @@ mod tests { let long = "JST_PH_B2B-PH-K_1x02_P2.00mm_Vertical"; let pad = Pad { number: "1".into(), x_mm: 0.0, y_mm: 0.0, w_mm: 1.0, h_mm: 1.0, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None };- let mk = |name: &str| Footprint { name: name.into(), pads: vec![pad.clone()], graphics: vec![] };+ let mk = |name: &str| Footprint { name: name.into(), pads: vec![pad.clone()], graphics: vec![], polys: vec![] }; let bytes = encode_pcblib_many(&[mk(long), mk(&format!("{long}2"))]).expect("encode"); let mut comp = cfb::CompoundFile::open(std::io::Cursor::new(bytes)).unwrap(); let tops: Vec<String> = comp.read_root_storage().filter(|e| e.is_storage())@@ -253,8 +281,8 @@ mod tests { b.pins.push(Pin { number: "3".into(), name: "C".into(), x: -100, y: -300, length: Some(100), rotation: 0 }); let sch = encode_schlib(&[a, b]).expect("encode schlib"); let pad = |n: &str, x: f64| Pad { number: n.into(), x_mm: x, y_mm: 0.0, w_mm: 1.0, h_mm: 1.0, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None };- let fpa = Footprint { name: "FPA".into(), pads: vec![pad("1", 0.0)], graphics: vec![] };- let fpb = Footprint { name: "FPB".into(), pads: vec![pad("1", 0.0), pad("2", 2.0)], graphics: vec![] };+ let fpa = Footprint { name: "FPA".into(), pads: vec![pad("1", 0.0)], graphics: vec![], polys: vec![] };+ let fpb = Footprint { name: "FPB".into(), pads: vec![pad("1", 0.0), pad("2", 2.0)], graphics: vec![], polys: vec![] }; let pcb = encode_pcblib_many(&[fpa, fpb]).expect("encode pcblib"); let pb = decode_altium(Some(&sch), Some(&pcb), "PARTB").expect("decode PARTB");@@ -285,6 +313,7 @@ mod tests { let fp = Footprint { name: "LAYERFP".into(), pads: vec![Pad { number: "1".into(), x_mm: 0.0, y_mm: 0.0, w_mm: 1.0, h_mm: 1.0, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None }],+ polys: vec![], graphics: vec![ line(FpLayer::Silk, 1.0), line(FpLayer::Fab, 2.0),@@ -375,6 +404,7 @@ mod tests { Pad { number: "2".into(), x_mm: 0.762, y_mm: 0.0, w_mm: 1.016, h_mm: 1.27, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None }, ], graphics: vec![],+ polys: vec![], }; let bytes = encode_pcblib(&fp).expect("encode"); let decoded = decode_pcblib(&bytes).expect("decode our own output");
crates/altium-codec/src/model.rs+22@@ -182,6 +182,28 @@ pub struct Footprint { /// layer-tagged so every EDA target lands them on the right layer. #[serde(default)] pub graphics: Vec<FpGraphic>,+ /// FILLED polygons (KiCad `fp_poly`), the shape 97 percent of the catalog+ /// draws its pin-1 marker with. Carried as its own OPTIONAL field rather+ /// than a new FpGraphic variant on purpose: FpGraphic is a serde-tagged+ /// enum in the on-disk adom-lbr JSON, and an unknown VARIANT fails the+ /// whole deserialization in every already-installed binary, while an+ /// unknown FIELD is skipped. Old binaries simply do not see polygons.+ #[serde(default, skip_serializing_if = "Vec::is_empty")]+ pub polys: Vec<FpPoly>,+}++/// A filled polygon on a footprint layer, vertices in mm, Y-down neutral.+#[derive(Debug, Clone, Serialize, Deserialize)]+pub struct FpPoly {+ pub layer: FpLayer,+ pub points: Vec<[f64; 2]>,+ /// Outline stroke width in mm (0 for purely filled shapes).+ #[serde(default)]+ pub width: f64,+ /// KiCad `(fill yes|solid)`. A pin-1 marker is filled; a fab outline is+ /// not, and painting it solid buries the footprint (adr4525's F.Fab).+ #[serde(default)]+ pub filled: bool, } /// Footprint layer role — format-neutral. Each EDA adapter maps it to its own
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.37.0",+ "version": "2.38.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.37.0",+ "version": "2.38.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" } ]-}+}
src/manage.rs+18@@ -733,6 +733,9 @@ fn footprint_svg(fp: &Footprint, names: &std::collections::HashMap<String, Strin FpGraphic::Circle { cx, cy, r, .. } | FpGraphic::Arc { cx, cy, r, .. } => { upd(cx - r, cy - r); upd(cx + r, cy + r); } } }+ for poly in &fp.polys {+ for pt in &poly.points { upd(pt[0], pt[1]); }+ } if a > c { return empty_svg("no footprint"); } let pad = 0.4; let (a, b, c, d) = (a - pad, b - pad, c + pad, d + pad);@@ -754,6 +757,21 @@ fn footprint_svg(fp: &Footprint, names: &std::collections::HashMap<String, Strin }; body += &s.replace("{}", layer_color(layer)); }+ // Filled polygons: the pin-1 marker shape (wiki #16). Rendered FILLED with+ // the layer colour, which is exactly what the .kicad_mod drew.+ for poly in &fp.polys {+ let pts: String = poly.points.iter()+ .map(|pt| format!("{:.3},{:.3}", pt[0], pt[1]))+ .collect::<Vec<_>>().join(" ");+ let col = layer_color(&poly.layer);+ if poly.filled {+ body += &format!("<polygon class='ly-{}' points='{}' fill='{}' stroke='none'/>",+ layer_key(&poly.layer), pts, col);+ } else {+ body += &format!("<polygon class='ly-{}' points='{}' fill='none' stroke='{}' stroke-width='{:.3}'/>",+ layer_key(&poly.layer), pts, col, poly.width.max(0.05));+ }+ } for p in &fp.pads { let col = layer_color(&FpLayer::Copper); let rx = if p.shape == "circle" || p.shape == "roundrect" { p.w_mm.min(p.h_mm) * 0.25 } else { 0.0 };