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
v1.9.10: async GLB export + demo_progress id fix
6 files changed
+116−19
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.9+1.9.10
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.9",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.9/adom-bridge-fusion-v1.9.9.zip",- "sha256": "3748091b9d011afbfc3bef9d595304d2245c1b0c4babf942e3bd1c3430f19100",- "size": 357270,+ "version": "1.9.10",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.10/adom-bridge-fusion-v1.9.10.zip",+ "sha256": "b3bebb34d99940a281d317253dec76ce6e4891123e10a490624093f90d0a787e",+ "size": 358515, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.9",+ "version": "1.9.10", "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+2−2@@ -206,8 +206,8 @@ _T = [ ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}), ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}), ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),- ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),- ("fusion_fetch_optimized_glb", "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.", {"jobId": "required str", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}),+ ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)", "async": "bool - RECOMMENDED true: fires the whole export+optimize pipeline in the background and returns {exportId} instantly (the sync form can outlive the relay window on a cold Fusion session); poll fusion_fetch_optimized_glb {exportId}"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),+ ("fusion_fetch_optimized_glb", "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.", {"jobId": "str (service job form)", "exportId": "str (async form from export_optimized_glb async:true - returns {phase, done, glbPath} instantly per poll)", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}), ("fusion_board_stackup", "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}), ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}), ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}),
server.py+92−1@@ -5705,7 +5705,9 @@ def _handle_demo_progress(args: dict) -> dict: demo_id = args.get("demoId") or "" with _DEMO_TOURS_LOCK: state = _DEMO_TOURS.get(demo_id)- if state is None and len(_DEMO_TOURS) == 1:+ # Fall back to "the single running tour" ONLY when no id was given at all -+ # a WRONG id must refuse (the suite's #unknown negative test enforces this).+ if state is None and not demo_id and len(_DEMO_TOURS) == 1: state = list(_DEMO_TOURS.values())[0] if state is None: return {"success": False, "error": "unknown demoId " + demo_id,@@ -5724,6 +5726,91 @@ def _handle_demo_progress(args: dict) -> dict: return out +++# ── Async wrapper for export_optimized_glb (2026-08-14): the verb's STEP-export phase+# alone can outlive ab's 60s relay window (fresh-session Fusion), so the sync form dies+# at the relay even though the work completes. async:true fires the whole pipeline in a+# thread and returns an exportId instantly; fusion_fetch_optimized_glb {exportId} reports+# phase -> jobId -> complete. Same pattern as the demo tour.+_GLB_ASYNC: dict = {}+_GLB_ASYNC_LOCK = threading.Lock()+++def _start_glb_export_async(args: dict) -> dict:+ import time+ import uuid+ export_id = "glbx-" + uuid.uuid4().hex[:8]+ state = {"exportId": export_id, "phase": "exporting-step", "done": False,+ "failed": False, "started": time.time()}+ with _GLB_ASYNC_LOCK:+ _GLB_ASYNC[export_id] = state++ inner = {k: v for k, v in args.items() if k != "async"}+ inner.setdefault("wait", 20)++ def _run():+ begin_background_envelope(400.0)+ try:+ res = _orchestrate_export_optimized_glb(inner)+ state["result"] = res+ if res.get("glbPath") and res.get("success"):+ state.update(phase="complete", done=True)+ return+ job = res.get("jobId")+ if not (res.get("success") and job):+ state.update(phase="failed", failed=True, done=True,+ error=str(res.get("error"))[:200])+ return+ state.update(phase="optimizing", jobId=job)+ deadline = time.time() + 600+ while time.time() < deadline:+ f = _orchestrate_fetch_optimized_glb(+ {"jobId": job, "outputPath": inner.get("outputPath"), "wait": 20})+ state["result"] = f+ if f.get("glbPath") and not f.get("pending"):+ state.update(phase="complete", done=True)+ return+ if not f.get("success") and not f.get("pending"):+ state.update(phase="failed", failed=True, done=True,+ error=str(f.get("error"))[:200])+ return+ time.sleep(5)+ state.update(phase="failed", failed=True, done=True, error="optimizer did not finish within 10 min")+ except Exception as e:+ state.update(phase="failed", failed=True, done=True, error=str(e)[:200])++ threading.Thread(target=_run, daemon=True, name="glb-export-async").start()+ return {"success": True, "pending": True, "exportId": export_id,+ "statusVerb": "fusion_fetch_optimized_glb",+ "_hint": ("Export running ASYNC (the sync form can outlive the relay window on a cold "+ "Fusion session). Poll fusion_fetch_optimized_glb {\"exportId\": \"%s\"} "+ "every few seconds: phase walks exporting-step -> optimizing -> complete, and "+ "the final poll carries glbPath/sizeBytes.") % export_id}+++def _glb_export_async_status(args: dict) -> dict:+ import time+ export_id = args.get("exportId") or ""+ with _GLB_ASYNC_LOCK:+ state = _GLB_ASYNC.get(export_id)+ if state is None:+ return {"success": False, "error": "unknown exportId " + export_id,+ "_hint": "Start one with fusion_export_optimized_glb {async: true}; its response carries the exportId."}+ out = {"success": not state.get("failed"), "exportId": export_id,+ "phase": state.get("phase"), "done": state.get("done"),+ "elapsedSec": int(time.time() - state["started"])}+ if state.get("jobId"):+ out["jobId"] = state["jobId"]+ if state.get("error"):+ out["error"] = state["error"]+ if state.get("done") and isinstance(state.get("result"), dict):+ for k in ("glbPath", "sizeBytes", "meshesAfter", "moleculeAnchored", "silkscreenApplied"):+ if k in state["result"]:+ out[k] = state["result"][k]+ return out++ def _orchestrate_demo(args: dict) -> dict: """FIRST-TIME-USER DEMO: take a brand-new user from nothing to "wow" in one verb. @@ -6306,8 +6393,12 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> if command == "generate_package": return _apply_failure_dialogs(_orchestrate_generate_package(args)) if command == "export_optimized_glb":+ if (args or {}).get("async"):+ return _start_glb_export_async(args) return _orchestrate_export_optimized_glb(args) if command == "fetch_optimized_glb":+ if (args or {}).get("exportId"):+ return _glb_export_async_status(args) return _orchestrate_fetch_optimized_glb(args) if command == "demo": if (args or {}).get("mode") == "tour":
tests/run_verb_tests.py+16−10@@ -131,7 +131,8 @@ t("fusion_get_preferences", "daily", {}, {"ok": True, "fields": ["current"]}) t("fusion_set_preference", "daily", {"theme": "darkblue"}, {"ok": True}, "sets the value it already has") t("fusion_mcp_status", "daily", {}, {"ok": True, "fields": ["enabled"]}) t("fusion_mcp_tools", "weekly", {}, {"ok": True}, "only meaningful when MCP enabled")-t("fusion_mcp_call", "weekly", {"tool": "fusion_mcp_read", "arguments": {"queryType": "summary"}}, {"ok": True})+t("fusion_mcp_call", "weekly", {"tool": "fusion_mcp_read", "arguments": {"queryType": "document"}}, {"ok": True},+ "queryType must be a schema enum value (apiDocumentation|screenshot|document|projects|activeCommand) - 'summary' was a guess that failed validation") t("fusion_mcp_resources", "weekly", {}, {"ok": True}) t("fusion_mcp_enable", "manual", note="drives the prefs UI, takes foreground") t("fusion_prefs_open", "manual", note="opens a dialog on the user's screen")@@ -194,9 +195,10 @@ t("fusion_export_iges", "weekly", {"outputPath": f"{SCRATCH}/verbtest.iges"}, {" t("fusion_export_sat", "weekly", {"outputPath": f"{SCRATCH}/verbtest.sat"}, {"ok": True}) for fmt in ("obj", "fbx", "usdz", "dxf", "dwg", "skp"): t(f"fusion_export_{fmt}", "manual", note="build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly")-t("fusion_export_optimized_glb", "weekly", {"outputPath": f"{SCRATCH}/board.glb", "wait": 10}, {"ok": True})-t("fusion_fetch_optimized_glb", "weekly", {"_dynamic": "glb_job"}, {"ok": True},- "jobId chained from export_optimized_glb; SKIPs if the export completed inline")+t("fusion_export_optimized_glb", "weekly", {"outputPath": f"{SCRATCH}/board.glb", "async": True}, {"ok": True, "fields": ["exportId"]},+ "async form: sync outlived ab's 60s relay window on a cold session (2026-08-14)")+t("fusion_fetch_optimized_glb", "weekly", {"_dynamic": "glb_job"}, {"ok": True, "fields": ["glbPath"]},+ "polls the async exportId to completion - the final status must carry the GLB") t("fusion_set_design_rules", "manual", note="mutates the board's DRC rules") # ── stage 5: libraries (weekly) + the show verbs (issue #29) ────────────────@@ -332,12 +334,16 @@ def stage_dynamic(target, kind): return {"filePath": SCRATCH + "/" + fname}, None if kind == "glb_job": prev = LAST_RESULTS.get("fusion_export_optimized_glb") or {}- job = prev.get("jobId")- if not job:- if prev.get("glbPath") or prev.get("success"):- return None, "SKIP: export completed inline (no jobId to fetch)"- return None, "export_optimized_glb produced neither jobId nor glbPath"- return {"jobId": job, "outputPath": SCRATCH + "/board.glb", "wait": 30}, None+ eid = prev.get("exportId")+ if not eid:+ return None, "export_optimized_glb returned no exportId (async start failed)"+ deadline = time.time() + 420+ while time.time() < deadline:+ st, _ = call(target, "fusion_fetch_optimized_glb", {"exportId": eid}, timeout=45)+ if st.get("done"):+ break+ time.sleep(10)+ return {"exportId": eid}, None if kind == "urn_from_search": r, _ = call(target, "fusion_aps_search", {"query": "BQ25792", "limit": 5}, timeout=90) for item in (r.get("results") or r.get("items") or []):