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
1.9.150: #49 workspace race + progress-keyed hang ceiling
7 files changed
+114−27
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.149+1.9.150
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.149",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.149/adom-bridge-fusion-v1.9.149.zip",- "sha256": "c4a1afea4fda8299a2cbf402a0f614b50e50e4b07ee600f25abfcafea005f462",- "size": 480834,+ "version": "1.9.150",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.150/adom-bridge-fusion-v1.9.150.zip",+ "sha256": "bd0dc44eb9bb3cff7b8116b12e0ad6cc65bb2bf1133ac4af17160628d5a7817d",+ "size": 482346, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.149",+ "version": "1.9.150", "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.",
fusion_detect.py+44@@ -830,6 +830,50 @@ def _fusion_main_hwnd_quick(): return None +class _FILETIME(ctypes.Structure):+ _fields_ = [("dwLowDateTime", ctypes.wintypes.DWORD),+ ("dwHighDateTime", ctypes.wintypes.DWORD)]+++def fusion_using_cpu(sample_s: float = 0.7) -> bool:+ """True if a Fusion-family process is ACTIVELY consuming CPU right now (#49 part 10, John+ 2026-08-23: a heavy first-ever cloud mint can exceed a fixed wall-clock ceiling while still+ WORKING, and the hang gate then kills the very run the long-op registry exists to protect).++ A GIL-starved-but-progressing mint (STEP import, geometry, upload) burns CPU; a genuinely+ wedged process sits at ~0%. Sample the family's summed kernel+user CPU time twice and see if+ it advanced. Best-effort: any failure (non-Windows, no handle) returns False so the caller+ falls back to its wall-clock rule - never a reason to SKIP a kill on error."""+ try:+ k32 = ctypes.windll.kernel32+ k32.OpenProcess.restype = ctypes.wintypes.HANDLE+ PQLI = 0x1000 # PROCESS_QUERY_LIMITED_INFORMATION++ def _ticks():+ total = 0+ for pid in (_fusion_family_pids() or set()):+ h = k32.OpenProcess(PQLI, False, int(pid))+ if not h:+ continue+ try:+ ct, et, kt, ut = _FILETIME(), _FILETIME(), _FILETIME(), _FILETIME()+ if k32.GetProcessTimes(h, ctypes.byref(ct), ctypes.byref(et),+ ctypes.byref(kt), ctypes.byref(ut)):+ total += (kt.dwLowDateTime | (kt.dwHighDateTime << 32))+ total += (ut.dwLowDateTime | (ut.dwHighDateTime << 32))+ finally:+ k32.CloseHandle(h)+ return total++ t0 = _ticks()+ time.sleep(sample_s)+ t1 = _ticks()+ # CPU time is in 100ns ticks; >5ms of CPU burned in the window == actively working.+ return (t1 - t0) > 50000+ except Exception:+ return False++ def begin_background_envelope(seconds: float, minimize: bool = False): """Guard the next `seconds` against Fusion-family focus theft. Fire-and-forget.""" global _ENVELOPE_COUNT
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.172",+ "version": "1.9.173", "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+10−7@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.172",+ "version": "1.9.173", "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+53−13@@ -2752,10 +2752,25 @@ def _confirmed_hung() -> dict: import time as _t # A bridge-dispatched long op (create_library mint/bind) legitimately wedges the main # thread for minutes - never declare hung while one is in flight and under its ceiling.- if _LONG_OP["since"] and (_t.time() - _LONG_OP["since"]) < 900:- return {"hung": False, "phase": "long-op-in-flight",- "longOp": _LONG_OP["name"],- "longOpElapsedSec": round(_t.time() - _LONG_OP["since"], 1)}+ # PROGRESS-KEYED, not a fixed wall-clock (#49 part 10, John 2026-08-23: a 94-connect part's+ # first-ever cloud mint ran ~1600s and the old 900s wall turned the gate back into the kill+ # the registry exists to prevent). Under the (raised) ceiling: always protected. PAST it:+ # still protected as long as Fusion is actively burning CPU (a slow-but-working mint) - only+ # a long-op that is BOTH past the ceiling AND idle (genuinely wedged) falls through to the+ # kill-grade rules below.+ if _LONG_OP["since"]:+ _lo_el = _t.time() - _LONG_OP["since"]+ _protected = _lo_el < 1800+ if not _protected:+ try:+ from fusion_detect import fusion_using_cpu+ _protected = fusion_using_cpu()+ except Exception:+ _protected = False+ if _protected:+ return {"hung": False, "phase": "long-op-in-flight",+ "longOp": _LONG_OP["name"], "longOpElapsedSec": round(_lo_el, 1),+ "keptBy": ("under-ceiling" if _lo_el < 1800 else "fusion-cpu-active")} try: from handlers.fusion_ui import is_fusion_hung _h = is_fusion_hung()@@ -3293,20 +3308,32 @@ def _ensure_library_active(lbr_path: str, timeout_s: int = 60) -> dict: import time as _t stem = lbr_path.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0].lower() deadline = _t.time() + timeout_s- tried_activate = False+ last_ws = None while _t.time() < deadline: try: st = _proxy_to_addin("get_app_state", {}, timeout=6)- active = str((st.get("data") or {}).get("activeDocument") or "")- if st.get("success") and stem in active.lower():- return {"ok": True, "activeDocument": active}- if st.get("success") and not tried_activate:- tried_activate = True+ _d = st.get("data") or {}+ active = str(_d.get("activeDocument") or "")+ # THE WORKSPACE, not just the document (#49 parts 6/7, John 2026-08-23): the 3D+ # bind leaves the .lbr the ACTIVE DOCUMENT but the WORKSPACE on "Design" (the 3D+ # view), and EDIT then fails "Not in Electronics editor (current: Design)". So the+ # library is only truly ready when the doc matches AND the workspace is an+ # ELECTRONICS EDITOR (Library / Schematic / Board), never Design / the container.+ ws = str(_d.get("activeWorkspace") or "")+ last_ws = ws+ _wl = ws.lower()+ ws_ok = ("librar" in _wl or "schematic" in _wl or "board" in _wl+ or "pcb" in _wl) and "electronics design" not in _wl+ if st.get("success") and stem in active.lower() and ws_ok:+ return {"ok": True, "activeDocument": active, "activeWorkspace": ws}+ # Re-activate EVERY iteration until the workspace actually switches - a single+ # activate can lose the race with the 3D step's workspace change (was tried once).+ if st.get("success"): _proxy_to_addin("activate_document", {"name": stem}, timeout=10) except Exception: pass- _t.sleep(2)- return {"ok": False, "activeDocument": None}+ _t.sleep(1.5)+ return {"ok": False, "activeDocument": None, "activeWorkspace": last_ws} def _create_3d_package(lbr_path: str, timeout: int = 60) -> dict:@@ -7436,7 +7463,20 @@ def _show_library_view(name, suffix, lbr_path=None): for _busy_try in range(7): edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15) _etxt = str(edit.get("error") or "")- if edit.get("success") or ("BUSY" not in _etxt and "busy" not in _etxt):+ if edit.get("success"):+ break+ # NOT-IN-ELECTRONICS-EDITOR RETRY (#49 parts 6/7, 2026-08-23): the 3D step can leave+ # Fusion on the Design workspace, and even after _ensure_library_active a stray EDIT+ # can land a beat before the switch completes. Re-ensure the library editor and retry+ # rather than losing the frame - the same "transient, wait it out" law as BUSY.+ if lbr_path and ("not in electronics editor" in _etxt.lower()+ or "current: design" in _etxt.lower()):+ print("[Fusion Bridge] EDIT %s.%s hit Design workspace (attempt %d) - re-entering "+ "the library editor and retrying" % (name, suffix, _busy_try + 1))+ _ensure_library_active(lbr_path, timeout_s=20)+ time.sleep(1.0)+ continue+ if "BUSY" not in _etxt and "busy" not in _etxt: break print("[Fusion Bridge] EDIT %s.%s deferred - main thread busy (attempt %d), waiting 15s" % (name, suffix, _busy_try + 1))