← Commit history

v1.9.66: Fusion window auto-sizing

John Lauer ·cc3f71ca6c ·1mo ago ·parent 7ee1a9a
6 files changed +120−5
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.65\ No newline at end of file+1.9.66\ No newline at end of file
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: