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.19: deny-grabs lock + background PostMessage keys
7 files changed
+72−15
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.18+1.9.19
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.18",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.18/adom-bridge-fusion-v1.9.18.zip",- "sha256": "1eaa480c6c410aa41143e01dd2edcb580093a38dcbf1d093ff2c70632c95e558",- "size": 360521,+ "version": "1.9.19",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.19/adom-bridge-fusion-v1.9.19.zip",+ "sha256": "88cc7d1c6bdb87b84a5e4f51672221a3eee2ae83f5162d86dcbc107f7ccafe95",+ "size": 361294, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.18",+ "version": "1.9.19", "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+13−5@@ -28,9 +28,14 @@ foreground mid-keystroke is the single most user-hostile thing this bridge can d 1. **API / text command** - the add-in API, EAGLE text commands, and modeling scripts need no window at all. Most "UI" work is really this. 2. **Background window driving** - hwnd-targeted PrintWindow capture, UI Automation- (find_control / ui_click / ui_set), and SendMessage-style input all work on windows- at the BOTTOM of the z-order. A thing being "in the UI" does not make it a- foreground thing.+ (find_control / ui_click / ui_set), and PostMessage/SendMessage input all work on+ windows at the BOTTOM of the z-order. A thing being "in the UI" does not make it a+ foreground thing. The KiCad bridge proved the strong form (John, 2026-08-15): it+ reads the app's REAL pulldown menus and drives them with PostCommand/WM_COMMAND+ instead of foreground+SendInput, "and now kicad works better." Our equivalents:+ `send_key_background` (PostMessage WM_KEYDOWN/UP, the DEFAULT for fusion_send_key+ since 1.9.19) and `_dismiss_dialogs_bg` (WM_CLOSE). SendInput is the explicit,+ captioned exception (`method: "sendinput"`), never the default. 3. **Background + demotion envelope** - if the operation makes Fusion raise windows or dialogs, run it inside the envelope so every grab is denied/demoted (incl. the drain sweep below).@@ -57,8 +62,11 @@ before trusting its exception status. overlapping verbs nest and only the last drain restores the user's settings. It does three things for its duration: -1. Raises the Windows foreground-lock timeout, so focus steals are DENIED up front and- become taskbar flashes (this kills the visible flicker; the watcher alone cannot).+1. DENIES grabs up front: raises the foreground-lock timeout AND calls+ LockSetForegroundWindow for the envelope's duration (Fusion self-activates on every+ document/workspace switch; before the lock, each one FLASHED for the instant before+ the watcher demoted it - John counted three in one proof run, 2026-08-15). Both are+ best-effort; the watcher below is the backstop that always works. 2. Disables the minimize animation, so any demotion is an instant vanish. 3. Runs a watcher: any Autodesk-FAMILY window that still reaches the foreground is demoted. Two modes:
fusion_detect.py+14@@ -743,6 +743,15 @@ def begin_background_envelope(seconds: float, minimize: bool = False): _ENVELOPE_SAVED["anim"] = _get_min_animate() _set_fg_lock_timeout(300000) _set_min_animate(0)+ # DENY grabs outright, don't just undo them: Fusion self-activates on+ # document/workspace switches and each one FLASHED before the watcher+ # demoted it (John counted three, 2026-08-15). LockSetForegroundWindow+ # blocks SetForegroundWindow globally until unlock; best-effort (Windows+ # only honors it from some process states) - watcher stays as backstop.+ try:+ _ENVELOPE_SAVED["lsfw"] = bool(ctypes.windll.user32.LockSetForegroundWindow(1))+ except Exception:+ _ENVELOPE_SAVED["lsfw"] = False threading.Thread(target=_envelope_watch, args=(seconds, minimize), daemon=True).start() except Exception: pass@@ -812,6 +821,11 @@ def _envelope_watch(seconds: float, minimize: bool): _set_fg_lock_timeout(_ENVELOPE_SAVED["fg"]) if _ENVELOPE_SAVED.get("anim") is not None: _set_min_animate(_ENVELOPE_SAVED["anim"])+ if _ENVELOPE_SAVED.get("lsfw"):+ try:+ ctypes.windll.user32.LockSetForegroundWindow(2)+ except Exception:+ pass def _focus_guard(seconds: float = 150.0, restore_fg_lock=None, restore_min_anim=None):
handlers/fusion_ui.py+28@@ -628,6 +628,34 @@ def click_fusion(x, y, relative=True, hwnd=None) -> dict: # --- Keyboard --- +def send_key_background(key: str, hwnd=None) -> dict:+ """Send a key via PostMessage - NO foreground, no focus steal, the KiCad-bridge+ lesson (John, 2026-08-15: kicad reads real menus + PostCommand instead of+ foreground+SendInput and 'now kicad works better'). Works for the dialog keys that+ matter (escape/enter/tab) on Win32 and most Qt dialogs; SendInput stays as the+ explicit, captioned last resort for CEF surfaces that ignore posted input.+ """+ if hwnd:+ hwnd = int(hwnd)+ if not user32.IsWindow(hwnd):+ return {"success": False, "error": f"HWND {hwnd} is not a valid window."}+ else:+ hwnd = _find_fusion_hwnd()+ if not hwnd:+ return {"success": False, "error": "Fusion 360 window not found."}+ vk = VK_MAP.get(key.lower().strip())+ if vk is None and len(key) == 1:+ vk = user32.VkKeyScanW(ord(key)) & 0xFF+ if vk in (None, 0xFF):+ return {"success": False, "error": f"Cannot map key '{key}' for the background path."}+ WM_KEYDOWN, WM_KEYUP = 0x0100, 0x0101+ ok1 = user32.PostMessageW(hwnd, WM_KEYDOWN, vk, 0x00000001)+ ok2 = user32.PostMessageW(hwnd, WM_KEYUP, vk, 0xC0000001)+ if not (ok1 and ok2):+ return {"success": False, "error": "PostMessage refused the key (window gone or access denied)."}+ return {"success": True, "key": key, "vk": vk, "method": "postmessage-background"}++ def send_key_to_fusion(key: str, hwnd=None) -> dict: """Send a key to Fusion 360 (or a specific dialog) via SendInput.
server.py+11−4@@ -30,6 +30,7 @@ from handlers.open_design import handle_open_design from handlers.close_fusion import handle_close_fusion, handle_fusion_stop, handle_fusion_kill from handlers.dismiss_recovery import dismiss_recovery_dialog from handlers.fusion_ui import (+ send_key_background, screenshot_fusion_window, screenshot_hwnd, click_fusion,@@ -1059,14 +1060,20 @@ def _handle_click_fusion(fusion_info: dict, args: dict) -> dict: def _handle_send_key(fusion_info: dict, args: dict) -> dict:- """Send a key to Fusion or a specific dialog via SendInput."""+ """Send a key to Fusion or a dialog - BACKGROUND PostMessage first (the KiCad-bridge+ lesson: real window messages beat foreground+SendInput); SendInput only on explicit+ {"method": "sendinput"} or when the posted key visibly failed, and then ALWAYS with+ the user-facing caption explaining WHY (John's rule, 2026-08-15).""" key = args.get("key", "") if not key: return {"success": False, "error": "No key specified. Use 'enter', 'escape', 'tab', etc."} hwnd = args.get("hwnd") or args.get("dialogHwnd")- # SendInput keystrokes REQUIRE foreground (steals the user's focus). Announce it first -- # prefer background verbs (desktop_ui_set / WM_CLOSE); this path is the last resort.- _notify_before_foreground(args.get("reason") or f"sending the '{key}' key, which Fusion only accepts in the foreground")+ if args.get("method") != "sendinput":+ bg = send_key_background(key, hwnd=hwnd)+ if bg.get("success"):+ bg["output"] = f"Sent key '{key}' via background PostMessage (no focus taken)."+ return bg+ _notify_before_foreground(args.get("reason") or f"sending the '{key}' key, which this surface only accepts in the foreground") result = send_key_to_fusion(key, hwnd=hwnd) if result.get("success"): result["output"] = f"Sent key '{key}' to Fusion."