← Commit history
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-lbr"-version = "1.1.1"+version = "1.1.2" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "1.1.1"+version = "1.1.2" edition = "2021" description = "EAGLE .lbr generator, linter, and validator for Fusion 360 Electronics. Converts KiCad symbols + footprints to complete EAGLE libraries." 
SKILL.md+14−8
@@ -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> (--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 bind-3d  (--lbr <part.lbr> | --record-only) (--wip-urn <…fs.file:vf…?version=N> | --urn <…eagle:package…>) [--lineage-urn <…dm.lineage…>] [--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@@ -71,13 +71,19 @@ a round-tripped `.flbr`):    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.+   `model.path` → orient (your `placement` overrides) → **FINISH** → Save. This+   both binds and renders; the in-Fusion library is the runtime source of truth.+3. **Record the URN** the bridge reports (`fusion_aps_file_info {query:"<pkg>"}`+   after Save → `fs.file:vf…?version=N`):+   `bind-3d --record-only --sidecar part.fusion3d.json --wip-urn <that>` writes+   `binding.wip_urn` + `status: bound` (and `--lineage-urn` for optional+   provenance) WITHOUT touching the `.lbr`.+4. **Optional fast-path** — bake the binding into the `.lbr`:+   `bind-3d --lbr part.lbr --wip-urn <…fs.file:vf…?version=N>`+   (`urn=""`, `wip_urn=…`, `locally_modified="yes"`). `--urn <…eagle:package…>`+   is managed/library.io only. NB a pre-written `wip_urn` shows a gray placeholder+   until the model is brought into a session, and `dm.lineage` is refused for the+   `.lbr` (provenance only) — 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`.
VERSION+1−1
@@ -1 +1 @@-1.1.1\ No newline at end of file+1.1.2\ No newline at end of file
bin/adom-lbr
⋯ 1 unchanged line ⋯
docs/fusion-export.md+36−15
@@ -127,30 +127,48 @@ names `model.path`, `package`, and the placement: ### Step 2 — the bridge attaches the model (this both binds AND renders)  The Fusion bridge: opens the `.lbr` (library **active**) → `Electron.Create3DPackage`-→ imports `model.path` → auto-orients → `Package3DStop` (**FINISH**) → **Save**.+→ imports `model.path` → auto-orients (your `placement` is an **override** for its+"rotate smallest bbox dim to Z" default) → `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.) -This is the whole job for the normal case — no `bind-3d` needed, because the-FINISH step is what makes the body actually render.+This is the whole job for the normal case — the FINISH step is what makes the body+actually render. The **in-Fusion library is the runtime source of truth.** -### Optional — pre-write the binding (fast-path)+### Step 3 — record the URN back (provenance) -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`:+After Save, the bridge reports the saved version's URN —+`fusion_aps_file_info {query:"<package>"}` returns the tip+`fs.file:vf.<id>?version=N`. adom-lbr **owns the sidecar record** and writes it:++```bash+adom-lbr bind-3d --record-only --sidecar part.fusion3d.json \+  --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=N" \+  --lineage-urn "urn:adsk.wipprod:dm.lineage:<id>"   # optional provenance+```++`--record-only` writes `binding.wip_urn` + `status: "bound"` (and the optional+`lineage_urn`) **without touching the `.lbr`** — bookkeeping for idempotency and+provenance. `dm.lineage` is kept here only as the stable cross-version id; it is+**never** written into the `.lbr` (Fusion pins the versioned `fs.file` URN).++### Optional — pre-write the binding into the `.lbr` (fast-path)++Once recorded/known-synced, you can bake the binding into the `.lbr` for a+self-contained library:  ```bash adom-lbr bind-3d --lbr part.lbr \-  --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=1" --sidecar part.fusion3d.json+  --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=N" --sidecar part.fusion3d.json # managed/library.io part instead?  --urn "urn:adsk.eagle:package:NNN/V" ```  `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.+re-lints, and updates the sidecar. It's **idempotent**, and refuses a `dm.lineage`+URN for the `.lbr`. **Caveat:** a pre-written `wip_urn` shows a **gray placeholder**+on a cold open — Fusion renders the body only once the model is brought into a+session via FINISH. So this is for known-synced models, not a substitute for the+bridge attach.  ## End-to-end @@ -161,10 +179,13 @@ adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --model part.step -o # 2. Hand part.lbr + part.fusion3d.json + the STEP to the Fusion bridge. #    It does Create3DPackage → import → orient → FINISH → Save (binds + renders). -# 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+# 3. Record the URN the bridge reports (fusion_aps_file_info) back into the sidecar:+adom-lbr bind-3d --record-only --sidecar part.fusion3d.json --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=N"++# 4. (Optional) bake the binding into the .lbr for a self-contained library:+adom-lbr bind-3d --lbr part.lbr --wip-urn "urn:adsk.wipprod:fs.file:vf.<id>?version=N" --sidecar part.fusion3d.json -# 4. Verify the 2D library+# 5. Verify the 2D library adom-lbr validate part.lbr        # complete deviceset, 0 errors ``` 
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "1.1.1",+  "version": "1.1.2",   "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": {},
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-lbr",-  "version": "1.1.1",+  "version": "1.1.2",   "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+80−29
@@ -54,23 +54,31 @@ enum Commands {     /// placeholder until the model is brought into a Fusion session.     #[command(name = "bind-3d")]     Bind3d {-        /// Path to the .lbr to patch+        /// Path to the .lbr to patch (omit with --record-only)         #[arg(long)]-        lbr: PathBuf,+        lbr: Option<PathBuf>,         /// Self-hosted model: the versioned file URN-        /// (urn:adsk.wipprod:fs.file:vf.<id>?version=1). The usual case.+        /// (urn:adsk.wipprod:fs.file:vf.<id>?version=N). 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: Option<String>,+        /// Optional dm.lineage URN — recorded as sidecar provenance ONLY,+        /// never written into the .lbr (Fusion pins the versioned fs.file URN).+        #[arg(long)]+        lineage_urn: Option<String>,+        /// Record the URN into the sidecar only; don't touch the .lbr. Use after+        /// the bridge attaches the model (in-Fusion library is source of truth).+        #[arg(long)]+        record_only: bool,         /// 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 binding + status to bound+        /// Sidecar manifest to record the binding + status into         #[arg(long)]         sidecar: Option<PathBuf>,     },@@ -706,37 +714,73 @@ fn write_fusion3d_sidecar(             "units": "mm"         },         "placement": {-            // .kicad_mod footprints are centred on the package origin (0,0),-            // seat plane at Z=0. Identity unless chipfit reports an offset.+            // Orientation hint/override for the bridge's auto-orient (which+            // rotates the smallest bbox dim to Z). .kicad_mod footprints are+            // centred on the package origin (0,0), seat plane Z=0; set a known+            // pin-1 rotation/offset here when a part needs it.             "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 the FINISH step."+            "note": "Orientation hint/override. 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."         },-        // 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.+        // How the model is linked once the bridge has attached it. Self-hosted+        // is the normal path; managed is only for library.io parts. adom-lbr+        // OWNS this record (the in-Fusion library is the runtime source of+        // truth); it's filled in by `bind-3d` once the bridge reports the URN.         "binding": {             "scheme": "self-hosted",+            // The versioned file URN that goes in the .lbr. Recorded after the+            // attach (the bridge reports it via fusion_aps_file_info+            // {query:"<package>"} after Save). Always fs.file:vf …?version=N.             "wip_urn": serde_json::Value::Null,+            // Optional provenance only — the stable cross-version id for+            // re-resolving the latest version. NEVER written into the .lbr.+            "lineage_urn": serde_json::Value::Null,             "package3d_attrs": {                 "urn": "",-                "wip_urn": "<urn:adsk.wipprod:fs.file:vf.<id>?version=1>",+                "wip_urn": "<urn:adsk.wipprod:fs.file:vf.<id>?version=N>",                 "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."+            "note": "Self-uploaded model → wip_urn = versioned fs.file:vf …?version=N (urn=\"\" empty). NEVER dm.lineage in the .lbr (keep lineage as sidecar provenance only). urn:adsk.eagle:package is MANAGED-only. A bare wip_urn does NOT auto-render on a cold open — the model must be brought into a session via the FINISH."         },         "status": "pending-fusion-attach",         "bridge_steps": [             "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."),+            format!("Electron.Create3DPackage for package '{package}' → import model.path → auto-orient (placement overrides if set) → 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."),+            format!("After Save, report the URN: fusion_aps_file_info {{query:\"{package}\"}} returns the tip fs.file:vf.<id>?version=N. Record it: `adom-lbr bind-3d --record-only --sidecar <this.json> --wip-urn <that-urn>` (writes binding.wip_urn + status=bound; pass --lineage-urn for optional provenance)."),+            "(Optional fast-path) once recorded/known-synced, `adom-lbr bind-3d --lbr <lbr> --wip-urn <…fs.file:vf…?version=N>` pre-writes the binding into the .lbr. NB a pre-written wip_urn 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") } +/// Record a 3D binding into the `*.fusion3d.json` sidecar: the versioned URN+/// (`binding.wip_urn` for self-hosted, `binding.urn` for managed), an optional+/// `dm.lineage` provenance URN, and `status: "bound"`. adom-lbr owns this record;+/// the in-Fusion library is the runtime source of truth.+fn update_sidecar_binding(side: &std::path::Path, link: &str, managed: bool, lineage: Option<&str>) {+    match fs::read_to_string(side).ok().and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()) {+        Some(mut v) => {+            if managed {+                v["binding"]["urn"] = serde_json::Value::String(link.to_string());+                v["binding"]["scheme"] = serde_json::Value::String("managed".into());+            } else {+                v["binding"]["wip_urn"] = serde_json::Value::String(link.to_string());+                v["binding"]["scheme"] = serde_json::Value::String("self-hosted".into());+            }+            if let Some(l) = lineage {+                v["binding"]["lineage_urn"] = serde_json::Value::String(l.to_string());+            }+            v["status"] = serde_json::Value::String("bound".into());+            let _ = fs::write(side, serde_json::to_string_pretty(&v).unwrap() + "\n");+            println!("OK: Recorded binding in sidecar {} (status -> bound)", side.display());+        }+        None => eprintln!("WARN: Could not update sidecar {}", side.display()),+    }+}+ /// 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@@ -1218,7 +1262,7 @@ fn main() {             }         } -        Commands::Bind3d { lbr, wip_urn, urn, package, output, sidecar } => {+        Commands::Bind3d { lbr, wip_urn, urn, lineage_urn, record_only, 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),@@ -1232,6 +1276,27 @@ fn main() {                     process::exit(1);                 }             };+            // A dm.lineage URN must never land in the .lbr — Fusion pins the+            // versioned fs.file URN. Route lineage to provenance instead.+            if link.contains("dm.lineage") {+                eprintln!("ERROR: that's a dm.lineage URN — it must not go in the .lbr. Pass the versioned fs.file:vf…?version=N URN to --wip-urn, and the lineage URN to --lineage-urn (sidecar provenance only).");+                process::exit(1);+            }++            // Record-only: update the sidecar provenance, leave the .lbr alone.+            if record_only {+                let Some(side) = sidecar.as_ref() else {+                    eprintln!("ERROR: --record-only needs --sidecar <manifest.json> to write into.");+                    process::exit(1);+                };+                update_sidecar_binding(side, &link, managed, lineage_urn.as_deref());+                return;+            }++            let Some(lbr) = lbr else {+                eprintln!("ERROR: bind-3d needs --lbr <file.lbr> (or --record-only to update just the sidecar).");+                process::exit(1);+            };             let content = fs::read_to_string(&lbr).unwrap_or_else(|e| {                 eprintln!("ERROR: Cannot read {}: {}", lbr.display(), e);                 process::exit(1);@@ -1258,21 +1323,7 @@ fn main() {             }              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) => {-                        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 {} (status -> bound)", side.display());-                    }-                    None => eprintln!("WARN: Could not update sidecar {}", side.display()),-                }+                update_sidecar_binding(side, &link, managed, lineage_urn.as_deref());             }              if !lint.passed {