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
Update 103 file(s)
9 files changed
+233−92
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.320+1.9.321
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.320",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.320/adom-bridge-fusion-v1.9.320.zip",- "sha256": "1b4207723298410474a377ff4f7166704f3613f0c64e901038f88c4a64c2a820",- "size": 599846,+ "version": "1.9.321",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.321/adom-bridge-fusion-v1.9.321.zip",+ "sha256": "d3f15f4830e3431219c6bf71769685ec4e4181db1137744c4d3979735f860330",+ "size": 601633, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.320",+ "version": "1.9.321", "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://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.320"+version = "1.9.321" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.320"+version = "1.9.321" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.320"+DASH_VERSION="1.9.321" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.320",+ "version": "1.9.321", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.320",+ "version": "1.9.321", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+212−74@@ -15785,13 +15785,116 @@ _AUTODESK_SIGNIN_HOST = "signin.autodesk.com" _LIBIO_DIP = { "manage_tab": (296, 42), "group": (455, 103),- "item0": (466, 125), # Push changes to library.io- "item1": (466, 146), # Pull (when managed) / Link (when not)- "item2": (466, 166), # Link (when managed) / Unlink (when not)- "item3": (466, 187), # Unlink (when managed) "open_icon": (352, 80), # OPEN icon - its default action is Open Library Manager } +# The LIBRARY.IO dropdown, read off the screen 2026-09-01. It has these FOUR items in this+# order, and the order does NOT change with link state (an unlinked library still shows Pull+# and Unlink, greyed):+# 0 Push changes to managed library on library.io+# 1 Pull changes from managed library on library.io+# 2 Link to managed library on library.io+# 3 Unlink from managed library on library.io+_LIBIO_MENU_ITEMS = ["push", "pull", "link", "unlink"]+++def _popup_menu_rect(timeout_s=5.0):+ """Locate the open LIBRARY.IO dropdown and return its rect.++ The menu is NOT part of Fusion's main window - it is a separate always-on-top Qt window+ (className Qt683QWindow, title "Fusion360"), which is why a window capture of Fusion shows+ no menu and a capture of the popup itself comes back black. So we never try to READ it; we+ MEASURE it with desktop_list_windows and derive each item's centre from the rect. That is+ what replaced the hardcoded per-item DIP offsets, which silently pointed at the DESIGN+ ribbon whenever a library was not actually in front.++ Fusion's docked panels share the title "Fusion360", so filter on the exact class and a+ menu-sized rect, and take the topmost (lowest z).+ """+ import time as _t+ deadline = _t.time() + timeout_s+ while _t.time() < deadline:+ try:+ resp = _ad_call("desktop_list_windows", {}, timeout=25)+ wins = _find_key(resp, "windows") or []+ cands = []+ for w in wins:+ r = w.get("rect") or {}+ if str(w.get("className") or "") != "Qt683QWindow":+ continue+ if str(w.get("title") or "") != "Fusion360":+ continue+ if not (200 <= int(r.get("width") or 0) <= 1400):+ continue+ if not (60 <= int(r.get("height") or 0) <= 700):+ continue+ cands.append((int(w.get("z") or 99), w, r))+ if cands:+ cands.sort(key=lambda c: c[0])+ _z, w, r = cands[0]+ return {"hwnd": w.get("hwnd"), "left": int(r["left"]), "top": int(r["top"]),+ "width": int(r["width"]), "height": int(r["height"])}+ except Exception:+ pass+ _t.sleep(0.4)+ return None+++def _wait_for_top_window(title_substr, timeout_s=25.0):+ """Wait for a top-level window whose title contains title_substr, via desktop_list_windows.+ Fusion's own dialogs (Select Managed Library, Library Manager) are REAL windows and are+ fully UIA-readable, unlike its ribbon and its menus."""+ import time as _t+ want = title_substr.lower()+ deadline = _t.time() + timeout_s+ while _t.time() < deadline:+ try:+ resp = _ad_call("desktop_list_windows", {}, timeout=25)+ for w in (_find_key(resp, "windows") or []):+ if want in str(w.get("title") or "").lower():+ return w+ except Exception:+ pass+ _t.sleep(0.8)+ return None+++_PICKER_COLUMNS = ["name", "type", "version", "managedFolder", "description"]+++def _picker_rows(hwnd):+ """Read the Select Managed Library table into records.++ Its rows are UIA TreeItems under one QTreeWidget and stream in row-major order, five cells+ per row. Only REALIZED rows are exposed (Qt virtualizes the list), so this returns what is+ on screen, not necessarily the whole catalogue."""+ try:+ resp = _ad_call("desktop_ui_tree", {"hwnd": hwnd, "maxDepth": 14}, timeout=60)+ except Exception as e:+ return [], str(e)+ cells = []++ def walk(n):+ if isinstance(n, dict):+ if n.get("role") == "TreeItem" and n.get("name"):+ r = n.get("rect") or {}+ cells.append({"text": str(n["name"]), "x": r.get("centerX"), "y": r.get("centerY")})+ for v in n.values():+ if isinstance(v, (dict, list)):+ walk(v)+ elif isinstance(n, list):+ for i in n:+ walk(i)++ walk(resp)+ rows = []+ for i in range(0, len(cells) - (len(_PICKER_COLUMNS) - 1), len(_PICKER_COLUMNS)):+ chunk = cells[i:i + len(_PICKER_COLUMNS)]+ row = {k: c["text"] for k, c in zip(_PICKER_COLUMNS, chunk)}+ row["_at"] = [chunk[0]["x"], chunk[0]["y"]]+ rows.append(row)+ return rows, None+ def _fusion_hwnd_rect(): """(hwnd, rect, scale) for Fusion's main window, or None."""@@ -15836,15 +15939,39 @@ def _ribbon_click(win, key, why, popup=False): return {"step": key, "at": [x, y], "scale": scale} -def _libio_menu(win, item_key, why, steps):- """MANAGE tab -> LIBRARY.IO group -> one menu item."""+def _libio_menu(win, item, why, steps):+ """MANAGE tab -> LIBRARY.IO group -> one menu item, by NAME not by guessed offset.++ Returns None on success, or a typed error dict when the dropdown never opened - which is+ the honest answer for "a library was not really in front", the failure that used to show up+ as a mystery `picker_missing` after the clicks had quietly driven the Design ribbon.+ """ import time as _t+ if item not in _LIBIO_MENU_ITEMS:+ return {"success": False, "errorCode": "bad_menu_item",+ "error": "%r is not a LIBRARY.IO menu item; expected one of %s"+ % (item, ", ".join(_LIBIO_MENU_ITEMS))} steps.append(_ribbon_click(win, "manage_tab", "open the library editor's MANAGE tab")) _t.sleep(1.4) steps.append(_ribbon_click(win, "group", "open the LIBRARY.IO command group"))- _t.sleep(1.4)- steps.append(_ribbon_click(win, item_key, why, popup=True))+ _t.sleep(1.0)+ rect = _popup_menu_rect()+ if not rect:+ return {"success": False, "errorCode": "libio_menu_missing", "steps": steps,+ "error": "The LIBRARY.IO dropdown did not open.",+ "_hint": "Almost always means a LIBRARY is not really in front - Fusion's API "+ "can report a library as active while an empty Untitled design holds "+ "the visible tab. Re-open the library, confirm the ribbon reads "+ "COMPONENT/DOCUMENT/MANAGE (not SOLID/SURFACE), then retry."}+ idx = _LIBIO_MENU_ITEMS.index(item)+ n = len(_LIBIO_MENU_ITEMS)+ x = int(rect["left"] + rect["width"] / 2)+ y = int(rect["top"] + (idx + 0.5) * rect["height"] / n)+ # A popup item goes through the raw mouse verb: re-focusing Fusion here would dismiss it.+ _ad_call("desktop_mouse_click", {"x": x, "y": y, "reason": why}, timeout=25)+ steps.append({"step": item, "at": [x, y], "menuRect": rect}) _t.sleep(2.0)+ return None def _library_in_front(expect_library=True):@@ -15985,7 +16112,9 @@ def _handle_managed_library_push(fusion_info: dict, args: dict) -> dict: if err: return err steps = []- _libio_menu(ctx["win"], "item0", "Push changes to library.io", steps)+ menu_err = _libio_menu(ctx["win"], "push", "Push changes to library.io", steps)+ if menu_err:+ return menu_err dlg = _wait_for_dialog("Managed Library", 25) if dlg: # an unlinked library answers with the Create dialog instead return {"success": False, "errorCode": "not_linked_yet", "steps": steps,@@ -16004,12 +16133,15 @@ def _handle_managed_library_pull(fusion_info: dict, args: dict) -> dict: if err: return err steps = []- _libio_menu(ctx["win"], "item1", "Pull changes from the managed library on library.io", steps)+ menu_err = _libio_menu(ctx["win"], "pull",+ "Pull changes from the managed library on library.io", steps)+ if menu_err:+ return menu_err alerts = _fusion_alerts(120) return {"success": not alerts, "library": ctx["doc"], "steps": steps, "alerts": alerts,- "_hint": "Pull requested. On an UNLINKED library this menu slot is Link instead - "- "check alerts[] and fusion_managed_library_status." if not alerts else- "Fusion raised an alert - read alerts[]."}+ "_hint": "Pull requested. Fusion greys this item out on a library that was never "+ "linked, so a silent no-op there is expected - link it first." if not alerts+ else "Fusion raised an alert - read alerts[] and treat this as failed."} def _handle_managed_library_unlink(fusion_info: dict, args: dict) -> dict:@@ -16018,78 +16150,84 @@ def _handle_managed_library_unlink(fusion_info: dict, args: dict) -> dict: if err: return err steps = []- # linked -> Unlink is slot 3; unlinked -> slot 2 (and is a no-op)- _libio_menu(ctx["win"], args.get("slot") or "item3",- "Unlink from the managed library on library.io", steps)+ menu_err = _libio_menu(ctx["win"], "unlink",+ "Unlink from the managed library on library.io", steps)+ if menu_err:+ return menu_err alerts = _fusion_alerts(120) return {"success": not alerts, "library": ctx["doc"], "steps": steps, "alerts": alerts,- "_hint": "Pass slot:'item2' if this library was never linked (the menu is shorter)."}+ "_hint": "Unlink requested. Fusion greys this item out on a library that was never "+ "linked, so a silent no-op there is expected." if not alerts+ else "Fusion raised an alert - read alerts[] and treat this as failed."} def _handle_managed_library_link(fusion_info: dict, args: dict) -> dict:- """Link this library to an EXISTING managed library. With no `name`, returns the picker's- contents so a caller can choose; with `name`, selects that row and confirms.-- The LIBRARY.IO menu is ORDER-SENSITIVE and its length depends on state (measured live):- unlinked -> Push, Link, Unlink (Link is slot 1)- linked -> Push, Pull, Link, Unlink (Link is slot 2)- Fusion exposes no UIA for the popup, so the verb tries the likely slot and self-corrects- rather than guessing once and reporting a false negative."""+ """Link this library to an EXISTING managed library on library.io.++ With no `name`, returns the picker's contents (name, type, version, managed folder,+ description) and cancels, so a caller can choose. With `name`, selects that row and+ confirms.++ Both the menu and the picker are handled by measurement, not by guesswork: the LIBRARY.IO+ dropdown is located as a window and its items derived from its rect, and the picker is a+ REAL dialog whose rows are readable over UIA.+ """ err, ctx = _managed_precheck(require_hub=False) if err: return err import time as _t steps = []- dlg = None- for slot in ("item1", "item2"):- _libio_menu(ctx["win"], slot, "Link to a managed library on library.io", steps)- dlg = _wait_for_dialog("Select Managed Library", 12)- if dlg:- steps.append({"step": "picker", "slot": slot})- break+ menu_err = _libio_menu(ctx["win"], "link", "Link to a managed library on library.io", steps)+ if menu_err:+ return menu_err+ dlg = _wait_for_top_window("Select Managed Library", 25) if not dlg: return {"success": False, "errorCode": "picker_missing", "steps": steps, "alerts": _fusion_alerts(120),- "_hint": "The Select Managed Library picker never appeared from either menu slot. "- "Check that a LIBRARY (not a design) is in front and that Fusion is "- "maximized."}- tree = _ad_call("desktop_ui_tree", {"hwnd": dlg.get("hwnd"), "maxDepth": 8,- "maxNodes": 400}, timeout=90) or {}- names = []-- def _walk(n, d=0):- if d > 8 or not isinstance(n, dict):- return- nm = (n.get("name") or "").strip()- if nm and len(nm) < 80:- names.append({"name": nm, "center": [(n.get("rect") or {}).get("centerX"),- (n.get("rect") or {}).get("centerY")]})- for c in (n.get("children") or []):- _walk(c, d + 1)- _walk(tree.get("root") or {})+ "error": "The LIBRARY.IO menu opened but Select Managed Library never appeared.",+ "_hint": "Fusion greys out Link on a library that is already linked - call "+ "fusion_managed_library_unlink first, or check alerts[]."}+ hwnd = dlg.get("hwnd")+ rows, read_err = _picker_rows(hwnd) want = (args.get("name") or "").strip()++ def _close(why):+ _ad_call("desktop_ui_click", {"hwnd": hwnd, "name": "Cancel", "reason": why}, timeout=30)+ if not want:- _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Cancel",- "reason": "close the picker after listing it"}, timeout=30)+ _close("close the Select Managed Library picker after listing it read-only") return {"success": True, "listedOnly": True, "steps": steps,- "managedLibraries": [n["name"] for n in names][:80],- "_hint": "Pass name:<one of these> to link. The list is what THIS Autodesk "- "account can reach - a managed library owned by another account does not "- "appear unless its managed folder is shared."}- hit = next((n for n in names if n["name"].lower() == want.lower()), None)- if not hit or not (hit["center"] or [None])[0]:- _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Cancel",- "reason": "close the picker; requested library not listed"}, timeout=30)+ "managedLibraries": rows, "count": len(rows), "readError": read_err,+ "_hint": "Pass name:<one of these> to link. This is what THIS Autodesk account "+ "can reach: library.io's public managed folders plus any managed folder "+ "this account belongs to. A managed library owned by another account "+ "does NOT appear until its managed folder is shared with this one. Qt "+ "virtualizes the list, so only realized rows are returned - scroll or "+ "filter in the dialog if the one you want is missing."}+ hit = next((r for r in rows if r["name"].lower() == want.lower()), None)+ if not hit or not (hit.get("_at") or [None])[0]:+ _close("close the picker; the requested managed library was not listed") return {"success": False, "errorCode": "not_listed", "steps": steps,- "managedLibraries": [n["name"] for n in names][:80],- "error": "%r is not in this account's managed-library list." % want}- _ad_call("desktop_click", {"x": int(hit["center"][0]), "y": int(hit["center"][1]),- "cursor": False, "reason": "select %s in the picker" % want}, timeout=25)+ "managedLibraries": rows, "count": len(rows),+ "error": "%r is not in this account's managed-library list." % want,+ "_hint": "Names are exact. If you expect a library shared from another Autodesk "+ "account, it only appears once its MANAGED FOLDER is shared with this "+ "account on library.io - being in the same Fusion hub is not enough."}+ _ad_call("desktop_click", {"x": int(hit["_at"][0]), "y": int(hit["_at"][1]), "cursor": False,+ "reason": "select %s in the Select Managed Library picker" % want},+ timeout=25) _t.sleep(1)- ok = _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "OK",+ ok = _ad_call("desktop_ui_click", {"hwnd": hwnd, "name": "OK", "reason": "confirm the managed-library link"}, timeout=30) or {}- steps.append({"step": "confirm", "ok": bool(ok.get("success"))})- return {"success": True, "linkedTo": want, "steps": steps, "alerts": _fusion_alerts(120)}+ steps.append({"step": "confirm", "row": hit["name"], "clicked": bool(_find_key(ok, "success"))})+ _t.sleep(2)+ gone = _wait_for_top_window("Select Managed Library", 6) is None+ alerts = _fusion_alerts(120)+ return {"success": bool(gone) and not alerts, "linkedTo": hit["name"], "library": ctx["doc"],+ "managedFolder": hit.get("managedFolder"), "version": hit.get("version"),+ "steps": steps, "alerts": alerts, "dialogClosed": gone,+ "_hint": "The picker closed and Fusion raised no alert." if gone and not alerts else+ "The picker is still up or Fusion alerted - treat this as NOT linked."} def _handle_library_manager(fusion_info: dict, args: dict) -> dict:@@ -16205,18 +16343,18 @@ def _handle_managed_library_create(fusion_info: dict, args: dict) -> dict: # window-fraction math (925,273). cursor:false matters too - the glowing cursor overlay # dismisses Fusion's Qt popup menus. win["dpiScale"] = _window_dpi_scale(win.get("hwnd"))- _libio_menu(win, "item0", "Push changes to library.io", steps)- dlg = _wait_for_dialog("Create Managed Library", 25)+ menu_err = _libio_menu(win, "push", "Push changes to library.io", steps)+ if menu_err:+ return menu_err+ dlg = _wait_for_top_window("Create Managed Library", 25) if not dlg: return {"success": False, "errorCode": "create_dialog_missing", "steps": steps, "alerts": _fusion_alerts(120),- "error": "The Create Managed Library dialog never appeared - the ribbon click "- "probably missed (the ribbon is invisible to UIA, so this verb drives "- "measured DIP offsets).",+ "error": "The LIBRARY.IO menu opened but Create Managed Library never appeared.", "windowRect": win.get("rect"),- "_hint": "The Fusion window must be MAXIMIZED (the offsets are window-rect "- "fractions). Recalibrate _RIBBON_FRAC from a desktop_screenshot_screen "- "if Fusion's ribbon layout changed."}+ "_hint": "Push offers Create only for a library that is NOT yet linked. If this "+ "library is already linked, fusion_managed_library_push pushes a new "+ "version instead - check fusion_alerts."} ok = _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Create", "reason": "create the managed library on library.io"}, timeout=30) or {}