← Commit history
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\ No newline at end of file+1.1.1\ No newline at end of file
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()),                 }