skill
Adom SF Convert
Public Made by Adomby adom
Native Rust converter: EAGLE/Fusion .lbr + Altium .SchLib/.PcbLib → KiCad, rendering each format's own geometry.
← Commit history
Publish source
13 files changed
+1007−2
Cargo.lockadded+320@@ -0,0 +1,320 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adom-sfconvert"+version = "0.1.0"+dependencies = [+ "altium-pcblib",+ "altium-schlib",+ "anyhow",+ "regex",+ "serde_json",+]++[[package]]+name = "aho-corasick"+version = "1.1.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"+dependencies = [+ "memchr",+]++[[package]]+name = "altium-pcblib"+version = "0.1.0"+dependencies = [+ "anyhow",+ "cfb",+ "serde_json",+]++[[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 = "regex"+version = "1.12.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"+dependencies = [+ "aho-corasick",+ "memchr",+ "regex-automata",+ "regex-syntax",+]++[[package]]+name = "regex-automata"+version = "0.4.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"+dependencies = [+ "aho-corasick",+ "memchr",+ "regex-syntax",+]++[[package]]+name = "regex-syntax"+version = "0.8.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"++[[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+17@@ -0,0 +1,17 @@+[package]+name = "adom-sfconvert"+version = "0.1.0"+edition = "2021"+description = "Convert foreign EDA libraries (EAGLE/Fusion .lbr, Altium .SchLib/.PcbLib) into standard KiCad symbol + footprint, so the rest of the Adom EDA family (adom-symbol/adom-footprint) can render + compare them."++[[bin]]+name = "adom-sfconvert"+path = "src/main.rs"++[dependencies]+anyhow = "1"+regex = "1"+serde_json = "1"+# Native Altium readers — pure Rust, no KiCad / no service round-trip.+altium-pcblib = { path = "../altium-pcblib" }+altium-schlib = { path = "../altium-schlib" }
README.md+57−2@@ -1,3 +1,58 @@-# adom-sfconvert+## What this is -Native Rust converter: EAGLE/Fusion .lbr + Altium .SchLib/.PcbLib → KiCad, rendering each format's own geometry.+`adom-sfconvert` converts foreign EDA libraries — **EAGLE / Fusion 360 `.lbr`**+and **Altium `.SchLib` / `.PcbLib` / `.IntLib`** — into standard KiCad symbol ++footprint, and renders each format's **own** geometry as a native SVG. The name+echoes `adom-ds2sf`: **sf** = **s**ymbol + **f**ootprint.++++*The same part's footprint from four sources — KiCad, Fusion, Altium (native), and+the datasheet — each rendered from its OWN geometry so you can see what disagrees.*++It is **fully native** — pure Rust, runs in your container, **no KiCad install+and no service round-trip**. Each format is read by the right native reader:++| Input | Read by | Native? |+|---|---|---|+| EAGLE / Fusion `.lbr` (XML) | built-in `eagle.rs` | ✅ |+| Altium `.PcbLib` (footprints) | [`altium-pcblib`](https://wiki.adom.inc/skill/altium-pcblib) | ✅ |+| Altium `.SchLib` (symbols) | [`altium-schlib`](https://wiki.adom.inc/skill/altium-schlib) | ✅ |++## Why native matters++The point of pulling a part's CAD from many sources is to **compare what's right+and what's wrong**. If every format is normalised through KiCad's importer first,+the sources converge — they all become "KiCad's interpretation," and the+disagreements you wanted to catch are erased. `adom-sfconvert` renders each+library from its own bytes, so EAGLE looks like EAGLE and Altium looks like+Altium. Conversion to `.kicad_sym` / `.kicad_mod` happens **only on export**.++## Usage++```bash+adom-sfconvert convert part.lbr --out-dir ./out --name part # EAGLE/Fusion+adom-sfconvert convert part.SchLib --out-dir ./out --name part # Altium symbol+adom-sfconvert convert part.PcbLib --out-dir ./out --name part # Altium footprint+adom-sfconvert version+```++Each `convert` emits, into `--out-dir`:++- `<name>.kicad_sym` / `<name>.kicad_mod` — the **export** files.+- `<name>-symbol.svg` / `<name>-footprint.svg` — the **native thumbnails**,+ drawn straight from the source geometry.++## Where it's used++`chip-fetcher` calls `adom-sfconvert` to turn every downloaded EAGLE/Fusion and+Altium library into a per-source thumbnail, shown side-by-side with the KiCad and+datasheet renders so you can pick what to export to your preferred EDA app. After+this tool went native, **nothing foreign touches the KiCad service** — only+KiCad's own format is rendered remotely.++## Companions++- `adom-ds2sf` — extract symbol + footprint from a datasheet PDF.+- `altium-pcblib` / `altium-schlib` — the native Altium readers this builds on.+- `chip-fetcher` — the consumer that shows every source for comparison.
SKILL.mdadded+59@@ -0,0 +1,59 @@+---+name: adom-sfconvert+description: >+ Native Rust CLI that converts foreign EDA libraries — EAGLE/Fusion 360 .lbr and+ Altium .SchLib/.PcbLib/.IntLib — into KiCad symbol + footprint, and renders each+ format's OWN geometry as a native SVG thumbnail. Pure Rust, no KiCad install, no+ service round-trip: EAGLE/Fusion via built-in eagle.rs, Altium footprints via+ altium-pcblib, Altium symbols via altium-schlib. "sf" = symbol + footprint (like+ ds2sf). Conversion to .kicad_sym/.kicad_mod is export-only; rendering stays native+ so cross-source comparison shows each format's truth instead of one normalised+ KiCad interpretation. Used by chip-fetcher for per-source thumbnails.+---++# adom-sfconvert++Convert + natively render EAGLE/Fusion `.lbr` and Altium `.SchLib`/`.PcbLib`.++## When to reach for this++- You have a non-KiCad library (EAGLE/Fusion or Altium) and want a **KiCad+ symbol/footprint** out of it — without installing KiCad.+- You want a **native thumbnail** of a foreign library that shows its own+ geometry (for side-by-side source comparison), not a KiCad-normalised version.++## Command++```bash+adom-sfconvert convert <file> [--out-dir <dir>] [--name <stem>]+```++Recognised inputs: `.lbr` (EAGLE/Fusion XML), `.SchLib` / `.PcbLib` / `.IntLib`+(Altium OLE2 binary, auto-detected by the `D0CF11E0` magic).++Outputs per run: `<stem>.kicad_sym` / `<stem>.kicad_mod` (export) and+`<stem>-symbol.svg` / `<stem>-footprint.svg` (native thumbnails).++## Architecture (all native, all local)++```+.lbr → eagle.rs → KiCad sym/mod + native SVG+.PcbLib → altium-pcblib → KiCad mod + native SVG+.SchLib → altium-schlib → KiCad sym + native SVG+```++No `kicad-cli`, no service POST. The only thing in the wider pipeline that still+calls the shared KiCad service is rendering KiCad's *own* format — correct, since+that isn't a foreign conversion.++## Design rule++**Render native, convert on export.** Converting a foreign format to KiCad just+to draw it throws away exactly the differences a comparison tool exists to find.+Keep originals; emit `.kicad_*` only when the user picks KiCad as the target.++## Companions++- `adom-ds2sf` — datasheet PDF → symbol + footprint.+- `altium-pcblib` / `altium-schlib` — the native Altium readers.+- `chip-fetcher` — shows every source's render for compare-and-contrast.
install.shadded+7@@ -0,0 +1,7 @@+#!/usr/bin/env bash+set -e+HERE="$(cd "$(dirname "$0")" && pwd)"+DEST="$HOME/.claude/skills/adom-sfconvert"+mkdir -p "$DEST"+cp -f "$HERE/skills/adom-sfconvert/SKILL.md" "$DEST/SKILL.md"+echo "Installed adom-sfconvert skill → $DEST"
package.jsonadded+39@@ -0,0 +1,39 @@+{+ "slug": "adom-sfconvert",+ "version": "1.0.0",+ "type": "skill",+ "description": "Native Rust converter: EAGLE/Fusion .lbr + Altium .SchLib/.PcbLib \u2192 KiCad symbol+footprint, rendering each format's OWN geometry. No KiCad install, no service round-trip.",+ "dependencies": {+ "altium-pcblib": "^1.0.0",+ "altium-schlib": "^1.0.0"+ },+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "tags": [+ "eagle",+ "fusion",+ "altium",+ "kicad",+ "rust",+ "eda",+ "converter",+ "footprint",+ "symbol"+ ],+ "hero": {+ "headline": "One converter, every format, native",+ "subhead": "EAGLE/Fusion + Altium \u2192 KiCad, each rendered from its OWN geometry. No KiCad install, no service round-trip.",+ "screenshot": "screenshots/four-source-footprint.png"+ },+ "discovery_triggers": [+ "convert eagle to kicad",+ "convert altium to kicad",+ "convert fusion lbr",+ "altium schlib pcblib",+ "native eda conversion",+ "sfconvert",+ "symbol footprint convert"+ ]+}
screenshots/eagle-native.pngadded⋯ 1 unchanged line ⋯
screenshots/four-source-footprint.pngadded⋯ 1 unchanged line ⋯
skills/adom-sfconvert/SKILL.mdadded+59@@ -0,0 +1,59 @@+---+name: adom-sfconvert+description: >+ Native Rust CLI that converts foreign EDA libraries — EAGLE/Fusion 360 .lbr and+ Altium .SchLib/.PcbLib/.IntLib — into KiCad symbol + footprint, and renders each+ format's OWN geometry as a native SVG thumbnail. Pure Rust, no KiCad install, no+ service round-trip: EAGLE/Fusion via built-in eagle.rs, Altium footprints via+ altium-pcblib, Altium symbols via altium-schlib. "sf" = symbol + footprint (like+ ds2sf). Conversion to .kicad_sym/.kicad_mod is export-only; rendering stays native+ so cross-source comparison shows each format's truth instead of one normalised+ KiCad interpretation. Used by chip-fetcher for per-source thumbnails.+---++# adom-sfconvert++Convert + natively render EAGLE/Fusion `.lbr` and Altium `.SchLib`/`.PcbLib`.++## When to reach for this++- You have a non-KiCad library (EAGLE/Fusion or Altium) and want a **KiCad+ symbol/footprint** out of it — without installing KiCad.+- You want a **native thumbnail** of a foreign library that shows its own+ geometry (for side-by-side source comparison), not a KiCad-normalised version.++## Command++```bash+adom-sfconvert convert <file> [--out-dir <dir>] [--name <stem>]+```++Recognised inputs: `.lbr` (EAGLE/Fusion XML), `.SchLib` / `.PcbLib` / `.IntLib`+(Altium OLE2 binary, auto-detected by the `D0CF11E0` magic).++Outputs per run: `<stem>.kicad_sym` / `<stem>.kicad_mod` (export) and+`<stem>-symbol.svg` / `<stem>-footprint.svg` (native thumbnails).++## Architecture (all native, all local)++```+.lbr → eagle.rs → KiCad sym/mod + native SVG+.PcbLib → altium-pcblib → KiCad mod + native SVG+.SchLib → altium-schlib → KiCad sym + native SVG+```++No `kicad-cli`, no service POST. The only thing in the wider pipeline that still+calls the shared KiCad service is rendering KiCad's *own* format — correct, since+that isn't a foreign conversion.++## Design rule++**Render native, convert on export.** Converting a foreign format to KiCad just+to draw it throws away exactly the differences a comparison tool exists to find.+Keep originals; emit `.kicad_*` only when the user picks KiCad as the target.++## Companions++- `adom-ds2sf` — datasheet PDF → symbol + footprint.+- `altium-pcblib` / `altium-schlib` — the native Altium readers.+- `chip-fetcher` — shows every source's render for compare-and-contrast.
src/altium.rsadded+71@@ -0,0 +1,71 @@+//! Altium `.SchLib` / `.PcbLib` / `.IntLib` → KiCad, **fully native**.+//!+//! These are OLE2 compound-binary files. We read them with the pure-Rust+//! `altium-schlib` (symbols) and `altium-pcblib` (footprints) crates — no KiCad+//! install, no service round-trip. Each format renders its OWN geometry from its+//! OWN bytes (the native-fidelity rule), and we emit `.kicad_sym` / `.kicad_mod`+//! only as an EXPORT convenience.++use anyhow::{anyhow, Result};+use std::path::Path;++/// What a conversion produced, so callers can wire thumbnails + exports.+pub struct AltiumOut {+ pub files: Vec<String>,+}++/// Convert an Altium library. `ext` routes which half we read: `.SchLib` →+/// symbol, `.PcbLib` → footprint, `.IntLib` (or a bare OLE) → try both. Writes+/// `<stem>.kicad_sym` / `<stem>.kicad_mod` (export) and, when `svg` is true, the+/// native `<stem>-symbol.svg` / `<stem>-footprint.svg` thumbnails.+pub fn convert(file: &Path, out_dir: &Path, stem: &str, ext: &str, svg: bool) -> Result<AltiumOut> {+ std::fs::create_dir_all(out_dir).ok();+ let want_sym = matches!(ext, "schlib" | "intlib") || ext.is_empty();+ let want_fp = matches!(ext, "pcblib" | "intlib") || ext.is_empty();++ let mut files = Vec::new();+ let mut errs = Vec::new();++ if want_sym {+ match altium_schlib::parse_altium_schlib_path(file) {+ Ok(symbol) => {+ let ks = altium_schlib::to_kicad_sym(&symbol, stem);+ let p = out_dir.join(format!("{stem}.kicad_sym"));+ std::fs::write(&p, ks)?;+ files.push(p.display().to_string());+ if svg {+ let s = altium_schlib::render_symbol_svg(&symbol);+ let p = out_dir.join(format!("{stem}-symbol.svg"));+ std::fs::write(&p, s)?;+ files.push(p.display().to_string());+ }+ }+ Err(e) => errs.push(format!("sym: {e}")),+ }+ }+ if want_fp {+ match altium_pcblib::parse_altium_pcblib_path(file) {+ Ok(fp) => {+ let km = altium_pcblib::to_kicad_mod(&fp, stem);+ let p = out_dir.join(format!("{stem}.kicad_mod"));+ std::fs::write(&p, km)?;+ files.push(p.display().to_string());+ if svg {+ let s = altium_pcblib::render_footprint_svg(&fp);+ let p = out_dir.join(format!("{stem}-footprint.svg"));+ std::fs::write(&p, s)?;+ files.push(p.display().to_string());+ }+ }+ Err(e) => errs.push(format!("fp: {e}")),+ }+ }++ if files.is_empty() {+ return Err(anyhow!(+ "Altium import produced nothing: {}",+ if errs.is_empty() { "empty library".into() } else { errs.join("; ") }+ ));+ }+ Ok(AltiumOut { files })+}
src/eagle.rsadded+259@@ -0,0 +1,259 @@+//! EAGLE / Fusion 360 `.lbr` (XML) → KiCad `.kicad_sym` + `.kicad_mod`.+//!+//! Fusion 360 inherited EAGLE's XML library format, so one parser covers both.+//! Coordinates in the EAGLE XML are millimetres (KiCad's unit too); EAGLE's Y+//! axis points UP while KiCad footprints point DOWN, so we negate Y.+//!+//! This is a faithful native conversion — no KiCad install, no service round-trip+//! — so each format renders its OWN geometry (export drift between formats shows).++use anyhow::{anyhow, Result};+use regex::Regex;+use std::collections::HashMap;++fn attr(tag: &str, name: &str) -> Option<String> {+ let re = Regex::new(&format!(r#"{name}="([^"]*)""#)).ok()?;+ re.captures(tag).map(|c| c[1].to_string())+}+fn fattr(tag: &str, name: &str) -> Option<f64> {+ attr(tag, name).and_then(|v| v.trim().parse().ok())+}+/// EAGLE rotation string "R90" / "MR180" → degrees (mirror flag ignored here).+fn rot_deg(tag: &str) -> f64 {+ attr(tag, "rot")+ .and_then(|r| r.trim_start_matches(['R', 'M', 'S']).parse::<f64>().ok())+ .unwrap_or(0.0)+}++/// Extract the first `<TAG ...>...</TAG>` (or the whole tag) block by name.+fn block<'a>(xml: &'a str, tag: &str) -> Option<&'a str> {+ let open = format!("<{tag}");+ let start = xml.find(&open)?;+ let close = format!("</{tag}>");+ let end = xml[start..].find(&close).map(|e| start + e + close.len())?;+ Some(&xml[start..end])+}++/// pin-name → pad-number, from the deviceset `<connect .../>` rows.+fn connect_map(xml: &str) -> HashMap<String, String> {+ let mut m = HashMap::new();+ for c in Regex::new(r"<connect\b[^>]*/?>").unwrap().find_iter(xml) {+ let t = c.as_str();+ if let (Some(pin), Some(pad)) = (attr(t, "pin"), attr(t, "pad")) {+ m.insert(pin, pad);+ }+ }+ m+}++/// Convert a `.lbr` to (kicad_sym, kicad_mod). Either may be empty if the+/// library has no symbol / no package.+pub fn convert(lbr: &str, name: &str) -> Result<(String, String)> {+ if !lbr.contains("<eagle") {+ return Err(anyhow!("not an EAGLE/Fusion .lbr (no <eagle> root)"));+ }+ let kicad_mod = block(lbr, "package").map(|p| package_to_mod(p, name)).unwrap_or_default();+ let kicad_sym = block(lbr, "symbol").map(|s| symbol_to_sym(s, name, &connect_map(lbr))).unwrap_or_default();+ Ok((kicad_sym, kicad_mod))+}++// ------------------------------------------------------------------------+// Native SVG renderers — draw straight from the EAGLE geometry, the same way+// altium-pcblib/altium-schlib do, so the .lbr renders its OWN geometry with no+// KiCad round-trip. (EAGLE coords are mm, Y-up → negate Y to screen space.)+// ------------------------------------------------------------------------++fn xesc(s: &str) -> String { s.replace('&', "&").replace('<', "<").replace('>', ">") }++/// Render the EAGLE `<package>` (footprint) to a standalone SVG: SMD + THT pads+/// plus silk/fab wires. Returns "" if the library has no package.+pub fn render_footprint_svg(lbr: &str, name: &str) -> String {+ let Some(pkg) = block(lbr, "package") else { return String::new() };+ // Collect pad rects (x,y,w,h) and silk lines, computing the bbox as we go.+ let mut rects: Vec<(f64, f64, f64, f64, String)> = Vec::new();+ let mut lines: Vec<(f64, f64, f64, f64, f64, String)> = Vec::new();+ let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ let mut bump = |x: f64, y: f64, hw: f64, hh: f64, mnx: &mut f64, mny: &mut f64, mxx: &mut f64, mxy: &mut f64| {+ *mnx = mnx.min(x - hw); *mny = mny.min(y - hh); *mxx = mxx.max(x + hw); *mxy = mxy.max(y + hh);+ };+ for m in Regex::new(r"<smd\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(x), Some(y), Some(dx), Some(dy)) = (fattr(t, "x"), fattr(t, "y"), fattr(t, "dx"), fattr(t, "dy")) else { continue };+ let n = attr(t, "name").unwrap_or_default();+ let (sy, hw, hh) = (-y, dx / 2.0, dy / 2.0);+ bump(x, sy, hw, hh, &mut minx, &mut miny, &mut maxx, &mut maxy);+ rects.push((x - hw, sy - hh, dx, dy, n));+ }+ for m in Regex::new(r"<pad\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(x), Some(y)) = (fattr(t, "x"), fattr(t, "y")) else { continue };+ let n = attr(t, "name").unwrap_or_default();+ let d = fattr(t, "diameter").unwrap_or_else(|| fattr(t, "drill").map(|dr| dr + 0.5).unwrap_or(1.5));+ let (sy, hw) = (-y, d / 2.0);+ bump(x, sy, hw, hw, &mut minx, &mut miny, &mut maxx, &mut maxy);+ rects.push((x - hw, sy - hw, d, d, n));+ }+ for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(x1), Some(y1), Some(x2), Some(y2)) = (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };+ let layer = attr(t, "layer").unwrap_or_default();+ let col = match layer.as_str() { "21" | "22" => "#cfd8dc", "51" | "52" => "#6b7785", _ => continue };+ lines.push((x1, -y1, x2, -y2, fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.12), col.into()));+ }+ if !minx.is_finite() { return String::new(); }+ let pad = 1.0;+ let (vx, vy, vw, vh) = (minx - pad, miny - pad, (maxx - minx) + 2.0 * pad, (maxy - miny) + 2.0 * pad);+ let mut s = format!(+ r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vx:.3} {vy:.3} {vw:.3} {vh:.3}" width="{:.0}" height="{:.0}" font-family="monospace">"#,+ vw * 50.0, vh * 50.0+ );+ s += &format!(r##"<rect x="{vx:.3}" y="{vy:.3}" width="{vw:.3}" height="{vh:.3}" fill="#0d1117"/>"##);+ for (x1, y1, x2, y2, w, col) in &lines {+ s += &format!(r#"<line x1="{x1:.3}" y1="{y1:.3}" x2="{x2:.3}" y2="{y2:.3}" stroke="{col}" stroke-width="{w:.3}"/>"#);+ }+ for (x, y, w, h, n) in &rects {+ s += &format!(r##"<rect x="{x:.3}" y="{y:.3}" width="{w:.3}" height="{h:.3}" rx="0.05" fill="#ff9a4a" stroke="#7a3c10" stroke-width="0.03"/>"##);+ s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="{:.2}" text-anchor="middle" dominant-baseline="middle" fill="#111">{}</text>"##,+ x + w / 2.0, y + h / 2.0, (w.min(*h) * 0.55).clamp(0.2, 0.6), xesc(n));+ }+ s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="0.6" fill="#00e6dc">{} (EAGLE)</text>"##, vx + 0.3, vy + 0.9, xesc(name));+ s += "</svg>\n";+ s+}++/// Render the EAGLE `<symbol>` to a standalone SVG: pins (with names + pad+/// numbers from `<connect>`) plus the body wires. Returns "" if no symbol.+pub fn render_symbol_svg(lbr: &str, name: &str) -> String {+ let Some(sym) = block(lbr, "symbol") else { return String::new() };+ let connects = connect_map(lbr);+ let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ struct P { x: f64, y: f64, len: f64, rot: f64, name: String, num: String }+ let mut pins: Vec<P> = Vec::new();+ for m in Regex::new(r"<pin\b[^>]*/?>").unwrap().find_iter(sym) {+ let t = m.as_str();+ let (Some(pname), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };+ let len = match attr(t, "length").as_deref() { Some("point") => 0.0, Some("short") => 2.54, Some("long") => 7.62, _ => 5.08 };+ let num = connects.get(&pname).cloned().unwrap_or_else(|| pname.clone());+ let (sx, sy) = (x, -y);+ minx = minx.min(sx); maxx = maxx.max(sx); miny = miny.min(sy); maxy = maxy.max(sy);+ pins.push(P { x: sx, y: sy, len, rot: rot_deg(t), name: pname, num });+ }+ let mut lines: Vec<(f64, f64, f64, f64, f64)> = Vec::new();+ for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(sym) {+ let t = m.as_str();+ let (Some(x1), Some(y1), Some(x2), Some(y2)) = (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };+ for (vx, vy) in [(x1, -y1), (x2, -y2)] { minx = minx.min(vx); maxx = maxx.max(vx); miny = miny.min(vy); maxy = maxy.max(vy); }+ lines.push((x1, -y1, x2, -y2, fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.15)));+ }+ if pins.is_empty() && lines.is_empty() { return String::new(); }+ let pad = 4.0;+ let (vx, vy, vw, vh) = (minx - pad, miny - pad, (maxx - minx) + 2.0 * pad, (maxy - miny) + 2.0 * pad);+ let mut s = format!(+ r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vx:.3} {vy:.3} {vw:.3} {vh:.3}" width="{:.0}" height="{:.0}" font-family="monospace">"#,+ vw * 14.0, vh * 14.0+ );+ s += &format!(r##"<rect x="{vx:.3}" y="{vy:.3}" width="{vw:.3}" height="{vh:.3}" fill="#0d1117"/>"##);+ for (x1, y1, x2, y2, w) in &lines {+ s += &format!(r##"<line x1="{x1:.3}" y1="{y1:.3}" x2="{x2:.3}" y2="{y2:.3}" stroke="#00b8b1" stroke-width="{w:.3}"/>"##);+ }+ for p in &pins {+ // pin extends from (x,y) outward along its rotation; R0 points right.+ let (dx, dy) = match ((p.rot / 90.0).round() as i64).rem_euclid(4) {+ 0 => (p.len, 0.0), 1 => (0.0, -p.len), 2 => (-p.len, 0.0), _ => (0.0, p.len),+ };+ let (ex, ey) = (p.x + dx, p.y + dy);+ s += &format!(r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#7cb6ff" stroke-width="0.15"/>"##, p.x, p.y, ex, ey);+ s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="1.2" fill="#e6edf3" text-anchor="middle">{}</text>"##, ex, ey - 0.6, xesc(&p.name));+ s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="0.9" fill="#8b949e" text-anchor="middle">{}</text>"##, ex, ey + 1.4, xesc(&p.num));+ }+ s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="1.6" fill="#00e6dc">{} (EAGLE, {} pins)</text>"##, vx + 0.5, vy + 2.0, xesc(name), pins.len());+ s += "</svg>\n";+ s+}++/// EAGLE `<package>` → KiCad `.kicad_mod` (SMD + THT pads + silk lines).+fn package_to_mod(pkg: &str, name: &str) -> String {+ let mut out = format!(+ "(footprint \"{name}\" (version 20221018) (generator adom-sfconvert) (layer \"F.Cu\")\n \+ (attr smd)\n \+ (fp_text reference \"REF**\" (at 0 -3) (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n \+ (fp_text value \"{name}\" (at 0 3) (layer \"F.Fab\") (effects (font (size 1 1) (thickness 0.15))))\n"+ );+ // SMD pads+ for m in Regex::new(r"<smd\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(n), Some(x), Some(y), Some(dx), Some(dy)) =+ (attr(t, "name"), fattr(t, "x"), fattr(t, "y"), fattr(t, "dx"), fattr(t, "dy")) else { continue };+ let r = rot_deg(t);+ let round = fattr(t, "roundness").unwrap_or(0.0) / 100.0;+ let (shape, extra) = if round > 0.0 {+ ("roundrect".to_string(), format!(" (roundrect_rratio {:.3})", (round / 2.0).min(0.5)))+ } else { ("rect".to_string(), String::new()) };+ let at = if r != 0.0 { format!("{x:.4} {:.4} {r}", -y) } else { format!("{x:.4} {:.4}", -y) };+ out += &format!(+ " (pad \"{n}\" smd {shape} (at {at}) (size {dx:.4} {dy:.4}) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"){extra})\n"+ );+ }+ // THT pads+ for m in Regex::new(r"<pad\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(n), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };+ let drill = fattr(t, "drill").unwrap_or(0.6);+ let dia = fattr(t, "diameter").unwrap_or(drill + 0.5);+ out += &format!(+ " (pad \"{n}\" thru_hole circle (at {x:.4} {:.4}) (size {dia:.4} {dia:.4}) (drill {drill:.4}) (layers \"*.Cu\" \"*.Mask\"))\n",+ -y+ );+ }+ // Silk / fab wires (layer 21 = tPlace silk, 51 = tDocu fab).+ for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(pkg) {+ let t = m.as_str();+ let (Some(x1), Some(y1), Some(x2), Some(y2)) =+ (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };+ let layer = attr(t, "layer").unwrap_or_default();+ let kl = match layer.as_str() { "21" | "22" => "F.SilkS", "51" | "52" => "F.Fab", _ => continue };+ let w = fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.12);+ out += &format!(+ " (fp_line (start {x1:.4} {:.4}) (end {x2:.4} {:.4}) (stroke (width {w:.3}) (type solid)) (layer \"{kl}\"))\n",+ -y1, -y2+ );+ }+ out += ")\n";+ out+}++/// EAGLE `<symbol>` → KiCad `.kicad_sym` (pins + the body rectangle).+fn symbol_to_sym(sym: &str, name: &str, connects: &HashMap<String, String>) -> String {+ let mut pins = String::new();+ let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+ for m in Regex::new(r"<pin\b[^>]*/?>").unwrap().find_iter(sym) {+ let t = m.as_str();+ let (Some(pname), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };+ // EAGLE pin length keyword → mm; the pin's (x,y) is its CONNECTION end.+ let len = match attr(t, "length").as_deref() {+ Some("point") => 0.0, Some("short") => 2.54, Some("long") => 7.62, _ => 5.08,+ };+ let rot = rot_deg(t); // R0 pin body is to the LEFT, pin points right (angle 0 in KiCad means pin extends to the right from its body anchor)+ let num = connects.get(&pname).cloned().unwrap_or_else(|| pname.clone());+ minx = minx.min(x); maxx = maxx.max(x); miny = miny.min(y); maxy = maxy.max(y);+ pins += &format!(+ " (pin bidirectional line (at {x:.3} {y:.3} {rot}) (length {len:.2})\n \+ (name \"{}\" (effects (font (size 1.27 1.27)))) (number \"{}\" (effects (font (size 1.27 1.27)))))\n",+ esc(&pname), esc(&num)+ );+ }+ if minx > maxx { return String::new(); }+ let pad = 2.54;+ format!(+ "(kicad_symbol_lib (version 20211014) (generator adom-sfconvert)\n \+ (symbol \"{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\" \"{name}\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n \+ (symbol \"{name}_1_1\"\n \+ (rectangle (start {:.2} {:.2}) (end {:.2} {:.2}) (stroke (width 0.254) (type default)) (fill (type background)))\n{pins} )\n )\n)\n",+ maxy + pad + 1.27, miny - pad - 1.27,+ minx + pad, maxy + pad, maxx - pad, miny - pad,+ )+}++fn esc(s: &str) -> String { s.replace('"', "'") }
src/main.rsadded+116@@ -0,0 +1,116 @@+//! adom-sfconvert — convert foreign EDA libraries into standard KiCad symbol ++//! footprint, so the Adom EDA family (adom-symbol / adom-footprint) can render+//! and compare them. Named like ds2sf: "sf" = symbol + footprint.+//!+//! Supported today:+//! • EAGLE / Fusion 360 .lbr → KiCad (native, faithful — renders own geometry)+//! Detected but not yet converted (honest):+//! • Altium .SchLib / .PcbLib → needs KiCad's Altium importer (OLE binary)++mod altium;+mod eagle;++use anyhow::{anyhow, Context, Result};+use std::path::PathBuf;++fn main() {+ if let Err(e) = run() {+ eprintln!("ERROR: {e:#}");+ std::process::exit(1);+ }+}++fn usage() -> ! {+ eprintln!(+ "adom-sfconvert {}\n\n\+ USAGE:\n \+ adom-sfconvert convert <library-file> [--out-dir <dir>] [--name <name>]\n \+ adom-sfconvert version\n\n\+ Converts EAGLE/Fusion 360 .lbr (and, soon, Altium .SchLib/.PcbLib) into\n \+ <name>.kicad_sym + <name>.kicad_mod — the standard format every Adom EDA\n \+ app renders. Original files are never modified.",+ env!("CARGO_PKG_VERSION")+ );+ std::process::exit(2);+}++fn run() -> Result<()> {+ let args: Vec<String> = std::env::args().skip(1).collect();+ match args.first().map(String::as_str) {+ Some("version") | Some("--version") | Some("-V") => {+ println!("adom-sfconvert {}", env!("CARGO_PKG_VERSION"));+ Ok(())+ }+ Some("convert") => convert(&args[1..]),+ _ => usage(),+ }+}++fn convert(args: &[String]) -> Result<()> {+ let mut file: Option<PathBuf> = None;+ let mut out_dir: Option<PathBuf> = None;+ let mut name: Option<String> = None;+ let mut i = 0;+ while i < args.len() {+ match args[i].as_str() {+ "--out-dir" => { out_dir = args.get(i + 1).map(PathBuf::from); i += 2; }+ "--name" => { name = args.get(i + 1).cloned(); i += 2; }+ s if file.is_none() => { file = Some(PathBuf::from(s)); i += 1; }+ _ => i += 1,+ }+ }+ let file = file.ok_or_else(|| anyhow!("convert needs a library file"))?;+ let out_dir = out_dir.unwrap_or_else(|| file.parent().map(PathBuf::from).unwrap_or_else(|| PathBuf::from(".")));+ let stem = name.unwrap_or_else(|| file.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "part".into()));++ let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();+ let bytes = std::fs::read(&file).with_context(|| format!("reading {}", file.display()))?;++ // Altium = OLE compound binary (magic d0 cf 11 e0). Read it NATIVELY with+ // altium-schlib / altium-pcblib — pure Rust, no KiCad, no service round-trip.+ // Emits the native SVG thumbnail + .kicad_sym/.kicad_mod (export).+ let is_ole = bytes.starts_with(&[0xD0, 0xCF, 0x11, 0xE0]);+ if ext == "schlib" || ext == "pcblib" || ext == "intlib" || is_ole {+ let out = altium::convert(&file, &out_dir, &stem, &ext, true)?;+ println!("OK: {} (Altium → KiCad, native) → {}", file.display(), out.files.join(" + "));+ return Ok(());+ }++ let text = String::from_utf8_lossy(&bytes);+ if ext == "lbr" || text.contains("<eagle") {+ let (sym, modf) = eagle::convert(&text, &stem)?;+ std::fs::create_dir_all(&out_dir).ok();+ let mut wrote = Vec::new();+ if !sym.is_empty() {+ let p = out_dir.join(format!("{stem}.kicad_sym"));+ std::fs::write(&p, &sym).with_context(|| format!("writing {}", p.display()))?;+ wrote.push(p.display().to_string());+ }+ if !modf.is_empty() {+ let p = out_dir.join(format!("{stem}.kicad_mod"));+ std::fs::write(&p, &modf).with_context(|| format!("writing {}", p.display()))?;+ wrote.push(p.display().to_string());+ }+ // Native SVG thumbnails — drawn straight from the EAGLE geometry, no+ // KiCad round-trip (same as the Altium path).+ let sym_svg = eagle::render_symbol_svg(&text, &stem);+ if !sym_svg.is_empty() {+ let p = out_dir.join(format!("{stem}-symbol.svg"));+ std::fs::write(&p, &sym_svg).ok();+ wrote.push(p.display().to_string());+ }+ let fp_svg = eagle::render_footprint_svg(&text, &stem);+ if !fp_svg.is_empty() {+ let p = out_dir.join(format!("{stem}-footprint.svg"));+ std::fs::write(&p, &fp_svg).ok();+ wrote.push(p.display().to_string());+ }+ if wrote.is_empty() {+ return Err(anyhow!("EAGLE library had no <symbol> or <package> to convert"));+ }+ println!("OK: {} (EAGLE/Fusion, native) → {}", file.display(), wrote.join(" + "));+ return Ok(());+ }++ Err(anyhow!("unrecognized library format: {} (expected .lbr / .SchLib / .PcbLib)", file.display()))+}
uninstall.shadded+3@@ -0,0 +1,3 @@+#!/usr/bin/env bash+rm -rf "$HOME/.claude/skills/adom-sfconvert"+echo "Removed adom-sfconvert skill"