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
Publish 1.8.0
12 files changed
+573−20
Cargo.lock+151−1@@ -4,9 +4,10 @@ version = 4 [[package]] name = "adom-lbr"-version = "1.7.0"+version = "1.8.0" dependencies = [ "anyhow",+ "cfb", "clap", "clap_complete", "quick-xml",@@ -78,6 +79,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]]+name = "bumpalo"+version = "3.20.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"++[[package]]+name = "byteorder"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"++[[package]]+name = "cfb"+version = "0.10.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d8a4f8e55be323b378facfcf1f06aa97f6ec17cf4ac84fb17325093aaf62da41"+dependencies = [+ "byteorder",+ "fnv",+ "uuid",+]++[[package]]+name = "cfg-if"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"+ [[package]] name = "chunked_transfer" version = "1.5.0"@@ -139,6 +169,36 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]]+name = "fnv"+version = "1.0.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"++[[package]]+name = "futures-core"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"++[[package]]+name = "futures-task"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"++[[package]]+name = "futures-util"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"+dependencies = [+ "futures-core",+ "futures-task",+ "pin-project-lite",+ "slab",+]+ [[package]] name = "heck" version = "0.5.0"@@ -163,6 +223,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]]+name = "js-sys"+version = "0.3.103"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"+dependencies = [+ "cfg-if",+ "futures-util",+ "wasm-bindgen",+]+ [[package]] name = "log" version = "0.4.32"@@ -175,12 +246,24 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"+ [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"+ [[package]] name = "proc-macro2" version = "1.0.106"@@ -215,6 +298,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" +[[package]]+name = "rustversion"+version = "1.0.22"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"+ [[package]] name = "serde" version = "1.0.228"@@ -258,6 +347,12 @@ dependencies = [ "zmij", ] +[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"+ [[package]] name = "strsim" version = "0.11.1"@@ -299,6 +394,61 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]]+name = "uuid"+version = "1.23.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasm-bindgen"+version = "0.2.126"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.126"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.126"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.126"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"+dependencies = [+ "unicode-ident",+]+ [[package]] name = "windows-link" version = "0.2.1"
Cargo.toml+2−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "1.7.0"+version = "1.8.0" edition = "2021" description = "EAGLE .lbr generator, linter, and validator for Fusion 360 Electronics. Converts KiCad symbols + footprints to complete EAGLE libraries." @@ -17,6 +17,7 @@ serde_json = "1" quick-xml = { version = "0.36", features = ["serialize"] } tiny_http = "0.12" anyhow = "1"+cfb = "0.10" [profile.release] opt-level = "z"
README.md+9−1@@ -33,7 +33,7 @@ part-library/ ├── kicad/ part.kicad_sym + part.pretty/part.kicad_mod ← native KiCad ├── fusion/ part.lbr [+ part.fusion3d.json] ← EAGLE + 3D handoff ├── orcad/ part.edn ← EDIF 2.0.0 (via adom-orcad → native .olb)-├── altium/ part.lbr + sources + ALTIUM-README.md ← direct import + native-bridge hand-off+├── altium/ part.SchLib + part.PcbLib (native, no Altium!) ← + .lbr & bridge-manifest fallbacks └── EXPORT.md ← per-tool import steps ``` @@ -54,6 +54,14 @@ a `⛔ INCOMPLETE BOM` reprimand telling the caller to re-run with them. here; the linter's `name-value-overlap` check warns when that text lands on the pads so you can fix it upstream. +**Native Altium, no Altium (new in 1.8):** the Altium target now writes real+binary **`.SchLib` + `.PcbLib`** directly in pure Rust — OLE2/CFB containers whose+streams and pin/pad records are byte-structurally conformant to what Altium itself+produces (reverse-engineered from manufacturer libraries). No Altium install, no+bridge needed to *create* them. ⚠️ Experimental: not yet round-tripped through a+real Altium loader, so the `.lbr` Import-Wizard path and the bridge manifest ship+alongside as fallbacks.+ ## Real footprints & 3D models (new in 1.1) 
SKILL.md+8−7@@ -106,13 +106,14 @@ the real footprint, and the chip outline are identical across them: - **OrCAD** — shells the **adom-orcad** converter to emit EDIF 2.0.0 (`.edn`), which Capture imports to a native `.olb`. If adom-orcad isn't installed it writes an `ORCAD-README.md` with the exact command instead of failing.-- **Altium** — a bridge-ready import package: **`<part>.altium.json`** (manifest,- schema `adom-lbr.altium/1` — part, BOM part numbers incl. `LCSC Part #`, staged- sources, target `.SchLib/.PcbLib`, mode) — the Altium analog of `fusion3d.json`.- The orchestrating AI hands it to the **Altium bridge** (`adom/adom-desktop-altium-bridge`- / `altium-part-importer`) on the Altium machine to build native libraries.- `--altium-lib <base>` / `--altium-mode create|append|reuse` set the target. The- staged `.lbr` is also Altium-Import-Wizard-ready as a no-tooling fallback.+- **Altium** — **native binary `<part>.SchLib` + `<part>.PcbLib`, written in pure+ Rust with NO Altium** (the default). BOM part numbers (incl. `LCSC Part #`)+ land as native component parameters. ⚠️ EXPERIMENTAL: byte-structurally+ conformant to real Altium output (streams + pin/pad records match manufacturer+ libraries) but not yet round-tripped through a real Altium install. Fallbacks+ also emitted: the `.lbr` (Import Wizard) and **`<part>.altium.json`** (manifest+ for the **Altium bridge** — `adom/adom-desktop-altium-bridge` / `altium-part-importer`;+ `--altium-lib`/`--altium-mode` set its target). `--targets` limits the set (e.g. `--targets kicad,fusion`). This is still a pure exporter: it writes files and hands off; the orchestrating AI does any
VERSION+1−1@@ -1 +1 @@-1.7.0+1.8.0
assets/altium-template.PcbLibadded⋯ 1 unchanged line ⋯
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "1.7.0",+ "version": "1.8.0", "type": "app", "description": "One KiCad part \u2192 every major EDA tool. `adom-lbr export` writes a folder per tool: native KiCad, Fusion 360 (.lbr + 3D handoff), OrCAD (EDIF via adom-orcad), and an Altium bridge hand-off \u2014 real-footprint accurate. Bidirectional KiCad converter + 8-check linter + a live library viewer. Includes a focused fusion-export sub-skill.", "dependencies": {},
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "1.7.0",+ "version": "1.8.0", "type": "app", "description": "One KiCad part → every major EDA tool. `adom-lbr export` writes a folder per tool: native KiCad, Fusion 360 (.lbr + 3D handoff), OrCAD (EDIF via adom-orcad), and an Altium bridge hand-off — real-footprint accurate. Bidirectional KiCad converter + 8-check linter + a live library viewer. Includes a focused fusion-export sub-skill.", "dependencies": {},
src/altium_pcblib_write.rsadded+188@@ -0,0 +1,188 @@+//! Native Altium `.PcbLib` WRITER — pure Rust, no Altium. The inverse of the+//! reader. Strategy: take a real `.PcbLib` as a structural TEMPLATE (its+//! `/Library/*` board/layer/stackup blob is hard to synthesize and easy to keep+//! verbatim), then:+//! * rebuild the footprint `/<fp>/Data` stream from the part's pads — each pad+//! record cloned byte-for-byte from a template pad and patched at the+//! documented geometry offsets (so pads are byte-conformant to Altium's own),+//! * rename the footprint storage + its `Parameters` + the library's+//! `ComponentParamsTOC`, and length-safe-patch `/Library/Data`.+//!+//! NB: not yet round-tripped through a real Altium install (none available);+//! validated by re-reading with this crate's parser + byte-conformance of pad+//! records against manufacturer `.PcbLib` files. Labeled experimental upstream.++use anyhow::{anyhow, Result};+use std::io::{Cursor, Read, Write};++/// A pad to emit, in Altium internal units (1 unit = 1/10000 mil = 2.54e-6 mm).+/// `x`/`y` are the pad centre; Altium Y is up. `hole` 0 = SMD. `shape`: 1 round,+/// 2 rect, 9 rounded-rect.+pub struct WritePad {+ pub designator: String,+ pub x: i32,+ pub y: i32,+ pub xsize: u32,+ pub ysize: u32,+ pub hole: u32,+ pub shape: u8,+ pub rotation: f64,+ pub layer: u8,+}++fn u32at(d: &[u8], o: usize) -> usize {+ u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]) as usize+}++/// Pull a footprint storage name + a template pad's 6 sub-blocks out of a+/// footprint `Data` stream.+fn template_pad(fdata: &[u8]) -> Result<Vec<Vec<u8>>> {+ let nbl = u32at(fdata, 0);+ let mut i = 4 + nbl;+ while i < fdata.len() {+ let t = fdata[i];+ let mut j = i + 1;+ let nsub = match t { 0x02 => 6, 0x05 => 2, _ => 1 };+ let mut blocks = Vec::with_capacity(nsub);+ for _ in 0..nsub {+ if j + 4 > fdata.len() { return Err(anyhow!("truncated record")); }+ let bl = u32at(fdata, j);+ j += 4;+ if j + bl > fdata.len() { return Err(anyhow!("truncated sub-block")); }+ blocks.push(fdata[j..j + bl].to_vec());+ j += bl;+ }+ if t == 0x02 { return Ok(blocks); }+ i = j;+ }+ Err(anyhow!("no pad record in template footprint"))+}++/// Emit one pad record: clone the template's 6 sub-blocks, patch sub-block 1+/// (designator) + sub-block 5 (geometry) per the documented byte map.+fn emit_pad(tmpl: &[Vec<u8>], p: &WritePad) -> Vec<u8> {+ let mut blocks: Vec<Vec<u8>> = tmpl.to_vec();+ // sub-block 1 = designator (Pascal string)+ let db = p.designator.as_bytes();+ let n = db.len().min(255);+ let mut des = vec![n as u8];+ des.extend_from_slice(&db[..n]);+ blocks[0] = des;+ // sub-block 5 = main pad data+ let b5 = &mut blocks[4];+ if b5.len() >= 49 {+ b5[0] = p.layer;+ b5[13..17].copy_from_slice(&p.x.to_le_bytes());+ b5[17..21].copy_from_slice(&p.y.to_le_bytes());+ for off in [21usize, 29, 37] { // top / mid / bottom x,y sizes (SMD: all equal)+ b5[off..off + 4].copy_from_slice(&p.xsize.to_le_bytes());+ b5[off + 4..off + 8].copy_from_slice(&p.ysize.to_le_bytes());+ }+ b5[45..49].copy_from_slice(&p.hole.to_le_bytes());+ if b5.len() > 51 { b5[49] = p.shape; b5[50] = p.shape; b5[51] = p.shape; }+ if b5.len() >= 60 { b5[52..60].copy_from_slice(&p.rotation.to_le_bytes()); }+ }+ let mut out = vec![0x02u8];+ for b in &blocks {+ out.extend_from_slice(&(b.len() as u32).to_le_bytes());+ out.extend_from_slice(b);+ }+ out+}++fn framed(payload: &[u8]) -> Vec<u8> {+ let mut out = Vec::with_capacity(payload.len() + 5);+ out.extend_from_slice(&(payload.len() as u16).to_le_bytes());+ out.extend_from_slice(&0u16.to_le_bytes());+ out.extend_from_slice(payload);+ out+}+fn text_payload(s: &str) -> Vec<u8> { let mut p = s.as_bytes().to_vec(); p.push(0); p }++fn build_params(fp: &str, height: &str, descr: &str) -> Vec<u8> {+ framed(&text_payload(&format!(+ "|PATTERN={fp}|HEIGHT={height}|DESCRIPTION={descr}|ITEMGUID=|REVISIONGUID=")))+}+fn build_toc(fp: &str, npads: usize, height: &str, descr: &str) -> Vec<u8> {+ framed(&text_payload(&format!(+ "Name={fp}|Pad Count={npads}|Height={height}|Description={descr}")))+}++/// Length-preserving byte replacement (only safe when from.len()==to.len()).+fn raw_replace(buf: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {+ if from.len() != to.len() || from.is_empty() { return buf.to_vec(); }+ let mut out = buf.to_vec();+ let mut i = 0;+ while i + from.len() <= out.len() {+ if &out[i..i + from.len()] == from {+ out[i..i + from.len()].copy_from_slice(to);+ i += from.len();+ } else { i += 1; }+ }+ out+}++/// Emit a native `.PcbLib` for one footprint, using `template` (a real `.PcbLib`)+/// for the library/board/layer housekeeping. The footprint storage is rebuilt+/// from `pads`; the rest is cloned + renamed.+pub fn to_pcblib_bytes(+ template: &[u8],+ fpname: &str,+ description: &str,+ height: &str,+ pads: &[WritePad],+) -> Result<Vec<u8>> {+ let mut v = template.to_vec();+ if v.len() < 1024 * 1024 { v.resize(1024 * 1024, 0xFF); }+ let mut c = cfb::CompoundFile::open(Cursor::new(v))?;+ let tfp = c.read_root_storage()+ .find(|e| e.is_storage() && !e.name().starts_with('\u{1}') && e.name() != "Library" && e.name() != "FileVersionInfo")+ .map(|e| e.name().to_string())+ .ok_or_else(|| anyhow!("no footprint storage in template"))?;++ let mut tdata = Vec::new();+ c.open_stream(format!("/{tfp}/Data"))?.read_to_end(&mut tdata)?;+ let tmplpad = template_pad(&tdata)?;++ // new footprint Data: name block + pad records+ let mut nd = Vec::new();+ let nb_name = fpname.as_bytes();+ let nn = nb_name.len().min(255);+ let mut nb = vec![nn as u8];+ nb.extend_from_slice(&nb_name[..nn]);+ nd.extend_from_slice(&(nb.len() as u32).to_le_bytes());+ nd.extend_from_slice(&nb);+ for p in pads { nd.extend_from_slice(&emit_pad(&tmplpad, p)); }++ // rewrite container, renaming the footprint storage + patching name refs+ let entries: Vec<_> = c.walk().map(|e| (e.path().to_path_buf(), e.is_storage())).collect();+ let mut out = cfb::CompoundFile::create(Cursor::new(Vec::new()))?;+ let old_root = format!("/{tfp}");+ let new_root = format!("/{fpname}");+ for (p, is_stg) in &entries {+ let ps = p.to_string_lossy().to_string();+ if ps == "/" { continue; }+ let nps = if ps == old_root || ps.starts_with(&format!("{old_root}/")) {+ ps.replacen(&old_root, &new_root, 1)+ } else { ps.clone() };+ if *is_stg {+ let _ = out.create_storage(&nps);+ } else {+ let mut buf = Vec::new();+ c.open_stream(&ps)?.read_to_end(&mut buf)?;+ if ps == format!("/{tfp}/Data") {+ buf = nd.clone();+ } else if ps == format!("/{tfp}/Parameters") {+ buf = build_params(fpname, height, description);+ } else if ps == "/Library/ComponentParamsTOC/Data" {+ buf = build_toc(fpname, pads.len(), height, description);+ } else if ps == "/Library/Data" {+ // only length-safe; otherwise leave the template's refs intact+ buf = raw_replace(&buf, tfp.as_bytes(), fpname.as_bytes());+ }+ out.create_stream(&nps)?.write_all(&buf)?;+ }+ }+ out.flush()?;+ Ok(out.into_inner().into_inner())+}
src/altium_schlib_write.rsadded+128@@ -0,0 +1,128 @@+//! Native Altium `.SchLib` WRITER — pure Rust, no Altium. The inverse of the+//! reader: emits an OLE2/CFB container whose streams and records are+//! byte-structurally identical to what real Altium produces (verified by+//! re-reading with this crate's parser and by byte-diffing pin records against+//! manufacturer `.SchLib` files).+//!+//! Container: `/FileHeader` (component list) · `/Storage` (icon placeholder) ·+//! `/\x01Sh33tJ5` (the constant "7262" marker) · `/<part>` storage · `/<part>/Data`+//! (the record stream). Every stream is framed `<u16 len><u16 flags><payload>`.++use anyhow::Result;+use std::io::{Cursor, Write};++/// A schematic pin to emit, in Altium SCH internal units (1 unit = 2.54/2560 mm).+/// `x`/`y` are the pin's connection-end location; Altium Y is up. `rotation` is+/// 0/90/180/270 degrees.+pub struct WritePin {+ pub name: String,+ pub designator: String,+ pub x: i32,+ pub y: i32,+ pub length: i32,+ pub rotation: i32,+}++/// The fixed 26-byte prefix of an Altium pin record (record type `0x02`),+/// captured byte-for-byte from a real manufacturer `.SchLib`. Bytes [14] (pin+/// conglomerate / orientation), [15..17] (length), [17..19] (X) and [19..21] (Y)+/// are overwritten per pin; the rest (owner-part id, electrical defaults, the+/// trailing sentinels) carry through unchanged — which is why generated pins are+/// byte-identical to Altium's own.+const PIN_PREFIX: [u8; 26] = [+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,+ 0x01, 0x04, 0x3a, 0x14, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,+];++fn put_rec(out: &mut Vec<u8>, flags: u16, payload: &[u8]) {+ out.extend_from_slice(&(payload.len() as u16).to_le_bytes());+ out.extend_from_slice(&flags.to_le_bytes());+ out.extend_from_slice(payload);+}++/// Altium length-prefixed (1-byte) string.+fn put_pstr(out: &mut Vec<u8>, s: &str) {+ let b = s.as_bytes();+ let n = b.len().min(255);+ out.push(n as u8);+ out.extend_from_slice(&b[..n]);+}++/// A text record (`|KEY=VAL|…`) — payload is the text plus a trailing NUL, the+/// way Altium frames its FileHeader and component header records.+fn put_text(out: &mut Vec<u8>, flags: u16, body: &str) {+ let mut p = body.as_bytes().to_vec();+ p.push(0);+ put_rec(out, flags, &p);+}++/// Deterministic 8-uppercase-letter UniqueID from an index (Altium wants a+/// stable A–Z token per primitive; exact value is cosmetic).+fn uid(i: usize) -> String {+ let mut s = String::new();+ let mut n = i + 1;+ for _ in 0..8 {+ s.push((b'A' + (n % 26) as u8) as char);+ n = n / 26 + 1;+ }+ s+}++/// Emit a native `.SchLib` for one component. `body` is the symbol body+/// rectangle (x1,y1,x2,y2) in Altium SCH units. `params` become hidden+/// `RECORD=41` parameters (Manufacturer / MPN / "LCSC Part #" / …) — exactly the+/// fields Altium surfaces in the BOM.+pub fn to_schlib_bytes(+ part: &str,+ description: &str,+ body: (i32, i32, i32, i32),+ pins: &[WritePin],+ params: &[(String, String)],+) -> Result<Vec<u8>> {+ // ----- component Data stream -----+ let mut d = Vec::new();+ put_text(&mut d, 0, &format!(+ "|RECORD=1|LibReference={part}|ComponentDescription={description}|PartCount=1|DisplayModeCount=1|IndexInSheet=-1|OwnerPartId=-1|CurrentPartId=1|LibraryPath=*|SourceLibraryName=*|SheetPartFileName=*|TargetFileName=*|UniqueID={}|AreaColor=11599871|Color=128|PartIDLocked=T|AllPinCount={}",+ uid(0), pins.len()));+ for (k, (name, val)) in params.iter().enumerate() {+ put_text(&mut d, 0, &format!(+ "|RECORD=41|IndexInSheet={k}|OwnerPartId=-1|Color=8388608|FontID=1|IsHidden=T|Text={val}|Name={name}|UniqueID={}",+ uid(100 + k)));+ }+ let (lx, ly, cx, cy) = body;+ put_text(&mut d, 0, &format!(+ "|RECORD=14|IsNotAccesible=T|OwnerPartId=1|Location.X={lx}|Location.Y={ly}|Corner.X={cx}|Corner.Y={cy}|LineWidth=1|Color=128|AreaColor=11599871|IsSolid=T|UniqueID={}",+ uid(50)));+ for p in pins {+ let mut h = PIN_PREFIX.to_vec();+ h[14] = (h[14] & !0x03) | (((p.rotation.rem_euclid(360) / 90) & 0x03) as u8);+ h[15..17].copy_from_slice(&(p.length as i16).to_le_bytes());+ h[17..19].copy_from_slice(&(p.x as i16).to_le_bytes());+ h[19..21].copy_from_slice(&(p.y as i16).to_le_bytes());+ let mut pl = h;+ put_pstr(&mut pl, &p.name);+ put_pstr(&mut pl, &p.designator);+ put_pstr(&mut pl, "0");+ put_pstr(&mut pl, "0");+ put_pstr(&mut pl, "");+ put_rec(&mut d, 0x0100, &pl);+ }++ // ----- FileHeader (the component list) -----+ let mut fh = Vec::new();+ put_text(&mut fh, 0, &format!(+ "|HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0|Weight=80|MinorVersion=3|UniqueID=ADOMLBRX|FontIdCount=2|Size1=10|FontName1=Times New Roman|Size2=10|Underline2=T|FontName2=Times New Roman|UseMBCS=T|IsBOC=T|SheetStyle=9|BorderOn=T|SheetNumberSpaceSize=12|AreaColor=16317695|SnapGridOn=T|SnapGridSize=10|VisibleGridOn=T|VisibleGridSize=10|CustomX=18000|CustomY=18000|UseCustomSheet=T|ReferenceZonesOn=T|Display_Unit=0|CompCount=1|LibRef0={part}|CompDescr0={description}|PartCount0=1|AlwaysShowCD=T"));++ let mut storage = Vec::new();+ put_text(&mut storage, 0, "|HEADER=Icon storage");++ // ----- assemble the OLE2/CFB container -----+ let mut comp = cfb::CompoundFile::create(Cursor::new(Vec::new()))?;+ comp.create_stream("/FileHeader")?.write_all(&fh)?;+ comp.create_stream("/Storage")?.write_all(&storage)?;+ comp.create_stream("/\u{1}Sh33tJ5")?.write_all(b"7262")?;+ comp.create_storage(&format!("/{part}"))?;+ comp.create_stream(&format!("/{part}/Data"))?.write_all(&d)?;+ comp.flush()?;+ Ok(comp.into_inner().into_inner())+}
src/main.rs+84−7@@ -5,6 +5,9 @@ mod server; use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; use std::fs;+mod altium_schlib_write;+mod altium_pcblib_write;+ use std::path::PathBuf; use std::process; @@ -1799,6 +1802,65 @@ fn report_bom(attrs: &PartAttrs, notes: &[String]) { } } +// ── Native Altium writers (no Altium needed) ──+// Altium internal units: schematic 1 unit = 2.54/2560 mm; PCB 1 unit = 2.54e-6 mm.+const ALTIUM_SCH_UNIT_PER_MM: f64 = 2560.0 / 2.54;+const ALTIUM_PCB_UNIT_PER_MM: f64 = 1.0 / 0.000_002_54;++/// BOM attributes → Altium component parameter names (what Altium's BOM reads).+fn altium_params(attrs: &PartAttrs) -> Vec<(String, String)> {+ let mut v = Vec::new();+ let mut push = |k: &str, o: &Option<String>| {+ if let Some(s) = o { if !s.trim().is_empty() { v.push((k.to_string(), s.trim().to_string())); } }+ };+ push("Manufacturer", &attrs.mfr);+ push("Manufacturer Part Number", &attrs.mpn);+ push("Mouser Part Number", &attrs.mouser);+ push("Digi-Key Part Number", &attrs.digikey);+ push("LCSC Part #", &attrs.lcsc);+ v+}++/// Native `.SchLib` from the parsed symbol (KiCad mm → Altium SCH units; both+/// Y-up, so no flip). Pins carry through at their real positions.+fn build_altium_schlib(sym: &ParsedSymbol, attrs: &PartAttrs) -> anyhow::Result<Vec<u8>> {+ use altium_schlib_write::WritePin;+ let u = ALTIUM_SCH_UNIT_PER_MM;+ let pins: Vec<WritePin> = sym.pins.iter().map(|p| WritePin {+ name: p.name.clone(),+ designator: p.number.clone(),+ x: (p.x * u).round() as i32,+ y: (p.y * u).round() as i32,+ length: (p.length * u).round() as i32,+ rotation: p.angle as i32,+ }).collect();+ let body = sym.body.as_ref()+ .map(|b| ((b.left * u).round() as i32, (b.bottom * u).round() as i32,+ (b.right * u).round() as i32, (b.top * u).round() as i32))+ .unwrap_or((0, 0, 2000, 2000));+ altium_schlib_write::to_schlib_bytes(&sym.name, &sym.description, body, &pins, &altium_params(attrs))+}++/// Native `.PcbLib` from the `.kicad_mod` pads (KiCad mm, Y-down → Altium units,+/// Y-up). Uses an embedded real `.PcbLib` for the library/board housekeeping.+fn build_altium_pcblib(fpname: &str, mod_pads: &[ModPad], descr: &str) -> anyhow::Result<Vec<u8>> {+ use altium_pcblib_write::WritePad;+ const TEMPLATE: &[u8] = include_bytes!("../assets/altium-template.PcbLib");+ let u = ALTIUM_PCB_UNIT_PER_MM;+ let pads: Vec<WritePad> = mod_pads.iter().map(|p| WritePad {+ designator: p.name.clone(),+ x: (p.x * u).round() as i32,+ y: (-p.y * u).round() as i32,+ xsize: (p.dx * u).round() as u32,+ ysize: (p.dy * u).round() as u32,+ hole: p.drill.map(|d| (d * u).round() as u32).unwrap_or(0),+ shape: 2, // rectangular — universally valid; refine per-pad later+ rotation: p.angle,+ layer: 1, // top copper; hole>0 marks thru-hole+ }).collect();+ altium_pcblib_write::to_pcblib_bytes(TEMPLATE, fpname, descr, "0mil", &pads)+}+ #[allow(clippy::too_many_arguments)] fn run_export( sym: Option<PathBuf>,@@ -1961,8 +2023,22 @@ fn run_export( if want("altium") { let adir = out_dir.join("altium"); let _ = fs::create_dir_all(&adir);- // Altium imports an EAGLE .lbr directly via its Import Wizard. Also stage- // the KiCad sources + model for the native bridge path.+ // DEFAULT: native binary Altium libraries written in pure Rust (no Altium).+ // EXPERIMENTAL — byte-structurally conformant to real Altium output but not+ // yet round-tripped through an actual Altium install.+ let mut native = Vec::new();+ match build_altium_schlib(&parsed, &attrs) {+ Ok(b) => { let _ = fs::write(adir.join(format!("{part}.SchLib")), &b); native.push(format!("{part}.SchLib")); }+ Err(e) => eprintln!("WARN: native .SchLib failed: {e}"),+ }+ let mod_pads = fp_content.as_deref().map(parse_mod_pads).unwrap_or_default();+ if !mod_pads.is_empty() {+ match build_altium_pcblib(&part, &mod_pads, &parsed.description) {+ Ok(b) => { let _ = fs::write(adir.join(format!("{part}.PcbLib")), &b); native.push(format!("{part}.PcbLib")); }+ Err(e) => eprintln!("WARN: native .PcbLib failed: {e}"),+ }+ }+ // Also stage the .lbr (Import-Wizard fallback) + KiCad sources + bridge manifest. let _ = fs::write(adir.join(format!("{part}.lbr")), &lbr); let _ = fs::write(adir.join(format!("{part}.kicad_sym")), &kicad_sym_text); if let Some(fc) = &fp_content {@@ -1984,7 +2060,8 @@ fn run_export( altium_lib.as_deref(), altium_mode.as_deref(), ); let _ = fs::write(adir.join("ALTIUM-README.md"), altium_readme(&part, fp_content.is_some(), model.is_some()));- status.push(("Altium".into(), "ok".into(), format!("{part}.altium.json (bridge manifest) + {part}.lbr (direct import) + sources")));+ let native_str = if native.is_empty() { "(native write failed)".to_string() } else { format!("native {}", native.join(" + ")) };+ status.push(("Altium".into(), "ok".into(), format!("{native_str} [experimental] + {part}.lbr fallback + bridge manifest"))); } // ── index + summary ──@@ -2084,9 +2161,9 @@ fn write_altium_manifest( fn altium_readme(part: &str, has_fp: bool, has_model: bool) -> String { let mut s = String::new(); s.push_str(&format!("# {part} → Altium\n\n"));- s.push_str(&format!("**Primary input: `{part}.altium.json`** — a machine-readable import manifest\n(schema `adom-lbr.altium/1`) describing the part, its BOM part numbers, the staged\nsources, and the target library. Hand it to the Altium bridge (path 1). It's the\nAltium analog of the Fusion `fusion3d.json` sidecar.\n\nTwo paths to get a library:\n\n"));- s.push_str("## 1. Native .SchLib / .PcbLib (Altium bridge) — recommended\n\n");- s.push_str(&format!("adom-lbr is a pure exporter — it does **not** drive Altium. The orchestrating AI\nfeeds `{part}.altium.json` to the **Altium bridge** on the machine running Altium\n+ Adom Desktop (Altium has no headless mode, so the bridge drives a real Altium\nvia DelphiScript). The manifest points at the staged sources:\n\n"));+ s.push_str(&format!("**`{part}.SchLib` + `{part}.PcbLib` are native Altium binary libraries**, written\ndirectly by adom-lbr in pure Rust — **no Altium required**. Add them to a project\nor integrated library and use them as-is.\n\n> ⚠️ EXPERIMENTAL: byte-structurally conformant to real Altium output (streams +\n> pin/pad records match manufacturer libraries) but **not yet round-tripped\n> through an actual Altium install**. If a part won't open, use the `.lbr`\n> Import-Wizard fallback or the bridge manifest below — and please report it.\n\nOther paths to the same library:\n\n"));+ s.push_str("## 1. Bridge (drives a real Altium to native libraries)\n\n");+ s.push_str(&format!("Hand `{part}.altium.json` (manifest, schema `adom-lbr.altium/1`) to the **Altium\nbridge** on a machine running Altium + Adom Desktop (Altium has no headless mode,\nso the bridge drives a real Altium via DelphiScript). The manifest points at the\nstaged sources:\n\n")); s.push_str(&format!("- `{part}.kicad_sym` — schematic symbol\n")); if has_fp { s.push_str(&format!("- `{part}.kicad_mod` — PCB footprint\n")); } if has_model { s.push_str("- the 3D model (`*.step` / `*.glb`) — 3D body\n"); }@@ -2120,7 +2197,7 @@ fn export_index(part: &str, status: &[(String, String, String)], has_fp: bool, h let fnote = if has_model { " Then the bridge attaches the 3D body (Create3DPackage → FINISH → Save)." } else { "" }; s.push_str(&format!("| **Fusion 360** | `fusion/{part}.lbr`{fsc} | Open the `.lbr` as an Electronics library.{fnote} |\n")); s.push_str(&format!("| **OrCAD** | `orcad/{part}.edn` | Capture ▸ Accessories/Import ▸ EDIF 2.0.0 ▸ select the `.edn` → native `.olb`. (Falls back to a README if adom-orcad isn't installed.) |\n"));- s.push_str(&format!("| **Altium** | `altium/{part}.altium.json` + `.lbr` + sources | Hand `{part}.altium.json` (bridge manifest) to the Altium bridge for native `.SchLib/.PcbLib`; or Import Wizard the `.lbr` directly. See `altium/ALTIUM-README.md`. |\n\n"));+ s.push_str(&format!("| **Altium** | `altium/{part}.SchLib` + `{part}.PcbLib` (native, **experimental**) | Add the native binary libraries directly — no Altium needed to create them. Fallbacks: `.lbr` Import Wizard or the bridge manifest. See `altium/ALTIUM-README.md`. |\n\n")); let _ = status; s.push_str("adom-lbr is a pure exporter: it writes these files and hands off. The orchestrating AI does any app-driving (Fusion bridge, Altium bridge).\n"); s