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 15 file(s)
9 files changed
+119−20
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.212+1.9.213
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.212",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.212/adom-bridge-fusion-v1.9.212.zip",- "sha256": "0b083f79d09c8007ae271509509eb691d3a37301260e858f72c5174abf407767",- "size": 500144,+ "version": "1.9.213",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.213/adom-bridge-fusion-v1.9.213.zip",+ "sha256": "79f355e5256271db6c151801635535ecd010bb5f0f5aff029f13ae8ff673882e",+ "size": 502904, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.212",+ "version": "1.9.213", "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.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.212"+version = "1.9.213" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.212"+version = "1.9.213" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.212"+DASH_VERSION="1.9.213" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.212",+ "version": "1.9.213", "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+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.212",+ "version": "1.9.213", "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",@@ -75,7 +79,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.",@@ -104,9 +111,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+98−2@@ -3305,6 +3305,9 @@ def _orchestrate_open_lbr(args: dict) -> dict: results.append(f"Verification: exported={verification.get('exported', False)}") import os+ # Remember it so a later capture/EDIT can pre-check names offline instead of letting+ # Fusion raise "Create new footprint 'X'?" and dismissing it after the fact.+ _remember_open_library(file_path) response = { "success": True, "output": f"Opened library: {os.path.basename(file_path)}",@@ -4206,16 +4209,65 @@ def _orchestrate_capture_library_views(args: dict) -> dict: # EDIT is async - the editor takes a beat to actually SWITCH the view. Capturing too soon grabs # the PREVIOUS view (a mislabeled shot). Settle AFTER the EDIT (before fit/capture); tunable. settle = float(args.get("settle", 1.5))+ # ⛔ PRE-CHECK EVERY NAME OFFLINE, BEFORE ANY EDIT REACHES FUSION.+ # This verb used to fire EDIT on whatever it was handed and then call+ # _dismiss_dialogs_bg() to clear the "Create new footprint 'X'?" modal it had just+ # caused. That is backwards three ways: the modal freezes the add-in's main thread+ # (so every later verb reads as a crash), it dings the machine's alert sound at a user+ # who did not do anything, and dismissal is a RACE we lose whenever Fusion is slow.+ # The .lbr on disk already knows every symbol/package/deviceset name, so a name that+ # would prompt is knowable in milliseconds without Fusion. Resolve here; never ask.+ lbr_path = (args.get("libraryPath") or "").replace("\\", "/") or _remembered_open_library()+ resolved_names = {}+ name_errors = []+ if lbr_path:+ for pkg in pkgs:+ for v in views:+ ext = view_ext.get(v, v)+ r, err = _resolve_artifact_name(pkg, ext, lbr_path)+ if err:+ name_errors.append({"package": pkg, "view": label_of.get(ext, ext),+ "error": err.get("error"),+ "available": err.get("available"),+ "closestMatches": err.get("closestMatches")})+ else:+ resolved_names[(pkg, ext)] = r out = [] for pkg in pkgs: for v in views: ext = view_ext.get(v, v)- er = _proxy_to_addin("electron_run", {"command": f"EDIT {pkg}.{ext}"}, timeout=30)+ if lbr_path and (pkg, ext) not in resolved_names:+ # Would have prompted. Report it as a skip; do NOT hand it to Fusion.+ _bad = next((e for e in name_errors+ if e["package"] == pkg and e["view"] == label_of.get(ext, ext)), {})+ out.append({"package": pkg, "view": label_of.get(ext, ext), "path": None,+ "ok": False, "skipped": "name_not_in_library",+ "error": _bad.get("error"),+ "available": (_bad.get("available") or [])[:20],+ "closestMatches": _bad.get("closestMatches"),+ "_hint": "No EDIT was issued and no dialog was raised - this name "+ "is not in the library, so opening it would have popped "+ "Fusion's blocking 'Create new ...?' modal. Pick a name "+ "from `available`."})+ continue+ name = resolved_names.get((pkg, ext), pkg)+ er = _proxy_to_addin("electron_run", {"command": f"EDIT {name}.{ext}", "confirm": "no"}, timeout=30) _t.sleep(settle) # let the editor LOAD the new view before framing/capturing it # ALWAYS catch + dismiss any dialog the EDIT raised - in the BACKGROUND (no focus steal). # Common one: "Create new symbol/footprint '<name>'?" when <name> is a DEVICESET name but # the symbol/package is named by the shared combo. Dismissing (Cancel/No) avoids a stray.+ # BACKSTOP ONLY. With the pre-check above this should never fire; if it does,+ # the pre-check has a hole (a library opened by a route that did not record its+ # path, or a name EAGLE renamed on load) and that is a BUG to fix at the source,+ # not a dialog to keep sweeping. Log it loudly so it cannot stay invisible. dismissed = _dismiss_dialogs_bg()+ if dismissed:+ print("[Fusion Bridge] ⚠ PRE-CHECK HOLE: EDIT %s.%s still raised a dialog "+ "(lbr_path=%r). Prevention failed; dismissal should not be load-bearing."+ % (name, ext, lbr_path))+ _bridge_action_ledger("dialog_precheck_hole",+ {"name": name, "ext": ext, "lbrPath": lbr_path,+ "dismissed": dismissed}) _proxy_to_addin("electron_run", {"command": "WINDOW FIT"}, timeout=20) _t.sleep(0.6) shot = _capture_labeled(f"{pkg}_{label_of.get(ext, ext)}")@@ -7542,6 +7594,49 @@ def _artifact_name_from(args, *frag_keys): +def _remember_open_library(lbr_path: str):+ """Record the .lbr we just opened so LATER verbs can pre-check names without a path.++ ⛔ THIS EXISTS TO PREVENT A MODAL, NOT TO BE TIDY (John, 2026-08-26, third report of the+ same dialog). `_resolve_artifact_name` could always stop a Create-new prompt, but it+ needs the .lbr on disk, and `fusion_capture_library_views` never took a libraryPath - so+ the ONE verb that EDITs the most names was the one verb structurally unable to run the+ guard, and it "handled" the prompt by raising it and dismissing it afterwards. Persisted+ (not just module state) because AD reaps and respawns this bridge between calls, which+ would drop in-memory state exactly when a capture follows an open.+ """+ # LOCAL imports + a LOUD except. Both matter: this module imports os/time inside+ # functions, so a module-level `time.time()` here raised NameError - and the bare+ # `except: pass` I first wrote ATE it, leaving the guard silently disarmed while the+ # code read as correct. A prevention guard that fails quietly is worse than none,+ # because it removes the pressure to notice.+ try:+ import pathlib as _pl, json as _js, os as _os, time as _tm+ d = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge"+ d.mkdir(parents=True, exist_ok=True)+ (d / "open_library.json").write_text(_js.dumps(+ {"libraryPath": lbr_path.replace("\\", "/"), "at": _tm.time()}))+ except Exception as e:+ print("[Fusion Bridge] ⚠ could not remember the open library (%s: %s) - the "+ "Create-new-dialog pre-check will fall back to SET CONFIRM only"+ % (type(e).__name__, e))+++def _remembered_open_library():+ """The last .lbr opened, if it is still on disk. None when we genuinely do not know."""+ try:+ import pathlib as _pl, json as _js, os as _os+ f = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge" / "open_library.json"+ if not f.exists():+ return None+ lp = (_js.loads(f.read_text()) or {}).get("libraryPath") or ""+ return lp if lp and _os.path.exists(lp) else None+ except Exception as e:+ print("[Fusion Bridge] ⚠ could not read the remembered library (%s: %s)"+ % (type(e).__name__, e))+ return None++ def _library_artifact_names(lbr_path: str) -> dict: """The names that actually EXIST in a .lbr: {"sym": [...], "pac": [...], "dev": [...]}. @@ -7660,7 +7755,8 @@ def _show_library_view(name, suffix, lbr_path=None): # instead of failing the frame. edit = None for _busy_try in range(7):- edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15)+ edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix),+ "confirm": "no"}, timeout=15) _etxt = str(edit.get("error") or "") if edit.get("success"): break