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 7 file(s)
7 files changed
+128−35
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.129+1.9.130
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.129",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.145/adom-bridge-fusion-v1.9.129.zip",- "sha256": "8185b0cdaf6412ee50200edad0e7971a140341cf5aa59abe1bbcdbfc4ce34ce2",- "size": 464119,+ "version": "1.9.130",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.146/adom-bridge-fusion-v1.9.130.zip",+ "sha256": "a01c28c9da36414cb2ae1e8854ecc144a0f00da33987f5fc7da6cca5f6be7b96",+ "size": 465935, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.129",+ "version": "1.9.130", "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@@ -14,6 +14,7 @@ _STATUS = "fusion_get_app_state" # Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example) _T = [ # ── Lifecycle ──────────────────────────────────────────────────────────+ ("fusion_preflight_lbr", "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.", {"libraryPath": "str (Windows .lbr path)"}, {"success": "bool", "preflight": "{ok, checks[], errors[]}"}, 10, None, True, {"libraryPath": "C:/Temp/PART.lbr"}), ("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, {}),
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.145",+ "version": "1.9.146", "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.145",+ "version": "1.9.146", "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+110−21@@ -3005,34 +3005,35 @@ def _orchestrate_open_lbr(args: dict) -> dict: # its blocking "has errors and cannot be opened" Error modal ON EVERY RETRY and left # an empty Untitled design behind each time. The file's validity is knowable offline # in milliseconds - an unparseable library must NEVER reach Fusion's parser.- # NOTE the _ospre alias: this function has a LOCAL `import os` further down, which- # makes `os` local to the WHOLE scope - the 1.9.128 guard referenced bare `os` here,- # crashed with UnboundLocalError on EVERY open, and reported its own crash as "not a- # parseable EAGLE library" (caught by the matrix re-run within the hour, issue #47).+ # PRE-ANALYZE before Fusion ever sees the file (John, 2026-08-21; the 77-byte+ # ads1220irvar corpse raised Fusion's blocking Error modal on every retry). Only the+ # HARD opens are gated here - a parseable library with soft findings (empty+ # devicesets etc.) still opens, because open_lbr's contract is opening, but a file+ # Fusion's parser will refuse never leaves this function. NOTE the _ospre alias:+ # this function has a LOCAL `import os` further down that makes `os` local to the+ # whole scope - referencing it bare here was the 1.9.128 UnboundLocalError. import os as _ospre- try:- _pf = file_path.replace("\\", "/")- if _pf and _ospre.path.exists(_pf):- import xml.etree.ElementTree as _ETpre- _rootpre = _ETpre.parse(_pf).getroot()- if _rootpre.tag != "eagle":- raise ValueError("root element is <%s>, not <eagle>" % _rootpre.tag)- except Exception as _epre:+ _pfr = _preflight_lbr(file_path)+ _hard = {c["check"]: c for c in _pfr["checks"]}+ if not all(_hard.get(k, {}).get("ok", True)+ for k in ("file-readable", "size-sane", "xml-parses", "root-is-eagle")):+ _pff = file_path.replace("\\", "/") try:- _sz = _ospre.path.getsize(_pf)- _head = open(_pf, "rb").read(48)+ _sz = _ospre.path.getsize(_pff)+ _head = open(_pff, "rb").read(48) except Exception: _sz, _head = None, b"" return {"success": False, "errorCode": "lbr_invalid",- "error": "%s is not a parseable EAGLE library (%s) - refusing to hand it to "- "Fusion, whose only response is a blocking Error modal plus an "- "orphaned empty design." % (file_path, str(_epre)[:120]),+ "preflight": _pfr,+ "error": "%s will not open in Fusion: %s - refusing to hand it over (the "+ "only possible outcome is a blocking Error modal plus an orphaned "+ "empty design)." % (file_path, "; ".join(_pfr["errors"])[:240]), "fileSize": _sz, "fileHead": repr(_head[:32]),- "_hint": ("The file on THIS box is bad (size %s bytes). Re-stage it from the "- "source and compare sizes/hashes - a transfer that truncates or "- "re-encodes is the usual culprit. Nothing was opened and no dialog "- "was raised." % _sz)}+ "_hint": ("The file on THIS box is bad. Re-stage it from the source and "+ "compare hashes - a transfer that truncates or re-encodes is the "+ "usual culprit. fusion_preflight_lbr re-runs these checks in "+ "isolation. Nothing was opened and no dialog was raised.")} # PRE-CLOSE a stale dirty copy of this same library (deterministic-close rule): opening # over an unsaved same-name document is how duplicate docs piled up (15 at peak) and how # later closes rained Save-As prompts. If a dirty copy is open, resolve it NOW - it is@@ -9630,6 +9631,81 @@ COMMAND_HANDLERS["prove_library"] = _handle_prove_library +def _preflight_lbr(path: str) -> dict:+ """PRE-ANALYZE a .lbr: will Fusion open it? Every check is offline and named.++ John, 2026-08-21: "what if you just pre-analyze a library to make sure its going to+ open ok?" - this is that, as one reusable gate. It encodes every rejection Fusion has+ actually produced on this bridge: a truncated binary file (77-byte ads1220irvar, the+ blocking Error modal), empty <packages> (Fusion's silent "No Components"), dead+ <connects> (renders fine, wires nothing), and absent devicesets (EDIT prompts).+ Returns {ok, checks: [{check, ok, detail}], errors: [...]} - callers show the list.+ """+ import xml.etree.ElementTree as _ET+ checks = []+ def _c(name, ok, detail=""):+ checks.append({"check": name, "ok": bool(ok), "detail": detail})+ return ok+ p = (path or "").replace("\\", "/")+ try:+ sz = os.path.getsize(p)+ except Exception as e:+ _c("file-readable", False, str(e)[:120])+ return {"ok": False, "checks": checks,+ "errors": [c["check"] + ": " + c["detail"] for c in checks if not c["ok"]]}+ _c("file-readable", True, "%d bytes" % sz)+ if not _c("size-sane", sz >= 200,+ "%d bytes - a real EAGLE library is never this small (truncated transfer?)" % sz):+ pass+ root = None+ if sz >= 200:+ try:+ root = _ET.parse(p).getroot()+ _c("xml-parses", True)+ except Exception as e:+ _c("xml-parses", False, str(e)[:160])+ if root is not None:+ _c("root-is-eagle", root.tag == "eagle", "root is <%s>" % root.tag)+ pkgs = [x.get("name") for x in root.iter("package")]+ syms = [x.get("name") for x in root.iter("symbol")]+ devs = [x.get("name") for x in root.iter("deviceset")]+ _c("packages-nonempty", bool(pkgs),+ "%d package(s)" % len(pkgs) if pkgs else+ "EMPTY <packages> - Fusion opens this to a silent 'No Components' library")+ _c("symbols-present", bool(syms), "%d symbol(s)" % len(syms))+ _c("devicesets-present", bool(devs),+ "%d deviceset(s)" % len(devs) if devs else+ "no <deviceset> - EDIT <name>.dev will raise Fusion's Create-new prompt")+ if pkgs and syms:+ conn = _check_connects(p)+ _c("connects-resolve", bool(conn.get("ok")),+ ("%d connect(s), all resolving" % (conn.get("connects") or 0))+ if conn.get("ok") else str(conn.get("why") or "invalid")[:160])+ ok = all(c["ok"] for c in checks)+ return {"ok": ok, "checks": checks,+ "errors": [c["check"] + ": " + c["detail"] for c in checks if not c["ok"]]}+++def _handle_preflight_lbr(fusion_info: dict, args: dict) -> dict:+ """Verb wrapper: will this .lbr open cleanly in Fusion? Offline, instant, no Fusion."""+ p = (args.get("libraryPath") or args.get("filePath") or "").replace("\\", "/")+ if not p:+ return {"success": False, "error": "libraryPath required."}+ pf = _preflight_lbr(p)+ return {"success": pf["ok"], "preflight": pf, "libraryPath": p,+ "errorCode": None if pf["ok"] else "lbr_preflight_failed",+ "_hint": ("Every check green - Fusion will open this library cleanly."+ if pf["ok"] else+ "Fix the named check(s) BEFORE any Fusion verb touches this file - "+ "each one maps to a real Fusion failure (blocking Error modal, silent "+ "No Components, Create-new prompt, or dead wiring). Re-stage and "+ "hash-compare if size-sane or xml-parses failed: transfer corruption "+ "is the usual cause.")}+++COMMAND_HANDLERS["preflight_lbr"] = _handle_preflight_lbr++ def _kicad_ingest_unavailable_error() -> dict: """The shape-B refusal, shared by import_kicad and the create_library fast-fail. @@ -9908,6 +9984,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # Knowable up front, so refused up front; fusion_readiness carries the same flag. if (sym_p or mod_p) and not _find_adom_lbr(): return _kicad_ingest_unavailable_error()+ # SHAPE A PRE-ANALYSIS at the same front door (John, 2026-08-21: "pre-analyze a+ # library to make sure its going to open ok"): a bad .lbr must never cost the caller+ # a Fusion LAUNCH to discover. Same named checks as fusion_preflight_lbr.+ if lbr_p and not (sym_p or mod_p):+ _pf = _preflight_lbr(lbr_p)+ if not _pf["ok"]:+ return {"success": False, "errorCode": "lbr_preflight_failed",+ "preflight": _pf,+ "error": "Pre-analysis says Fusion will NOT open this library: " ++ "; ".join(_pf["errors"])[:400],+ "_hint": "Nothing was launched and no dialog was raised. Fix the named "+ "checks (fusion_preflight_lbr re-runs them in isolation), "+ "re-stage with a hash compare, then re-call."} import uuid as _uuid run_id = _uuid.uuid4().hex[:12]