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.12: background-first doctrine + foreground:true opt-in everywhere
6 files changed
+53−12
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.11+1.9.12
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.11",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.11/adom-bridge-fusion-v1.9.11.zip",- "sha256": "7b3896d37966565029b928caff13189f43fa57037c3adc8612e73990bb861690",- "size": 358860,+ "version": "1.9.12",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.12/adom-bridge-fusion-v1.9.12.zip",+ "sha256": "0980efdacd06ebc1ebf6ceec5cfbfa06eae7ac8498d49dec7588c7fcb34e7cd4",+ "size": 358995, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.11",+ "version": "1.9.12", "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.",
describe.py+3−3@@ -197,9 +197,9 @@ _T = [ ("fusion_export_skp", "Export to SketchUp (.skp).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.skp"}), # ── Library SHOW verbs + bare-artifact wrapping (issue #29, shipped 1.9.9) ──- ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),- ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),- ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),+ ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+ ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+ ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}), ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots}. Poll about once a second and render a REAL progress bar. When done:true the shots array carries base64 images (pass imagesAs:paths to skip the encoding).", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list"}, 15, None, False, {"demoId": "tour-abc123"}), ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),
dev-skills/fusion-background-etiquette/SKILL.md+35@@ -5,6 +5,17 @@ description: The invariant every code change must preserve - Fusion windows NEVE # fusion-background-etiquette - Fusion never steals the foreground +**The doctrine (John, 2026-08-15, verbatim):**++> all of your verbs should do their work in the background if they can, meaning its rude+> to force foreground a fusion window cuz a user using your bridge is doing other ai+> thread prompts and typing while you do your work. so make sure all verbs try to work in+> the background when they open windows. i know sometimes you have to foreground (fg) to+> do ui stuff that can only be done in the fg, but its rude. so you have to try super+> hard to figure out how to do it in the background, and then in the worst cases ONLY+> then do it in the foreground. but of course always let the user choose to fg a window+> if they want to cuz they may want to look at what you're doing.+ **The invariant (John, 2026-08-14):** a user is typing in their AI threads while this bridge drives Fusion. Any window this bridge causes to appear - the app itself, a document, a picker, a recovery dialog - must arrive in the BACKGROUND. Screenshots,@@ -12,6 +23,30 @@ exports, view switches, and builds all work on background windows (PrintWindow r occluded windows fine), so there is no functional excuse for a focus steal. A stolen foreground mid-keystroke is the single most user-hostile thing this bridge can do. +## The escalation ladder (exhaust each rung before the next)++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.+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).+4. **Foreground, the documented last resort** - ONLY when input genuinely requires+ focus (real keystrokes into a focused field that UIA cannot set). Every such verb:+ (a) is a conscious, documented exception here, (b) announces itself with an+ on-screen caption first, (c) restores the user's foreground after.++**The user can ALWAYS choose foreground:** every window-raising verb accepts+`foreground: true` - the dispatcher then skips the envelope entirely so the user can+watch. Their choice is the only non-rude foreground.++**Standing audit item:** the legacy deliberate-foreground exceptions (prefs_open,+mcp_enable, demo sign-in leg) predate rung 2 - re-audit each against background UIA+before trusting its exception status.+ ## The mechanism: the background envelope `fusion_detect.begin_background_envelope(seconds, minimize=False)` - refcounted, so
server.py+9−3@@ -2599,7 +2599,8 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: # The attach flow raises documents AND modal dialogs over several minutes - arm its own # long envelope so the Save dialog at the end is still covered (2026-08-15: the dispatch # envelope had expired by dialog time and the dialog kept the user's foreground).- begin_background_envelope(600.0)+ if not args.get("foreground"):+ begin_background_envelope(600.0) open_res = _orchestrate_open_lbr({"filePath": file_path}) if not open_res.get("success"): return {"success": False, "error": "open_lbr failed: " + str(open_res.get("error")), "data": {"steps": steps}}@@ -6259,8 +6260,13 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> "show_symbol", "show_footprint", "show_3d_package", } if command in _WINDOW_RAISING:- _sec = ADDIN_COMMAND_TIMEOUTS.get(command, 60) + 20- begin_background_envelope(float(_sec), minimize=(command == "start"))+ if (args or {}).get("foreground"):+ # The USER's choice, always honored (John, 2026-08-15): they may want to WATCH.+ # No envelope - the window may come to front and stay there.+ args = {k: v for k, v in args.items() if k != "foreground"}+ else:+ _sec = ADDIN_COMMAND_TIMEOUTS.get(command, 60) + 20+ begin_background_envelope(float(_sec), minimize=(command == "start")) # Direct handlers (don't need the add-in) handler = COMMAND_HANDLERS.get(command)