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.6.1: capture_library_views + merge-aware binding injection + retry + relay-timeout hint
7 files changed
+166−31
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.0+1.6.1
CHANGELOG.md+16@@ -3,6 +3,22 @@ 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.6.1 - 2026-06-29++- **NEW `fusion_capture_library_views` - the views EEs actually trust.** Given a list of packages+ (and the .lbr open), it runs `EDIT <deviceset>.sym/.pac/.dev` + `WINDOW FIT` + a background+ screenshot and returns, per part: the **SYMBOL** (full pinout), the **FOOTPRINT** (pad array ++ Copper/Silkscreen/Mask layer stack), and the **COMPONENT** view (Content Manager: symbol + the+ package table with the footprint<->package **Mapped ✓** + pin/pad counts). The 3D before/after+ shots never showed these.+- **`_inject_package3d_bindings` is now MERGE-AWARE + idempotent.** It reads bindings already in the+ .lbr and accumulates the new ones (new wins), so re-running `fusion_build_library_3d` for just the+ parts that failed NO LONGER wipes the parts that already succeeded (the "a subset re-run dropped my+ earlier bindings" trap). `fusion_build_library_3d` now reads its own `outLbrPath` when re-run, each+ part **self-retries once** on a transient first-part failure, and its `_hint` documents the+ **relay-timeout reality** (a many-part run exceeds the ~60s relay request timeout but keeps running+ server-side - verify by the bound .lbr + the PNGs, then re-run any missing parts).+ ## 1.6.0 - 2026-06-29 - **NEW `fusion_build_library_3d` - build a whole RENDERING multi-part 3D library in ONE call.** Give
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.0",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.0/adom-bridge-fusion-v1.6.0.zip",- "sha256": "c37957abf38dc8a723df2fbb0664a131772ab74bdd6d8b8ef7f5c5cf79c074e9",- "size": 277562,+ "version": "1.6.1",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.1/adom-bridge-fusion-v1.6.1.zip",+ "sha256": "602dd8aa07e5d3e0b376a2f0d0d87743189634b735d0a957894923cd84692e2e",+ "size": 282802, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-29T16:00:00Z"+ "released_at": "2026-06-29T17:30:00Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.0",+ "version": "1.6.1", "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.",
describe.py+3@@ -83,6 +83,9 @@ _T = [ ("fusion_build_library_3d", "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.", {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"}, {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),+ ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",+ {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)"},+ {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}), ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.", {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"}, {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),
server.py+106−24@@ -1251,16 +1251,31 @@ def _capture_labeled(label) -> str | None: def _inject_package3d_bindings(lbr_text: str, bindings: list) -> str:- """Inject EAGLE <packages3d> + per-device <package3dinstances> into an .lbr in ONE pass.+ """Inject EAGLE <packages3d> + per-device <package3dinstances> into an .lbr - MERGE-AWARE+ and IDEMPOTENT (safe to re-run with any subset of parts). - bindings: [{package: <pkg name>, wip_urn: <urn>}]. Adds a library-level <package3d> for- each (between </packages> and <symbols>) and, into every <device> that uses that package,- a <package3dinstances> right after </connects>. This is the exact XML surgery the AI used- to hand-write per part - now one deterministic call. Returns the new .lbr text."""+ bindings: [{package: <pkg name>, wip_urn: <urn>}]. The function reads any bindings ALREADY in+ the .lbr, merges the new ones on top (new wins on conflict), strips all prior <packages3d> ++ <package3dinstances>, then re-emits the full merged set: a library-level <package3d> per part+ (between </packages> and <symbols>) and a <package3dinstances> inside every <device> that uses+ that package (right after </connects>). So calling it again with just the parts that failed last+ time ACCUMULATES instead of wiping the parts that already succeeded - the fix for the+ 'a re-run dropped the earlier bindings' trap. Returns the new .lbr text.""" import re as _re- blocks = []+ # 1. read existing bindings already in the file; new bindings override+ merged = {}+ em = _re.search(r"<packages3d>(.*?)</packages3d>", lbr_text, _re.S)+ if em:+ for pm in _re.finditer(r'<package3d name="([^"]+)"[^>]*wip_urn="([^"]+)"', em.group(1)):+ merged[pm.group(1)] = pm.group(2) for b in bindings:- pkg = b["package"]; urn = b["wip_urn"]+ merged[b["package"]] = b["wip_urn"]+ # 2. strip ALL prior package3d markup so re-injection is clean (idempotent)+ lbr_text = _re.sub(r"\s*<packages3d>.*?</packages3d>", "", lbr_text, flags=_re.S)+ lbr_text = _re.sub(r"\s*<package3dinstances>.*?</package3dinstances>", "", lbr_text, flags=_re.S)+ # 3. library-level <packages3d> block (all merged parts)+ blocks = []+ for pkg, urn in merged.items(): blocks.append( f'<package3d name="{pkg}" urn="" wip_urn="{urn}" locally_modified="yes" type="model">' f'<description>{pkg}</description>'@@ -1268,18 +1283,14 @@ def _inject_package3d_bindings(lbr_text: str, bindings: list) -> str: f'</package3d>' ) pkgs3d = "<packages3d>\n" + "\n".join(blocks) + "\n</packages3d>\n"- if "<packages3d>" in lbr_text:- lbr_text = _re.sub(r"<packages3d>.*?</packages3d>\s*", pkgs3d, lbr_text, flags=_re.S)- else:- lbr_text = lbr_text.replace("</packages>", "</packages>\n" + pkgs3d, 1)- urn_by_pkg = {b["package"]: b["wip_urn"] for b in bindings}+ lbr_text = lbr_text.replace("</packages>", "</packages>\n" + pkgs3d, 1) + # 4. per-device <package3dinstances> def _dev_repl(m): dev = m.group(0) pm = _re.search(r'package="([^"]+)"', dev)- if pm and pm.group(1) in urn_by_pkg and "<package3dinstances>" not in dev and "</connects>" in dev:- urn = urn_by_pkg[pm.group(1)]- inst = (f'<package3dinstances><package3dinstance package3d_urn="{urn}"/>'+ if pm and pm.group(1) in merged and "</connects>" in dev:+ inst = (f'<package3dinstances><package3dinstance package3d_urn="{merged[pm.group(1)]}"/>' f'</package3dinstances>') dev = dev.replace("</connects>", "</connects>\n" + inst, 1) return dev@@ -1450,12 +1461,19 @@ def _orchestrate_build_library_3d(args: dict) -> dict: results.append({"package": pkg, "success": False, "error": "package, lbrPath (per-part .lbr) and modelPath (.step) all required"}) continue- mk = _orchestrate_make_3d_package({+ mk_args = { "lbrPath": per_lbr, "modelPath": step, "projectId": project_id, "folderId": folder_id, "fileName": f"{pkg}_3d", "captureLabel": (pkg if capture else None),- })+ }+ mk = _orchestrate_make_3d_package(mk_args)+ # Retry once: the FIRST part of a run often fails transiently while Fusion settles from a+ # prior view/library; a single retry recovers it (the dropped-first-part trap).+ if not mk.get("success"):+ import time as _t+ _t.sleep(2)+ mk = _orchestrate_make_3d_package(mk_args) entry = {"package": pkg, "success": bool(mk.get("success"))} if mk.get("success"): entry["wip_urn"] = mk.get("wip_urn"); entry["dims_mm"] = mk.get("dims_mm")@@ -1467,11 +1485,14 @@ def _orchestrate_build_library_3d(args: dict) -> dict: if mk.get("after"): shots.append({"package": pkg, "stage": "after", "path": mk["after"]}) results.append(entry)- # inject every binding in ONE pass, write the bound library+ # inject every binding in ONE pass, write the bound library. Read the ALREADY-BOUND output if it+ # exists so a re-run ACCUMULATES (merge-aware) onto prior successes instead of starting clean. bound = None if bindings: try:- with open(combined, "r", encoding="utf-8") as fh:+ import os as _os+ src = out_path if _os.path.exists(out_path) else combined+ with open(src, "r", encoding="utf-8") as fh: txt = fh.read() txt = _inject_package3d_bindings(txt, bindings) with open(out_path, "w", encoding="utf-8") as fh:@@ -1504,10 +1525,69 @@ def _orchestrate_build_library_3d(args: dict) -> dict: "front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail " "download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] " "on the Windows box (C:/tmp/conduit-screenshots) - pull them with desktop_pull_file (or "- "re-capture via desktop_screenshot_window) for the demo video. PITFALLS: needs Fusion "- "running; on long runs Fusion can reset between parts (fusion_start, then re-run just the "- "failed parts - each is independent); any failed part has its error in parts[]. Full recipe: "- "the fusion-multipart-libraries skill."+ "re-capture via desktop_screenshot_window) for the demo video. "+ "AFTER (recommended): call fusion_capture_library_views per showcase part to grab the "+ "symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust "+ "the library, and the 3D-only shots miss them. "+ "PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the "+ "adom-desktop relay times out the REQUEST at ~60s - you may get 'Request timed out' even "+ "though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, "+ "then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs in "+ "C:/tmp/conduit-screenshots. If a part is missing, just re-run build_library_3d for the "+ "missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE "+ "and idempotent, so it accumulates onto the existing bindings (it no longer wipes the "+ "parts that already succeeded). Each part also self-retries once on a transient first-part "+ "failure. Full recipe: the fusion-multipart-libraries skill."+ ),+ }+++def _orchestrate_capture_library_views(args: dict) -> dict:+ """Capture the LIBRARY-EDITOR views that make EEs trust a part: the schematic SYMBOL, the+ FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: the symbol ++ the package table with the footprint<->package Mapped check + pin/pad counts). These are what+ developers want to see - the 3D before/after shots alone don't show them.++ Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). For each+ package it runs the EAGLE/Electron EDIT <pkg>.sym / .pac / .dev, WINDOW FIT to frame, and a+ background hwnd screenshot (never fullscreen). Returns the saved PNG paths on the Windows box+ (C:/tmp/conduit-screenshots - pull them with desktop_pull_file).++ args: {packages: [<deviceset name>, ...] (or a single 'package'),+ views?: subset of ['component','symbol','footprint'] (default all three)}+ """+ import time as _t+ pkgs = args.get("packages") or ([args["package"]] if args.get("package") else [])+ if not pkgs:+ return {"success": False,+ "error": "packages: [<deviceset name>] (or package: <name>) required.",+ "_hint": "Open the .lbr first (fusion_open_lbr). Names are the deviceset names, e.g. ESP32-S3FN8."}+ view_ext = {"component": "dev", "symbol": "sym", "footprint": "pac",+ "dev": "dev", "sym": "sym", "pac": "pac"}+ label_of = {"dev": "component", "sym": "symbol", "pac": "footprint"}+ views = args.get("views") or ["component", "symbol", "footprint"]+ out = []+ for pkg in pkgs:+ for v in views:+ ext = view_ext.get(v, v)+ er = _proxy_to_addin("electron_run", {"command": f"EDIT {pkg}.{ext}"}, timeout=30)+ _proxy_to_addin("electron_run", {"command": "WINDOW FIT"}, timeout=20)+ _t.sleep(0.4)+ shot = _capture_labeled(f"{pkg}_{label_of.get(ext, ext)}")+ out.append({"package": pkg, "view": label_of.get(ext, ext), "path": shot,+ "ok": bool(er.get("success", True)) and bool(shot)})+ n_ok = sum(1 for o in out if o.get("ok"))+ return {+ "success": n_ok > 0,+ "captured": out,+ "_hint": (+ f"Captured {n_ok}/{len(out)} library-editor views to C:/tmp/conduit-screenshots (pull with "+ "desktop_pull_file). SYMBOL = full pinout; FOOTPRINT = pads + layer stack; COMPONENT = the "+ "Content Manager device view with the footprint<->package Mapped check + pin/pad counts. "+ "PITFALLS: the .lbr must be OPEN in the Electronics Library workspace (fusion_open_lbr first; "+ "fusion_get_app_state activeWorkspace == 'Electronics Library'); EDIT uses the DEVICESET "+ "name (e.g. ESP32-S3FN8); a blank capture usually means a wrong name or the library is not "+ "the active tab." ), } @@ -1581,7 +1661,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", "make_3d_package", "build_library_3d") or command in _OPEN_WITH_SCREENSHOT:+ if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views") or command in _OPEN_WITH_SCREENSHOT: if not fusion_info.get("installed"): return { "success": False,@@ -1689,6 +1769,8 @@ def dispatch_command(command: str, args: dict) -> dict: return _orchestrate_make_3d_package(args) if command == "build_library_3d": return _orchestrate_build_library_3d(args)+ if command == "capture_library_views":+ return _orchestrate_capture_library_views(args) # Add-in proxy commands if command in ADDIN_COMMANDS:
skills/fusion-multipart-libraries/SKILL.md+34@@ -43,6 +43,40 @@ fusion_build_library_3d { uploads; just re-run the whole `parts[]` (re-making a few f3ds is cheap) - the injection rewrites all bindings in one pass, so a single final call always yields a complete library. +### ⚠️ Relay timeout - a many-part run "times out" but KEEPS GOING (don't panic)++The `adom-desktop` relay times out the *request* at ~60s, but `fusion_build_library_3d` for a dozen+parts takes minutes. So you will often get **`status: timeout / Request timed out` while the build is+still running server-side and finishes fine.** Do NOT treat that as failure. Instead:++1. Wait, then **verify by the artifacts, not the response**: read the `boundLbr` and count+ `<package3d name=` (should equal your part count), and list the before/after PNGs in+ `C:/tmp/conduit-screenshots`.+2. If a part is missing (the FIRST part of a run is the usual casualty, as Fusion settles from a prior+ view), **just re-run `fusion_build_library_3d` for the missing parts with `outLbrPath` pointing at+ the SAME file.** The binding injection is **merge-aware + idempotent** (v1.6.1+): it reads the+ bindings already in the file and accumulates, so re-running a subset NO LONGER wipes the parts that+ already succeeded. (Each part also self-retries once on a transient first-part failure.)++This is the fix for the "a re-run dropped my earlier bindings" trap - earlier the injection replaced+the whole `<packages3d>` block, so a subset re-run left you with only that subset.++### 🔍 Show the library views (what makes EEs trust it) - `fusion_capture_library_views`++The 3D before/after shots prove the chip renders, but they do NOT show the **symbol, footprint, or+device** - the views developers actually want. After building, open the library and call:++```jsonc+fusion_capture_library_views { "packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT", "R_4k7"] }+```++It runs `EDIT <deviceset>.sym / .pac / .dev` + `WINDOW FIT` + a background screenshot per view and+returns the PNG paths (on the box; `desktop_pull_file` them). You get, per part: the **SYMBOL** (full+pinout), the **FOOTPRINT** (pad array + the Copper/Silkscreen/Mask layer stack), and the+**COMPONENT** view (Content Manager: the symbol + the package table with the footprint<->package+**Mapped ✓** check + pin/pad counts). Put these in the demo video alongside the 3D - they are the warm+-and-fuzzy shots. (Manual equivalent: `fusion_open_lbr` then `fusion_electron_run "EDIT X.sym"` etc.)+ Everything below is the MANUAL equivalent (what the verb does internally) - read it to understand or debug, but reach for `fusion_build_library_3d` first.