← Commit history
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)  ![ADS8588SIPM LQFP-64 — the package built from the real .kicad_mod: 16 pads per side, true 4-sided ring](docs/screenshot-footprint-4sided.png)
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\ No newline at end of file+1.8.0\ No newline at end of file
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