← Commit history
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-lbr"-version = "1.2.1"+version = "1.3.0" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "1.2.2"+version = "1.3.0" edition = "2021" description = "EAGLE .lbr generator, linter, and validator for Fusion 360 Electronics. Converts KiCad symbols + footprints to complete EAGLE libraries." 
README.md+2
@@ -95,6 +95,7 @@ This is the whole point. Fusion will silently show "No Components" or refuse to - **`bind-3d`** — pre-write a 3D binding (self-hosted `wip_urn`, or managed `urn`) into a `.lbr` so the body travels with the library; idempotent. The normal path is the Fusion bridge attach. - **`lint` / `validate`** — catch the things Fusion rejects: missing connects, incomplete devicesets, bad pad maps. - **`import`** — EAGLE `.lbr` → KiCad `.kicad_sym`.+- **`generate --symbol-art <outline.svg>`** — draws adom-symbol's vector chip outline onto the schematic symbol as raw lines (layer 94), sized to the body like adom-symbol does (`--symbol-art-scale` to tune). One `.lbr` → the outline shows in Fusion / Altium / OrCAD. - **`embed`** — a self-contained 3-column (symbol / footprint / 3D) viewer as one `.html`. - **Live app** — a viewer that lints the loaded `.lbr` and shows symbols / packages / devicesets / pins / pads at a glance. @@ -121,6 +122,7 @@ Used by **chip-fetcher** and the Send-to flow across **adom-symbol** and **adom- adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --name PART --output part.lbr adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o part.lbr  # + 3D sidecar adom-lbr bind-3d --lbr part.lbr --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json+adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --symbol-art part-3d-outline.svg -o part.lbr  # draw the chip on the symbol adom-lbr import part.lbr -o part.kicad_sym   # reverse: EAGLE → KiCad adom-lbr serve --port 8784          # live linter app (AI-drivable) adom-lbr embed --dir <chip-dir>     # Fusion-style 3-column viewer as one .html
SKILL.md+16
@@ -61,6 +61,22 @@ almost always: regenerate with `--fp`.  Auto-lints before writing; exits 2 if lint fails. +## Chip outline on the symbol — `--symbol-art`++If the adom-symbol input carries a **3D chip outline** (`<mpn>-3d-outline-*.svg`),+pass it with `--symbol-art` and adom-lbr draws it onto the schematic symbol as raw+`<wire>` lines (layer 94), centred and scaled to ~0.38× the body — matching+adom-symbol's own placement. Tune with `--symbol-art-scale`. The one `.lbr` carries+it into Fusion / Altium / OrCAD.++**Standing directive (owner):** whenever adom-symbol gives you a 3D outline, embed+it — as close to adom-symbol's rendering as you can — on **every** export, not just+Fusion. Today the `.lbr` covers Fusion/Altium/OrCAD; the KiCad `.kicad_sym`+conversion and future native exporters are TODO. See [docs/ROADMAP.md](docs/ROADMAP.md).++Heavy outlines bloat the library (a `-blueprint` ≈ 3000 wires / ~300 KB) — the tool+warns and suggests a lighter variant (`-bold`/`-thin`).+ ## The Fusion 3D attach  A 3D body only renders once Fusion brings the model into a session, so the `.lbr`
VERSION+1−1
@@ -1 +1 @@-1.2.2\ No newline at end of file+1.3.0\ No newline at end of file
bin/adom-lbr
⋯ 1 unchanged line ⋯
docs/ROADMAP.mdadded+41
@@ -0,0 +1,41 @@+# adom-lbr roadmap / standing directives++## Standing directive — match adom-symbol's 3D chip outline on EVERY export++**When a thread asks adom-lbr to make a library and the adom-symbol input carries+a 3D chip outline, ALWAYS embed that outline on the schematic symbol, placed as+close to what adom-symbol renders as possible — for every export endpoint, not+just Fusion.** (Owner's directive, 2026-06-28.)++adom-symbol does the hard work of drawing the chip's vector outline and sizing it+to the symbol body + pin layout. adom-lbr should stay faithful to that, never+invent its own look.++### Status by endpoint++| Endpoint | Path | Outline embedded? |+|---|---|---|+| **Fusion 360** | EAGLE `.lbr` (`generate --symbol-art`) | ✅ done — wires on layer 94 |+| **Altium** | imports the **same** `.lbr` | ✅ done (free — same file) |+| **OrCAD** | imports the **same** `.lbr` | ✅ done (free — same file) |+| **KiCad** | `import` / `deliver kicad` → `.kicad_sym` | ⬜ **TODO** — carry the outline into the `.kicad_sym` (gfx polylines). NB adom-symbol is KiCad-native, so this only matters for the `.lbr → .kicad_sym` round-trip. |+| Future native exporters (Altium `.SchLib`, OrCAD `.olb`, …) | not built yet | ⬜ **TODO** when those land — embed the outline there too. |++So one `.lbr` already covers the three EAGLE-family tools. The open work is the+**KiCad conversion path** and any **future native exporters**.++### How the matching works today (and how to make it exact)++- `generate --symbol-art <mpn>-3d-outline-*.svg` transcribes adom-symbol's+  flattened outline polylines → EAGLE `<wire>`s, centred and scaled to ~0.38× of+  the symbol body's smaller side (tunable with `--symbol-art-scale`). The 0.38 is+  reverse-engineered from adom-symbol's own `-symbol.svg` placement — it's close,+  not exact.+- **To make it pixel-exact**, adom-symbol should emit either (a) the outline+  already in symbol mm-space (same viewBox as `-symbol.svg`), or (b) a sidecar+  with the `{center, scale}` transform + the canonical default variant. See the+  contract proposed to the adom-symbol thread (2026-06-28). When that lands,+  switch adom-lbr to a 1:1 transcription and drop the 0.38 guess.+- Detailed outlines are heavy (`-blueprint` ≈ 3000 `<wire>`s, ~300 KB). adom-lbr+  warns and suggests a lighter variant; ideally adom-symbol emits a decimated+  "lite" outline for embedding.
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "1.2.2",+  "version": "1.3.0",   "type": "app",   "description": "KiCad \u2192 EAGLE .lbr generator for the whole EAGLE family (Fusion 360 / Altium / OrCAD), real-footprint accurate, with 3D-model handoff. Bidirectional KiCad converter + 7-check linter. Includes a focused fusion-export sub-skill.",   "dependencies": {},
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "1.2.2",+  "version": "1.3.0",   "type": "app",   "description": "KiCad → EAGLE .lbr generator for the whole EAGLE family (Fusion 360 / Altium / OrCAD), real-footprint accurate, with 3D-model handoff. Bidirectional KiCad converter + 7-check linter. Includes a focused fusion-export sub-skill.",   "dependencies": {},
src/main.rs+123−3
@@ -37,6 +37,15 @@ enum Commands {         /// Symbol name override         #[arg(long)]         name: Option<String>,+        /// Path to an adom-symbol outline SVG (a vector line-drawing of the+        /// chip). When given, it's drawn as raw `<wire>` lines on the schematic+        /// symbol, centred in the symbol body.+        #[arg(long)]+        symbol_art: Option<PathBuf>,+        /// Size of the symbol art as a fraction of the symbol body's smaller+        /// side (default 0.38, matching adom-symbol's placement). Higher = bigger.+        #[arg(long, default_value_t = 0.38)]+        symbol_art_scale: f64,         /// Path to a 3D model (STEP/STP/GLB) to associate with the package.         /// The .lbr stays 2D-only (a valid Fusion 3D ref needs an Autodesk         /// cloud URN that can only be minted by Fusion). Instead, a sidecar@@ -882,7 +891,88 @@ fn bind_3d_urn(content: &str, link: &str, managed: bool, package: Option<&str>)     Ok(out) } -fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>) -> String {+/// Transcribe an adom-symbol outline SVG (a flattened line drawing of the chip)+/// into EAGLE symbol `<wire>`s on layer 94, so the schematic symbol carries a+/// little vector picture of the part. The SVG uses `<polyline>`/`<polygon>`+/// `points=` in a Y-down pixel space; we decimate the dense sampling, fit the+/// drawing inside the symbol body (centred), and negate Y for EAGLE's Y-up.+/// Returns the `<wire>` block plus the segment count (for a hint).+fn symbol_art_wires(svg: &str, body: Option<&KicadBody>, scale_frac: f64) -> (String, usize) {+    let poly_re = regex_lite::Regex::new(r#"<(polyline|polygon)\b[^>]*\bpoints="([^"]+)""#).unwrap();+    let mut polys: Vec<Vec<(f64, f64)>> = Vec::new();+    for c in poly_re.captures_iter(svg) {+        let closed = &c[1] == "polygon";+        let mut pts: Vec<(f64, f64)> = c[2]+            .split_whitespace()+            .filter_map(|p| {+                let mut it = p.split(',');+                let x: f64 = it.next()?.trim().parse().ok()?;+                let y: f64 = it.next()?.trim().parse().ok()?;+                Some((x, y))+            })+            .collect();+        if closed {+            if let Some(&first) = pts.first() { pts.push(first); }+        }+        polys.push(pts);+    }+    // Global bbox of every point (in SVG units).+    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);+    for poly in &polys {+        for &(x, y) in poly {+            minx = minx.min(x); miny = miny.min(y);+            maxx = maxx.max(x); maxy = maxy.max(y);+        }+    }+    let (sw, sh) = (maxx - minx, maxy - miny);+    if !(sw > 0.0 && sh > 0.0) {+        return (String::new(), 0);+    }+    // Fit a modest, centred icon — matching adom-symbol's own placement, which+    // sizes the outline to ~half the body's smaller side so it sits between the+    // pins/labels without crowding them.+    let (target, cx, cy) = match body {+        Some(b) => {+            let bw = (b.right - b.left).abs();+            let bh = (b.top - b.bottom).abs();+            (bw.min(bh) * scale_frac, (b.left + b.right) / 2.0, (b.top + b.bottom) / 2.0)+        }+        None => (26.0 * scale_frac, 0.0, 0.0),+    };+    let scale = target / sw.max(sh);+    // Decimate in OUTPUT space: drop points within `tol_mm` of the last kept one,+    // so curves stay smooth but the dense sampling collapses.+    let tol_mm = 0.12;+    let (smx, smy) = ((minx + maxx) / 2.0, (miny + maxy) / 2.0);+    let map = |x: f64, y: f64| (cx + (x - smx) * scale, cy - (y - smy) * scale);++    let mut out = String::new();+    let mut segs = 0usize;+    for poly in &polys {+        let mut kept: Vec<(f64, f64)> = Vec::new();+        let n = poly.len();+        for (i, &(px, py)) in poly.iter().enumerate() {+            let p = map(px, py);+            if i == 0 || i == n - 1 {+                kept.push(p);+            } else if let Some(&last) = kept.last() {+                if ((p.0 - last.0).powi(2) + (p.1 - last.1).powi(2)).sqrt() >= tol_mm {+                    kept.push(p);+                }+            }+        }+        for w in kept.windows(2) {+            out.push_str(&format!(+                "  <wire x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\" width=\"0.1016\" layer=\"94\"/>\n",+                w[0].0, w[0].1, w[1].0, w[1].1+            ));+            segs += 1;+        }+    }+    (out, segs)+}++fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&str>, symbol_art_scale: f64) -> String {     let mut xml = String::with_capacity(8192);      xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -999,6 +1089,13 @@ fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>) -> String {         xml.push_str(&format!("  <wire x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" width=\"0.254\" layer=\"94\"/>\n", body.left, body.bottom, body.left, body.top));     } +    // Optional: a vector drawing of the chip (adom-symbol's outline SVG) drawn+    // as raw lines, centred inside the symbol body.+    if let Some(svg) = symbol_art {+        let (art, _) = symbol_art_wires(svg, sym.body.as_ref(), symbol_art_scale);+        xml.push_str(&art);+    }+     let name_y = sym.body.as_ref().map(|b| b.top + 2.54).unwrap_or(10.0);     let value_y = sym.body.as_ref().map(|b| b.bottom - 2.54).unwrap_or(-10.0);     xml.push_str(&format!("  <text x=\"0\" y=\"{}\" size=\"1.27\" layer=\"95\" align=\"bottom-center\">&gt;NAME</text>\n", name_y));@@ -1209,7 +1306,13 @@ fn main() {     let cli = Cli::parse();      match cli.command {-        Commands::Generate { sym, fp, output, name, model } => {+        Commands::Generate { sym, fp, output, name, model, symbol_art, symbol_art_scale } => {+            let symbol_art_content = symbol_art.as_ref().map(|p| {+                fs::read_to_string(p).unwrap_or_else(|e| {+                    eprintln!("ERROR: Cannot read symbol art {}: {}", p.display(), e);+                    process::exit(1);+                })+            });             let fp_content = fp.as_ref().map(|p| {                 fs::read_to_string(p).unwrap_or_else(|e| {                     eprintln!("ERROR: Cannot read footprint {}: {}", p.display(), e);@@ -1244,7 +1347,7 @@ fn main() {             };              let out_default = sym.as_ref().or(fp.as_ref()).cloned().unwrap_or_else(|| PathBuf::from(&parsed.name));-            let lbr = generate_lbr(&parsed, fp_content.as_deref());+            let lbr = generate_lbr(&parsed, fp_content.as_deref(), symbol_art_content.as_deref(), symbol_art_scale);              // Lint before writing             let lint = lint_lbr(&lbr);@@ -1283,6 +1386,23 @@ fn main() {                     println!("HINT: add --model <part.step> to also emit a Fusion 3D handoff sidecar (the bridge attaches the body).");                 }             }+            if let Some(svg) = &symbol_art_content {+                let (_, segs) = symbol_art_wires(svg, parsed.body.as_ref(), symbol_art_scale);+                let kb = lbr.len() / 1024;+                if segs == 0 {+                    println!("HINT: --symbol-art SVG had NO <polyline>/<polygon> points — nothing drawn. Give a flattened outline SVG (adom-symbol's `<mpn>-3d-outline-*.svg`), not a raster or a <path>-only file.");+                } else {+                    println!("HINT: drew the chip outline on the symbol — {segs} segments on layer 94, centred + scaled to the symbol body (matching adom-symbol's placement).");+                    if segs > 2500 || kb > 200 {+                        println!("WARN: HEAVY symbol art — {segs} segments, ~{kb} KB .lbr. Detailed outlines (e.g. `-blueprint`) bloat the library and can lag Fusion's symbol editor.");+                        println!("      → Pass a SIMPLER outline instead (`<mpn>-3d-outline-bold.svg` or `-thin.svg` are ~1/3 the size), or drop the art for a 19 KB symbol.");+                    } else if kb > 80 {+                        println!("HINT: moderate weight (~{kb} KB). Fine for one part; if you're batching a whole library, prefer the lighter `-bold`/`-thin` outlines.");+                    } else {+                        println!("HINT: light + clean (~{kb} KB). Want the full iso wireframe? a `-blueprint` outline has more detail (bigger file).");+                    }+                }+            }             println!("NEXT: this .lbr imports directly into Fusion 360 / Altium / OrCAD. For KiCad: `adom-lbr import {} -o part.kicad_sym`.", out_path.display());              // 3D model: emit a sidecar manifest for the Fusion bridge. A valid