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.198: chain hygiene stops chasing Fusion's respawning Untitled (12 phantom closes per run -> the real ~2), and the lose-changes upload modal is answered No + close retried instead of reaching the user (#94)
3 files changed
+96−41
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.197+1.9.198
CHANGELOG.md+17@@ -3,6 +3,23 @@ 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.198 - 2026-08-24 (chain hygiene: no phantom closes, no lose-changes modal)++- **The Untitled sweep recognizes Fusion's resting state.** Fusion always keeps one document+ open: closing the very last doc makes it respawn a fresh empty Untitled, so the chain-end+ sweep chased that phantom for all twelve bounded rounds on EVERY run - the log said+ "closed 12 scratch doc(s): Untitled x12" while the user watched tabs flutter, and the count+ was noise (a real chain opens ~2-4 scratch docs). The sweep now stops at one remaining doc.+- **The lose-changes modal is handled, not just guarded against.** The hasActiveJobs drain+ before a close fails its QUERY exactly when the add-in is busiest (which is when uploads+ are likeliest in flight), and the old code treated a failed ask as drained - so "Packages+ are being uploaded to your Fusion Hub..." still reached the user's screen mid-chain+ (caught live on a TMP100 mint). The drain now retries a failed ask, and if the modal rises+ anyway the close answers NO (upload preserved), re-waits the jobs, and retries the close -+ bounded, never left standing, never answered Yes.+- Verified live: a fresh-mint chain (bind 28.5s with a real Hub upload, prove 4/4) ran with+ zero dialogs on screen sampled at 5s cadence, and hygiene closed exactly 2 scratch docs.+ ## 1.9.76 - 2026-08-18 (ralph, dashboard) - **"Fusion is opening right now" LED (John's ask).** A launch takes 30-90s and the board used to
server.py+78−40@@ -10080,6 +10080,13 @@ def _close_docs_opened_since(before: set) -> list: for _ in range(12): st = _proxy_to_addin("document_info", {}, timeout=8) docs = (st.get("data") or {}).get("openDocuments") or []+ # Fusion ALWAYS keeps one document open: closing the very last doc just makes+ # it respawn a fresh empty Untitled, so a sweep that keeps going "closes" the+ # same phantom twelve times per chain (every run logged "closed 12 scratch+ # doc(s): Untitled x12" while the user watched tabs flutter; measured live+ # 2026-08-24). One remaining doc IS Fusion's resting state - leave it.+ if len(docs) <= 1:+ break u = next((dd for dd in docs if str(dd.get("name") or "").startswith("Untitled") and not (dd.get("isModified") or dd.get("modified"))), None)@@ -10119,49 +10126,80 @@ def _close_bridge_doc_verified(name: str, timeout_s: int = 30) -> dict: # returns, and a close issued during them raises exactly that modal - blocking the # main thread AND offering to destroy the upload. app.hasActiveJobs is the # deterministic signal; wait for it to drain (cap 120s) before touching the close.- _jobs_deadline = _t.time() + 120- while _t.time() < _jobs_deadline:- try:- jr = _proxy_to_addin("run_modeling_script",- {"script": "import adsk.core\n"- "result={'jobs': adsk.core.Application.get().hasActiveJobs}"},- timeout=10)- jd = (jr.get("data") or {}).get("result") or {}- if not jd.get("jobs"):- break- print("[Fusion Bridge] close(%r): Fusion Hub upload in flight - waiting, not "- "closing (a close now would raise the lose-changes modal)" % n)- except Exception:- break # cannot ask -> proceed; the dialog answer below is the backstop- _t.sleep(5)- else:- return {"closed": False, "deferred": True,- "why": "Fusion Hub uploads still active after 120s - close deferred so the "- "upload is not destroyed; retry after the jobs drain"}- try:- _proxy_to_addin("close_document", {"name": n, "save": False}, timeout=20)- except Exception:- pass- deadline = _t.time() + timeout_s- while _t.time() < deadline:- try:- di = _proxy_to_addin("document_info", {}, timeout=6)- docs = [str(d.get("name") or "").rstrip("*").strip().lower()- for d in ((di.get("data") or {}).get("openDocuments") or [])]- if di.get("success") and n.lower() not in docs:- return {"closed": True}- except Exception:- pass- # A never-saved doc can raise Save-As despite save:false - answer it NOW, same call.+ for _round in range(3):+ # hasActiveJobs drain. A BUSY add-in fails the QUERY exactly when uploads are+ # likeliest in flight (caught live 2026-08-24: the lose-changes modal reached the+ # user's screen mid-chain), so a failed ask is retried, not treated as drained.+ _ask_fails = 0+ _drained = False+ _jobs_deadline = _t.time() + 120+ while _t.time() < _jobs_deadline:+ try:+ jr = _proxy_to_addin("run_modeling_script",+ {"script": "import adsk.core\n"+ "result={'jobs': adsk.core.Application.get().hasActiveJobs}"},+ timeout=10)+ jd = (jr.get("data") or {}).get("result") or {}+ _ask_fails = 0+ if not jd.get("jobs"):+ _drained = True+ break+ print("[Fusion Bridge] close(%r): Fusion Hub upload in flight - waiting, not "+ "closing (a close now would raise the lose-changes modal)" % n)+ except Exception:+ _ask_fails += 1+ if _ask_fails >= 3:+ break # cannot ask repeatedly -> proceed; the dialog answer below is the backstop+ _t.sleep(5)+ if not _drained and _ask_fails < 3 and _t.time() >= _jobs_deadline:+ return {"closed": False, "deferred": True,+ "why": "Fusion Hub uploads still active after 120s - close deferred so the "+ "upload is not destroyed; retry after the jobs drain"} try:- got = _dismiss_save_prompt_if_bridge_doc()- if got.get("handled"):- print("[Fusion Bridge] close(%r): answered the Save prompt inside the same "- "call (deterministic close, never left on screen)" % n)+ _proxy_to_addin("close_document", {"name": n, "save": False}, timeout=20) except Exception: pass- _t.sleep(2)- return {"closed": False, "why": "document still open after %ds" % timeout_s}+ deadline = _t.time() + timeout_s+ _upload_modal = False+ while _t.time() < deadline:+ try:+ di = _proxy_to_addin("document_info", {}, timeout=6)+ docs = [str(d.get("name") or "").rstrip("*").strip().lower()+ for d in ((di.get("data") or {}).get("openDocuments") or [])]+ if di.get("success") and n.lower() not in docs:+ return {"closed": True}+ except Exception:+ pass+ # A never-saved doc can raise Save-As despite save:false - answer it NOW, same call.+ try:+ got = _dismiss_save_prompt_if_bridge_doc()+ if got.get("handled"):+ print("[Fusion Bridge] close(%r): answered the Save prompt inside the same "+ "call (deterministic close, never left on screen)" % n)+ except Exception:+ pass+ # The lose-changes modal ("Packages are being uploaded to your Fusion Hub...")+ # means the drain lied or raced: answer NO (upload preserved), then re-enter+ # the drain and retry the close. Never left standing, never answered Yes.+ try:+ _ups = [dd for dd in (classify_blocking_dialogs() or [])+ if dd.get("category") == "upload_in_progress"]+ if _ups:+ for _d in _ups:+ _unwrap(_ad_call("desktop_ui_click", {"hwnd": _d.get("hwnd"), "name": "No"}, timeout=12))+ print("[Fusion Bridge] close(%r): upload-in-progress modal answered NO "+ "(upload preserved); re-waiting for Hub jobs before retrying the close" % n)+ _upload_modal = True+ break+ except Exception:+ pass+ _t.sleep(2)+ if not _upload_modal:+ return {"closed": False, "why": "document still open after %ds" % timeout_s}+ _t.sleep(5)+ return {"closed": False, "deferred": True,+ "why": "the upload-in-progress modal kept rising across 3 close attempts - close "+ "deferred, upload preserved; retry after the Hub sync settles"}