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 74 file(s)
10 files changed
+174−74
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.262+1.9.263
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.262",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.262/adom-bridge-fusion-v1.9.262.zip",- "sha256": "322d4aee01e4522fed0ce6adeda757b7b353142b4435f10536118d428ba164b0",- "size": 554511,+ "version": "1.9.263",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.263/adom-bridge-fusion-v1.9.263.zip",+ "sha256": "67b364b0418e02b31bf1fb282b3363f5fd351b0c0a27592f074d2ebd6f69cf07",+ "size": 556122, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.262",+ "version": "1.9.263", "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.262"+version = "1.9.263" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.262"+version = "1.9.263" edition = "2021" [dependencies]
handlers/taskbar_overlay.py+1−1@@ -6,7 +6,7 @@ glance; the same signal belongs on FUSION's button whenever this bridge drives i THE LESSON (stolen from pup, who paid for it first - src/server.js): an in-bridge ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY. The COM call returns S_OK cross-process and the shell silently ignores it (verified live here too: badge never-rendered). The working path is AD-core's `desktop_taskbar` verb (ab >=1.9.106): hand+rendered). The working path is ab-core's `desktop_taskbar` verb (ab >=1.9.106): hand it the badge PNG as base64 and ab paints the overlay natively, cross-process. set: desktop_taskbar {hwnd, overlay: {icon: <b64 png>, tooltip}}
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.262"+DASH_VERSION="1.9.263" 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.262",+ "version": "1.9.263", "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.262",+ "version": "1.9.263", "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+152−55@@ -3970,6 +3970,31 @@ def _orchestrate_make_3d_package(args: dict) -> dict: return {"success": False, "error": "Create3DPackage failed: " + str(cp.get("error")), "_hint": "The .lbr must be adom-lbr-generated; its FIRST package's footprint is loaded into the generator."} _time.sleep(2)+ # FOOTPRINT IDENTITY (John, 2026-08-29): the generator loads the FIRST package of the OPEN+ # library and names its document after that package. Read the name back and refuse when it+ # is not the package this mint is for: a chip on the wrong footprint must fail here, not+ # get uploaded, bound and "proven".+ _expected_pkg = ""+ try:+ import xml.etree.ElementTree as _ETg+ _pk = _ETg.parse(lbr_path).getroot().find(".//library/packages/package")+ _expected_pkg = (_pk.get("name") if _pk is not None else "") or ""+ except Exception:+ _expected_pkg = ""+ _gen_doc = ""+ try:+ _di = _proxy_to_addin("document_info", {}, timeout=8)+ _ad = (_di.get("data") or {}).get("activeDocument")+ _gen_doc = str((_ad.get("name") if isinstance(_ad, dict) else _ad) or "").rstrip("*").strip()+ except Exception:+ _gen_doc = ""+ if _expected_pkg and _gen_doc and _gen_doc.lower() != _expected_pkg.lower() \+ and not _gen_doc.lower().startswith("untitled"):+ return {"success": False, "errorCode": "wrong_footprint_loaded",+ "error": ("The Package3D generator loaded footprint %r but this mint is for package %r; "+ "the chip would have landed on the wrong footprint." % (_gen_doc, _expected_pkg)),+ "_hint": "Mint each part on a library whose FIRST package is that part's footprint "+ "(fusion_install_library_bundle splits a merged library per part for this reason)."} # BEFORE shot: the footprint (pads + courtyard) loaded in the 3D viewer, no chip yet. before_shot = None if cap_label:@@ -4300,6 +4325,44 @@ def _reconcile_orphaned_runs(): return 0 +def _sweep_orphan_scratch_docs() -> dict:+ """Close scratch documents a chain that died with a bridge respawn left open.++ Only when a run is on file as bridge_restarted_mid_run and not yet swept; only documents+ that are bridge scratch by construction: never-modified 'Untitled*' and '<pkg>_3d' mint+ designs. A user's named or modified document is never touched. Ledgered.+ """+ import tempfile as _tf, json as _js+ _p = os.path.join(_tf.gettempdir(), "fusion-prove-runs.json")+ if not os.path.exists(_p):+ return {"swept": 0}+ runs = _js.loads(open(_p, encoding="utf-8").read()) or {}+ pending = [rid for rid, st in runs.items()+ if st.get("errorCode") == "bridge_restarted_mid_run" and not st.get("scratchSwept")]+ if not pending:+ return {"swept": 0}+ if not _addin_round_trip_ready(tries=1):+ return {"swept": 0, "why": "add-in not answering"}+ di = _proxy_to_addin("document_info", {}, timeout=8)+ closed = []+ for d in ((di.get("data") or {}).get("openDocuments") or []):+ n = str(d.get("name") or "").rstrip("*").strip()+ modified = bool(d.get("isModified") or d.get("modified") or str(d.get("name") or "").endswith("*"))+ if (n.lower().startswith("untitled") and not modified) or n.lower().endswith("_3d"):+ try:+ r = _close_bridge_doc_verified(n)+ if r.get("closed"):+ closed.append(n)+ except Exception:+ pass+ for rid in pending:+ runs[rid]["scratchSwept"] = True+ runs[rid]["scratchSweptDocs"] = closed+ open(_p, "w", encoding="utf-8").write(_js.dumps(runs))+ _bridge_action_ledger("orphan_scratch_docs_swept", {"runs": pending, "closed": closed})+ return {"swept": len(closed), "closed": closed}++ def _publish_run_state(run_id: str, state: dict): """Write a run's state to the SAME per-run store fusion_prove_result already reads. @@ -10918,7 +10981,7 @@ def _open_doc_names() -> set: return set() -def _close_docs_opened_since(before: set) -> list:+def _close_docs_opened_since(before: set, keep: set = None) -> list: """Close every document that was NOT open when `before` was snapshotted. Used by the create_library chain on BOTH success and failure. Everything a chain opens is bridge scratch by construction: Create3DPackage's generator design (an 'Untitled(N)' - one in@@ -10927,8 +10990,11 @@ def _close_docs_opened_since(before: set) -> list: by part 3 of a 20-part matrix (2026-08-23): ~2 leaked per chain, and Fusion slows with every open doc. Never touches a doc that pre-dates the chain (the user's own work).""" closed = []+ keep = {str(k).lower() for k in (keep or ())} try: for n in sorted(_open_doc_names() - set(before or ())):+ if n.rstrip("*").strip().lower() in keep:+ continue # the finished library stays open for the user _note_bridge_doc(n) # so the save-prompt sentinel owns any prompt it raises r = _close_bridge_doc_verified(n) if r.get("closed"):@@ -12018,7 +12084,8 @@ COMMAND_HANDLERS["prove_result"] = _handle_prove_result -def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries") -> dict:+def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries",+ replace_existing: bool = False) -> dict: """Save the bound library into the user's cloud project so Fusion's Library Manager lists it. Folder is created if missing (project root/<folder>). A same-named library already there is@@ -12035,15 +12102,36 @@ def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libr out["folderCreated"] = bool(r.get("success")) and not bool((r.get("data") or {}).get("existed")) except Exception as e: out["folderNote"] = str(e)[:120]+ _older = [] try: lst = _proxy_to_addin("list_cloud_files", {"folderPath": folder}, timeout=60)- names = [str(f.get("name") or "") for f in ((lst.get("data") or {}).get("files") or [])]- out["duplicatesBefore"] = sum(1 for n in names if n.lower() == name.lower())+ for f in ((lst.get("data") or {}).get("files") or []):+ if str(f.get("name") or "").lower() == name.lower():+ _older.append({"id": f.get("id"), "versionNumber": f.get("versionNumber")})+ out["duplicatesBefore"] = len(_older)+ out["olderSameName"] = [f.get("id") for f in _older] except Exception: out["duplicatesBefore"] = None- act = _ensure_library_active(lbr_path)- if not act.get("ok"):- out["error"] = "library document is not active, so it could not be saved: " + str(act.get("why") or "")[:120]+ # Make the library the active document ourselves (the generic helper also demands a+ # library workspace and gave up after the prove left a 3D document in front, 2026-08-29).+ import time as _tpa+ _deadline = _tpa.time() + 120+ _seen = None+ _active_ok = False+ while _tpa.time() < _deadline:+ try:+ _proxy_to_addin("activate_document", {"name": stem}, timeout=10)+ st = _proxy_to_addin("get_app_state", {}, timeout=8)+ _seen = (st.get("data") or {})+ if stem.lower() in str(_seen.get("activeDocument") or "").lower():+ _active_ok = True+ break+ except Exception as e:+ _seen = {"error": str(e)[:80]}+ _tpa.sleep(2)+ if not _active_ok:+ out["error"] = ("library document %r could not be made active (last seen: %s), so it was not "+ "saved" % (stem, json.dumps(_seen)[:160])) return out r = _proxy_to_addin("save_to_cloud", {"name": name, "folderPath": folder, "description": "Imported by Adom (bound 3D packages)"},@@ -12054,8 +12142,32 @@ def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libr return out out.update(saved=True, project=d.get("project"), folder=d.get("folder") or folder, fileId=d.get("fileId"), versionId=d.get("versionId"), versionNumber=d.get("versionNumber"))+ # UPDATE IN PLACE (John, 2026-08-29: a re-import must update THE library, not mint a second).+ # Fusion's saveAs always creates a new document, so one-library-per-name means retiring the+ # older same-named documents in OUR folder once the new save has landed. That removes cloud+ # documents, so it runs only when the caller asks (replaceExisting:true), only for files in+ # <project>/<folder> with this exact name, by id, and never the one just saved. Without the+ # flag the older copies are left alone and named in olderSameName so the caller can decide.+ replaced = []+ if replace_existing and d.get("fileId") and _older:+ for f in _older:+ fid = f.get("id")+ if not fid or fid == d.get("fileId"):+ continue+ try:+ rr = _proxy_to_addin("delete_cloud_file", {"fileId": fid, "folderPath": folder}, timeout=60)+ if rr.get("success"):+ replaced.append(fid)+ except Exception:+ pass+ out["replaced"] = replaced+ out["note"] = (("replaced %d older library document(s) of the same name in %s; a board that "+ "referenced an old document by id must re-link to this one" % (len(replaced), folder))+ if replaced else+ ("%d older library document(s) of the same name remain in %s (pass replaceExisting:true "+ "to retire them)" % (len(_older), folder) if _older else "first library of this name in %s" % folder)) _bridge_action_ledger("library_saved_to_cloud", {"name": name, "folder": folder,- "fileId": d.get("fileId")})+ "fileId": d.get("fileId"), "replaced": replaced}) return out def _handle_create_library(fusion_info: dict, args: dict) -> dict:@@ -12116,6 +12228,14 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: "restarted under a user today. Poll this runId." % (_rid, _rv.get("stage"))} except Exception: pass+ # HYGIENE AFTER A RESPAWN (2026-08-29): a chain killed by a bridge restart cannot close its+ # own scratch documents (its list died with the process; five Untitled tabs were left). Any+ # run the reconcile marked bridge_restarted_mid_run and nobody swept gets swept here, before+ # a new chain adds to the pile: never-modified Untitled docs and *_3d mint designs only.+ try:+ _sweep_orphan_scratch_docs()+ except Exception:+ pass if not lbr_p and not (sym_p or mod_p): return {"success": False, "errorCode": "no_input", "error": "Give me EITHER libraryPath (a ready .lbr) OR kicadSymbolPath / "@@ -12495,7 +12615,8 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: state["stageLabel"] = "saving the library to your cloud Library Manager" _save() state["cloudLibrary"] = _publish_library_to_cloud(- target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries")+ target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries",+ replace_existing=bool(_pub.get("replaceExisting"))) except Exception as _pe: state["cloudLibrary"] = {"saved": False, "error": str(_pe)[:200]} _save()@@ -12547,10 +12668,15 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # responding and one part's mess failed every part after it). Close THIS # chain's library document on success AND failure - prove's own close is one # path here, this is the guarantee. Idempotent when already closed.+ # LEAVE THE LIBRARY OPEN FOR THE USER (John, 2026-08-29: "otherwise they then have+ # to go find it"). A bundle import ends with the saved library in front of the+ # user and only the scratch closed. Other callers keep the old close-everything+ # contract (batch matrices sweep between parts).+ _keep_open = bool(args.get("_publishToCloud")) and bool((state.get("bind") or {}).get("success"))+ _lbr_done = (state.get("libraryPath") or lbr_p or "")+ _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0] if _lbr_done else "" try:- _lbr_done = (state.get("libraryPath") or lbr_p or "")- if _lbr_done:- _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+ if _lbr_done and not _keep_open: _closed = _close_bridge_doc_verified(_stem_done) state["libraryDocClosed"] = bool(_closed.get("closed")) _save()@@ -12560,10 +12686,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # 'Untitled' generators, the saved 3D, anything a frame left behind), not just # the library. Pre-existing docs are untouched. try:- state["scratchDocsClosed"] = _close_docs_opened_since(_docs_before)+ state["scratchDocsClosed"] = _close_docs_opened_since(+ _docs_before, keep={_stem_done.lower()} if _keep_open else set()) _save() except Exception: pass+ if _keep_open and _stem_done:+ try:+ _proxy_to_addin("activate_document", {"name": _stem_done}, timeout=10)+ state["libraryDocClosed"] = False+ state["libraryLeftOpen"] = True+ _save()+ except Exception:+ pass # THE RUN MUST BE ON FILE BEFORE THE RUNID LEAVES THIS FUNCTION (2026-08-28, cold-path # run bc3141915abf): the page polled 0.4s after receiving the runId and got@@ -12726,7 +12861,10 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict: # the end state the user is after: ONE library in # Fusion's Library Manager, not a file on disk "_publishToCloud": {"name": lib_name,- "folder": "Adom Libraries"}})+ "folder": "Adom Libraries",+ # retire older same-named+ # copies only when asked+ "replaceExisting": bool(args.get("replaceExisting"))}}) out.update({k: chain.get(k) for k in ("started", "attached", "runId", "statusVerb", "stage")}) if not chain.get("success"): out.update(success=False, error=chain.get("error"), errorCode=chain.get("errorCode"))@@ -13958,47 +14096,6 @@ def _handle_live_feed(fusion_info: dict, args: dict) -> dict: "container and publish it. kind is main|popup; proof frames come " "from fusion_prove_result evidence[] with kind:'proof'. Pass " "since:<tick> to get only what you have not seen.")})- if action == "probe":- # DIAGNOSTIC (2026-08-29): which capture call shape returns owned popups from inside the- # bridge? The browser lane gets the full capture (screenshots[]); the bridge's own calls- # get a bare inline image. Try the variants and report the reply keys for each.- info = get_fusion_window_info() or {}- hwnd = info.get("hwnd") or (info.get("mainWindow") or {}).get("hwnd")- if not hwnd:- return {"success": False, "error": "no Fusion window"}- out = {"success": True, "hwnd": hwnd, "variants": {}}- def _shape(r):- if not isinstance(r, dict):- return {"type": type(r).__name__}- d = r.get("data") if isinstance(r.get("data"), dict) else None- return {"keys": sorted(r.keys())[:14], "dataKeys": sorted(d.keys())[:14] if d else None,- "popups": len(r.get("screenshots") or (d or {}).get("screenshots") or []),- "error": str(r.get("error") or r.get("errorCode") or "")[:80]}- for label, args in (("plain", {"hwnd": int(hwnd)}), ("inlineFalse", {"hwnd": int(hwnd), "inline": False}),- ("store", {"hwnd": int(hwnd), "store": True}), ("noResize", {"hwnd": int(hwnd), "resizeMax": 0}),- ("popups", {"hwnd": int(hwnd), "includePopups": True, "children": True})):- try:- out["variants"]["direct:" + label] = _shape(_ad_direct_api_call("desktop_screenshot_window", args, timeout=20))- except Exception as e:- out["variants"]["direct:" + label] = {"raised": str(e)[:80]}- # the same POST with NO bridge token and NO delegate header (what a web page sends)- try:- base = (os.environ.get("ADOM_DIRECT_API_URL") or "").strip()- if not base:- base = "http://127.0.0.1:%s" % open(os.path.join(os.path.expanduser("~"), ".adom", "direct-api-port")).read().strip()- body = json.dumps({"command": "desktop_screenshot_window", "args": {"hwnd": int(hwnd)}}).encode()- req = urllib.request.Request(base.rstrip("/") + "/command", data=body,- headers={"Content-Type": "application/json"}, method="POST")- with urllib.request.urlopen(req, timeout=20) as resp:- out["variants"]["loopback:noheaders"] = _shape(json.loads(resp.read()))- out["directApiBase"] = base- except Exception as e:- out["variants"]["loopback:noheaders"] = {"raised": str(e)[:120]}- try:- out["variants"]["ad_client"] = _shape(ad_client.call("desktop_screenshot_window", {"hwnd": int(hwnd)}, timeout=20))- except Exception as e:- out["variants"]["ad_client"] = {"raised": str(e)[:80]}- return out if action == "stop": _LIVE_FEED["stop"] = True return {"success": True, "stopped": True, "tick": _LIVE_FEED["tick"]}