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.9.21: dialog-free make_test_project + user-yield watcher
6 files changed
+88−21
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.20+1.9.21
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.20",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.20/adom-bridge-fusion-v1.9.20.zip",- "sha256": "ad0886d59240463f882dca229661e046b5d274052cf5e4f0d0e9f8fd5594d442",- "size": 364613,+ "version": "1.9.21",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.21/adom-bridge-fusion-v1.9.21.zip",+ "sha256": "be152ecff1067ca2676be2084cccff429b2c21dfc58ebcc29af6f7449420d6ee",+ "size": 365481, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.20",+ "version": "1.9.21", "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.",
dev-skills/fusion-background-etiquette/SKILL.md+15−2@@ -75,8 +75,21 @@ three things for its duration: z-order WITHOUT minimizing - a minimized window renders stale in PrintWindow, and background captures must keep working mid-operation. -The watcher is surgical: it acts only when the CURRENT foreground window belongs to the-Fusion family. A user who deliberately clicks into Fusion is never fought.+The watcher is surgical two ways: it acts only when the CURRENT foreground window+belongs to the Fusion family, AND it stands down whenever real keyboard/mouse input+happened in the last 2 seconds (GetLastInputInfo) - because a foreground change right+after user input IS the user. This second rule was added 2026-08-15 after the watcher+demoted John's own clicks and he could not raise Fusion at all; "never fights a+deliberate user" was a false claim until the input guard made it true.++**Native modal dialogs are a hard NO from automation.** ImportSCHAndBRDCmd's Open+dialogs wedged the user's Fusion (queued dialogs, disabled main window, uncancellable+by the user because the watcher buried them). Any flow that would pop a native dialog+must use the dialog-free primitive instead (Document.newDesignFromLocal covers .lbr,+.brd with placed elements, and .sch), or be a captioned, deliberate-foreground manual+verb. Recovery ritual if one ever wedges again: desktop_screenshot_window {hwnd} to see+the popups + their hwnds, then desktop_ui_click {name:"Cancel", scope:"popups"} per+queued dialog - all background. ## Where it is armed
fusion_detect.py+17−1@@ -772,6 +772,20 @@ def _envelope_watch(seconds: float, minimize: bool): user32.SetWindowPos.restype = ctypes.wintypes.BOOL HWND_BOTTOM = HWND(1) SWP_FLAGS = 0x0001 | 0x0002 | 0x0010 # NOSIZE | NOMOVE | NOACTIVATE+ # NEVER fight the user (John, 2026-08-15: the watcher demoted HIS clicks and he+ # could not bring Fusion up at all): if real keyboard/mouse input happened in the+ # last 2s, the foreground change is HIS - stand down for that beat.+ class _LII(ctypes.Structure):+ _fields_ = [("cbSize", ctypes.wintypes.UINT), ("dwTime", ctypes.wintypes.DWORD)]+ def _user_active_recently():+ try:+ lii = _LII(); lii.cbSize = ctypes.sizeof(_LII)+ if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):+ idle = ctypes.windll.kernel32.GetTickCount() - lii.dwTime+ return idle < 2000+ except Exception:+ pass+ return False end = time.time() + seconds pids, last_refresh = set(), 0.0 while time.time() < end:@@ -781,7 +795,7 @@ def _envelope_watch(seconds: float, minimize: bool): last_refresh = now try: fg = user32.GetForegroundWindow()- if fg and pids:+ if fg and pids and not _user_active_recently(): wpid = ctypes.wintypes.DWORD(0) user32.GetWindowThreadProcessId(fg, ctypes.byref(wpid)) if wpid.value in pids:@@ -799,6 +813,8 @@ def _envelope_watch(seconds: float, minimize: bool): grace_end = time.time() + 20 while time.time() < grace_end: try:+ if _user_active_recently():+ break # the user is driving - the foreground is THEIRS now fg = user32.GetForegroundWindow() if not (fg and pids): break
server.py+50−12@@ -5619,9 +5619,13 @@ def _orchestrate_demo_tour(args: dict) -> dict: ("device", "Component view: symbol + package mapping", 8), ("3d-package", "3D chip: the Package3D environment", 30), ]- if include_design and query:+ if include_design:+ # Design side is SELF-SUFFICIENT by default (John, 2026-08-15: customers have+ # only a component page, never a canonical cloud board): generate + import a+ # test project around the sample part. A query switches to the cloud-open path.+ steps += ([("open-design", "Opening the cloud electronics design", 150)] if query+ else [("make-project", "Generating + importing a test project around the sample part", 60)]) steps += [- ("open-design", "Opening the cloud electronics design", 150), ("schematic", "Schematic view", 12), ("2d-board", "2D board layout", 12), ("3d-board", "3D board", 20),@@ -5694,6 +5698,15 @@ def _orchestrate_demo_tour(args: dict) -> dict: else: state.setdefault("warnings", []).append("3d-package step skipped: " + str(cp.get("error"))) state.setdefault("failedSteps", []).append(key)+ elif key == "make-project":+ r = _orchestrate_make_test_project({"libraryPath": state["sampleLbr"]})+ if r.get("success"):+ time.sleep(3)+ _snap(label)+ else:+ state.setdefault("failedSteps", []).append(key)+ state.setdefault("warnings", []).append("make-project: " + str(r.get("error"))[:120])+ state["designOpenFailed"] = True elif key == "open-design": dispatch_command("open_cloud_file", {"fileName": query, "wait": False}) deadline = time.time() + 170@@ -6018,16 +6031,41 @@ def _orchestrate_make_test_project(args: dict) -> dict: brd_path = os.path.join(d, base + ".brd").replace("\\", "/") open(sch_path, "w", encoding="utf-8").write(sch_text) open(brd_path, "w", encoding="utf-8").write(brd_text)- res = _handle_new_electronics_from_eagle({"schPath": sch_path, "brdPath": brd_path})- out = {"success": bool(res.get("success")), "schPath": sch_path, "brdPath": brd_path,- "deviceset": deviceset, "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset}- if not res.get("success"):- out["error"] = "import failed: " + str(res.get("error"))[:200]- out["_hint"] = str(res.get("_hint") or "")[:200]- return out- out["_hint"] = ("Test project imported as a NEW electronics design. Now the whole chain works: "- "fusion_show_schematic -> fusion_show_2d_board -> fusion_show_3d_board. "- "Generated from NOTHING but the component's .lbr - no cloud, no sign-in.")+ # ⛔ DIALOG-FREE ONLY (John, 2026-08-15, after ImportSCHAndBRDCmd's native Open dialogs+ # wedged his Fusion): Document.newDesignFromLocal opens each file with ZERO dialogs and+ # ZERO foreground - the same primitive open_lbr/open_board use. The .brd (elements+ # placed + embedded libraries) INSTANTIATES the board; the .sch opens as its own+ # drawn schematic document. Never route this through new_electronics_from_eagle.+ import time as _t+ steps = []+ for label, fpath in (("board", brd_path), ("schematic", sch_path)):+ r = _proxy_to_addin("execute_text_command",+ {"command": "Document.newDesignFromLocal %s" % fpath}, timeout=120)+ okd = bool(r.get("success"))+ if not okd:+ expected = fpath.rsplit("/", 1)[-1].rsplit(".", 1)[0].lower()+ for _ in range(10):+ _t.sleep(3)+ st = _proxy_to_addin("get_app_state", {}, timeout=10)+ if expected in str((st.get("data") or {}).get("activeDocument", "")).lower():+ okd = True+ break+ steps.append("%s open: %s" % (label, "ok" if okd else "FAILED: " + str(r.get("error"))[:80]))+ if not okd:+ return {"success": False, "schPath": sch_path, "brdPath": brd_path,+ "error": "%s open failed: %s" % (label, str(r.get("error"))[:150]),+ "data": {"steps": steps}}+ _t.sleep(2)+ out = {"success": True, "schPath": sch_path, "brdPath": brd_path,+ "deviceset": deviceset, "steps": steps,+ "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset,+ "_hint": ("Test project opened as TWO documents (board instantiated with placed parts, "+ "schematic as its drawn sheet) via the dialog-free Document.newDesignFromLocal "+ "path. The BOARD document is active: fusion_show_2d_board / fusion_show_3d_board "+ "work now; for the schematic view activate its document (it opened last... the "+ "SCHEMATIC document is actually active) - board legs may need the board doc "+ "re-activated via Document.newDesignFromLocal or open_board. Generated from "+ "NOTHING but the component's .lbr - no cloud, no sign-in.")} return out