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.
import-altium: select the named component from a multi-component library (#21) #5
Fixes #21. Base: 2.34.0 (wiki 02a48d93 / GH 0d45d9e). Surgical, decode-path only: no generate/assemble output changes, so no git-wiki re-vendor is needed.
What
import-altium --mpn now SELECTS the named component from a multi-component
.SchLib/.PcbLib instead of always returning the first storage:
decode_schlib_symboltakes the selector: case-insensitive match on the component storage name (which the encoder keeps equal to LibReference). An empty selector keeps the historical first-component behaviour; an unknown name fails listing every component the library holds, never silently wrong.- The footprint now follows the CHOSEN symbol's model link (RECORD=45 ModelName) rather than taking the first footprint storage. Matching is on the footprint's FULL name from its data stream first (a >31-char name is truncated in the OLE storage name but intact in the stream), then the storage name; a link the PcbLib lacks falls back to the first footprint rather than failing the part.
Proof
New unit test multi_component_decode_selects_by_name pins: named selection,
model-link following, case-insensitivity, empty-selector compatibility, and the
available-components error. All 17 altium-codec tests pass.
Against the real 7-part Cloud Library bundle (the #21 repro):
TPS73733DCQR: pins=6 footprint=SOT-223-6 pads=6
QCPL-7847-500E: pins=8 footprint=DIP-8_W7.62mm pads=8
AT7456E: pins=28 footprint=TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x6.7mm pads=35
TX6143: pins=5 footprint=SOT-89-5 pads=5
--mpn NOPE -> ERROR: SchLib has no component named "NOPE"; available: AT7456E, CN3125, QCPL-7847-500E, TP4056, TPS73733DCQR, TX4140, TX6143
Before the fix every one of those returned the identical wrong part (28 pins, DIP-8).
Diff Skip to comments
@@ -1,548 +1,594 @@⋯ 24 unchanged lines ⋯ /// `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![],⋯ 6 unchanged lines ⋯ 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)⋯ 106 unchanged lines ⋯ // 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();⋯ 204 unchanged lines ⋯ // 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")⋯ 3 unchanged lines ⋯ }) .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>⋯ 151 unchanged lines ⋯ Some(FpGraphic::Arc { layer, cx, cy, r, start: -end, end: -start, width }) } }+@@ -1,277 +1,316 @@⋯ 160 unchanged lines ⋯ 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.⋯ 108 unchanged lines ⋯ assert!((w - 1.016).abs() < 1e-3, "w was {w}"); } }+
Comments
Log in to comment.
No comments yet.