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.66: Fusion window auto-sizing
6 files changed
+120−5
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.65+1.9.66
CHANGELOG.md+11@@ -3,6 +3,17 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.9.66 - 2026-08-18++- **Fusion's window is now a usable size** (John: "why is your fusion window always kind of small+ and not like 90% of the screen size?"). Fusion reopens at whatever geometry it last had, which+ after an automated launch is a small default - bad for the human looking at it and wasteful of+ screenshot resolution. The bridge now grows it to ~92% of the monitor WORK AREA, centered,+ after launch and on the background sweep (once per window). Done with SetWindowPos ++ SWP_NOACTIVATE|SWP_NOZORDER - NOT ShowWindow(SW_MAXIMIZE), which would activate the window and+ break the background etiquette. A window the user maximized, or already sized to >=60% of the+ work area, is never touched. Sticky opt-out: fusion_set_window_sizing {enabled:false}.+ ## 1.9.65 - 2026-08-18 - **THE AUTOMATION DOCTRINE, enforced in code (John: "never make the adom user do anything
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.65",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.65/adom-bridge-fusion-v1.9.65.zip",- "sha256": "27c33da22d67295b986b1dc624443b9af7ee6fe1d41c2fc55462e278c9bc402a",- "size": 405829,+ "version": "1.9.66",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.66/adom-bridge-fusion-v1.9.66.zip",+ "sha256": "6de0e6fd0dc5b22cf4a92aa3906a5f25e3b404517f058d3d2cce561cbb057b6d",+ "size": 407679, "verbPrefixes": [ "fusion_" ],
describe.py+1@@ -207,6 +207,7 @@ _T = [ ("fusion_bind_library_3d", "The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).", {"libraryPath": "required str (WINDOWS .lbr path)", "stepPath": "required str (WINDOWS .step path)", "deviceset": "optional str (defaults to the library's first)"}, {"success": "bool", "wip_urn": "str", "bound": "bool", "reopened": "bool"}, 480, None, True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}), ("fusion_show_library", "Eight-surfaces contract: the LIBRARY surface. libraryPath (WINDOWS .lbr) opens it; name alone handles the already-open case (symmetric with kicad_show_library). Background; returns a screenshot path.", {"libraryPath": "optional str (WINDOWS .lbr)", "name": "optional str (already-open library name)"}, {"success": "bool", "view": "str", "screenshot": "str"}, 240, None, True, {"libraryPath": "C:/Temp/PART.lbr"}), ("fusion_show_project", "Eight-surfaces contract: bring the PROJECT/design to its top level. No args = activate this session's generated test project (fusion_make_test_project). query = cloud design name (already-open match wins, else fire-and-poll open). urn = open_by_urn passthrough. Background; returns a screenshot path.", {"query": "optional str (cloud design name)", "urn": "optional str", "fileName": "alias of query"}, {"success": "bool", "view": "str", "document": "str", "screenshot": "str"}, 240, "fusion_get_app_state", True, {"query": "BQ25792"}),+ ("fusion_set_window_sizing", "Read/toggle auto-sizing Fusion's WINDOW after launch (STICKY, default ON). Fusion opens at whatever geometry it last had, which after an automated launch is often a small default; the bridge grows it to ~92% of the monitor work area, centered, using SetWindowPos with NOACTIVATE (never ShowWindow/maximize) so focus is never stolen. A window the user maximized, or already sized to >=60% of the work area, is left alone. No args = read.", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "sizeFusionWindow": "bool"}, 15, "fusion_state", False, {}), ("fusion_set_driving_badge", "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "showDrivingBadge": "bool"}, 15, "fusion_state", False, {}), ("fusion_set_seat_takeover", "Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). When Autodesk pops 'Active Sessions Exceeded' mid-session, the bridge clicks Continue through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends RESUMABLY, the user gets one toast. No args = read current value. Gates only the passive sweep - a verb-driven fusion_start still grabs the seat (the user asked for Fusion, so the seat is implied).", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "autoTakeOverSeat": "bool"}, 15, "fusion_readiness", False, {}), ("fusion_state", "The RENDER-ME verb (kicad_state parity, issues #28/#30): running + window + dialog + busy state PLUS a fresh background screenshot of whatever Fusion is showing - the one verb that makes every background fusion_show_* result visible. SERVER-SIDE ONLY by design: it answers even while the add-in main thread is busy or wedged (activeDocument is best-effort with a short budget and skipped when busy). blockingDialogs carries the CLASSIFIED modal list (cause + resolution). imageAs:base64 inlines the PNG (auto-refused over 4MB - pull the screenshot path via pull_file instead).", {"screenshot": "optional bool (default true)", "imageAs": "optional str path|base64 (default path)"}, {"success": "bool", "running": "bool", "activeDocument": "str", "mainThreadBusy": "dict|null", "blockingDialogs": "list", "screenshot": "str path", "screenshotBase64": "str (imageAs:base64 only)"}, 60, None, False, {}),
handlers/fusion_ui.py+53@@ -826,3 +826,56 @@ def get_fusion_window_info() -> dict: }, "dialogs": dialogs, }++# ── Window SIZING (John, 2026-08-18: "why is your fusion window always kind of small and not+# like 90% of the screen size?") ─────────────────────────────────────────────────────────────+# Fusion opens at whatever geometry it last had, which after a headless/automated launch is+# often a small default - fine for the bridge (PrintWindow captures any size) but lousy for the+# human who then looks at it, and it wastes screenshot resolution for the AI too. So after a+# launch we grow the window to ~92% of the monitor's WORK AREA, centered.+#+# CRITICAL: this must not steal focus. SetWindowPos with SWP_NOACTIVATE|SWP_NOZORDER resizes a+# window WITHOUT activating it or changing z-order - unlike ShowWindow(SW_MAXIMIZE), which+# activates and would violate the background etiquette. We also never touch a window the user+# already maximized or already sized large (>=60% of the work area) - that is their choice.++class _MONITORINFO(ctypes.Structure):+ _fields_ = [("cbSize", ctypes.wintypes.DWORD),+ ("rcMonitor", ctypes.wintypes.RECT),+ ("rcWork", ctypes.wintypes.RECT),+ ("dwFlags", ctypes.wintypes.DWORD)]+++def ensure_reasonable_window(fraction: float = 0.92, hwnd=None) -> dict:+ """Grow a small Fusion window to `fraction` of its monitor's work area, centered.+ Background-safe (no activation, no z-order change). Never raises."""+ try:+ user32 = ctypes.windll.user32+ hwnd = int(hwnd) if hwnd else _find_fusion_hwnd()+ if not hwnd:+ return {"sized": False, "why": "no fusion window"}+ if user32.IsZoomed(hwnd):+ return {"sized": False, "why": "already maximized (user's choice)"}+ mon = user32.MonitorFromWindow(hwnd, 2) # MONITOR_DEFAULTTONEAREST+ mi = _MONITORINFO()+ mi.cbSize = ctypes.sizeof(_MONITORINFO)+ if not user32.GetMonitorInfoW(mon, ctypes.byref(mi)):+ return {"sized": False, "why": "GetMonitorInfo failed"}+ wa_w = mi.rcWork.right - mi.rcWork.left+ wa_h = mi.rcWork.bottom - mi.rcWork.top+ left, top, right, bottom = _get_window_rect(hwnd)+ cur_w, cur_h = right - left, bottom - top+ if cur_w * cur_h >= 0.60 * wa_w * wa_h:+ return {"sized": False, "why": "already large enough",+ "current": [cur_w, cur_h], "workArea": [wa_w, wa_h]}+ new_w, new_h = int(wa_w * fraction), int(wa_h * fraction)+ x = mi.rcWork.left + (wa_w - new_w) // 2+ y = mi.rcWork.top + (wa_h - new_h) // 2+ SWP_NOZORDER, SWP_NOACTIVATE, SWP_NOOWNERZORDER = 0x0004, 0x0010, 0x0200+ ok = bool(user32.SetWindowPos(hwnd, 0, x, y, new_w, new_h,+ SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER))+ return {"sized": ok, "from": [cur_w, cur_h], "to": [new_w, new_h],+ "workArea": [wa_w, wa_h], "hwnd": hwnd,+ "why": "grew to %d%% of the work area without activating it" % int(fraction * 100)}+ except Exception as e:+ return {"sized": False, "why": "error: %s" % str(e)[:120]}
server.py+50@@ -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 (+ ensure_reasonable_window, send_key_background, screenshot_fusion_window, screenshot_hwnd,@@ -628,6 +629,14 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: # No seat dialog -> is the add-in actually serving? (ground truth for drivable) if _probe_addin(timeout=1.5): addin_ok = True+ # Give the human a window worth looking at (background, no activation).+ try:+ if _window_sizing_enabled():+ _sz = ensure_reasonable_window()+ if _sz.get("sized"):+ print("[Fusion Bridge] window sized: %s" % _sz.get("why"))+ except Exception:+ pass break try: dismiss_recovery_dialog()@@ -4247,6 +4256,7 @@ COMMAND_HANDLERS["set_auto_update"] = _handle_set_auto_update _BRIDGE_DOCS: set = set() # lowercase doc-name fragments the bridge created _UNATTRIBUTED_PROMPT_STRIKES: dict = {} # hwnd -> consecutive sightings without attribution+_SIZED_HWNDS: set = set() # windows we already grew (size once, then respect the user) _NO_DONTSAVE_HWNDS: set = set() # hwnds probed once with NO Don't-Save button (UIA-less) _DONT_SAVE_LABELS = ("Don't Save", "Don’t Save") # Names the bridge generates by CONVENTION - always ours, even after a respawn wiped the@@ -5179,6 +5189,32 @@ def _note_driving_activity(command: str): +def _window_sizing_enabled() -> bool:+ """Default TRUE: a Fusion window the user has to squint at is a bug, not a preference."""+ return bool(_get_bridge_pref("sizeFusionWindow", True))+++def _handle_set_window_sizing(fusion_info: dict, args: dict) -> dict:+ """Read/toggle auto-sizing Fusion's window to ~92% of the screen after launch. STICKY."""+ if "enabled" not in (args or {}):+ cur = _window_sizing_enabled()+ return {"success": True, "sizeFusionWindow": cur,+ "_hint": ("Fusion window auto-sizing is %s. ON BY DEFAULT: after a launch the "+ "bridge grows a small Fusion window to ~92%% of the monitor work area, "+ "centered, WITHOUT activating it (SetWindowPos NOACTIVATE, never "+ "ShowWindow/maximize, so your focus is never stolen). A window you "+ "maximized or already sized large is left alone. Opt out: "+ "fusion_set_window_sizing {\"enabled\": false} - STICKY."+ % ("ON" if cur else "OFF")),+ "statusVerb": "fusion_state"}+ enabled = bool(args.get("enabled"))+ _set_bridge_pref("sizeFusionWindow", enabled)+ return {"success": True, "sizeFusionWindow": enabled,+ "narrate": ("I'll size Fusion's window to fill most of your screen when it launches."+ if enabled else "I'll leave Fusion's window size exactly as Fusion opens it."),+ "statusVerb": "fusion_state"}++ def _driving_badge_enabled() -> bool: """Default TRUE. The taskbar overlay is identity, not noise - but a user who dislikes it can switch it off permanently (John, 2026-08-18)."""@@ -5249,6 +5285,7 @@ def _handle_set_seat_takeover(fusion_info: dict, args: dict) -> dict: # lines earlier) is the import-time NameError that crash-looped the bridge on 2026-07-22. COMMAND_HANDLERS["set_seat_takeover"] = _handle_set_seat_takeover COMMAND_HANDLERS["set_driving_badge"] = _handle_set_driving_badge+COMMAND_HANDLERS["set_window_sizing"] = _handle_set_window_sizing def _seat_sweep_tick():@@ -5272,6 +5309,19 @@ def _seat_sweep_tick(): except Exception: pass + # Keep Fusion's window a usable size (once per hwnd; never touches a maximized or+ # already-large window, never activates).+ try:+ if _window_sizing_enabled() and _is_fusion_running():+ from fusion_detect import _fusion_main_hwnd_quick as _hwq+ _h = _hwq()+ if _h and _h not in _SIZED_HWNDS:+ _r = ensure_reasonable_window(hwnd=_h)+ _SIZED_HWNDS.add(_h)+ if _r.get("sized"):+ print("[Fusion Bridge] window sized on sweep: %s" % _r.get("why"))+ except Exception:+ pass # Self-heal the badge: if driving is RECENT and Fusion is up but the badge is missing # (launch race, respawn, a manual clear), put it back on the next tick. try: