← Commit history

2.10.1: import-kicad honors pinmap.json / --pin-map and warns on dropped pins (adom-lbr#9)

John Lauer ·df8ccca21c ·1mo ago ·parent 41c7f3e
9 files changed +84−32
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-lbr"-version = "2.10.0"+version = "2.10.1" dependencies = [  "altium-codec",  "anyhow",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.10.0"+version = "2.10.1" 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." 
SKILL.md+2−2
@@ -122,8 +122,8 @@ bottom-left, buses contiguous, every name horizontal. `keep` preserves the .kicad_sym placement verbatim; `two-column` forces the re-layout.  Duplicate PAD names (KiCad same-net arrays: thermal vias, ground paddles,-shield tabs) are resolved to the EAGLE `@`-suffix convention — `17`, `17@2`,-`17@3`, ... — with one space-separated multi-pad `<connect>` binding the whole+shield tabs) are resolved to the EAGLE `@`-suffix convention (`17`, `17@2`,+`17@3`, ...) with one space-separated multi-pad `<connect>` binding the whole group to its pin, so the electrical meaning is preserved. Fusion refuses the entire library over a raw duplicate. 
VERSION+1−1
@@ -1 +1 @@-2.10.0+2.10.1
crates/altium-codec/src/kicad.rs+28−12
@@ -471,26 +471,42 @@ fn import_footprint_graphics(root: &Sx) -> Vec<FpGraphic> { /// link is identity-mapped (pin number == pad number) where they coincide, and /// the symbol is given the footprint name so the Altium writer links them. pub fn import_kicad(sym_text: &str, mod_text: &str, mpn: &str) -> Result<AdomLbrPart> {+    import_kicad_with_pin_map(sym_text, mod_text, mpn, &Default::default()).map(|(part, _)| part)+}++/// Like [`import_kicad`], with an explicit pin-number → pad-name map for parts+/// where the KiCad convention (pin NUMBER binds pad NAME) does not hold, e.g. a+/// USB-C shield pin "S1" against a pad "SH". Also returns the pin numbers that+/// still matched no pad — the caller must surface those instead of letting a+/// pin vanish from `connect[]` silently (adom/adom-lbr#9: a shield that+/// connects to nothing only shows up in hardware).+pub fn import_kicad_with_pin_map(+    sym_text: &str,+    mod_text: &str,+    mpn: &str,+    pin_map: &std::collections::HashMap<String, String>,+) -> Result<(AdomLbrPart, Vec<String>)> {     let mut symbol = import_symbol(sym_text, mpn)?;     let footprint = import_footprint(mod_text)?;     symbol.footprint = Some(footprint.name.clone());     // Center the symbol on the origin (KiCad sources are often drawn off-origin).     symbol.center(); -    // identity connect map over shared designators+    // identity connect map over shared designators, pin_map as the escape hatch     let pad_nums: std::collections::HashSet<&str> =         footprint.pads.iter().map(|p| p.number.as_str()).collect();-    let connect = symbol-        .pins-        .iter()-        .filter(|p| pad_nums.contains(p.number.as_str()))-        .map(|p| ConnectEntry {-            pin: p.number.clone(),-            pad: p.number.clone(),-        })-        .collect();+    let mut connect = Vec::new();+    let mut dropped = Vec::new();+    for p in &symbol.pins {+        let pad = pin_map.get(&p.number).unwrap_or(&p.number);+        if pad_nums.contains(pad.as_str()) {+            connect.push(ConnectEntry { pin: p.number.clone(), pad: pad.clone() });+        } else {+            dropped.push(p.number.clone());+        }+    } -    Ok(AdomLbrPart {+    Ok((AdomLbrPart {         schema_version: 2,         mpn: mpn.to_string(),         manufacturer: String::new(),@@ -502,7 +518,7 @@ pub fn import_kicad(sym_text: &str, mod_text: &str, mpn: &str) -> Result<AdomLbr         connect,         model_3d: None,         provenance: Some(serde_json::json!({"source":"kicad_import"})),-    })+    }, dropped)) }  #[cfg(test)]
crates/altium-codec/src/lib.rs+1−1
@@ -18,7 +18,7 @@ pub mod sexpr;  pub use altium_decode::{decode_altium, decode_intlib_part}; pub use intlib::{decode_intlib, encode_intlib};-pub use kicad::import_kicad;+pub use kicad::{import_kicad, import_kicad_with_pin_map}; pub use kicad_export::{export_kicad_footprint, export_kicad_symbol}; pub use model::{AdomLbrPart, BodyRect, ConnectEntry, Footprint, FpGraphic, FpLayer, Graphic, Pad, Parameter, Pin, Symbol}; pub use pcblib::{decode_pcblib, encode_pcblib};
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "2.10.0",+  "version": "2.10.1",   "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+9−11
@@ -1,8 +1,8 @@ {   "slug": "adom-lbr",-  "version": "2.10.0",+  "version": "2.10.1",   "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",@@ -18,7 +18,7 @@   ],   "hero": {     "type": "image",-    "path": "screenshots/hero.png"+    "path": "screenshots/hero-v2.png"   },   "scripts": {     "install": "./install.sh",@@ -52,23 +52,21 @@   ],   "files": [     "bin/**",-    "assets/**",     "SKILL.md",-    "skills/**",     "README.md",     "install.sh",     "uninstall.sh",     "screenshots/**"   ],-  "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",@@ -76,15 +74,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/main.rs+40−2
@@ -181,6 +181,11 @@ enum Commands {         /// Output adom-lbr JSON path (default: <mpn>.adom-lbr.json)         #[arg(long)]         out: Option<PathBuf>,+        /// JSON map from SYMBOL PIN NUMBER to FOOTPRINT PAD NAME, for parts+        /// where the two disagree (a USB-C shield pin "S1" against a pad "SH").+        /// Defaults to `pinmap.json` beside the symbol when that file exists.+        #[arg(long)]+        pin_map: Option<PathBuf>,     },     /// Convert an adom-lbr JSON into native Altium .SchLib + .PcbLib (no Altium needed).     ExportAltium {@@ -2020,11 +2025,44 @@ fn main() {             }         } -        Commands::ImportKicad { sym, fp, mpn, out } => {+        Commands::ImportKicad { sym, fp, mpn, out, pin_map } => {             let sym_txt = read_or_exit(&sym);             let fp_txt = read_or_exit(&fp);-            let part = altium_codec::import_kicad(&sym_txt, &fp_txt, &mpn)+            // Same pin-map convention as generate: explicit --pin-map wins, else+            // a pinmap.json sidecar beside the symbol just works.+            let pin_map_path = pin_map.or_else(|| {+                sym.parent().map(|d| d.join("pinmap.json")).filter(|p| p.exists())+            });+            let pin_map: std::collections::HashMap<String, String> = match &pin_map_path {+                Some(path) => serde_json::from_str(&read_or_exit(path)).unwrap_or_else(|e| {+                    eprintln!("ERROR: {} is not a JSON object of \"pin number\": \"pad name\": {}", path.display(), e);+                    process::exit(1);+                }),+                None => Default::default(),+            };+            if !pin_map.is_empty() {+                println!("HINT: pin map applied ({} entries) from {}", pin_map.len(),+                    pin_map_path.as_ref().map(|p| p.display().to_string()).unwrap_or_default());+            }+            let (part, dropped) = altium_codec::import_kicad_with_pin_map(&sym_txt, &fp_txt, &mpn, &pin_map)                 .unwrap_or_else(|e| { eprintln!("ERROR: {e:#}"); process::exit(1); });+            if !dropped.is_empty() {+                // A pin that matches no pad is a broken netlist in EVERY export+                // target — say so loudly instead of letting it vanish (adom-lbr#9).+                let referenced: std::collections::HashSet<&str> =+                    part.connect.iter().map(|c| c.pad.as_str()).collect();+                let mut unref: Vec<&str> = part.footprint.pads.iter()+                    .map(|p| p.number.as_str())+                    .filter(|n| !n.is_empty() && !referenced.contains(n))+                    .collect();+                unref.sort();+                unref.dedup();+                eprintln!("WARN: {} symbol pin(s) match no footprint pad and were left out of connect[]: {}",+                    dropped.len(), dropped.join(", "));+                if !unref.is_empty() {+                    eprintln!("      unreferenced pads in footprint: {} — if one of these is the intended target, supply --pin-map (or a pinmap.json sidecar)", unref.join(", "));+                }+            }             let outp = out.unwrap_or_else(|| PathBuf::from(format!("{mpn}.adom-lbr.json")));             fs::write(&outp, serde_json::to_string_pretty(&part).unwrap()).ok();             println!("OK: wrote {} (adom-lbr JSON — the canonical hub)", outp.display());