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
Update 6 file(s)
6 files changed
+125−27
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.121+1.9.122
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.121",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.136/adom-bridge-fusion-v1.9.121.zip",- "sha256": "eec4f3ba279d3dd1a6aa349b3df8c8d459541fbe4d7e3a8f7081b19ea819fd3b",- "size": 456629,+ "version": "1.9.122",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.137/adom-bridge-fusion-v1.9.122.zip",+ "sha256": "6c654c30b623b07ed26a5b388d969eebba4180f66bac205c30e467bc7c3a3847",+ "size": 458429, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.121",+ "version": "1.9.122", "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.",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.136",+ "version": "1.9.137", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+10−7@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.136",+ "version": "1.9.137", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -70,7 +74,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -99,9 +106,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }+ ] }
server.py+108−13@@ -2623,6 +2623,49 @@ def _check_addin_status(timeout: float = 3.0) -> dict | None: return None +_HUNG_SINCE = [0.0] # first time is_fusion_hung() reported hung; 0 = not currently hung+++def _confirmed_hung() -> dict:+ """A kill-grade hang verdict. IsHungAppWindow ALONE must never authorize a kill.++ Found the hard way (2026-08-21, three dead Fusions in one afternoon): a heavy+ main-thread modeling script (STEP import, saveAs) starves the add-in's HTTP thread+ via the GIL, so the 3s status probe AND the 5s health probe both time out, and the+ host window legitimately stops pumping messages - IsHungAppWindow goes true on a+ process that is WORKING. The old gate then taskkilled a healthy Fusion mid-bind.+ Same law as the busy guard (#41): a remediation that destroys state needs a+ STRONGER signal than one snapshot.++ Kill-grade means ALL of: (1) is_fusion_hung() true NOW, (2) a LONG status probe+ (20s) gets nothing - a merely GIL-starved add-in answers intermittently within+ that, (3) the condition has PERSISTED for 180s+ since first observed. Anything+ less returns hung:false with a `phase` explaining what to do (wait).+ """+ import time as _t+ try:+ from handlers.fusion_ui import is_fusion_hung+ _h = is_fusion_hung()+ except Exception:+ _h = {"hung": False, "signals": []}+ if not _h.get("hung"):+ _HUNG_SINCE[0] = 0.0+ return {"hung": False, "phase": "pumping"}+ if _check_addin_status(timeout=20.0) is not None:+ _HUNG_SINCE[0] = 0.0+ return {"hung": False, "phase": "busy-not-hung",+ "signals": _h.get("signals")}+ now = _t.time()+ if not _HUNG_SINCE[0]:+ _HUNG_SINCE[0] = now+ held = now - _HUNG_SINCE[0]+ if held < 180:+ return {"hung": False, "phase": "suspect", "heldSeconds": round(held, 1),+ "signals": _h.get("signals")}+ return {"hung": True, "phase": "confirmed", "heldSeconds": round(held, 1),+ "signals": _h.get("signals")}++ def _handle_addin_status() -> dict: """Bridge-level handler for addin_status — wraps /status in standard format.""" status = _check_addin_status(timeout=3.0) or {"busy": False}@@ -2885,14 +2928,24 @@ def _diagnose_addin_timeout(command: str, original_result: dict) -> dict: # responses (page issue #38): the add-in being down is recoverable over IPC, a HUNG # HOST PROCESS is not. Reporting the second as the first sent a reader hunting a # blocking dialog that did not exist.- try:- from handlers.fusion_ui import is_fusion_hung- _hung = is_fusion_hung()- except Exception:- _hung = {"hung": False, "signals": []}+ _hung = _confirmed_hung()+ if _hung.get("phase") in ("suspect", "busy-not-hung"):+ return {+ "success": False,+ "error": "Fusion's main thread is not answering right now - most likely WORKING "+ "on something heavy, not hung (the probes it starves are the very ones "+ "this gate reads).",+ "errorCode": "fusion_addin_busy",+ "hostResponding": _hung.get("phase") == "busy-not-hung",+ "hungHeldSeconds": _hung.get("heldSeconds"),+ "_hint": "WAIT and re-poll. A real hang is only declared after 180s of continuous "+ "silence including a 20s probe, and then the bridge restarts Fusion itself.",+ } if _hung.get("hung"): # DOCTRINE: this is mechanically fixable and the user should never have to do it. # A hung process will not close politely, so go straight to the force path.+ # (_confirmed_hung: kill-grade evidence only - persistence + a refused 20s probe -+ # after this gate taskkilled three healthy, working Fusions in one day, 2026-08-21.) _healed = False try: dispatch_command("kill", {})@@ -3475,7 +3528,13 @@ def _orchestrate_make_3d_package(args: dict) -> dict: "_hint": "Saved DIRECTLY to the cloud workspace - no upload step needed. " "Bind this wip_urn into the library's <packages3d>."} if not f3d_path or not str(f3d_path).endswith(".f3d"):- return {"success": False, "error": "f3d saveAs did not produce a .f3d path: " + str(res),+ # Carry the add-in's OWN error up: res={} with the script error dropped on the+ # floor cost a whole diagnosis round (2026-08-21 - the real story was Fusion being+ # killed mid-script, visible only in r['error']).+ return {"success": False,+ "error": "f3d saveAs did not produce a .f3d path: " + str(res)+ + (" | addin: " + str(r.get("error"))[:200]+ if isinstance(r, dict) and r.get("error") else ""), "before": before_shot, "after": after_shot, "_hint": "Confirm modelPath is a valid Windows .step path and Fusion is running."} up = aps.handle_upload({"projectId": project_id, "folderId": folder_id,@@ -7237,6 +7296,27 @@ def _orchestrate_show_3d_package(args: dict) -> dict: if step_path: res = _merge_dialog_array(_orchestrate_attach_3d_package( {"filePath": library_path, "modelPath": step_path, "packageName": name}))+ # THE ATTACH-FAILED BRANCH OF THE PLACEHOLDER GUARD (page issue #44): when the+ # attach we just attempted did not actually land a <package3d> in the library,+ # what renders next is Fusion's grey Placeholder - and this verb used to return+ # the attach result's success over it. Same offline check as no_3d_package_bound,+ # run on the branch where the bridge TRIED and failed rather than was never asked.+ _p3d_after = _library_artifact_names(library_path).get("p3d", [])+ if not _p3d_after:+ _up_err = str(res.get("error") or res.get("attachError") or "attach reported no error but bound nothing")+ return {"success": False,+ "errorCode": "3d_attach_failed",+ "error": "Attaching the STEP failed, so the library still has no 3D "+ "package - refusing to show Fusion's Placeholder as if it "+ "were the part.",+ "attachError": _up_err[:400],+ "libraryPath": library_path,+ "_hint": ("The upstream failure is in attachError (a 409 here usually "+ "means the f3d already exists in the cloud - the bind path "+ "reuses it as of 1.9.11x, so retry fusion_bind_library_3d "+ "and read ITS message). Nothing was rendered because a "+ "Placeholder screenshot photographs exactly like a real "+ "chip and must never count as one.")} if wrapped: res["wrappedLibrary"] = wrapped res["view"] = "3d-package"@@ -10057,11 +10137,11 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No pass # Fall through to busy gate elif not wait_for_addin(timeout=5): # Hung HOST vs down ADD-IN (page issue #38) - opposite remedies, so say which.- try:- from handlers.fusion_ui import is_fusion_hung- _h = is_fusion_hung()- except Exception:- _h = {"hung": False}+ # KILL-GRADE evidence only (_confirmed_hung): a busy main thread starves these+ # very probes and stops pumping messages, so one failed snapshot looks exactly+ # like a hang. Three healthy Fusions were taskkilled mid-work by this gate+ # before the persistence rule (2026-08-21).+ _h = _confirmed_hung() if _h.get("hung"): _ok = False try:@@ -10073,8 +10153,9 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No pass return { "success": False,- "error": "Fusion 360 itself is NOT RESPONDING (hung host process); the "- "add-in cannot answer while it is.",+ "error": "Fusion 360 itself is NOT RESPONDING (hung host process, held "+ "%ss with the add-in refusing a 20s probe); the add-in cannot "+ "answer while it is." % _h.get("heldSeconds"), "errorCode": "fusion_host_hung", "hungSignals": _h.get("signals"), "restartAttempted": _ok,@@ -10083,6 +10164,20 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No + ("The bridge force-restarted it; poll fusion_readiness." if _ok else "Use fusion_kill then fusion_start.")), }+ if _h.get("phase") in ("suspect", "busy-not-hung"):+ return {+ "success": False,+ "error": "Fusion's main thread is not answering right now - most likely "+ "WORKING on something heavy (a STEP import or save starves the "+ "add-in's probes), not hung.",+ "errorCode": "fusion_addin_busy",+ "hostResponding": _h.get("phase") == "busy-not-hung",+ "hungHeldSeconds": _h.get("heldSeconds"),+ "_hint": ("WAIT and re-poll; do not restart anything. The bridge only "+ "declares a real hang after 180s of continuous silence "+ "including a 20s probe - if that happens it restarts Fusion "+ "itself and says so."),+ } return { "success": False, "error": "Fusion 360 is running but AdomBridge add-in not responding.",