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
2.20.0: generate --alt-sym, alternate symbols as extra gates of one deviceset
7 files changed
+192−76
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.19.0"+version = "2.20.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.19.0"+version = "2.20.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."
SKILL.md+23@@ -134,6 +134,29 @@ two columns: inputs left, outputs and bidirectional right, power grouped bottom-left, buses contiguous, every name horizontal. `keep` preserves the .kicad_sym placement verbatim; `two-column` forces the re-layout. +`--alt-sym path[=GATE[=art.svg]]` (repeatable, 2.20.0) adds ALTERNATE SYMBOLS of the+SAME part as extra GATES of the one deviceset: one component in Fusion's Content+Manager with several symbols under it, one footprint, one 3D package (John,+2026-08-29: "one chip called CH340G and it has 10 symbols under it, then 1+footprint, and 1 3d chip"), instead of N look-alike components. Every gate gets+its own `<connect>` set to the same pads; the primary symbol is the gate placed on+ADD (`addlevel="next"`), the alternates are `addlevel="can"` (INVOKE picks one) and+sit side by side in the deviceset so the component preview stays readable. GATE+defaults to the alternate's symbol name with `<primary>-` stripped+(`CH340G-SQ.kicad_sym` becomes gate `SQ`); reusing the primary .kicad_sym under a+new GATE renames the emitted symbol `<name>-<GATE>` (that is how `=CHIP=outline.svg`+makes the chip-outline twin of the standard symbol). An alternate must have the+same pin count as the primary. Proven live in Fusion 2026-08-29: CH340G with ten+gates (std, 2C, SQ, 2CG, SQG and their chip-outline twins) imported, bound to one+STEP and proved by fusion-bridge 1.9.273.++```+adom-lbr generate --sym CH340G.kicad_sym --fp CH340G.kicad_mod --part-metadata page.json \+ --alt-sym "CH340G.kicad_sym=CHIP=CH340G-3d-outline-bold.svg" \+ --alt-sym CH340G-SQ.kicad_sym --alt-sym "CH340G-SQ.kicad_sym=SQ-CHIP=CH340G-3d-outline-bold.svg" \+ --alt-sym CH340G-2CG.kicad_sym --alt-sym CH340G-SQG.kicad_sym -o CH340G.lbr+```+ 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
VERSION+1−1@@ -1 +1 @@-2.19.0+2.20.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.19.0",+ "version": "2.20.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": {},
src/main.rs+165−72@@ -99,6 +99,18 @@ enum Commands { /// verbatim. #[arg(long, default_value = "auto")] symbol_layout: String,+ /// ALTERNATE SYMBOLS for the SAME part: each becomes one more GATE of the one+ /// deviceset, on the one package, with its own connects, so Fusion shows ONE+ /// component with several symbols under it (John, 2026-08-29: "one chip called+ /// CH340G and it has 10 symbols under it, then 1 footprint, and 1 3d chip") instead+ /// of ten look-alike components. Repeatable. Form: `path[=GATE[=art.svg]]`.+ /// GATE defaults to the symbol's name with the primary name and a dash stripped+ /// (`CH340G-SQ.kicad_sym` -> gate `SQ`); when the same .kicad_sym is reused under a+ /// new GATE the emitted symbol is renamed `<name>-<GATE>`. An `art.svg` is that+ /// gate's own --symbol-art. Gates are addlevel `can`: the primary symbol places on+ /// ADD, the others are one INVOKE away.+ #[arg(long = "alt-sym")]+ alt_sym: Vec<String>, }, /// Lint an existing .lbr file for Fusion 360 compatibility Lint {@@ -1310,7 +1322,89 @@ fn part_attributes(path: &std::path::Path) -> Vec<(String, String)> { out } +/// One extra gate of the deviceset: an alternate symbol for the same part.+pub(crate) struct AltGate {+ pub sym: ParsedSymbol,+ pub gate: String,+ pub art: Option<SymbolArt>,+}++/// The `<symbol>` block of ONE symbol: body, chip drawing, pins, group labels.+/// Returns the pin names as emitted (deduplicated); the connects must reuse them.+fn emit_symbol_block(xml: &mut String, sym: &ParsedSymbol, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64) -> Vec<String> {+ xml.push_str(&format!("<symbol name=\"{}\">\n", esc_xml(&sym.name)));++ if let Some(body) = &sym.body {+ xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.top, body.right, body.top));+ xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.right, body.top, body.right, body.bottom));+ xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.right, body.bottom, body.left, body.bottom));+ xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.bottom, body.left, body.top));+ }++ // The chip drawing. A `--symbol-art` SVG is an explicit OVERRIDE; otherwise+ // transcribe the `.kicad_sym`'s OWN (polyline)/(arc) graphics as raw layer-94+ // wires (KiCad symbol space is Y-up, same as the pins → carried through 1:1).+ if let Some(art) = symbol_art {+ let (wires, _) = symbol_art_block(art, sym.body.as_ref(), symbol_art_scale);+ xml.push_str(&wires);+ } else {+ for g in &sym.graphics {+ match g {+ SymGraphic::Poly(pts) => {+ for w in pts.windows(2) {+ xml.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+ }+ }+ SymGraphic::Arc { start, mid, end } => {+ let pts = flatten_arc(*start, *mid, *end, 12);+ for w in pts.windows(2) {+ xml.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));+ }+ }+ }+ }+ }++ let name_y = sym.body.as_ref().map(|b| b.top + 2.54).unwrap_or(10.0);+ let value_y = sym.body.as_ref().map(|b| b.bottom - 2.54).unwrap_or(-10.0);+ xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"95\" align=\"bottom-center\">>NAME</text>\n", name_y));+ xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"96\" align=\"top-center\">>VALUE</text>\n", value_y));++ let deduped_names = dedup_pin_names(&sym.pins);+ for (i, pin) in sym.pins.iter().enumerate() {+ xml.push_str(&format!(+ " <pin name=\"{}\" x=\"{}\" y=\"{}\" length=\"{}\" direction=\"{}\"{}/>\n",+ esc_xml(&deduped_names[i]), pin.x, pin.y,+ pin_length_keyword(pin.length),+ pin_direction(&pin.pin_type),+ pin_rotation(pin.angle)+ ));+ }++ let body_center_x = sym.body.as_ref().map(|b| (b.left + b.right) / 2.0).unwrap_or(0.0);+ let body_left = sym.body.as_ref().map(|b| b.left).unwrap_or(-10.0);+ let body_right = sym.body.as_ref().map(|b| b.right).unwrap_or(10.0);+ for label in &sym.group_labels {+ let (lx, align) = if label.x > body_center_x {+ (body_right - 1.0, " align=\"right\"")+ } else {+ (body_left + 1.0, "")+ };+ xml.push_str(&format!(+ " <text x=\"{}\" y=\"{}\" size=\"0.762\" layer=\"94\" font=\"vector\" ratio=\"15\"{}>{}</text>\n",+ lx, label.y, align, esc_xml(&label.text)+ ));+ }++ xml.push_str("</symbol>\n");+ deduped_names+}+ pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)]) -> String {+ generate_lbr_gates(sym, fp_content, symbol_art, symbol_art_scale, pin_map, part_attrs, &[])+}++pub(crate) fn generate_lbr_gates(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, pin_map: &std::collections::HashMap<String, String>, part_attrs: &[(String, String)], alts: &[AltGate]) -> String { let mut xml = String::with_capacity(8192); xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -1429,71 +1523,10 @@ pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_ // Symbols section xml.push_str("<symbols>\n");- xml.push_str(&format!("<symbol name=\"{}\">\n", esc_xml(&sym.name)));-- if let Some(body) = &sym.body {- xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.top, body.right, body.top));- xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.right, body.top, body.right, body.bottom));- xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.right, body.bottom, body.left, body.bottom));- xml.push_str(&format!(" <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.bottom, body.left, body.top));- }-- // The chip drawing. A `--symbol-art` SVG is an explicit OVERRIDE; otherwise- // transcribe the `.kicad_sym`'s OWN (polyline)/(arc) graphics as raw layer-94- // wires (KiCad symbol space is Y-up, same as the pins → carried through 1:1).- if let Some(art) = symbol_art {- let (wires, _) = symbol_art_block(art, sym.body.as_ref(), symbol_art_scale);- xml.push_str(&wires);- } else {- for g in &sym.graphics {- match g {- SymGraphic::Poly(pts) => {- for w in pts.windows(2) {- xml.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));- }- }- SymGraphic::Arc { start, mid, end } => {- let pts = flatten_arc(*start, *mid, *end, 12);- for w in pts.windows(2) {- xml.push_str(&format!(" <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1524\" layer=\"94\"/>\n", w[0].0, w[0].1, w[1].0, w[1].1));- }- }- }- }- }-- let name_y = sym.body.as_ref().map(|b| b.top + 2.54).unwrap_or(10.0);- let value_y = sym.body.as_ref().map(|b| b.bottom - 2.54).unwrap_or(-10.0);- xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"95\" align=\"bottom-center\">>NAME</text>\n", name_y));- xml.push_str(&format!(" <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"96\" align=\"top-center\">>VALUE</text>\n", value_y));-- let deduped_names = dedup_pin_names(&sym.pins);- for (i, pin) in sym.pins.iter().enumerate() {- xml.push_str(&format!(- " <pin name=\"{}\" x=\"{}\" y=\"{}\" length=\"{}\" direction=\"{}\"{}/>\n",- esc_xml(&deduped_names[i]), pin.x, pin.y,- pin_length_keyword(pin.length),- pin_direction(&pin.pin_type),- pin_rotation(pin.angle)- ));- }-- let body_center_x = sym.body.as_ref().map(|b| (b.left + b.right) / 2.0).unwrap_or(0.0);- let body_left = sym.body.as_ref().map(|b| b.left).unwrap_or(-10.0);- let body_right = sym.body.as_ref().map(|b| b.right).unwrap_or(10.0);- for label in &sym.group_labels {- let (lx, align) = if label.x > body_center_x {- (body_right - 1.0, " align=\"right\"")- } else {- (body_left + 1.0, "")- };- xml.push_str(&format!(- " <text x=\"{}\" y=\"{}\" size=\"0.762\" layer=\"94\" font=\"vector\" ratio=\"15\"{}>{}</text>\n",- lx, label.y, align, esc_xml(&label.text)- ));- }-- xml.push_str("</symbol>\n");+ let deduped_names = emit_symbol_block(&mut xml, sym, symbol_art, symbol_art_scale);+ let alt_names: Vec<Vec<String>> = alts.iter()+ .map(|a| emit_symbol_block(&mut xml, &a.sym, a.art.as_ref(), symbol_art_scale))+ .collect(); xml.push_str("</symbols>\n"); // Deviceset with COMPLETE device (package + connects)@@ -1502,14 +1535,32 @@ pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_ xml.push_str(&format!("<deviceset name=\"{}\" prefix=\"{}\">\n", esc_xml(&sym.name), esc_xml(ref_prefix))); xml.push_str(&format!(" <description>{}</description>\n", esc_xml(&sym.description))); xml.push_str(" <gates>\n");- xml.push_str(&format!(" <gate name=\"G$1\" symbol=\"{}\" x=\"0\" y=\"0\"/>\n", esc_xml(&sym.name)));+ xml.push_str(&format!(" <gate name=\"G$1\" symbol=\"{}\" x=\"0\" y=\"0\"{}/>\n", esc_xml(&sym.name),+ if alts.is_empty() { "" } else { " addlevel=\"next\"" }));+ // Alternate gates sit SIDE BY SIDE in the deviceset (Fusion's component preview draws+ // every gate at its offset; ten gates at 0,0 were an unreadable pile on the first run).+ let width = |p: &ParsedSymbol| p.body.as_ref().map(|b| b.right - b.left).unwrap_or(20.0) + 2.0 * 7.62;+ let mut gx = width(sym) / 2.0;+ for a in alts {+ let w = width(&a.sym);+ gx += w / 2.0 + 5.08;+ let x = (gx / 2.54).round() * 2.54; // keep gate origins on the 0.1in grid+ xml.push_str(&format!(" <gate name=\"{}\" symbol=\"{}\" x=\"{:.2}\" y=\"0\" addlevel=\"can\"/>\n", esc_xml(&a.gate), esc_xml(&a.sym.name), x));+ gx += w / 2.0;+ } xml.push_str(" </gates>\n"); xml.push_str(" <devices>\n"); xml.push_str(&format!(" <device name=\"\" package=\"{}\">\n", esc_xml(&sym.name))); xml.push_str(" <connects>\n"); - // Generate connect mappings: gate pin name → pad number- for (i, pin) in sym.pins.iter().enumerate() {+ // Generate connect mappings: gate pin name → pad number, for the primary gate and+ // then for every alternate gate (same pads: they are the same physical pins).+ let mut gate_pins: Vec<(&str, &ParsedSymbol, &Vec<String>)> = vec![("G$1", sym, &deduped_names)];+ for (a, names) in alts.iter().zip(alt_names.iter()) {+ gate_pins.push((a.gate.as_str(), &a.sym, names));+ }+ for (gate_name, gsym, gnames) in gate_pins {+ for (i, pin) in gsym.pins.iter().enumerate() { // The pad reference is the symbol pin's NUMBER, which is the KiCad // convention and right for almost every part. A pin map is the escape // hatch for the ones where the footprint names that pad something else.@@ -1523,10 +1574,11 @@ pub(crate) fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_ None => pad.clone(), }; xml.push_str(&format!(- " <connect gate=\"G$1\" pin=\"{}\" pad=\"{}\"/>\n",- esc_xml(&deduped_names[i]), esc_xml(&pad_ref)+ " <connect gate=\"{}\" pin=\"{}\" pad=\"{}\"/>\n",+ esc_xml(gate_name), esc_xml(&gnames[i]), esc_xml(&pad_ref) )); }+ } xml.push_str(" </connects>\n"); // Attributes ride on <technology>, which is where Fusion's BOM export and the@@ -1887,7 +1939,7 @@ fn main() { let cli = Cli::parse(); match cli.command {- Commands::Generate { sym, fp, output, name, symbol_art, symbol_art_scale, pin_map, part_metadata, symbol_layout } => {+ Commands::Generate { sym, fp, output, name, symbol_art, symbol_art_scale, pin_map, part_metadata, symbol_layout, alt_sym } => { let fp_content = fp.as_ref().map(|p| { fs::read_to_string(p).unwrap_or_else(|e| { eprintln!("ERROR: Cannot read footprint {}: {}", p.display(), e);@@ -1986,7 +2038,48 @@ The geometry is fine but a BOM cannot order this part - pass --part-metadata or } } - let lbr = generate_lbr(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map, &part_attrs);+ // Alternate gates: `path[=GATE[=art]]`, each one more symbol of THIS part.+ let mut alts: Vec<AltGate> = Vec::new();+ for spec in &alt_sym {+ let mut it = spec.splitn(3, '=');+ let path = PathBuf::from(it.next().unwrap_or_default());+ let gate_opt = it.next().filter(|g| !g.is_empty()).map(|g| g.to_string());+ let art_opt = it.next().filter(|a| !a.is_empty() && *a != "-").map(PathBuf::from);+ let content = fs::read_to_string(&path).unwrap_or_else(|e| {+ eprintln!("ERROR: Cannot read --alt-sym {}: {}", path.display(), e);+ process::exit(1);+ });+ let mut alt = parse_kicad_sym(&content, None).unwrap_or_else(|e| {+ eprintln!("ERROR: --alt-sym {}: {}", path.display(), e);+ process::exit(1);+ });+ let gate = gate_opt.unwrap_or_else(|| {+ let pfx = format!("{}-", parsed.name);+ alt.name.strip_prefix(&pfx).map(|s| s.to_string()).unwrap_or_else(|| alt.name.clone())+ });+ if alt.name == parsed.name || alts.iter().any(|a| a.sym.name == alt.name) {+ alt.name = format!("{}-{}", parsed.name, gate);+ }+ if alt.pins.len() != parsed.pins.len() {+ eprintln!("ERROR: --alt-sym {} has {} pins, the primary symbol has {}: an alternate symbol must be the SAME part (same pin numbers)", path.display(), alt.pins.len(), parsed.pins.len());+ process::exit(1);+ }+ let art = match art_opt.as_ref().map(|p| resolve_symbol_art(p)) {+ Some(Ok(a)) => Some(a),+ Some(Err(e)) => { eprintln!("ERROR: --alt-sym art: {e}"); process::exit(1); }+ None => None,+ };+ if alts.iter().any(|a| a.gate == gate) || gate == "G$1" {+ eprintln!("ERROR: duplicate gate name {:?} in --alt-sym; give it path=GATE", gate);+ process::exit(1);+ }+ alts.push(AltGate { sym: alt, gate, art });+ }+ if !alts.is_empty() {+ println!("HINT: {} alternate symbol(s) as extra gates of {}: {}", alts.len(), parsed.name,+ alts.iter().map(|a| format!("{} ({})", a.gate, a.sym.name)).collect::<Vec<_>>().join(", "));+ }+ let lbr = generate_lbr_gates(&parsed, fp_content.as_deref(), art.as_ref(), symbol_art_scale, &pin_map, &part_attrs, &alts); // Lint before writing let lint = lint_lbr(&lbr);