← Commit history
.gitignoreadded+1
@@ -0,0 +1 @@+/target
Cargo.lockadded+262
@@ -0,0 +1,262 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "altium-schlib"+version = "0.1.0"+dependencies = [+ "anyhow",+ "cfb",+ "serde_json",+]++[[package]]+name = "anyhow"+version = "1.0.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"++[[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 = "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 = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "js-sys"+version = "0.3.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"+dependencies = [+ "cfg-if",+ "futures-util",+ "wasm-bindgen",+]++[[package]]+name = "memchr"+version = "2.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[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"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.46"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"+dependencies = [+ "proc-macro2",+]++[[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"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"+dependencies = [+ "serde_core",+]++[[package]]+name = "serde_core"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "serde_json"+version = "1.0.150"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "syn"+version = "2.0.118"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "uuid"+version = "1.23.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasm-bindgen"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.125"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "zmij"+version = "1.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
Cargo.tomladded+25
@@ -0,0 +1,25 @@+[package]+name = "altium-schlib"+version = "0.1.0"+edition = "2021"+description = "Native Rust parser for Altium .SchLib (and .IntLib) symbol libraries — extracts pins (number/name/xy/rotation) + body as KiCad-shaped JSON, renders SVG, emits .kicad_sym. Sibling to altium-pcblib."+authors = ["Adom <[email protected]>"]+license = "MIT"+repository = "https://github.com/adom-inc/altium-schlib"++[dependencies]+anyhow = "1"+serde_json = "1"+cfb = "0.10"++[[bin]]+name = "adom-altium-schlib-svg"+path = "src/bin/svg.rs"++[[bin]]+name = "altium-schlib-kicad"+path = "src/bin/kicad.rs"++[[bin]]+name = "altium-schlib-json"+path = "src/bin/json.rs"
LICENSEadded+21
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2026 Adom Industries Inc.++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
SKILL.mdadded+66
@@ -0,0 +1,66 @@+---+name: adom-altium-schlib+description: >+  Native Rust crate that parses Altium .SchLib (and .IntLib) binary schematic-+  symbol libraries — an OLE2 Compound File container holding a reverse-engineered+  binary pin-record format. Emits KiCad-shaped pin JSON, renders a native symbol+  SVG, and emits .kicad_sym for export. The symbol counterpart to altium-pcblib+  (footprints); together they read both halves of an Altium library natively, with+  no Altium install and no KiCad/service round-trip. Used by adom-sfconvert ++  chip-fetcher so each format renders its OWN geometry for cross-source comparison.+  Public Rust API: parse_altium_schlib_path(&Path), parse_altium_schlib_bytes(&[u8]),+  render_symbol_svg(&Value), to_kicad_sym(&Value, name).+  Validated 28/28 pins (number + name, overbars included) against KiCad 10's+  Altium importer on ADS1263IPWR.+---++# altium-schlib++Read Altium schematic-symbol libraries (`.SchLib` / `.IntLib`) in pure Rust.++## When to reach for this++- A tool needs the **pins** (numbers, names, sides, rotation) out of an Altium+  `.SchLib` without installing Altium or converting through KiCad.+- You want a **native symbol thumbnail** (SVG) that shows the Altium library's+  own geometry, for side-by-side comparison against other sources.+- You need to **export** an Altium symbol to `.kicad_sym`.++## API++| Call | Returns |+|---|---|+| `parse_altium_schlib_path(&Path)` | symbol JSON (`module_name`, `pin_count`, `pins[]`, `body`, `part_count`) |+| `parse_altium_schlib_bytes(&[u8])` | same, from raw bytes |+| `render_symbol_svg(&Value)` | standalone SVG, drawn natively |+| `to_kicad_sym(&Value, name)` | `.kicad_sym` text (export only) |++Each pin: `{ number, name, x, y, length, rotation, conglomerate }`.++## The binary pin record (the hard-won part)++Inside `/<Component>/Data`, text records are `<u16 len><u16 flags>|KEY=VAL|…`,+but **pins are binary** records whose payload starts with `0x02`:++```+[14]     PinConglomerate   orientation in bits 0..1 → 0/90/180/270°+[15..17] PinLength         i16+[17..19] Location.X        i16+[19..21] Location.Y        i16+[26..]   length-prefixed strings: Name, then Designator+```++Open the OLE2 container after padding the buffer to ≥1 MiB with `0xFF` (Altium's+FAT overshoots the real sector count, which `cfb` otherwise rejects).++## Don't re-implement this++If another Adom tool needs Altium symbols, depend on this crate — do **not**+re-walk OLE2 + re-decode the pin format. One source of truth, like+`altium-pcblib` is for footprints.++## Pairing++- Footprints → [`altium-pcblib`](https://wiki.adom.inc/skill/altium-pcblib)+- Converter that uses both → `adom-sfconvert`+- Consumer that shows the result → `chip-fetcher`
adom-altium-schlib-svgadded
⋯ 1 unchanged line ⋯
install.shadded+7
@@ -0,0 +1,7 @@+#!/usr/bin/env bash+set -e+HERE="$(cd "$(dirname "$0")" && pwd)"+DEST="$HOME/.claude/skills/adom-altium-schlib"+mkdir -p "$DEST"+cp -f "$HERE/skills/adom-altium-schlib/SKILL.md" "$DEST/SKILL.md"+echo "Installed adom-altium-schlib skill → $DEST"
package.jsonadded+33
@@ -0,0 +1,33 @@+{+  "slug": "adom-altium-schlib",+  "version": "1.0.5",+  "type": "skill",+  "description": "Native Rust parser for Altium .SchLib binary symbol libraries \u2014 pins + body to KiCad-shaped JSON, native SVG, .kicad_sym export. The symbol counterpart to altium-pcblib.",+  "dependencies": {},+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "tags": [+    "altium",+    "schlib",+    "kicad",+    "rust",+    "eda",+    "symbol",+    "parser"+  ],+  "hero": {+    "path": "screenshots/hero-v2.png"+  },+  "license": "MIT",+  "discovery_triggers": [+    "altium schlib",+    "parse altium symbol",+    "read altium .schlib",+    "altium symbol to kicad",+    "altium-schlib",+    "native altium symbol parser",+    "altium ole2 symbol"+  ]+}\ No newline at end of file
screenshots/ads1263-symbol.pngadded
⋯ 1 unchanged line ⋯
screenshots/hero-v2.pngadded
⋯ 1 unchanged line ⋯
screenshots/hero.pngadded
⋯ 1 unchanged line ⋯
screenshots/validation.pngadded
⋯ 1 unchanged line ⋯
skills/adom-altium-schlib/SKILL.mdadded+66
@@ -0,0 +1,66 @@+---+name: adom-altium-schlib+description: >+  Native Rust crate that parses Altium .SchLib (and .IntLib) binary schematic-+  symbol libraries — an OLE2 Compound File container holding a reverse-engineered+  binary pin-record format. Emits KiCad-shaped pin JSON, renders a native symbol+  SVG, and emits .kicad_sym for export. The symbol counterpart to altium-pcblib+  (footprints); together they read both halves of an Altium library natively, with+  no Altium install and no KiCad/service round-trip. Used by adom-sfconvert ++  chip-fetcher so each format renders its OWN geometry for cross-source comparison.+  Public Rust API: parse_altium_schlib_path(&Path), parse_altium_schlib_bytes(&[u8]),+  render_symbol_svg(&Value), to_kicad_sym(&Value, name).+  Validated 28/28 pins (number + name, overbars included) against KiCad 10's+  Altium importer on ADS1263IPWR.+---++# altium-schlib++Read Altium schematic-symbol libraries (`.SchLib` / `.IntLib`) in pure Rust.++## When to reach for this++- A tool needs the **pins** (numbers, names, sides, rotation) out of an Altium+  `.SchLib` without installing Altium or converting through KiCad.+- You want a **native symbol thumbnail** (SVG) that shows the Altium library's+  own geometry, for side-by-side comparison against other sources.+- You need to **export** an Altium symbol to `.kicad_sym`.++## API++| Call | Returns |+|---|---|+| `parse_altium_schlib_path(&Path)` | symbol JSON (`module_name`, `pin_count`, `pins[]`, `body`, `part_count`) |+| `parse_altium_schlib_bytes(&[u8])` | same, from raw bytes |+| `render_symbol_svg(&Value)` | standalone SVG, drawn natively |+| `to_kicad_sym(&Value, name)` | `.kicad_sym` text (export only) |++Each pin: `{ number, name, x, y, length, rotation, conglomerate }`.++## The binary pin record (the hard-won part)++Inside `/<Component>/Data`, text records are `<u16 len><u16 flags>|KEY=VAL|…`,+but **pins are binary** records whose payload starts with `0x02`:++```+[14]     PinConglomerate   orientation in bits 0..1 → 0/90/180/270°+[15..17] PinLength         i16+[17..19] Location.X        i16+[19..21] Location.Y        i16+[26..]   length-prefixed strings: Name, then Designator+```++Open the OLE2 container after padding the buffer to ≥1 MiB with `0xFF` (Altium's+FAT overshoots the real sector count, which `cfb` otherwise rejects).++## Don't re-implement this++If another Adom tool needs Altium symbols, depend on this crate — do **not**+re-walk OLE2 + re-decode the pin format. One source of truth, like+`altium-pcblib` is for footprints.++## Pairing++- Footprints → [`altium-pcblib`](https://wiki.adom.inc/skill/altium-pcblib)+- Converter that uses both → `adom-sfconvert`+- Consumer that shows the result → `chip-fetcher`
src/bin/json.rsadded+6
@@ -0,0 +1,6 @@+fn main() -> anyhow::Result<()> {+    let path = std::env::args().nth(1).unwrap();+    let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+    println!("{}", serde_json::to_string_pretty(&sym)?);+    Ok(())+}
src/bin/kicad.rsadded+10
@@ -0,0 +1,10 @@+//! altium-schlib-kicad <file.SchLib> [name] — emit a .kicad_sym (export path).+fn main() -> anyhow::Result<()> {+    let path = std::env::args().nth(1).ok_or_else(|| anyhow::anyhow!("usage: altium-schlib-kicad <file.SchLib> [name]"))?;+    let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+    let name = std::env::args().nth(2).unwrap_or_else(|| {+        sym.get("module_name").and_then(|v| v.as_str()).unwrap_or("SYM").to_string()+    });+    print!("{}", altium_schlib::to_kicad_sym(&sym, &name));+    Ok(())+}
src/bin/svg.rsadded+7
@@ -0,0 +1,7 @@+//! adom-altium-schlib-svg <file.SchLib> — print the native symbol SVG to stdout.+fn main() -> anyhow::Result<()> {+    let path = std::env::args().nth(1).ok_or_else(|| anyhow::anyhow!("usage: adom-altium-schlib-svg <file.SchLib>"))?;+    let sym = altium_schlib::parse_altium_schlib_path(std::path::Path::new(&path))?;+    print!("{}", altium_schlib::render_symbol_svg(&sym));+    Ok(())+}
src/lib.rsadded+362
@@ -0,0 +1,362 @@+//! Native Rust parser for Altium `.SchLib` (and `.IntLib`'s SchLib stream)+//! schematic-symbol libraries — the symbol counterpart to `altium-pcblib`.+//!+//! Altium `.SchLib` is a Microsoft OLE2 / Compound File Binary container. Each+//! component is a storage holding a `Data` stream of records:+//!   * **text records** — `<u16 len><u16 flags>` then `|KEY=VAL|KEY=VAL` ASCII.+//!     RECORD=1 is the component header, RECORD=14 a rectangle (body), RECORD=41+//!     a parameter.+//!   * **binary pin records** — `<u16 len><u16 flags>` then a payload whose first+//!     byte is `0x02`. Layout (offsets into the payload), reverse-engineered and+//!     validated pin-for-pin against KiCad 10's Altium importer:+//!         [14]      PinConglomerate  (u8) — orientation in bits 0..1+//!         [15..17]  PinLength        (i16)+//!         [17..19]  Location.X       (i16)+//!         [19..21]  Location.Y       (i16)+//!         [26..]    length-prefixed strings: Name, then Designator+//!+//! Output JSON mirrors `altium-pcblib`'s KiCad-shaped style so the rest of the+//! Adom EDA family consumes both the same way.++use anyhow::{anyhow, bail, Result};+use std::io::{Cursor, Read};+use std::path::Path;++/// Parse an Altium `.SchLib` from a file path → symbol JSON.+pub fn parse_altium_schlib_path(path: &Path) -> Result<serde_json::Value> {+    let bytes = std::fs::read(path)+        .map_err(|e| anyhow!("read .SchLib {}: {e}", path.display()))?;+    parse_altium_schlib_bytes(&bytes)+}++/// Parse an Altium `.SchLib` from raw bytes → symbol JSON.+pub fn parse_altium_schlib_bytes(bytes: &[u8]) -> Result<serde_json::Value> {+    if bytes.len() < 8 || bytes[..8] != [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {+        bail!("not an OLE2 compound document (bad magic)");+    }+    // Altium writes FATs that overshoot the file's real sector count; pad with+    // the CFBF free-sector sentinel (0xFF) and re-open. Same fix as altium-pcblib.+    let owned;+    let cursor = if bytes.len() < 1024 * 1024 {+        owned = {+            let mut v = bytes.to_vec();+            v.resize(1024 * 1024, 0xFF);+            v+        };+        Cursor::new(owned.as_slice())+    } else {+        Cursor::new(bytes)+    };+    let mut comp = cfb::CompoundFile::open(cursor).map_err(|e| anyhow!("open OLE2: {e}"))?;++    // Each non-housekeeping root storage is a component.+    let comps: Vec<String> = comp+        .read_root_storage()+        .filter_map(|e| {+            if !e.is_storage() {+                return None;+            }+            let n = e.name().to_string();+            if n.starts_with('\u{1}') || n == "Library" || n == "FileVersionInfo" {+                return None;+            }+            Some(n)+        })+        .collect();+    if comps.is_empty() {+        bail!("no component storages found in SchLib");+    }++    // Single-part library is the common chip-fetcher case: take the first.+    let name = &comps[0];+    let mut data = Vec::new();+    comp.open_stream(format!("/{name}/Data"))+        .map_err(|e| anyhow!("open /{name}/Data: {e}"))?+        .read_to_end(&mut data)?;+    parse_component_stream(name, &data)+}++fn parse_component_stream(comp_name: &str, d: &[u8]) -> Result<serde_json::Value> {+    let mut pins: Vec<serde_json::Value> = Vec::new();+    let mut description = String::new();+    let mut part_count = 1i64;+    let mut body: Option<serde_json::Value> = None;++    let mut i = 0usize;+    while i + 4 <= d.len() {+        let plen = u16::from_le_bytes([d[i], d[i + 1]]) as usize;+        let is_text = i + 4 < d.len() && d[i + 4] == b'|';+        i += 4;+        if plen == 0 || i + plen > d.len() {+            break;+        }+        let payload = &d[i..i + plen];+        i += plen;++        if is_text {+            let txt = String::from_utf8_lossy(payload);+            let rec = field(&txt, "RECORD").unwrap_or_default();+            match rec.as_str() {+                "1" => {+                    description = field(&txt, "ComponentDescription").unwrap_or_default();+                    part_count = field(&txt, "PartCount").and_then(|v| v.parse().ok()).unwrap_or(1);+                }+                "14" => {+                    // Rectangle = the symbol body. Coords are |Location.X|.Y|Corner.X|.Y.+                    let lx = field(&txt, "Location.X").and_then(|v| v.parse::<f64>().ok());+                    let ly = field(&txt, "Location.Y").and_then(|v| v.parse::<f64>().ok());+                    let cx = field(&txt, "Corner.X").and_then(|v| v.parse::<f64>().ok());+                    let cy = field(&txt, "Corner.Y").and_then(|v| v.parse::<f64>().ok());+                    if let (Some(lx), Some(ly), Some(cx), Some(cy)) = (lx, ly, cx, cy) {+                        body = Some(serde_json::json!({+                            "x1": lx, "y1": ly, "x2": cx, "y2": cy+                        }));+                    }+                }+                _ => {}+            }+            continue;+        }++        if payload.first() == Some(&0x02) {+            pins.push(parse_pin(payload));+        }+    }++    if pins.is_empty() {+        bail!("no pins parsed from component {comp_name}");+    }++    Ok(serde_json::json!({+        "module_name": comp_name,+        "description": description,+        "part_count": part_count,+        "pin_count": pins.len(),+        "pins": pins,+        "body": body,+        "_source": "altium_schlib",+    }))+}++/// Decode one binary pin record payload → pin JSON.+fn parse_pin(p: &[u8]) -> serde_json::Value {+    let i16at = |o: usize| p.get(o..o + 2).map(|b| i16::from_le_bytes([b[0], b[1]]) as i64).unwrap_or(0);+    let conglom = p.get(14).copied().unwrap_or(0);+    let length = i16at(15);+    let x = i16at(17);+    let y = i16at(19);+    // strings: Name then Designator, starting at offset 26+    let mut j = 26usize;+    let name = read_pstr(p, &mut j);+    let designator = read_pstr(p, &mut j);+    // Orientation: PinConglomerate bits 0..1 → 0/90/180/270°.+    let rotation = (conglom & 0x03) as i64 * 90;+    serde_json::json!({+        "number": designator,+        "name": name,+        "x": x,+        "y": y,+        "length": length,+        "rotation": rotation,+        "conglomerate": conglom,+    })+}++/// `<u8 len><bytes>` length-prefixed string.+fn read_pstr(d: &[u8], i: &mut usize) -> String {+    if *i >= d.len() {+        return String::new();+    }+    let l = d[*i] as usize;+    *i += 1;+    if *i + l > d.len() {+        return String::new();+    }+    let s = String::from_utf8_lossy(&d[*i..*i + l]).to_string();+    *i += l;+    s+}++/// Pull `KEY=VALUE` out of a `|KEY=VAL|KEY=VAL` Altium text record.+fn field(rec: &str, key: &str) -> Option<String> {+    let needle = format!("{key}=");+    for part in rec.split('|') {+        if let Some(v) = part.strip_prefix(&needle) {+            return Some(v.trim().to_string());+        }+    }+    None+}++// ------------------------------------------------------------------------+// Native SVG renderer — draws the symbol directly from the parsed pins, the+// same way altium-pcblib renders footprints. No KiCad, no service round-trip.+// ------------------------------------------------------------------------++/// Altium SCH coordinate → mm. Pin pitch is 2560 internal units = 100 mil =+/// 2.54 mm (the schematic-grid standard), so one unit = 2.54/2560 mm.+const MM_PER_UNIT: f64 = 2.54 / 2560.0;++/// Render the parsed symbol to a standalone SVG. Pins are grouped into a left+/// and right bank by X sign and stacked in designator order at a uniform pitch+/// — a clean, readable schematic symbol that preserves which side each pin is+/// on and its name/number (the data that actually matters for comparison).+pub fn render_symbol_svg(sym: &serde_json::Value) -> String {+    let module = sym.get("module_name").and_then(|v| v.as_str()).unwrap_or("?");+    let empty = vec![];+    let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);++    // Split into banks by X sign; preserve native vertical order (top→bottom).+    let mut left: Vec<&serde_json::Value> = Vec::new();+    let mut right: Vec<&serde_json::Value> = Vec::new();+    for p in pins {+        let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);+        if x < 0 { left.push(p); } else { right.push(p); }+    }+    let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };+    left.sort_by_key(key);+    right.sort_by_key(key);++    let pitch = 2.54_f64; // mm between pins+    let rows = left.len().max(right.len()).max(1) as f64;+    let body_w = 38.0_f64;+    let body_h = rows * pitch + pitch; // one row of padding top+bottom+    let pin_len = 7.62_f64;+    let name_pad = 1.4_f64;++    let total_w = body_w + 2.0 * pin_len + 44.0; // room for pin names outside? names go inside+    let total_h = body_h + 8.0;+    let ox = total_w / 2.0; // body centered horizontally+    let bx1 = ox - body_w / 2.0;+    let bx2 = ox + body_w / 2.0;+    let by1 = 4.0;+    let by2 = by1 + body_h;++    let mut s = String::new();+    s.push_str(&format!(+        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_w:.1} {total_h:.1}" width="{:.0}" height="{:.0}" font-family="monospace">"#,+        total_w * 14.0, total_h * 14.0+    ));+    s.push('\n');+    s.push_str(&format!(r##"<rect x="0" y="0" width="{total_w:.1}" height="{total_h:.1}" fill="#0d1117"/>"##));+    // body+    s.push_str(&format!(+        r##"<rect x="{bx1:.2}" y="{by1:.2}" width="{:.2}" height="{:.2}" fill="#10212a" stroke="#00b8b1" stroke-width="0.25"/>"##,+        bx2 - bx1, by2 - by1+    ));+    s.push('\n');++    let draw = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {+        for (idx, p) in bank.iter().enumerate() {+            let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");+            let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");+            let cy = by1 + pitch + idx as f64 * pitch;+            let (x_body, x_tip, anchor, nx) = if is_left {+                (bx1, bx1 - pin_len, "start", bx1 + name_pad)+            } else {+                (bx2, bx2 + pin_len, "end", bx2 - name_pad)+            };+            // pin line+            out.push_str(&format!(+                r##"<line x1="{x_body:.2}" y1="{cy:.2}" x2="{x_tip:.2}" y2="{cy:.2}" stroke="#7cb6ff" stroke-width="0.18"/>"##+            ));+            // pin number above the line, near the tip+            let numx = if is_left { x_tip + 0.6 } else { x_tip - 0.6 };+            let numanchor = if is_left { "start" } else { "end" };+            out.push_str(&format!(+                r##"<text x="{numx:.2}" y="{:.2}" font-size="1.0" fill="#8b949e" text-anchor="{numanchor}">{}</text>"##,+                cy - 0.5, esc(num)+            ));+            // pin name inside the body+            let display = altium_name_to_plain(name);+            out.push_str(&format!(+                r##"<text x="{nx:.2}" y="{:.2}" font-size="1.25" fill="#e6edf3" text-anchor="{anchor}">{}</text>"##,+                cy + 0.45, esc(&display)+            ));+            out.push('\n');+        }+    };+    draw(&left, true, &mut s);+    draw(&right, false, &mut s);++    // header+    s.push_str(&format!(+        r##"<text x="{ox:.2}" y="{:.2}" font-size="1.5" fill="#00e6dc" text-anchor="middle">{} ({} pins)</text>"##,+        by1 - 1.2, esc(module), pins.len()+    ));+    let _ = MM_PER_UNIT;+    s.push_str("\n</svg>\n");+    s+}++/// Altium overbar/special markup → plain readable text. Altium marks an active-+/// low signal by following each char with a backslash (`C\S\` = C̄S̄); collapse+/// those so a name reads as "CS".+fn altium_name_to_plain(s: &str) -> String {+    s.replace('\\', "")+}++fn esc(s: &str) -> String {+    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")+}++// ------------------------------------------------------------------------+// .kicad_sym emitter — for EXPORT only (when the user wants KiCad output).+// Rendering uses the native SVG above; this is the convert-on-export path.+// ------------------------------------------------------------------------++/// Emit a `.kicad_sym` from the parsed symbol. Banks pins left/right like the+/// SVG so the exported KiCad symbol is laid out sensibly.+pub fn to_kicad_sym(sym: &serde_json::Value, lib_name: &str) -> String {+    let empty = vec![];+    let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);+    let mut left: Vec<&serde_json::Value> = Vec::new();+    let mut right: Vec<&serde_json::Value> = Vec::new();+    for p in pins {+        let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);+        if x < 0 { left.push(p); } else { right.push(p); }+    }+    let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };+    left.sort_by_key(key);+    right.sort_by_key(key);++    let pitch = 2.54_f64;+    let rows = left.len().max(right.len()).max(1) as f64;+    let half_h = (rows * pitch) / 2.0 + pitch;+    let half_w = 19.05_f64;+    let pin_len = 5.08_f64;++    let mut body = String::new();+    let mut emit = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {+        for (idx, p) in bank.iter().enumerate() {+            let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");+            let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");+            let cy = half_h - pitch - idx as f64 * pitch;+            let (px, rot) = if is_left { (-half_w - pin_len, 0) } else { (half_w + pin_len, 180) };+            out.push_str(&format!(+                "      (pin bidirectional line (at {px:.2} {cy:.2} {rot}) (length {pin_len:.2})\n        (name \"{}\" (effects (font (size 1.27 1.27)))) (number \"{}\" (effects (font (size 1.27 1.27)))))\n",+                kicad_name(name), kesc(num)+            ));+        }+    };+    emit(&left, true, &mut body);+    emit(&right, false, &mut body);++    format!(+        "(kicad_symbol_lib (version 20211014) (generator altium-schlib)\n  (symbol \"{lib_name}\" (in_bom yes) (on_board yes)\n    (property \"Reference\" \"U\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    (property \"Value\" \"{lib_name}\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    (symbol \"{lib_name}_1_1\"\n      (rectangle (start {:.2} {:.2}) (end {:.2} {:.2}) (stroke (width 0.254) (type default)) (fill (type background)))\n{body}    )\n  )\n)\n",+        half_h + 1.27, -half_h - 1.27, -half_w, half_h, half_w, -half_h,+    )+}++/// KiCad uses `~{X}` for an overbar; Altium's `X\` per-char → wrap the whole+/// name in one overbar group when it's marked active-low.+fn kicad_name(s: &str) -> String {+    if s.contains('\\') {+        format!("~{{{}}}", s.replace('\\', ""))+    } else {+        kesc(s)+    }+}+fn kesc(s: &str) -> String {+    s.replace('"', "'")+}
uninstall.shadded+3
@@ -0,0 +1,3 @@+#!/usr/bin/env bash+rm -rf "$HOME/.claude/skills/adom-altium-schlib"+echo "Removed adom-altium-schlib skill"