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
Merge PR #73: import-altium: select the named component from a multi-component library (#21)
2 files changed
+95−12
crates/altium-codec/src/altium_decode.rs+57−12@@ -25,7 +25,9 @@ const UNITS_PER_MIL: f64 = 25.6; /// `None` (e.g. footprint-only), but at least one is required. pub fn decode_altium(schlib: Option<&[u8]>, pcblib: Option<&[u8]>, mpn: &str) -> Result<AdomLbrPart> { let symbol = match schlib {- Some(b) => decode_schlib_symbol(b)?,+ // `mpn` selects the component in a MULTI-component library; empty keeps+ // the first (the historical single-part behaviour).+ Some(b) => decode_schlib_symbol(b, mpn)?, None => Symbol { name: mpn.to_string(), pins: vec![],@@ -38,8 +40,12 @@ pub fn decode_altium(schlib: Option<&[u8]>, pcblib: Option<&[u8]>, mpn: &str) -> pin_names_hidden: None, }, };+ // The chosen symbol names the footprint it links to (RECORD=45 ModelName);+ // pick THAT footprint out of the PcbLib, not just the first storage, so a+ // multi-footprint library returns the pad set that belongs to this part.+ let want_fp = symbol.footprint.clone(); let footprint = match pcblib {- Some(b) => decode_pcblib_footprint(b)?,+ Some(b) => decode_pcblib_footprint(b, want_fp.as_deref())?, None => Footprint { name: mpn.to_string(), pads: vec![], graphics: vec![] }, }; build_part(symbol, footprint, mpn)@@ -152,18 +158,34 @@ fn field<'a>(txt: &'a str, key: &str) -> Option<&'a str> { // SchLib → Symbol // --------------------------------------------------------------------------- -fn decode_schlib_symbol(bytes: &[u8]) -> Result<Symbol> {+fn decode_schlib_symbol(bytes: &[u8], want: &str) -> Result<Symbol> { let streams = read_streams(bytes)?;- // first component storage's Data (skip housekeeping streams)- let (name, data) = streams+ // Every component storage's Data (skip housekeeping streams). The storage+ // name equals the component's LibReference (the encoder keeps them in sync),+ // so it is what `want` (an MPN / component name) is matched against.+ let comps: Vec<(String, &Vec<u8>)> = streams .iter() .filter(|(p, _)| p.ends_with("/Data") && !p.contains("FileHeader") && !p.contains("Storage")) .map(|(p, d)| { let comp = p.trim_start_matches('/').split('/').next().unwrap_or("SYM").to_string();- (comp, d.clone())+ (comp, d) })- .next()- .context("SchLib has no component Data stream")?;+ .collect();+ // Empty selector keeps the first component; a named selector picks that+ // component (case-insensitive) or fails naming what IS in the library, so a+ // multi-component library never silently returns the wrong part.+ let (name, data) = if want.is_empty() {+ let (n, d) = comps.into_iter().next().context("SchLib has no component Data stream")?;+ (n, d.clone())+ } else {+ match comps.iter().find(|(n, _)| n.eq_ignore_ascii_case(want)) {+ Some((n, d)) => (n.clone(), (*d).clone()),+ None => {+ let avail: Vec<&str> = comps.iter().map(|(n, _)| n.as_str()).collect();+ anyhow::bail!("SchLib has no component named {want:?}; available: {}", avail.join(", "));+ }+ }+ }; let mut pins = Vec::new(); let mut names_shown: Vec<bool> = Vec::new();@@ -374,9 +396,9 @@ fn decode_sym_graphic(txt: &str, rec: &str) -> Option<Graphic> { // PcbLib → Footprint (pads + silk/courtyard/fab graphics) // --------------------------------------------------------------------------- -fn decode_pcblib_footprint(bytes: &[u8]) -> Result<Footprint> {+fn decode_pcblib_footprint(bytes: &[u8], want: Option<&str>) -> Result<Footprint> { let streams = read_streams(bytes)?;- let (name, data) = streams+ let comps: Vec<(String, &Vec<u8>)> = streams .iter() .filter(|(p, _)| { p.ends_with("/Data")@@ -386,9 +408,32 @@ fn decode_pcblib_footprint(bytes: &[u8]) -> Result<Footprint> { }) .map(|(p, d)| { let comp = p.trim_start_matches('/').split('/').next().unwrap_or("FP").to_string();- (comp, d.clone())+ (comp, d) })- .next()+ .collect();+ // A footprint's FULL name lives in its data stream (a >31-char name is+ // truncated in the OLE storage name but intact here), so match the symbol's+ // linked footprint on the full name first, then the storage name. A symbol+ // may link a footprint the PcbLib lacks, so fall back to the first storage+ // rather than failing the whole part.+ let full_name = |data: &[u8]| -> String {+ let name_len = le_u32(data, 0) as usize;+ let blk = &data[4..(4 + name_len).min(data.len())];+ if blk.is_empty() {+ return String::new();+ }+ let n = blk[0] as usize;+ String::from_utf8_lossy(&blk[1..(1 + n).min(blk.len())]).to_string()+ };+ let picked = match want.filter(|w| !w.is_empty()) {+ Some(w) => comps+ .iter()+ .find(|(comp, d)| full_name(d).eq_ignore_ascii_case(w) || comp.eq_ignore_ascii_case(w))+ .or_else(|| comps.first()),+ None => comps.first(),+ };+ let (name, data) = picked+ .map(|(n, d)| (n.clone(), (*d).clone())) .context("PcbLib has no component Data stream")?; // component name block: <u32 len><name>
crates/altium-codec/src/lib.rs+38@@ -161,6 +161,44 @@ mod tests { assert_eq!(pins[0]["y"], 10); } + /// Multi-component selection (adom-lbr#21): `mpn` picks the NAMED component+ /// out of a multi-part library, and the footprint follows that SYMBOL's+ /// model link, never just the first storage. An unknown name fails naming+ /// what IS in the library; an empty selector keeps the historical+ /// first-component behaviour.+ #[test]+ fn multi_component_decode_selects_by_name() {+ let mut a = two_pin_symbol();+ a.name = "PARTA".into();+ a.footprint = Some("FPA".into());+ let mut b = two_pin_symbol();+ b.name = "PARTB".into();+ b.footprint = Some("FPB".into());+ 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 pcb = encode_pcblib_many(&[fpa, fpb]).expect("encode pcblib");++ let pb = decode_altium(Some(&sch), Some(&pcb), "PARTB").expect("decode PARTB");+ assert_eq!(pb.symbol.name, "PARTB");+ assert_eq!(pb.symbol.pins.len(), 3);+ assert_eq!(pb.footprint.name, "FPB", "footprint follows the symbol's model link");+ assert_eq!(pb.footprint.pads.len(), 2);++ let pa = decode_altium(Some(&sch), Some(&pcb), "parta").expect("selection is case-insensitive");+ assert_eq!(pa.symbol.pins.len(), 2);+ assert_eq!(pa.footprint.name, "FPA");+ assert_eq!(pa.footprint.pads.len(), 1);++ let first = decode_altium(Some(&sch), Some(&pcb), "").expect("empty selector keeps first");+ assert_eq!(first.symbol.name, "PARTA");++ let err = decode_altium(Some(&sch), Some(&pcb), "NOPE").unwrap_err().to_string();+ assert!(err.contains("PARTA") && err.contains("PARTB"), "error lists available components: {err}");+ }+ /// Pins the house layer convention: Silk and Fab both encode onto Mech13 /// (69) and decode back as Fab (deliberately lossy — see /// `pcblib::layer_enc`); Courtyard and Copper round-trip exactly.