← Commit history
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"     }   ]-}\ No newline at end of file+}
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 };