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 8 file(s)
8 files changed
+118−25
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.126+1.9.127
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.126",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.142/adom-bridge-fusion-v1.9.126.zip",- "sha256": "f4cdb87d2f4180078279d496c1d27fd95d25994f0f5dd9cf13be19cc00e1f7e9",- "size": 460314,+ "version": "1.9.127",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.143/adom-bridge-fusion-v1.9.127.zip",+ "sha256": "42a3212a44cab8a2e2abcdd3264d8fb96fbb8f9099975045ba23e6dd5af5bace",+ "size": 462339, "verbPrefixes": [ "fusion_" ],
aps.py+9−4@@ -1461,10 +1461,15 @@ def thumbnail_state(project_id: str, folder_id: str, wip_urn: str = "", file_nam u = str(u or "").split("?", 1)[0] return u.rsplit("vf.", 1)[-1] if "vf." in u else u want_tok = _vf_token(want_urn)- fn = str(file_name or "").lower()+ def _norm(s):+ # Part names round-trip through KiCad/EAGLE/upload with commas, dots, dashes and+ # underscores mangled differently at each hop ("TJA1051T_3,118" vs+ # "tja1051t-3-118" - issue #47 failure mode 4). Compare alphanumerics only.+ return "".join(c for c in str(s or "").lower() if c.isalnum())+ fn = _norm(file_name) for v in included: vid_tok = _vf_token(v.get("id"))- vname = str((v.get("attributes") or {}).get("displayName") or "").lower()+ vname = _norm((v.get("attributes") or {}).get("displayName")) # names match EITHER direction: the f3d is uploaded as ADOM-<part>-3D, so the # part name is a substring of the file, not a prefix (caught live: not_found on a # file that was sitting right there)@@ -1473,9 +1478,9 @@ def thumbnail_state(project_id: str, folder_id: str, wip_urn: str = "", file_nam hit_version = v break if hit_version is None and file_name:- fn2 = str(file_name).lower()+ fn2 = _norm(file_name) for it in items:- nm = str((it.get("attributes") or {}).get("displayName") or "").lower()+ nm = _norm((it.get("attributes") or {}).get("displayName")) if nm.startswith(fn2) or fn2 in nm: hit_item = it break
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.126",+ "version": "1.9.127", "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+1−1@@ -14,7 +14,7 @@ _STATUS = "fusion_get_app_state" # Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example) _T = [ # ── Lifecycle ──────────────────────────────────────────────────────────- ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates, prove carries the verdict). stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),+ ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}), ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.", {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}), ("fusion_stop", "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.142",+ "version": "1.9.143", "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.142",+ "version": "1.9.143", "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+91−6@@ -7091,6 +7091,21 @@ def _show_library_view(name, suffix, lbr_path=None): print("[Fusion Bridge] resolved %r -> %r from the library (avoids a Create-new prompt)" % (name, resolved)) name = resolved+ # ⚠️ EDIT DRIVES THE ACTIVE DOCUMENT, NOT A PATH (page issue #47, root cause of+ # failure modes 1 AND 2): in a batch, several libraries (and f3ds) are open at+ # once, and EDIT lands in whichever is active. The artifact scan read the right+ # FILE while EDIT drove the WRONG document - so Fusion offered to CREATE a part+ # that plainly existed, or refused with "Not in Electronics editor". Make the+ # target library active, verified, before every EDIT.+ act = _ensure_library_active(lbr_path, timeout_s=30)+ if not act.get("ok"):+ return {"success": False, "errorCode": "library_not_active",+ "error": "Could not make %s the ACTIVE document (active: %r), so EDIT "+ "was not issued - it would have driven the wrong library." % (+ lbr_path.rsplit("/", 1)[-1], act.get("activeDocument")),+ "_hint": "fusion_open_lbr the library, then re-call. In a batch, close "+ "the previous part's documents first (the create_library chain "+ "now does this itself)."} edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15) # WE MAY NOT HAVE BEEN ABLE TO PRE-CHECK (no libraryPath, nothing remembered, or the # library was opened by another route). If Fusion prompted anyway, answer it HERE - a@@ -9195,11 +9210,35 @@ def _prove_capture(label: str, view: str) -> dict: w, h = shot.get("width"), shot.get("height") out["width"], out["height"] = w, h out["ok"] = not (w and h) or (int(w) >= 400 and int(h) >= 300)+ # INLINE COPY FOR BROWSER CALLERS (issue #47): the path is unreadable from+ # wiki.adom.inc web control - it cannot open C:/tmp. A ~500px data URI per+ # frame keeps the whole four-frame set renderable straight from the poll+ # response while the full-res file stays on disk for same-box callers.+ try:+ out["thumbnailDataUrl"] = _frame_data_url(p, max_width=500)+ except Exception:+ pass except Exception as e:- out["error"] = str(e)[:120]+ out["error"] = str(e)[:400] return out +def _frame_data_url(path: str, max_width: int = 500) -> str:+ """Downscale an on-disk frame to a data: URI (best effort, PIL-free: Fusion boxes have+ no PIL, so serve the file bytes as-is when small enough, else skip the inline copy)."""+ import base64 as _b64+ try:+ raw = open(path, "rb").read()+ except Exception:+ return ""+ # The bridge's captures are relay-sized webp (~150-250KB). Cap the inline copy so a+ # four-frame poll response stays comfortably under relay body limits.+ if len(raw) > 300_000:+ return ""+ mime = "image/webp" if path.lower().endswith(".webp") else "image/png"+ return "data:%s;base64,%s" % (mime, _b64.b64encode(raw).decode())++ def _close_bridge_doc_verified(name: str, timeout_s: int = 30) -> dict: """Close a BRIDGE-owned scratch document deterministically - no dialog survives this call.@@ -9417,7 +9456,18 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict: _p3d_urn = _p3d_urn or (args.get("f3dUrn") or "").strip() opened3d = None if _p3d_urn:- opened3d = dispatch_command("open_by_urn", {"urn": _p3d_urn})+ # A JUST-MINTED f3d may not be durably openable yet (page issue #47 failure+ # mode 3: bind success + thumbnail ready, open refused moments later). The+ # lineage commits asynchronously - retry over ~45s before failing the frame.+ import time as _t3r+ for _attempt in range(3):+ opened3d = dispatch_command("open_by_urn", {"urn": _p3d_urn})+ if opened3d.get("success"):+ break+ if _attempt < 2:+ print("[Fusion Bridge] bound f3d not openable yet (attempt %d) - "+ "waiting 15s for the lineage to commit" % (_attempt + 1))+ _t3r.sleep(15) if opened3d and opened3d.get("success"): e3 = _prove_capture("package3d", "3d-editor") chk = _count_3d_bodies()@@ -9486,7 +9536,9 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict: # A failed FRAME belongs in `missing` too (run 7d4427d443f5 read "missing=[]" while # 2 of 4 frames had failed - the reader should never have to diff the evidence list # to learn what is absent).- missing += ["frame:%s(%s)" % (e.get("step"), str(e.get("error"))[:60])+ # FULL error text (issue #47: the [:60] cut landed exactly where the actionable half+ # of every message began - "Fusion offere[d ...]" - and cost the reporter a diagnosis)+ missing += ["frame:%s(%s)" % (e.get("step"), str(e.get("error"))) for e in evidence if not (e.get("ok") and e.get("screenshot"))] proven = (not missing) and len(captured) == 4 out = {"success": proven, "proven": proven, "libraryPath": lbr, "deviceset": want,@@ -9713,6 +9765,17 @@ def _handle_prove_result(fusion_info: dict, args: dict) -> dict: "error": "No fusion_prove_library run has completed on this box yet.", "_hint": "Run fusion_prove_library {libraryPath}; if the relay times out, the " "bridge keeps working - poll this verb until the result appears."}+ # INLINE FRAMES ARE OPT-IN (issue #47's web-control ask): each evidence entry carries+ # thumbnailDataUrl, but four base64 frames on EVERY 15s poll would bloat the relay+ # body. Pass {inlineFrames:true} on the FINAL fetch to get them; plain polls return+ # paths + metadata only.+ if not args.get("inlineFrames"):+ try:+ res = json.loads(json.dumps(res))+ for e in ((res.get("prove") or {}).get("evidence") or []) + (res.get("evidence") or []):+ e.pop("thumbnailDataUrl", None)+ except Exception:+ pass # LIVE PROGRESS FOR WEB-CONTROL (John, 2026-08-21: "as accurate of progress bars as we # can ... you should know how long stuff takes by measuring it"). Computed AT READ TIME # from the persisted stage plan + this machine's measured stage history, so every poll@@ -9888,10 +9951,28 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # the 3D frame never depends on re-parsing the lbr for it "f3dUrn": ((state.get("bind") or {}).get("wip_urn") or "")}) _leave("prove")- state.update(stage="done", success=bool(prove.get("proven")), prove=prove)+ state.update(stage="done", success=bool(prove.get("proven")),+ verdict=("proven" if prove.get("proven") else "not_proven"),+ prove=prove) _save() except Exception as e:- state.update(stage="failed", success=False, error=str(e)[:200]); _save()+ state.update(stage="failed", success=False, verdict="not_proven",+ error=str(e)[:400]); _save()+ finally:+ # A BATCH MUST NOT NEED THE CALLER TO SWEEP BETWEEN PARTS (page issue #47,+ # failure mode 5: unsaved library docs accumulated until the add-in stopped+ # 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.+ try:+ _lbr_done = (state.get("libraryPath") or lbr_p or "")+ if _lbr_done:+ _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+ _closed = _close_bridge_doc_verified(_stem_done)+ state["libraryDocClosed"] = bool(_closed.get("closed"))+ _save()+ except Exception:+ pass _th.Thread(target=_chain, daemon=True).start() return {"success": True, "started": True, "runId": run_id,@@ -9929,7 +10010,11 @@ def _handle_check_3d_thumbnail(fusion_info: dict, args: dict) -> dict: import xml.etree.ElementTree as _ET root = _ET.parse(lbr).getroot() for p3 in root.iter("package3d"):- urn = (p3.get("urn") or "").strip() or urn+ # wip_urn, not urn: this bridge writes <package3d urn="" wip_urn="...">+ # (the same one-attribute miss that broke prove's 3D frame, issue #46 -+ # and unfixed HERE it made tja1051t/type-c report thumbnail not_found+ # with the file sitting right there, issue #47 failure mode 4)+ urn = (p3.get("urn") or p3.get("wip_urn") or "").strip() or urn file_name = file_name or (p3.get("name") or "") except Exception: pass