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.6.54: 3D auto-orient tall-part guard (keep through-hole pins vertical) + orient passthrough
5 files changed
+156−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.53+1.6.54
CHANGELOG.md+11@@ -3,6 +3,17 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge ยท GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.54 - 2026-07-07++- **Fixed 3D auto-orient laying TALL through-hole parts on their side** (John caught it: a machine+ pin's 3D came in horizontal). `make_3d_package` / `attach_3d_package` / `build_library_3d` always+ rotated the SMALLEST bbox dim to Z (correct for a flat SMD chip, wrong for a tall pin whose long+ axis is already Z). Now a tall-part guard keeps a part vertical when its largest dimension is+ already Z, and only flattens a clearly-thin part whose thin axis isn't Z yet. Verified: machine+ pins stay vertical, a flat 0603 stays flat, a chip imported on its edge still gets flattened.+- `attach_3d_package` and `build_library_3d` parts[] now honor an `orient` flag (default true) so a+ caller can skip auto-orient for an already-correctly-oriented STEP.+ ## 1.6.53 - 2026-07-07 - **Fixed slow-machine opens being mislabeled "add-in not running" (caught live on a GPU-less Azure
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.53",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.53/adom-bridge-fusion-v1.6.53.zip",- "sha256": "93e1b055a799133d452813822f1979dd67a79d2af52bd586c5bf147a29e5758d",- "size": 334089,+ "version": "1.6.54",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.54/adom-bridge-fusion-v1.6.54.zip",+ "sha256": "e2920d0d79a89fcc53c808dc2f8456c068c8be3929952f2bb8fd4b096a85e5fe",+ "size": 337157, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-07-07T12:55:00Z"+ "released_at": "2026-07-07T16:20:00Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.53",+ "version": "1.6.54", "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/adom-desktop-fusion-bridge", "author": "Adom Inc.",
server.py+138−2@@ -235,6 +235,115 @@ def _handle_notify_owner(args: dict) -> dict: } +def _handle_new_electronics_from_eagle(args: dict) -> dict:+ """Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design+ so the parts actually INSTANTIATE (schematic + populated board), then land in the PCB+ editor. Proven live 2026-07-07 on winvm.++ THE KEY FINDING: `Document.newDesignFromLocal <file.sch>` opens the schematic EDITOR+ but does NOT instantiate the parts (board_info == 0). Fusion's REAL EAGLE importer is+ the internal command `ImportSCHAndBRDCmd` - it pops TWO native "Open" file dialogs (first+ the .sch, then the .brd) and builds a proper board-linked electronics design. This verb+ fires that command and drives BOTH dialogs entirely in the BACKGROUND via the AD direct+ API (desktop_ui_click by accessible name - no foreground, no coordinates). The Fusion-+ native .fsch/.fbrd container is opaque BINARY (Manifest.dat + BulkStream segments), so+ you CANNOT author it offline - going through Fusion's importer is the only way.++ args: {schPath: Windows path to the .sch (required), brdPath: Windows path to the .brd+ (optional; defaults to the .sch's sibling .brd)}+ """+ import time as _t+ sch = (args.get("schPath") or args.get("filePath") or "").replace("\\", "/")+ if not sch or not sch.lower().endswith(".sch"):+ return {"success": False, "error": "schPath (a .sch file on the Windows host) is required.",+ "_hint": "Author the EAGLE .sch + paired .brd (same basename, same folder), stage both "+ "to Windows (send_files), then call with {schPath, brdPath}."}+ brd = (args.get("brdPath") or (sch[:-4] + ".brd")).replace("\\", "/")+ if not ad_client.available():+ return {"success": False, "error": "AD direct API unavailable - cannot drive the import dialogs.",+ "_hint": "This verb needs AD >=1.9.84 (the bridge drives the two Open dialogs via desktop_ui_click). "+ "Fallback: fire 'Commands.Start ImportSCHAndBRDCmd' via fusion_execute_text_command and "+ "drive the two Open dialogs yourself (select the .sch then .brd by accessible name)."}++ def _find_open_dialog():+ res = ad_client.call("desktop_list_windows", {}) or {}+ out = res.get("output") if isinstance(res, dict) else None+ if isinstance(out, str):+ try: out = json.loads(out)+ except Exception: out = None+ wins = ((out or res).get("data", out or res) or {}).get("windows", []) if isinstance(out or res, dict) else []+ for w in wins:+ if str(w.get("title", "")).strip().lower() == "open":+ return w.get("hwnd")+ return None++ def _pick(hwnd, basename):+ # select the file by accessible name, then click Open - both background UIA+ ad_client.call("desktop_ui_click", {"hwnd": hwnd, "name": basename})+ _t.sleep(1.0)+ ad_client.call("desktop_ui_click", {"hwnd": hwnd, "name": "Open"})++ import os as _os+ sch_base, brd_base = _os.path.basename(sch), _os.path.basename(brd)++ # 1) Fire Fusion's EAGLE importer (opens the first Open dialog).+ _proxy_to_addin("execute_text_command", {"command": "Commands.Start ImportSCHAndBRDCmd"}, timeout=15)++ # 2) Drive the two Open dialogs (sch, then brd). Each appears after a beat; retry.+ picked = []+ for want, base in (("sch", sch_base), ("brd", brd_base)):+ dlg = None+ for _ in range(20):+ _t.sleep(1.5)+ dlg = _find_open_dialog()+ if dlg:+ break+ if not dlg:+ # brd dialog may not appear if Fusion inferred the sibling automatically+ if want == "brd" and picked:+ break+ return {"success": False, "error": f"The '{want}' Open dialog never appeared.",+ "_hint": "Screenshot the Fusion hwnd + READ screenshots[] for a blocking modal; the importer "+ "may have errored on the EAGLE source. Verify the .sch/.brd are valid EAGLE.",+ "data": {"picked": picked}}+ _pick(dlg, base)+ picked.append(base)+ _t.sleep(2.0)++ # 3) Verify by STATE - poll until the imported design is an electronics design.+ ready = False+ for _ in range(40): # up to ~2 min; import is slow, esp. software-rendered+ _t.sleep(3)+ try:+ st = _proxy_to_addin("get_app_state", {}, timeout=8)+ d = st.get("data") or {}+ if d.get("isElectronics") and str(d.get("activeWorkspace", "")).lower() in ("pcb editor", "schematic editor", "board layout", "3d pcb"):+ ready = True+ break+ except Exception:+ pass # app_state is None while a modal import dialog blocks - keep polling++ return {+ "success": ready,+ "imported": picked,+ "statusVerb": "fusion_board_info",+ "_hint": (+ "EAGLE design imported via Fusion's own ImportSCHAndBRDCmd (both Open dialogs driven in the "+ "background). It is now an electronics design in the PCB editor. NEXT: fusion_show_2d_board, "+ "then RATSNEST + 'AUTO ;' (fusion_electron_run) to autoroute, fusion_show_3d_board for the 3D. "+ "NOTE: board_info can read 0 right after import (wrong-view query) even though the board is "+ "populated - verify by screenshotting the Fusion hwnd (read screenshots[]) or fusion_show_2d_board "+ "+ WINDOW FIT. If 3D bodies are missing, the placed parts' package3d urns must resolve in THIS "+ "project - build the library's 3D into this project with fusion_build_library_3d (do NOT reuse "+ "another library's urns; they don't resolve cross-project)."+ if ready else+ "Import fired + both Open dialogs driven, but the design did not confirm as electronics within the "+ "budget (a slow VM import can run longer). Poll fusion_get_app_state, and screenshot the Fusion hwnd "+ "reading screenshots[] for a blocking import dialog."+ ),+ }++ def _handle_launch(fusion_info: dict, args: dict) -> dict: """Launch Fusion 360 and optionally wait for the AdomBridge add-in.""" # LIVE detect (not the stale bridge-start fusion_info snapshot) so a launch right@@ -1253,6 +1362,11 @@ COMMAND_HANDLERS = { "dismiss_recovery": _handle_dismiss_recovery, "relocate_recovery": _handle_relocate_recovery, "open_cloud_file": _handle_open_cloud_file,+ # Import a legacy EAGLE .sch (+ .brd) into a NEW populated Fusion electronics design via+ # Fusion's own ImportSCHAndBRDCmd (drives both Open dialogs in the background). This is the+ # ONLY way to author a board from EAGLE source - newDesignFromLocal opens the editor but does+ # NOT instantiate parts, and the .fsch/.fbrd binary container can't be built offline.+ "new_electronics_from_eagle": lambda fi, args: _handle_new_electronics_from_eagle(args), "screenshot_fusion": _handle_screenshot_fusion, "click_fusion": _handle_click_fusion, "send_key": _handle_send_key,@@ -1891,6 +2005,7 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: file_path = (args.get("filePath") or "").replace("\\", "/") model_path = (args.get("modelPath") or "").replace("\\", "/") package = args.get("packageName", "")+ orient_flag = args.get("orient", True) # 2026-07-07: expose orient (was hard-on) if not file_path or not model_path: return {"success": False, "error": "filePath (.lbr) and modelPath (.step) are required (Windows paths, e.g. C:/...).",@@ -1916,7 +2031,15 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: f"im.importToTarget(im.createSTEPImportOptions('{model_path}'), root)\n" "oc=root.occurrences.item(root.occurrences.count-1); bb=oc.boundingBox\n" "dx=bb.maxPoint.x-bb.minPoint.x; dy=bb.maxPoint.y-bb.minPoint.y; dz=bb.maxPoint.z-bb.minPoint.z\n"- "axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n"+ # Tall-part guard (2026-07-07): keep a part vertical if its largest dim is already Z; only+ # flatten a clearly-thin part whose thin axis isn't Z. Was: always tip smallest dim to Z,+ # which laid tall through-hole pins on their side. Honors the orient flag (default True).+ + ("axis=None\n" if not orient_flag else+ "thin=min(dx,dy,dz); big=max(dx,dy,dz)\n"+ "tall_z=(dz>=dx and dz>=dy)\n"+ "axis=None\n"+ "if (thin < 0.5*big) and not tall_z:\n"+ " axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n") + "if axis:\n" " mat=oc.transform2.copy(); rot=adsk.core.Matrix3D.create()\n" " rot.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))\n"@@ -2149,8 +2272,17 @@ def _orchestrate_make_3d_package(args: dict) -> dict: pass _time.sleep(0.4) before_shot = _capture_labeled(f"{cap_label}_before")+ # AUTO-ORIENT (fixed 2026-07-07): the old logic always rotated the SMALLEST bbox dim to Z,+ # which is right for a flat SMD chip lying down but TIPS A TALL THROUGH-HOLE PART (machine pin,+ # connector) onto its side - its long axis is already Z and must stay vertical. Now: keep the+ # part vertical if its largest dim is already Z (tall_z), and only flatten a clearly THIN part+ # whose thin axis isn't Z yet. A caller can still force orient:false to skip entirely. orient_block = (- "axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n"+ "thin=min(dx,dy,dz); big=max(dx,dy,dz)\n"+ "tall_z=(dz>=dx and dz>=dy)\n"+ "is_flat=(thin < 0.5*big)\n"+ "axis=None\n"+ "if is_flat and not tall_z:\n axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n" "if axis:\n mat=oc.transform2.copy(); r=adsk.core.Matrix3D.create()\n" " r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))\n" " mat.transformBy(r); oc.transform2=mat\n"@@ -2283,6 +2415,10 @@ def _orchestrate_build_library_3d(args: dict) -> dict: "projectId": project_id, "folderId": folder_id, "fileName": f"{pkg}_3d", "task": task, "captureLabel": (pkg if capture else None),+ # Per-part orient passthrough (2026-07-07): a caller can set orient:false on a part+ # whose STEP is already correctly oriented (e.g. a tall through-hole pin). Defaults+ # to True, and make_3d_package's tall-part guard now keeps tall parts vertical anyway.+ "orient": p.get("orient", True), } mk = _orchestrate_make_3d_package(mk_args) # Retry once: the FIRST part of a run often fails transiently while Fusion settles from a