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 + footp
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
//! altium-codec — encode + decode Altium `.SchLib` (symbols) and `.PcbLib`
//! (footprints) from a neutral part model.
//!
//! This is the Altium half of the universal EDA interchange writer: the
//! canonical JSON maps onto [`model`], the encoders emit native Altium binary
//! with **no Altium at runtime**, and the decoders (thin wrappers over the
//! validated `altium-schlib` / `altium-pcblib` readers) let every encode be
//! round-trip checked.
pub mod altium_decode;
pub mod intlib;
pub mod kicad;
pub mod kicad_export;
pub mod model;
pub mod pcblib;
pub mod schlib;
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_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};
pub use schlib::{add_symbols_to_schlib, decode_schlib_first, encode_schlib};
#[cfg(test)]
mod tests {
use super::*;
fn two_pin_symbol() -> Symbol {
Symbol {
name: "REF2".into(),
designator_prefix: Some("U".into()),
footprint: None,
body: None,
graphics: vec![],
parameters: vec![],
pin_numbers_hidden: None,
pin_names_hidden: None,
// pins in mils (connection point); length 100, angle 0 → Altium
// stores the body end = -100 + 100 = 0.
pins: vec![
Pin { number: "1".into(), name: "A".into(), x: -100, y: 100, length: Some(100), rotation: 0 },
Pin { number: "2".into(), name: "B".into(), x: -100, y: -100, length: Some(100), rotation: 0 },
],
}
}
#[test]
fn schlib_roundtrip_pins() {
let sym = two_pin_symbol();
let bytes = encode_schlib(&[sym.clone()]).expect("encode");
let decoded = decode_schlib_first(&bytes).expect("decode our own output");
assert_eq!(decoded["module_name"], "REF2");
assert_eq!(decoded["pin_count"], 2);
let pins = decoded["pins"].as_array().unwrap();
assert_eq!(pins[0]["name"], "A");
assert_eq!(pins[0]["number"], "1");
assert_eq!(pins[1]["name"], "B");
assert_eq!(pins[1]["number"], "2");
// body-end X = (-100 + 100) mil * 25.6 = 0; Y = 100 mil / 10 = 10
assert_eq!(pins[0]["x"], 0);
assert_eq!(pins[0]["y"], 10);
}
/// 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.
#[test]
fn layer_roundtrip_house_convention() {
use model::{FpGraphic, FpLayer};
let line = |layer: FpLayer, y: f64| FpGraphic::Line { layer, x1: -1.0, y1: y, x2: 1.0, y2: y, width: 0.1 };
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 }],
graphics: vec![
line(FpLayer::Silk, 1.0),
line(FpLayer::Fab, 2.0),
line(FpLayer::Courtyard, 3.0),
line(FpLayer::Copper, 4.0),
],
};
let bytes = encode_pcblib(&fp).expect("encode");
let part = decode_altium(None, Some(&bytes), "LAYERFP").expect("decode");
let layer_of = |y_want: f64| {
part.footprint.graphics.iter().find_map(|g| match g {
model::FpGraphic::Line { layer, y1, .. } if (y1 - y_want).abs() < 1e-3 => Some(layer.clone()),
_ => None,
})
};
assert!(matches!(layer_of(1.0), Some(FpLayer::Fab)), "silk merges onto Mech13 → decodes as Fab (house convention)");
assert!(matches!(layer_of(2.0), Some(FpLayer::Fab)), "fab round-trips");
assert!(matches!(layer_of(3.0), Some(FpLayer::Courtyard)), "courtyard round-trips");
assert!(matches!(layer_of(4.0), Some(FpLayer::Copper)), "copper round-trips");
}
/// The `|KEY=VALUE` text records have no escape mechanism — encoding must
/// REFUSE values that would desync the stream, not silently corrupt it.
#[test]
fn schlib_rejects_corrupting_inputs() {
// '|' in a parameter value
let mut s = two_pin_symbol();
s.parameters.push(Parameter { name: "Value".into(), value: "10k|1%".into(), visible: true });
assert!(encode_schlib(&[s]).is_err(), "pipe in parameter value must be rejected");
// '|' in the symbol name
let mut s = two_pin_symbol();
s.name = "BAD|NAME".into();
assert!(encode_schlib(&[s]).is_err(), "pipe in symbol name must be rejected");
// pin longer than the u16 length field can hold (max 2559 mil)
let mut s = two_pin_symbol();
s.pins[0].length = Some(3000);
assert!(encode_schlib(&[s]).is_err(), "oversize pin length must be rejected");
}
/// Storage name, FileHeader LibRef, and RECORD=1 LibReference must agree —
/// and a name like "LM358.A" must keep its dot (CFBF allows it).
#[test]
fn schlib_dotted_name_stays_resolvable() {
let mut s = two_pin_symbol();
s.name = "LM358.A".into();
let bytes = encode_schlib(&[s]).expect("encode");
let decoded = decode_schlib_first(&bytes).expect("decode");
assert_eq!(decoded["module_name"], "LM358.A");
// merging the same symbol again is a no-op (dedup on the storage name)
let mut again = two_pin_symbol();
again.name = "LM358.A".into();
let merged = add_symbols_to_schlib(&bytes, &[again]).expect("merge");
assert_eq!(merged, bytes, "re-adding an existing symbol must not change the library");
}
/// NPTH mounting holes must survive KiCad import → export: keep the drill
/// (not become an SMD copper pad) and stay `np_thru_hole`.
#[test]
fn npth_pad_roundtrips_through_kicad() {
let sym = "(kicad_symbol_lib (symbol \"M3\" (pin passive line (at 0 0 0) (length 2.54) (name \"1\") (number \"1\"))))";
let fpt = concat!(
"(footprint \"MountM3\" (layer \"F.Cu\")\n",
" (pad \"\" np_thru_hole circle (at 0 0) (size 3.2 3.2) (drill 3.2) (layers \"*.Cu\" \"*.Mask\"))\n",
" (pad \"1\" thru_hole circle (at 5 0) (size 1.7 1.7) (drill oval 1.0 1.6) (layers \"*.Cu\" \"*.Mask\"))\n",
")"
);
let part = import_kicad(sym, fpt, "M3").expect("import");
let npth = &part.footprint.pads[0];
assert!(npth.drill_mm > 3.1, "NPTH drill preserved, got {}", npth.drill_mm);
assert_eq!(npth.plated, Some(false));
let th = &part.footprint.pads[1];
assert_eq!(th.plated, Some(true));
assert!((th.drill_mm - 1.6).abs() < 1e-9, "slot drill takes the larger axis, got {}", th.drill_mm);
let out = export_kicad_footprint(&part.footprint);
assert!(out.contains("np_thru_hole"), "export keeps np_thru_hole:\n{out}");
assert!(out.contains("(drill 3.2)"), "export keeps the NPTH drill:\n{out}");
}
#[test]
fn pcblib_roundtrip_pads() {
let fp = Footprint {
name: "REFFP".into(),
pads: vec![
Pad { number: "1".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 },
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![],
};
let bytes = encode_pcblib(&fp).expect("encode");
let decoded = decode_pcblib(&bytes).expect("decode our own output");
assert_eq!(decoded["pad_count"], 2);
let pads = decoded["pads"].as_array().unwrap();
assert_eq!(pads[0]["number"], "1");
// -0.762 mm in, -0.762 mm back (within rounding)
let x = pads[0]["at"][0].as_f64().unwrap();
assert!((x - (-0.762)).abs() < 1e-3, "x was {x}");
let w = pads[0]["size"][0].as_f64().unwrap();
assert!((w - 1.016).abs() < 1e-3, "w was {w}");
}
}