app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
v1.5.11: fusion_make_3d_package verb (dialog-free f3d->urn per-package 3D); multipart skill SOLVED; README/SKILL link library docs; CHANGELOG
9 files changed
+278−165
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.5.10+1.5.11
CHANGELOG.mdadded+48@@ -0,0 +1,48 @@+# Changelog++All notable changes to the Adom Desktop Fusion 360 bridge, newest first.+Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge`++## 1.5.11 - 2026-06-29++- **NEW `fusion_make_3d_package` verb - a RENDERING per-package 3D model with ZERO GUI dialogs.** It+ imports a STEP into a throwaway Design, orients it flat, saves an `.f3d`, uploads it via APS, and+ returns a `fs.file:vf` `wip_urn` to hand-write into a library's `<packages3d>`. This is the+ dialog-free replacement for the Create3DPackage generator (whose FINISH is trapped behind two+ unbeatable "Fusion360" CEF modals). **Proven on a full 10-part Adom basic-parts library+ (R/C/L/LED/nRF54L15) - every package renders its real chip, flat on its footprint.**+- **`fusion-multipart-libraries` skill rewritten as SOLVED** - the f3d recipe end to end + the key+ gotcha (a *raw STEP* upload binds but does NOT render: "Thumbnail download failed"; an *f3d* renders)+ + the GUI-generator dead end to avoid.+- New skills **`fusion-driving`** (read the dialog/owned-popup array after every mutating op; never+ fullscreen-capture) and **`fusion-cloud-save`** (Hub-upload / saveAs discipline); `fusion-libraries`+ cross-linked to both.+- README + SKILL now link the component-library docs (the skills + `CREATING_BASIC_PARTS_LIBRARIES.md`).++## 1.5.10 - 2026-06-29++- Dialog-capture false-positive fix: a docked panel (Browser/Timeline) shares the "Fusion360" title+ but does NOT disable the main window, so a generic-titled candidate is now only surfaced when the+ main window is actually disabled (a real modal is up). Stops the auto dialog-capture crying wolf.++## 1.5.9 - 2026-06-28++- **Auto dialog-array capture after every mutating `fusion_*` verb:** enumerates + screenshots any+ owned popup / modal by hwnd (background, never fullscreen) and returns it plus an "analyze this+ before proceeding" hint, so the AI cannot fly blind past a Save / "are you sure you want to close" /+ recovery dialog.+- **NEW `fusion_check_dialogs` verb** - poll the current dialog/owned-popup array on demand.+- Dialog classifier: new rules for the Hub upload-in-progress ("packages are being uploaded... lose+ these changes") and "save was cancelled / cannot be saved while packages are being uploaded" modals;+ the generic/unknown fallback no longer says "dismiss" (which could click Yes on a data-loss confirm).++## 1.5.8 - 2026-06-28++- `bridge.json`: added `updateManifestUrl` (+ `healthEndpoint`) so Adom Desktop auto-update works (it+ was null, which broke auto-update - discussion #31).++## 1.5.7 and earlier++- `fusion_attach_3d_package` (GUI Package3D generator flow - superseded for multi-part work by+ `fusion_make_3d_package`), APS fast cloud search, the Electronics workflow verbs, and exports. See+ the git history on the wiki page.
CREATING_BASIC_PARTS_LIBRARIES.md+19−1@@ -8,7 +8,25 @@ and how each one was shown on camera. > Companion guides: [MAKING_LIBRARIES.md](MAKING_LIBRARIES.md) (one part, end to end) and > [LIBRARY_FINDINGS.md](LIBRARY_FINDINGS.md) (the hard-won findings). This file is the *many-part,-> all-the-tools* version.+> all-the-tools* version. **The per-package 3D method is in the+> [fusion-multipart-libraries skill](skills/fusion-multipart-libraries/SKILL.md) - read that first.**++## ✅ THE SOLVED METHOD (2026-06-29) - programmatic, zero GUI dialogs, every part renders++Getting a distinct, RENDERING 3D model onto each package in one `.lbr` is solved, and it does NOT use+the GUI Package3D generator (that path is trapped behind unbeatable CEF dialogs). Per part:++1. `fusion_make_3d_package {modelPath:"C:/.../PART.step", projectId, folderId}` - imports the STEP into+ a Design, orients it flat, saves an `.f3d`, uploads it, and returns a `wip_urn`. **One verb, no+ dialogs.** (projectId/folderId come from `fusion_aps_browse`.)+2. Hand-write that `wip_urn` into the combined `.lbr` (a `<package3d>` in `<packages3d>` + a+ `<package3dinstance>` in the device - exact markup in the skill).+3. `fusion_open_lbr` the merged library -> every chip RENDERS, flat on its footprint; save to cloud.++⛔ **Use an f3d, not a raw STEP:** a raw STEP upload binds but shows "Thumbnail download failed" (it is+not a renderable Fusion asset); `fusion_make_3d_package` produces an f3d, which renders. Proven on a+10-part Adom basic-parts library (R/C/L/LED/nRF54L15), every package rendering. Full recipe + the dead+ends to avoid: the [fusion-multipart-libraries skill](skills/fusion-multipart-libraries/SKILL.md). ## ⛔ THE HARD REQUIREMENT - the symbol must carry the 3D chip outline
README.md+16@@ -112,6 +112,22 @@ The APS Free tier requires a card **only to verify identity** — there is no ch Full reasoning + setup procedure live in the bundled skills: **`fusion-aps-search`** (why/architecture/auth model) and **`fusion-aps-signin`** (the AI-driven setup, step by step). +## Building component libraries (symbol + footprint + rendering 3D)++Build ONE `.lbr` holding many parts, each with a real, RENDERING 3D model - **fully programmatically,+no GUI dialogs.** The verb `fusion_make_3d_package` turns a STEP into a renderable 3D-package+`wip_urn` (import -> orient flat -> save f3d -> upload), which you hand-write into the library's+`<packages3d>`. Proven on a 10-part Adom basic-parts library (R/C/L/LED/nRF54L15), every package+rendering its real chip flat on its footprint.++- **Many parts in one library:** the [`fusion-multipart-libraries`](skills/fusion-multipart-libraries/SKILL.md)+ skill (the per-package 3D recipe + the dead ends to avoid) and the full tool-chain tutorial+ [CREATING_BASIC_PARTS_LIBRARIES.md](CREATING_BASIC_PARTS_LIBRARIES.md).+- **One part end to end:** the [`fusion-libraries`](skills/fusion-libraries/SKILL.md) skill ++ [MAKING_LIBRARIES.md](MAKING_LIBRARIES.md).+- **Operating Fusion safely** (read the dialog array, never fly blind) + **cloud-save discipline:**+ [`fusion-driving`](skills/fusion-driving/SKILL.md) + [`fusion-cloud-save`](skills/fusion-cloud-save/SKILL.md).+ ## Source code **This wiki page IS the git repository.** Every file you see in the [**Files tab**](https://wiki.adom.inc/adom/adom-desktop-fusion-bridge) is the live source, browsable in place: `server.py`, `aps.py`, `describe.py`, the whole Fusion add-in under `addin/AdomBridge/`, the `handlers/`, the bundled `skills/`, all of it. No GitHub account needed. Two ways to get it:
SKILL.md+1@@ -243,6 +243,7 @@ also install the pkg itself; then the skills **auto-update** via the pkg mechani ## Related +- **Building component libraries (symbol + footprint + RENDERING 3D):** **`fusion-libraries`** (one part) and **`fusion-multipart-libraries`** (many parts in one `.lbr`, each with real 3D) — the latter's SOLVED recipe is `fusion_make_3d_package` (STEP → orient → f3d → upload → `wip_urn`, hand-written into the library, **no GUI dialogs**). Full tutorial: `CREATING_BASIC_PARTS_LIBRARIES.md`. Operate Fusion safely with **`fusion-driving`** + **`fusion-cloud-save`**. - **`fusion-aps-search`** + **`fusion-aps-signin`** — APS fast-search reasoning + the AI-driven one-time setup (bundled in this repo's `skills/`). - `adom-desktop-kicad` — sibling bridge for KiCad. - `adom-desktop-direct-api` — the direct API contract that callers route through.
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.5.10",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.5.10/adom-bridge-fusion-v1.5.10.zip",- "sha256": "f5e15b26eba7efe1aa36eb09070aebeb1781e1aa3b257c36f1d973a4500d3626",- "size": 266197,+ "version": "1.5.11",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.5.11/adom-bridge-fusion-v1.5.11.zip",+ "sha256": "33b39c974667e8f31d4f28a48dad6ad71586c6b8a31eb437583eddbcccf854b4",+ "size": 270300, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-29T01:23:30Z"+ "released_at": "2026-06-29T14:30:05Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.5.10",+ "version": "1.5.11", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://github.com/adom-inc/adom-desktop-fusion-bridge", "author": "Adom Inc.",
server.py+87−1@@ -1216,6 +1216,90 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: } +def _orchestrate_make_3d_package(args: dict) -> dict:+ """Create a RENDERING 3D-package urn from a STEP, fully programmatically - NO GUI dialogs.++ This is the dialog-free replacement for the Create3DPackage generator (whose FINISH is trapped+ behind two unbeatable "Fusion360" CEF modals). It imports the STEP into a throwaway Design,+ orients it flat, saveAs an .f3d, aps_uploads the .f3d, and returns the fs.file:vf wip_urn to+ hand-write into a library's <packages3d>. KEY: a raw STEP upload does NOT render ("Thumbnail+ download failed"); an f3d does. See the fusion-multipart-libraries skill.++ args: {modelPath: Windows .step path, projectId, folderId (both from fusion_aps_browse),+ fileName?: str, orient?: bool (default true)}+ """+ import os as _os+ model_path = (args.get("modelPath") or "").replace("\\", "/")+ project_id = args.get("projectId"); folder_id = args.get("folderId")+ if not model_path:+ return {"success": False, "error": "modelPath (.step Windows path) is required."}+ if not project_id or not folder_id:+ return {"success": False,+ "error": "projectId + folderId are required.",+ "_hint": "Get them from fusion_aps_browse {} (hub->projects) then fusion_aps_browse {projectId} (folders)."}+ orient = args.get("orient", True)+ base = _os.path.basename(model_path).rsplit(".", 1)[0]+ name = (args.get("fileName") or (base + "_3d")).replace("'", "")+ f3d_name = name if name.endswith(".f3d") else name + ".f3d"+ orient_block = (+ "axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n"+ "if axis:\n mat=oc.transform2.copy(); r=adsk.core.Matrix3D.create()\n"+ " r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))\n"+ " mat.transformBy(r); oc.transform2=mat\n"+ ) if orient else ""+ script = (+ "import adsk.core, adsk.fusion, math\n"+ "app=adsk.core.Application.get()\n"+ "doc=app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)\n"+ "d=adsk.fusion.Design.cast(app.activeProduct); root=d.rootComponent\n"+ "im=app.importManager\n"+ f"im.importToTarget(im.createSTEPImportOptions('{model_path}'), root)\n"+ "oc=root.occurrences.item(root.occurrences.count-1); bb=oc.boundingBox\n"+ "dx=bb.maxPoint.x-bb.minPoint.x; dy=bb.maxPoint.y-bb.minPoint.y; dz=bb.maxPoint.z-bb.minPoint.z\n"+ + orient_block ++ "res={}\n"+ "try:\n"+ f" doc.saveAs('{name}', app.data.activeProject.rootFolder, '3d package', '')\n"+ " res['path']=doc.dataFile.id if doc.dataFile else None\n"+ " res['dims']=[round(dx*10,2),round(dy*10,2),round(dz*10,2)]\n"+ "except Exception as e: res['err']=str(e)[:80]\n"+ "result=res\n"+ )+ r = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=120)+ res = (r.get("data", {}) or {}).get("result", {}) if isinstance(r, dict) else {}+ f3d_path = res.get("path") if isinstance(res, dict) else None+ dims = res.get("dims") if isinstance(res, dict) else None+ if not f3d_path or not str(f3d_path).endswith(".f3d"):+ return {"success": False, "error": "f3d saveAs did not produce a .f3d path: " + str(res),+ "_hint": "Confirm modelPath is a valid Windows .step path and Fusion is running."}+ up = aps.handle_upload({"projectId": project_id, "folderId": folder_id,+ "localPath": f3d_path, "fileName": f3d_name})+ up_d = up.get("data", up) if isinstance(up, dict) else {}+ item_urn = (up_d or {}).get("itemUrn", "") if isinstance(up_d, dict) else ""+ try:+ _proxy_to_addin("run_modeling_script", {"script": "app.activeDocument.close(False)\nresult={}"}, timeout=20)+ except Exception:+ pass+ if "dm.lineage:" not in item_urn:+ return {"success": False, "error": "f3d upload did not return a lineage urn: " + str(up_d)}+ lid = item_urn.split("dm.lineage:")[-1]+ wip_urn = f"urn:adsk.wipprod:fs.file:vf.{lid}?version=1"+ return {+ "success": True,+ "wip_urn": wip_urn,+ "dims_mm": dims,+ "f3d": f3d_path,+ "_hint": (+ "RENDERING 3D-package urn created with NO GUI dialogs. Hand-write it into the library EAGLE "+ "XML: add a <package3d name=\"PKG\" urn=\"\" wip_urn=\"" + wip_urn + "\" locally_modified=\"yes\" "+ "type=\"model\"> (with <packageinstances><packageinstance name=\"PKG\"/></packageinstances>) into "+ "<packages3d> between </packages> and <symbols>, and a <package3dinstances><package3dinstance "+ "package3d_urn=\"" + wip_urn + "\"/></package3dinstances> inside the device after </connects>. Then "+ "fusion_open_lbr -> it renders. See the fusion-multipart-libraries skill."+ ),+ }++ def _orchestrate_save_lbr(args: dict) -> dict: """Save the currently open Electronics library as a .flbr file. @@ -1285,7 +1369,7 @@ def dispatch_command(command: str, args: dict) -> dict: # The user should launch Fusion themselves; we just report the status. # Commands that need Fusion running (add-in commands + orchestrated commands) _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}- if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package") or command in _OPEN_WITH_SCREENSHOT:+ if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package") or command in _OPEN_WITH_SCREENSHOT: if not fusion_info.get("installed"): return { "success": False,@@ -1389,6 +1473,8 @@ def dispatch_command(command: str, args: dict) -> dict: return _merge_dialog_array(_orchestrate_save_lbr(args)) if command == "attach_3d_package": return _merge_dialog_array(_orchestrate_attach_3d_package(args))+ if command == "make_3d_package":+ return _orchestrate_make_3d_package(args) # Add-in proxy commands if command in ADDIN_COMMANDS:
skills/fusion-multipart-libraries/SKILL.md+100−156@@ -1,165 +1,109 @@ --- name: fusion-multipart-libraries-description: How to build ONE Fusion 360 .lbr library that holds MANY parts where EACH package has its real 3D model attached. The single-part mechanism is CONFIRMED (Electron.Create3DPackage binds the OPEN library's FIRST package; the binding commits on save); binding ALL parts in one library is UNDER DERIVATION - read this for the proven primitive, the dead end to avoid (passing reordered files to one open library binds only 1/N), the two candidate paths (build-up+reorder, bind-then-merge), merging per-part .lbr files, and the pre-merge connect/pad validation that stops one bad part poisoning the whole library. Read this when attaching 3D to more than one part in a single library, or building a parts-library sampler. Trigger words - multi part library, multiple 3d packages, attach 3d to many parts, combined lbr, merge lbr, basic parts library, parts sampler, Electron.Create3DPackage first package, reorder lbr, build up library, per package 3d, package3d loop, deviceset library 3d, attach all 3d.+description: How to build ONE Fusion 360 .lbr library holding MANY parts where EACH package has its real 3D model attached and RENDERING - the SOLVED, fully programmatic, zero-GUI-dialog recipe. The method: for each part, import its STEP into a Design, orient it flat, saveAs an .f3d, aps_upload the .f3d, construct an fs.file:vf wip_urn from the upload lineage, and hand-write the package3d binding into the combined .lbr. Covers the exact binding markup, the orient, merging per-part .lbr files, the connect/pad pre-merge validation, and the dead ends to avoid (the GUI Package3D generator's unbeatable CEF dialogs; a raw STEP upload renders nothing). Read this when attaching 3D to more than one part in a single library, or building a parts-library sampler. Trigger words - multi part library, multiple 3d packages, attach 3d to many parts, combined lbr, merge lbr, basic parts library, parts sampler, per package 3d, package3d, f3d upload, fs.file vf urn, aps_upload 3d, programmatic 3d binding, render 3d package, thumbnail download failed, Create3DPackage dialogs, attach all 3d. --- -# Building a many-part Fusion library with real 3D on every package--This is the multi-part extension of [fusion-libraries](../fusion-libraries/SKILL.md) (which does ONE-part end to end). The hard part that was unsolved for years: getting Fusion to attach a **distinct 3D-model to each of many packages** inside a single `.lbr`. It is now cracked. This skill is the recipe.-It depends on [fusion-cloud-save](../fusion-cloud-save/SKILL.md) (saving is async/networked) and-[fusion-driving](../fusion-driving/SKILL.md) (read the dialog after every step).--## The core obstacle--`Electron.Create3DPackage <file.lbr>` opens the 3D Package Generator on the **FIRST footprint/package-in the file, always.** Passing a package name does not redirect it; the EAGLE `EDIT <pkg>` command-selects a package in the editor but does NOT change which one Create3DPackage targets. And re-opening-the 2D `.lbr` between parts **resets the document**, so naive looping attaches nothing that sticks.-That is why "attach 3D to part 5 of 10" had no obvious path.--## ⚠️ Per-package binding: CONFIRMED mechanism, multi-bind UNDER DERIVATION (2026-06-28)--Be honest about the state of this: the single-part attach works, but binding 3D to MANY packages in-ONE library is **not yet reliably solved.** Here is exactly what is and isn't known, so the next-session does not repeat the dead end.--**Confirmed:** `Electron.Create3DPackage` binds the 3D to the **FIRST package of the currently OPEN-library**, and the binding **commits when you SAVE the library.** Proven: opening the combined `.lbr`-(R_0R first), attaching, and saving bound R_0R.--**What does NOT work (do not repeat):** keeping ONE combined library open and calling-`Create3DPackage ro_<part>.lbr` with a different reordered file per part. Passing a reordered FILE-does **not** redirect the binding - it still targets the OPEN library's first package. A 10-part run-this way bound only **1/10** (R_0R, the open lib's first package); the other 9 imported correct-geometry but their bindings did not land on their own package. An export-count confirmed it:-`devicesets=10, package3d=1, bound_names=R_0R_0402`. (An earlier "verified R_100R" claim did NOT-reproduce - treat it as retracted.)--**The unsolved piece:** to bind part N, part N must be FIRST in the OPEN library, which means-re-opening a part-N-first file as the library - but re-opening resets in-memory bindings, so-accumulating all 10 in one library is the open problem. Two candidate paths, both UNVERIFIED:--- **build-up + reorder:** each step, export the CURRENT bound library, reorder THAT (preserving its- `<packages3d>` bindings) to put the next part first, reopen, attach, save. Open question: do prior- `wip_urn` bindings survive the reopen and still let you add one more?-- **bind-then-merge:** attach each part in its own one-part library (part first), then merge the- per-part `<package3d>` + `<package3dinstance>` XML into the combined `.lbr` directly (see- [LIBRARY_FINDINGS](../fusion-libraries/LIBRARY_FINDINGS.md) section 11-12). Open question: does- Fusion resolve N hand-merged `wip_urn` refs on open?--Until one path is verified end to end, do NOT claim multi-part 3D works. Re-derive empirically, save-the library after every attach (the binding only commits on save), and note Fusion may reset the-session between calls.--### The reorder helper (still the right primitive for either path)--### Reorder helper (put one package first)--```python-import re-def reorder_first(xml, tag, name):- # tag in {'packages','symbols','devicesets'}; move the child named `name` to the front- sec = re.search(rf'<{tag}>(.*?)</{tag}>', xml, re.S); body = sec.group(1)- item = rf'<{tag[:-1]} name="[^"]*">.*?</{tag[:-1]}>|<{tag[:-1]} name="[^"]*"/>'- items = re.findall(f'({item})', body, re.S)- tgt = [i for i in items if f'name="{name}"' in i[:60]]- rest = [i for i in items if f'name="{name}"' not in i[:60]]- return xml.replace(sec.group(0), f'<{tag}>{"".join(tgt+rest)}</{tag}>')--xml = open('AdomBasicParts.lbr').read()-for tag in ('packages','symbols','devicesets'):- xml = reorder_first(xml, tag, 'R_100R_0402')-open('ro_R_100R_0402.lbr','w').write(xml) # R_100R now first; Create3DPackage opens IT-```--### The single-part attach (CONFIRMED working - the building block)--This is the proven primitive (one part, first in the open library):--```-open the library (target package FIRST in it) # fusion_open_lbr-Electron.Create3DPackage C:/.../<lib>.lbr # opens the generator on the FIRST package-import <part>.step + orient # importToTarget, then rotate 90 about X (below)-Package3DStop (FINISH) # binds the 3D onto that first package-handle the Save dialog (3D package) # read it (fusion-driving), click Save-WAIT for the Hub upload to drain, then SAVE the lib # the binding COMMITS on save (fusion-cloud-save)-```--⛔ Do NOT then loop `Create3DPackage ro_<otherpart>.lbr` against the same open library expecting each-to bind - it does not (see the box above; you get 1/10). Extending this to all 10 is the UNDER--DERIVATION problem above; pick one of the two candidate paths and verify it before relying on it.--### Orient the imported STEP--STEP imports Y-up. Rotate 90 degrees about the model's **smallest bounding-box axis** so it lies flat-(thin in Z), centered on the footprint:--```python-im = app.importManager-im.importToTarget(im.createSTEPImportOptions(step_path), root)-oc = root.occurrences.item(root.occurrences.count - 1); bb = oc.boundingBox-dx, dy, dz = (bb.maxPoint.x-bb.minPoint.x, bb.maxPoint.y-bb.minPoint.y, bb.maxPoint.z-bb.minPoint.z)-axis = (1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)-if axis:- mat = oc.transform2.copy(); r = adsk.core.Matrix3D.create()- r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))- mat.transformBy(r); oc.transform2 = mat-```--Sanity-check dims (cm*10 = mm): a 0402 lands ~[1.0, 0.5, 0.4], an 0603 ~[1.6, 0.8, 0.8]. A wrong-orient shows up as the thin dimension not being Z.--## The build-up-one-by-one alternative (more durable, more re-opens)--Instead of reordering, build the library up incrementally: export a 1-part `.lbr`, open it, attach-3D, save; add part 2 via `adom-lbr`, reopen, attach 3D to the new part, save; repeat. Slower (a-reopen per part) but every part is durable the instant it saves (see fusion-cloud-save Rule 2). Use-this when durability matters more than speed, or when reordering a particular file is fiddly. The-reorder trick is cleaner for a known fixed set; build-up is safer for a long or uncertain run.--## Merging per-part .lbr files into one combined library--An EAGLE `.lbr` is a library of many devicesets, so combine the per-part files:--1. Keep ONE file's `<layers>` skeleton.-2. Concatenate every part's `<package>`, `<symbol>`, and `<deviceset>` (and `<package3d>` if already- bound) into the single file. MPN-unique names mean no collisions.-3. `adom-lbr validate combined.lbr`.+# Building a many-part Fusion library with real, RENDERING 3D on every package++The multi-part extension of [fusion-libraries](../fusion-libraries/SKILL.md) (one part, end to end).+Getting a distinct, RENDERING 3D model onto each of many packages inside one `.lbr` is **solved** -+and the solution is fully programmatic, with **zero GUI dialogs**. Built on+[fusion-cloud-save](../fusion-cloud-save/SKILL.md) and [fusion-driving](../fusion-driving/SKILL.md).+Proven 2026-06-29: a 10-part Adom basic-parts library (R/C/L/LED/MCU), every package rendering its+real chip, flat on its footprint, built entirely this way.++## ✅ THE RECIPE - per part, then merge (no Create3DPackage, no dialogs)++For each part you have a STEP for:++1. **Make a renderable f3d.** Via `fusion_run_modeling_script`: add a new Design doc, import the STEP,+ ORIENT it flat (below), then `saveAs` it. Read back `doc.dataFile.id` - it is the **local `.f3d`+ cache path**.+ ```python+ import adsk.core, adsk.fusion, math+ app = adsk.core.Application.get()+ doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)+ d = adsk.fusion.Design.cast(app.activeProduct); root = d.rootComponent+ im = app.importManager+ im.importToTarget(im.createSTEPImportOptions("C:/.../PART.step"), root)+ oc = root.occurrences.item(root.occurrences.count-1); bb = oc.boundingBox+ dx,dy,dz = bb.maxPoint.x-bb.minPoint.x, bb.maxPoint.y-bb.minPoint.y, bb.maxPoint.z-bb.minPoint.z+ axis = (1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None) # smallest -> flat in Z+ if axis:+ mat = oc.transform2.copy(); r = adsk.core.Matrix3D.create()+ r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))+ mat.transformBy(r); oc.transform2 = mat+ doc.saveAs("PART_f3d", app.data.activeProject.rootFolder, "3d", "")+ result = {"path": doc.dataFile.id} # local .f3d cache path+ # then close the doc: app.activeDocument.close(False)+ ```+2. **Upload the f3d to the cloud:**+ `fusion_aps_upload {projectId, folderId, localPath:<the .f3d>, fileName:"PART_3d.f3d"}`+ -> `{ "itemUrn": "urn:adsk.wipprod:dm.lineage:<id>", ... }`. (Find projectId/folderId once via+ `fusion_aps_browse {}` then `fusion_aps_browse {projectId}`.)+3. **Construct the binding urn:** `urn:adsk.wipprod:fs.file:vf.<id>?version=1` - the lineage `<id>`+ doubles as the version id for these single-version assets.+4. **Hand-write the binding** into the combined `.lbr` (markup below). Repeat per part, then open the+ merged library: it RESOLVES and **RENDERS** every chip, flat on its footprint, and saves to the+ cloud.++### The binding markup (exactly what Fusion writes)++- Library-level, inserted between `</packages>` and `<symbols>` (one `<package3d>` per part):+ ```xml+ <packages3d>+ <package3d name="PKG" urn="" wip_urn="urn:adsk.wipprod:fs.file:vf.<id>?version=1" locally_modified="yes" type="model">+ <description>PKG</description>+ <packageinstances><packageinstance name="PKG"/></packageinstances>+ </package3d>+ ...one per part...+ </packages3d>+ ```+- Inside each device, right after its `</connects>`:+ ```xml+ <package3dinstances><package3dinstance package3d_urn="urn:adsk.wipprod:fs.file:vf.<id>?version=1"/></package3dinstances>+ ```++Verify success: open the merged `.lbr`, `fusion_check_dialogs` returns 0 (no broken-ref), and each+device's lower-right 3D preview shows the real chip (NOT "Thumbnail download failed").++## ⛔ Why an f3d and not a raw STEP (the key gotcha)++A raw `.step` uploaded via `fusion_aps_upload` gives a urn that **resolves** (no broken-ref) and saves+- BUT the library 3D preview shows **"Thumbnail download failed"** and does NOT render. A raw STEP is+not a renderable Fusion 3D asset. An **f3d** (a real Fusion Design with the STEP imported) IS, and+binds + renders identically to a GUI-minted package. So always: STEP -> import into a Design ->+`saveAs` f3d -> upload the **f3d**. (Proven contrast: a STEP-bound nRF showed "Thumbnail download+failed"; the f3d-bound nRF renders a clean black QFN.)++## ⛔ Do NOT use the GUI Package3D generator for this (it is dialog-trapped)++`Electron.Create3DPackage` + import + `Package3DStop` (FINISH) is the manual/GUI way and it is a trap:+- FINISH pops a native Save dialog AND **two opaque "Fusion360" CEF modals** that are unreadable by+ Win32, by UIA (`desktop_find_control` finds nothing), and by PrintWindow (captures solid black), and+ they ignore programmatic escape/WM_CLOSE.+- `Create3DPackage` also binds to the **first package of the OPEN library**, not the file you pass it,+ so looping reordered files against one open library binds only 1/N.+The f3d recipe above sidesteps ALL of this. Use it; do not fight the generator.++## Merging per-part `.lbr` files into one combined library++An EAGLE `.lbr` is a library of many devicesets, so combine the per-part files: keep ONE file's+`<layers>` skeleton, then concatenate every part's `<package>`, `<symbol>`, `<deviceset>` (MPN-unique+names, no collisions), then add the `<packages3d>` + per-device `<package3dinstances>` from the recipe.+`adom-lbr validate combined.lbr`. ## ⚠️ VALIDATE connect/pad consistency BEFORE merging (one bad part poisons everything) -Fusion rejects the ENTIRE merged `.lbr` with a generic "has errors and cannot be opened" if ANY-device has an invalid `<connect>` - a pin mapped to a pad its package does not define. This bit-WSL2010: chip-fetcher gave it a 3-pin symbol but a 2-pad footprint, so pin 3 connected to a-nonexistent pad 3, and the whole combined library failed to open. A single-device `.lbr` may still-open, masking the bug until merged.+Fusion rejects the ENTIRE merged `.lbr` ("has errors and cannot be opened") if ANY device has a+`<connect pad="N">` referencing a pad its package does not define. A single-device `.lbr` may still+open, masking it until merged. Before merging, for every device confirm every `<connect pad="N">`+references an existing `<smd name="N">`/`<pad name="N">`. Passives are the usual offenders (symbol pin+count vs footprint pad count). `adom-lbr` lint did NOT catch this - validate it yourself. -**Before merging, for every device: every `<connect pad="N">` must reference an `<smd name="N">` or-`<pad name="N">` that exists in its package.** Passives are the usual offenders (symbol pin count vs-footprint pad count). `adom-lbr`'s lint did NOT catch this - validate it yourself and drop or fix any-part that fails.+## Capture the money shots (for a demo video) -## Verify the result--Export the saved library and confirm per-package bindings:--- Count `package3d` entries that are bound (have a real `wip_urn`), one per attached part.-- Each NON-attached package should read `Placeholder` (a clean negative control).-- Spot-check in Fusion's Content Manager: the 3D nests under the package under the deviceset, and the- Packages panel's `Package` column flips from `Placeholder` to the part name.--## Capture the money shots (for the demo video)--Per part, grab three hwnd-targeted shots (never fullscreen - see fusion-driving):--1. **Before:** the footprint/deviceset with a gray placeholder 3D.-2. **Chip lands:** the Package3D generator with the real chip seated on its matched footprint- (footprint alone in the 3D viewer, then the chip dropping in and lining up - the best shot).-3. **After:** the Content Manager / Packages view showing the 3D bound, `Package` column = part name,- preview refreshed to the real chip.--These three per part, swapped on camera, ARE the montage. See-[CREATING_BASIC_PARTS_LIBRARIES.md](../../CREATING_BASIC_PARTS_LIBRARIES.md) for the full tool-chain-tutorial and montage recipe.+Per part, hwnd-targeted (never fullscreen - see fusion-driving): the footprint alone, then the device+view with the real rendered chip on its footprint (the money shot), reopening the cloud library so the+3D preview refreshes. See [CREATING_BASIC_PARTS_LIBRARIES.md](../../CREATING_BASIC_PARTS_LIBRARIES.md)+for the full tool-chain tutorial + montage recipe.