app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
Publish 1.1.1
10 files changed
+219−127
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-lbr"-version = "1.1.0"+version = "1.1.1" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "1.1.0"+version = "1.1.1" edition = "2021" description = "EAGLE .lbr generator, linter, and validator for Fusion 360 Electronics. Converts KiCad symbols + footprints to complete EAGLE libraries."
README.md+12−9@@ -31,19 +31,22 @@ placeholder; that now survives only as the fallback when `--fp` is omitted.) adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod -o part.lbr ``` -**3D bodies for Fusion** are a handoff, by necessity: Fusion references a 3D model-by an Autodesk **cloud URN** (`urn:adsk.eagle:package:NNNN/V`) that only Fusion can-mint. So `--model part.step` emits the `.lbr` **plus** a `part.fusion3d.json`-manifest; the Fusion bridge uploads the model and mints the URN; then `bind-3d`-bakes it into the `.lbr` so the library is self-contained:+**3D bodies for Fusion** are a Fusion-side attach: a model only renders once Fusion+brings it into a session, so `--model part.step` keeps the `.lbr` 2D and emits a+`part.fusion3d.json` manifest pointing at the model + package. The **Fusion bridge**+does the attach (`Create3DPackage → import → orient → FINISH → Save`), which both+binds and renders. Optionally, if the model is already cloud-synced, `bind-3d`+pre-writes the binding into the `.lbr` (self-uploaded models use a `wip_urn`+`urn:adsk.wipprod:fs.file:vf…?version=1`, not `urn:adsk.eagle:package` — that's+managed-only): ```bash adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o part.lbr-adom-lbr bind-3d --lbr part.lbr --urn "urn:adsk.eagle:package:NNNN/1" --sidecar part.fusion3d.json+adom-lbr bind-3d --lbr part.lbr --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json ``` → **Full walkthrough: [docs/fusion-export.md](docs/fusion-export.md)** — pad-mapping,-the cloud-URN problem, the 3-step handoff, and Altium/OrCAD notes.+the verified 3D markup, the bridge attach vs. the `bind-3d` fast-path, and Altium/OrCAD notes. ## The 3-column library viewer — and an interactive embed @@ -84,7 +87,7 @@ This is the whole point. Fusion will silently show "No Components" or refuse to ## What it does - **`generate`** — `.kicad_sym` (+ real `.kicad_mod` via `--fp`) → native `.lbr`, lint-checked for Fusion compatibility. `--model` also emits a 3D handoff sidecar.-- **`bind-3d`** — bake a Fusion-minted cloud URN into a `.lbr` so the 3D body travels with the library (idempotent).+- **`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`. - **`embed`** — a self-contained 3-column (symbol / footprint / 3D) viewer as one `.html`.@@ -112,7 +115,7 @@ Used by **chip-fetcher** and the Send-to flow across **adom-symbol** and **adom- ```bash 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 --urn "urn:adsk.eagle:package:NNNN/1" --sidecar part.fusion3d.json+adom-lbr bind-3d --lbr part.lbr --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json 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+25−18@@ -30,7 +30,7 @@ native symbol via `import`. ``` adom-lbr generate --sym <part.kicad_sym> [--fp <part.kicad_mod>] [--model <part.step>] [-o out.lbr] [--name <override>]-adom-lbr bind-3d --lbr <part.lbr> --urn <urn:adsk.eagle:package:NNNN/V> [--package <name>] [--sidecar <part.fusion3d.json>] [-o out.lbr]+adom-lbr bind-3d --lbr <part.lbr> (--wip-urn <urn:adsk.wipprod:fs.file:vf.<id>?version=1> | --urn <urn:adsk.eagle:package:NNNN/V>) [--package <name>] [--sidecar <part.fusion3d.json>] [-o out.lbr] adom-lbr import <part.lbr> [-o out.kicad_sym] adom-lbr lint <part.lbr> # JSON error codes adom-lbr validate <part.lbr> # human-readable@@ -49,8 +49,9 @@ built from the real footprint: - one `<smd>` (or `<pad>` for thru-hole) per `.kicad_mod` pad, at its true position — a 4-sided LQFP stays 4-sided, BGA stays a grid, etc.-- KiCad is Y-down, EAGLE is Y-up → Y is negated; pad rotation maps to EAGLE- `rot=R90/R180/R270`.+- KiCad is Y-down, EAGLE is Y-up → Y is negated; the KiCad pad angle maps+ straight to EAGLE `rot=R90/R180/R270` (Fusion's own convention — top/bottom+ QFP/SOIC pads come out `R90`). - silkscreen outline derived from the footprint body (`F.SilkS`, else `F.Fab`, else pad extents). @@ -60,23 +61,28 @@ almost always: regenerate with `--fp`. Auto-lints before writing; exits 2 if lint fails. -## The Fusion 3D / cloud-URN handoff+## The Fusion 3D attach -A Fusion `.lbr` references a 3D body by an Autodesk **cloud URN**-(`urn:adsk.eagle:package:NNNN/V`) that only Fusion can mint when it uploads the-model. It can't be made offline. So:+A 3D body only renders once Fusion brings the model into a session, so the `.lbr`+stays 2D and the **Fusion bridge does the attach** (verified by the bridge against+a round-tripped `.flbr`): 1. `generate --model part.step` emits the `.lbr` **plus** a `part.fusion3d.json`- sidecar (schema `adom-lbr.fusion3d/1`): model path, format, units, pin-1/seat- alignment note, `urn: null`, `urn_status: "pending-fusion-mint"`, and- `bridge_steps` with the exact XML to write.-2. The Fusion bridge imports the `.lbr`, uploads `model.path`, Fusion mints the- URN.-3. `bind-3d --lbr part.lbr --urn <minted> --sidecar part.fusion3d.json` patches- `<packages3d>` + the device `<package3dinstance>` into the `.lbr` (idempotent)- and flips the sidecar to `urn_status: "bound"`.--See [docs/fusion-export.md](docs/fusion-export.md) for the full flow.+ sidecar (schema `adom-lbr.fusion3d/2`): `model.path`, format, units, placement,+ a `binding` block, and `status: "pending-fusion-attach"`. No URN is pre-minted.+2. The bridge opens the `.lbr` (library active) → `Create3DPackage` → import+ `model.path` → orient → **FINISH** → Save. This both binds and renders.+3. **Optional fast-path** — if the model is already cloud-synced:+ `bind-3d --lbr part.lbr --wip-urn <urn:adsk.wipprod:fs.file:vf.<id>?version=1>`+ pre-writes the binding (`urn=""`, `wip_urn=…`, `locally_modified="yes"`).+ Use `--urn <urn:adsk.eagle:package:…>` only for managed/library.io parts.+ Caveat: a pre-written `wip_urn` shows a gray placeholder until the model is+ brought into a session — so this is not a substitute for the bridge attach.++URN families: **self-hosted** model → `wip_urn` = versioned `fs.file:vf …?version=1`+(NOT the `dm.lineage` URN); **managed** part → `urn:adsk.eagle:package:NNN/V`.++See [docs/fusion-export.md](docs/fusion-export.md) for the full flow + exact markup. ## import @@ -106,7 +112,8 @@ in `<connects>` are unaffected. ``` adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o part.lbr # Fusion/Altium/OrCAD import part.lbr directly.-# Fusion 3D: bridge mints URN → adom-lbr bind-3d --lbr part.lbr --urn <urn> --sidecar part.fusion3d.json+# Fusion 3D: bridge attaches the model (Create3DPackage → FINISH → Save). Optional pre-write:+# adom-lbr bind-3d --lbr part.lbr --wip-urn <urn:adsk.wipprod:fs.file:vf.<id>?version=1> --sidecar part.fusion3d.json ``` ### EAGLE .lbr → KiCad
VERSION+1−1@@ -1 +1 @@-1.1.0+1.1.1
bin/adom-lbr⋯ 1 unchanged line ⋯
docs/fusion-export.md+64−47@@ -38,13 +38,14 @@ What the writer does with each `.kicad_mod` pad: | KiCad (`.kicad_mod`, mm, Y-down) | EAGLE (`.lbr`, mm, Y-up) | |---|---|-| `(pad "17" smd rect (at -3.75 5.738 90) (size 1.475 0.3))` | `<smd name="17" x="-3.75" y="-5.738" dx="1.475" dy="0.3" layer="1" rot="R270"/>` |+| `(pad "17" smd rect (at -3.75 5.738 90) (size 1.475 0.3))` | `<smd name="17" x="-3.75" y="-5.738" dx="1.475" dy="0.3" layer="1" rot="R90"/>` | | thru-hole `(pad … (at x y) (size …) (drill d))` | `<pad … x drill diameter shape="long"/>` | - **Y is negated** (KiCad Y-down → EAGLE Y-up).-- **Rotation** maps to `rot=R90/R180/R270`; because the Y flip reverses the- rotation sense, the angle is negated (`R90 ↔ R270`). Rectangular pads are- 180°-symmetric so this is cosmetic for them, but it's correct for any shape.+- **Rotation** maps the KiCad angle straight to `rot=R90/R180/R270`, matching+ Fusion's own library-export convention (verified by the Fusion bridge against a+ round-tripped `.flbr`): a KiCad-90 pad on a QFP/SOIC top/bottom side becomes+ `rot="R90"`, oriented along the lead. - **Silkscreen outline** is derived from the footprint body — `F.SilkS` lines first, then `F.Fab`, then a fallback bounding box of the pads. Each `fp_line` is matched to its *own* layer (a balanced s-expression parse), so courtyard@@ -64,30 +65,43 @@ Without `--fp` you still get a *valid* `.lbr` — just a two-column placeholder package. That's intentional for symbol-only parts. For anything you'll fabricate, pass `--fp`. -## The 3D model: a cloud-URN handoff+## The 3D model: a Fusion-side attach (not a flat-file write) -This is the part that can't be done in a flat file. In EAGLE/Fusion, a package's-3D body is **not embedded** — it's referenced by an Autodesk **cloud URN**:+This is the part that can't be baked into a flat file. In EAGLE/Fusion a+package's 3D body is **not embedded** — it's a cloud reference, and the model+only **renders once Fusion brings it into a session**. So adom-lbr keeps the+`.lbr` 2D and the **Fusion bridge does the attach**; the sidecar just points the+bridge at the model and the package.++> **Verified by the Fusion bridge** (round-tripping a bound `.flbr` and reading+> the EAGLE XML). The earlier assumption that Fusion mints an+> `urn:adsk.eagle:package:NNN/V` was **wrong for self-uploaded models** — that+> form is for *managed* (library.io) parts only.++### The real markup — self-uploaded model++A model you upload yourself is linked by **`wip_urn`** (the *versioned file* URN),+with `urn` left **empty** and `locally_modified="yes"`: ```xml <packages3d>- <package3d name="ADS8588SIPM" urn="urn:adsk.eagle:package:99887/2" type="model">+ <package3d name="ADS8588SIPM" urn="" wip_urn="urn:adsk.wipprod:fs.file:vf.<id>?version=1"+ locally_modified="yes" type="model"> <packageinstances><packageinstance name="ADS8588SIPM"/></packageinstances> </package3d> </packages3d> … <device name="" package="ADS8588SIPM"> <connects>…</connects>- <package3dinstances><package3dinstance package3d_urn="urn:adsk.eagle:package:99887/2"/></package3dinstances>+ <package3dinstances><package3dinstance package3d_urn="urn:adsk.wipprod:fs.file:vf.<id>?version=1"/></package3dinstances> <technologies><technology name=""/></technologies> </device> ``` -That URN is **minted by Fusion** when it uploads/generates the 3D package into-the managed cloud library. **It cannot be fabricated offline** — a made-up URN-won't resolve. So adom-lbr splits the job:+Use the `?version=1` **file** URN (`fs.file:vf`), **not** the `dm.lineage` URN+that an upload returns first. (`urn:adsk.eagle:package:NNN/V` is managed-only.) -### Step 1 — emit the `.lbr` + a 3D sidecar+### Step 1 — generate the `.lbr` + a 3D sidecar ```bash adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o part.lbr@@ -95,60 +109,62 @@ adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o # → part.fusion3d.json (the handoff manifest) ``` -The `.lbr` stays 2D. The sidecar (`schema: "adom-lbr.fusion3d/1"`) tells the-Fusion bridge what to do:+The sidecar (`schema: "adom-lbr.fusion3d/2"`) carries no pre-minted URN — it just+names `model.path`, `package`, and the placement: ```json {- "schema": "adom-lbr.fusion3d/1",+ "schema": "adom-lbr.fusion3d/2", "package": "ADS8588SIPM",- "library": "part.lbr", "model": { "path": "part.step", "format": "step", "units": "mm" },- "placement": {- "translation_mm": [0, 0, 0],- "rotation_deg": [0, 0, 0],- "note": "Footprint origin is the package centre; pin 1 per the .lbr <smd>. Run chipfit to confirm pin-1 alignment / seat Z before binding."- },- "urn": null,- "urn_status": "pending-fusion-mint",- "bridge_steps": [ "…import…", "…generate 3D package, capture URN…", "…bind…", "…save…", "…verify…" ]+ "placement": { "translation_mm": [0,0,0], "rotation_deg": [0,0,0], "note": "…run chipfit before FINISH" },+ "binding": { "scheme": "self-hosted", "wip_urn": null, "package3d_attrs": { "urn": "", "wip_urn": "<…vf…?version=1>", "locally_modified": "yes", "type": "model" } },+ "status": "pending-fusion-attach",+ "bridge_steps": [ "open .lbr (library ACTIVE)", "Create3DPackage → import model → orient → FINISH → Save", "…" ] } ``` -### Step 2 — Fusion mints the URN+### Step 2 — the bridge attaches the model (this both binds AND renders) -The Fusion bridge imports the `.lbr`, uploads `model.path`, and Fusion assigns a-URN like `urn:adsk.eagle:package:99887/2`. (Run [chipfit](../../adom-chipfit) here-to confirm pin-1 / seat-plane alignment before committing.)+The Fusion bridge: opens the `.lbr` (library **active**) → `Electron.Create3DPackage`+→ imports `model.path` → auto-orients → `Package3DStop` (**FINISH**) → **Save**.+The Content Manager tree then shows the 3D nested under the package. (Run+[chipfit](../../adom-chipfit) before FINISH to confirm pin-1 / seat-Z.) -### Step 3 — bake the URN back in+This is the whole job for the normal case — no `bind-3d` needed, because the+FINISH step is what makes the body actually render.++### Optional — pre-write the binding (fast-path)++If the model is **already cloud-synced** and you have its versioned file URN, you+can pre-write the binding so a downstream consumer gets a self-contained `.lbr`: ```bash-adom-lbr bind-3d --lbr part.lbr --urn "urn:adsk.eagle:package:99887/2" --sidecar part.fusion3d.json+adom-lbr bind-3d --lbr part.lbr \+ --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json+# managed/library.io part instead? --urn "urn:adsk.eagle:package:NNN/V" ``` -`bind-3d` inserts the `<packages3d>` block after `</packages>` and the-`<package3dinstance>` into the device, then re-lints. It's **idempotent** —-re-running with a new URN cleanly replaces the old binding (one `<packages3d>`,-one instance). With `--sidecar` it flips the manifest to `urn_status: "bound"`-and records the URN.--Result: a self-contained `.lbr` that places in Fusion *with* its 3D body —-ready to redistribute.+`bind-3d` inserts the `<packages3d>` block + the device `<package3dinstance>`,+re-lints, and (with `--sidecar`) flips the manifest to `status: "bound"`. It's+**idempotent**. **Caveat:** a pre-written `wip_urn` shows a **gray placeholder**+on a cold open — Fusion only renders the body once the model is brought into a+session via the FINISH step above. So `bind-3d` is for known-synced models, not a+substitute for the bridge attach. ## End-to-end ```bash-# 1. Generate the .lbr + sidecar from the KiCad part + STEP+# 1. Generate the 2D .lbr + sidecar from the KiCad part + STEP adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o part.lbr # 2. Hand part.lbr + part.fusion3d.json + the STEP to the Fusion bridge.-# It imports, uploads the model, and returns the minted cloud URN.+# It does Create3DPackage → import → orient → FINISH → Save (binds + renders). -# 3. Bake the URN in-adom-lbr bind-3d --lbr part.lbr --urn "<minted-urn>" --sidecar part.fusion3d.json+# 3. (Optional) if the model is already synced, bake the binding in:+adom-lbr bind-3d --lbr part.lbr --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json -# 4. Verify+# 4. Verify the 2D library adom-lbr validate part.lbr # complete deviceset, 0 errors ``` @@ -165,6 +181,7 @@ it on the wiki — adom-lbr's "Send to" already routes those targets. |---|---|---| | Footprint is two columns / a domino | generated without `--fp` | regenerate with `--fp part.kicad_mod` | | Fusion: "No Components" | empty `<packages>` or `<connects>` | `adom-lbr lint` — fix the reported code |-| 3D body missing in Fusion | `.lbr` has no `<packages3d>` URN | run `bind-3d` after Fusion mints the URN |-| 3D body offset / wrong pin 1 | placement not aligned | run chipfit, set `placement` in the sidecar before binding |-| Outline looks lopsided | (fixed in 0.2) | upgrade — silk lines are now matched per-layer |+| 3D body is a gray placeholder | binding pre-written but model not in session | let the bridge attach it (Create3DPackage → FINISH); that renders it |+| 3D body offset / wrong pin 1 | placement not aligned | run chipfit, set `placement` in the sidecar before FINISH |+| `bind-3d` rejected the URN | wrong URN family | self-uploaded → `--wip-urn` (`fs.file:vf …?version=1`); managed → `--urn` (`eagle:package`) |+| Outline looks lopsided | (fixed in 1.1) | upgrade — silk lines are now matched per-layer |
package.json+3−6@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "1.1.0",+ "version": "1.1.1", "type": "app", "description": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.", "dependencies": {},@@ -13,11 +13,8 @@ "viewer" ], "hero": {- "type": "billboard",- "headline": "See the part, three ways.",- "subhead": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.",- "screenshot": "screenshots/shot.png",- "accent": "#00b8b1"+ "type": "image",+ "path": "adom-lbr-hero-v4.png" }, "scripts": { "install": "./install.sh",
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "1.1.0",+ "version": "1.1.1", "type": "app", "description": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.", "dependencies": {},
src/main.rs+111−43@@ -46,25 +46,31 @@ enum Commands { #[arg(long)] model: Option<PathBuf>, },- /// Bind a Fusion-minted 3D-package cloud URN into an existing .lbr, making- /// it self-contained: adds the `<packages3d>` entry and the device's- /// `<package3dinstance>`. Run this after the Fusion bridge uploads the model- /// and mints `urn:adsk.eagle:package:NNN/V`. Idempotent (re-running re-binds).+ /// Pre-write a 3D-model binding into an existing .lbr (fast-path for a+ /// model that's already cloud-synced). Adds the `<packages3d>` entry + the+ /// device's `<package3dinstance>`. Idempotent. NB the normal path is to let+ /// the Fusion bridge attach the model (Create3DPackage → FINISH), which both+ /// binds AND renders; a pre-written self-hosted binding shows a gray+ /// placeholder until the model is brought into a Fusion session. #[command(name = "bind-3d")] Bind3d { /// Path to the .lbr to patch #[arg(long)] lbr: PathBuf,- /// The Autodesk cloud URN Fusion minted (urn:adsk.eagle:package:NNN/V)+ /// Self-hosted model: the versioned file URN+ /// (urn:adsk.wipprod:fs.file:vf.<id>?version=1). The usual case.+ #[arg(long, conflicts_with = "urn")]+ wip_urn: Option<String>,+ /// Managed (library.io) part: an urn:adsk.eagle:package:NNN/V URN. #[arg(long)]- urn: String,+ urn: Option<String>, /// Package to attach to (default: the .lbr's first/device package) #[arg(long)] package: Option<String>, /// Output path (default: patch --lbr in place) #[arg(short, long)] output: Option<PathBuf>,- /// Also patch this sidecar manifest's `urn`/`urn_status` to bound+ /// Also patch this sidecar manifest's binding + status to bound #[arg(long)] sidecar: Option<PathBuf>, },@@ -610,12 +616,14 @@ fn parse_mod_pads(km: &str) -> Vec<ModPad> { pads } -/// Map a KiCad pad rotation (degrees, Y-down) to an EAGLE `rot=` attribute.-/// Y is flipped going KiCad→EAGLE, so the rotation sense is negated. Rect pads-/// are 180°-symmetric, so R90/R270 are visually identical, but we negate for-/// correctness on non-symmetric shapes.+/// Map a KiCad pad rotation (degrees) to an EAGLE `rot=` attribute, matching+/// Fusion 360's own library export convention: the KiCad angle maps directly+/// (KiCad 90 → `R90`), so a non-square pad on a QFP/SOIC top/bottom side is+/// oriented along the lead. Rect pads are 180°-symmetric so the Y-flip sense+/// is moot; the direct mapping is what Fusion writes (verified by the Fusion+/// bridge against a round-tripped `.flbr`). fn smd_rotation(angle: f64) -> &'static str {- let a = (((360.0 - angle).round() as i64) % 360 + 360) % 360;+ let a = ((angle.round() as i64) % 360 + 360) % 360; match a { 90 => " rot=\"R90\"", 180 => " rot=\"R180\"",@@ -664,11 +672,16 @@ fn package_outline(km: &str, pads: &[ModPad]) -> (f64, f64, f64, f64) { } /// Write the `<out>.fusion3d.json` sidecar that hands the Fusion bridge the-/// 3D-model association the `.lbr` itself cannot carry. EAGLE/Fusion reference-/// a 3D body by an Autodesk cloud URN (`urn:adsk.eagle:package:NNN/V`) that is-/// only assigned when Fusion uploads/generates the 3D package — it can't be-/// minted offline. So we ship the model path + alignment intent and let the-/// bridge mint the URN and patch it back into the `.lbr`.+/// 3D-model association the `.lbr` itself cannot carry. The `.lbr` stays 2D; the+/// bridge attaches the body Fusion-side (Create3DPackage → import → orient →+/// FINISH → Save), which both binds the package AND renders it. The sidecar just+/// points the bridge at `model.path` + `package` — no URN is pre-minted.+///+/// Binding scheme (verified by the Fusion bridge against a round-tripped+/// `.flbr`): a SELF-UPLOADED model is linked by `wip_urn`+/// (`urn:adsk.wipprod:fs.file:vf.<id>?version=1` — the *versioned file* URN, not+/// the `dm.lineage` URN), with `urn=""` empty and `locally_modified="yes"`. The+/// `urn:adsk.eagle:package:NNN/V` form is only for MANAGED (library.io) parts. fn write_fusion3d_sidecar( side_path: &std::path::Path, package: &str,@@ -684,7 +697,7 @@ fn write_fusion3d_sidecar( other => other, }; let manifest = serde_json::json!({- "schema": "adom-lbr.fusion3d/1",+ "schema": "adom-lbr.fusion3d/2", "package": package, "library": lbr_path.to_string_lossy(), "model": {@@ -697,29 +710,52 @@ fn write_fusion3d_sidecar( // seat plane at Z=0. Identity unless chipfit reports an offset. "translation_mm": [0.0, 0.0, 0.0], "rotation_deg": [0.0, 0.0, 0.0],- "note": "Footprint origin is the package centre; pin 1 per the .lbr <smd>. Run chipfit to confirm pin-1 alignment / seat Z before binding."+ "note": "Footprint origin is the package centre; pin 1 per the .lbr <smd>. Run chipfit to confirm pin-1 alignment / seat Z before the FINISH step." },- "urn": serde_json::Value::Null,- "urn_status": "pending-fusion-mint",+ // How the bridge should link the model once it's in Fusion. Self-hosted+ // is the normal path; managed is only for library.io parts.+ "binding": {+ "scheme": "self-hosted",+ "wip_urn": serde_json::Value::Null,+ "package3d_attrs": {+ "urn": "",+ "wip_urn": "<urn:adsk.wipprod:fs.file:vf.<id>?version=1>",+ "locally_modified": "yes",+ "type": "model"+ },+ "note": "Self-uploaded model → wip_urn (versioned fs.file:vf, ?version=1) with urn=\"\" empty. urn:adsk.eagle:package is MANAGED-only. A bare wip_urn does NOT auto-render on a cold open — the model still has to be brought into a session via the generator FINISH."+ },+ "status": "pending-fusion-attach", "bridge_steps": [- "Import the .lbr into the Fusion library editor — the 2D symbol + footprint are already valid.",- format!("Generate a 3D package for package '{package}' by uploading model.path; Fusion mints an Autodesk cloud URN (urn:adsk.eagle:package:NNN/V)."),- format!("Bind it: add <packages3d><package3d name=\"{package}\" urn=\"<URN>\" type=\"model\"><packageinstances><packageinstance name=\"{package}\"/></packageinstances></package3d>, and on the device add <package3dinstances><package3dinstance package3d_urn=\"<URN>\"/></package3dinstances>."),- "Save the library and write the minted URN back into this manifest's `urn` field (urn_status -> bound).",+ "Open the .lbr in Fusion (library ACTIVE) — the 2D symbol + footprint are already valid.",+ format!("Electron.Create3DPackage for package '{package}' → import model.path → auto-orient → Package3DStop (FINISH) → Save. This BOTH binds the package and renders the model."),+ "The sidecar needs no pre-minted URN; the FINISH step is what makes the body appear.",+ format!("(Optional fast-path, only if the model is already cloud-synced) `adom-lbr bind-3d --lbr <lbr> --wip-urn <urn:adsk.wipprod:fs.file:vf.<id>?version=1>` pre-writes the binding for package '{package}'. Note: a pre-written wip_urn alone shows a gray placeholder until the model is brought into a session."), "Verify the 3D body seats on the footprint with pin 1 aligned." ] }); fs::write(side_path, serde_json::to_string_pretty(&manifest).unwrap() + "\n") } -/// Patch a minted Fusion cloud URN into a `.lbr`, making the 3D reference-/// self-contained. Adds (or replaces) the `<packages3d>` section after-/// `</packages>` and inserts a `<package3dinstance>` into the device(s) using-/// `package`. `package` defaults to the device's `package=` attribute. The-/// operation is idempotent — any prior 3D binding is replaced.-fn bind_3d_urn(content: &str, urn: &str, package: Option<&str>) -> Result<String, String> {- let urn = urn.trim();- if urn.is_empty() {+/// Patch a 3D-model binding into a `.lbr`, making the reference self-contained.+/// Adds (or replaces) the `<packages3d>` section after `</packages>` and inserts+/// a `<package3dinstance>` into the device(s) using `package` (defaults to the+/// device's `package=` attribute). Idempotent — any prior binding is replaced.+///+/// `managed` selects the URN scheme (verified against Fusion's own `.flbr`):+/// - `false` (self-hosted, the normal case): `link` is a `wip_urn`+/// (`urn:adsk.wipprod:fs.file:vf.<id>?version=1`); the `<package3d>` gets+/// `urn=""`, `wip_urn="<link>"`, `locally_modified="yes"`.+/// - `true` (managed / library.io): `link` is an `urn:adsk.eagle:package:NNN/V`+/// and goes straight into `urn="<link>"`.+///+/// Either way the device's `<package3dinstance package3d_urn="<link>"/>` uses+/// `link`. NB a pre-written self-hosted binding renders only once Fusion brings+/// the model into a session (cold-open shows a gray placeholder) — this is a+/// fast-path for known-synced models, not a substitute for the bridge attach.+fn bind_3d_urn(content: &str, link: &str, managed: bool, package: Option<&str>) -> Result<String, String> {+ let link = link.trim();+ if link.is_empty() { return Err("URN is empty".into()); } @@ -736,9 +772,19 @@ fn bind_3d_urn(content: &str, urn: &str, package: Option<&str>) -> Result<String let mut out = content.to_string(); // 1) Build the <packages3d> block and place it right after </packages>.+ // Self-hosted models use wip_urn + empty urn + locally_modified; managed+ // parts use the eagle:package urn directly.+ let pkg3d_attrs = if managed {+ format!("name=\"{p}\" urn=\"{u}\" type=\"model\"", p = esc_xml(&pkg), u = esc_xml(link))+ } else {+ format!(+ "name=\"{p}\" urn=\"\" wip_urn=\"{u}\" locally_modified=\"yes\" type=\"model\"",+ p = esc_xml(&pkg), u = esc_xml(link)+ )+ }; let block = format!(- "<packages3d>\n<package3d name=\"{p}\" urn=\"{u}\" type=\"model\">\n <packageinstances>\n <packageinstance name=\"{p}\"/>\n </packageinstances>\n</package3d>\n</packages3d>\n",- p = esc_xml(&pkg), u = esc_xml(urn)+ "<packages3d>\n<package3d {attrs}>\n <packageinstances>\n <packageinstance name=\"{p}\"/>\n </packageinstances>\n</package3d>\n</packages3d>\n",+ attrs = pkg3d_attrs, p = esc_xml(&pkg) ); // Drop any existing <packages3d>…</packages3d> so re-binding is clean. if let (Some(s), Some(e)) = (out.find("<packages3d>"), out.find("</packages3d>")) {@@ -757,7 +803,7 @@ fn bind_3d_urn(content: &str, urn: &str, package: Option<&str>) -> Result<String // existing <package3dinstances> in that device first. let inst = format!( " <package3dinstances>\n <package3dinstance package3d_urn=\"{}\"/>\n </package3dinstances>\n",- esc_xml(urn)+ esc_xml(link) ); let dev_re = regex_lite::Regex::new( &format!(r#"(?s)(<device\b[^>]*\bpackage="{}".*?</connects>\n)(\s*<package3dinstances>.*?</package3dinstances>\n)?"#, regex_lite::escape(&pkg))@@ -1162,7 +1208,7 @@ fn main() { if let Some(model_path) = &model { let side_path = out_path.with_extension("fusion3d.json"); match write_fusion3d_sidecar(&side_path, &parsed.name, &out_path, model_path) {- Ok(()) => println!("OK: Wrote 3D sidecar {} (model: {}) — hand to the Fusion bridge to mint+bind the cloud URN", side_path.display(), model_path.display()),+ Ok(()) => println!("OK: Wrote 3D sidecar {} (model: {}) — hand to the Fusion bridge to attach the model (Create3DPackage → FINISH)", side_path.display(), model_path.display()), Err(e) => eprintln!("WARN: Could not write 3D sidecar {}: {}", side_path.display(), e), } }@@ -1172,12 +1218,25 @@ fn main() { } } - Commands::Bind3d { lbr, urn, package, output, sidecar } => {+ Commands::Bind3d { lbr, wip_urn, urn, package, output, sidecar } => {+ // Exactly one of --wip-urn (self-hosted) or --urn (managed).+ let (link, managed) = match (wip_urn.as_deref(), urn.as_deref()) {+ (Some(w), None) => (w.to_string(), false),+ (None, Some(u)) => (u.to_string(), true),+ (None, None) => {+ eprintln!("ERROR: bind-3d needs --wip-urn <fs.file:vf URN> (self-hosted, usual) or --urn <eagle:package URN> (managed).");+ process::exit(1);+ }+ (Some(_), Some(_)) => {+ eprintln!("ERROR: pass only one of --wip-urn or --urn.");+ process::exit(1);+ }+ }; let content = fs::read_to_string(&lbr).unwrap_or_else(|e| { eprintln!("ERROR: Cannot read {}: {}", lbr.display(), e); process::exit(1); });- let patched = bind_3d_urn(&content, &urn, package.as_deref()).unwrap_or_else(|e| {+ let patched = bind_3d_urn(&content, &link, managed, package.as_deref()).unwrap_or_else(|e| { eprintln!("ERROR: {}", e); process::exit(1); });@@ -1193,15 +1252,24 @@ fn main() { eprintln!("ERROR: Cannot write {}: {}", out_path.display(), e); process::exit(1); });- println!("OK: Bound 3D URN {} into {}", urn, out_path.display());+ println!("OK: Bound 3D {} ({}) into {}", link, if managed { "managed" } else { "self-hosted wip_urn" }, out_path.display());+ if !managed {+ println!("NOTE: a pre-written wip_urn renders only once the model is brought into a Fusion session (cold-open shows a gray placeholder).");+ } if let Some(side) = &sidecar { match fs::read_to_string(side).ok().and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()) { Some(mut v) => {- v["urn"] = serde_json::Value::String(urn.clone());- v["urn_status"] = serde_json::Value::String("bound".into());+ if managed {+ v["binding"]["urn"] = serde_json::Value::String(link.clone());+ v["binding"]["scheme"] = serde_json::Value::String("managed".into());+ } else {+ v["binding"]["wip_urn"] = serde_json::Value::String(link.clone());+ v["binding"]["scheme"] = serde_json::Value::String("self-hosted".into());+ }+ v["status"] = serde_json::Value::String("bound".into()); let _ = fs::write(side, serde_json::to_string_pretty(&v).unwrap() + "\n");- println!("OK: Updated sidecar {} (urn_status -> bound)", side.display());+ println!("OK: Updated sidecar {} (status -> bound)", side.display()); } None => eprintln!("WARN: Could not update sidecar {}", side.display()), }