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)
12 files changed
+177−18
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.245+1.9.246
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.245",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.245/adom-bridge-fusion-v1.9.245.zip",- "sha256": "8dc2672822ad4c7a504009d0fba2e79b06b04d263fd1bb8fd158f4d0731f00b1",- "size": 541165,+ "version": "1.9.246",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.246/adom-bridge-fusion-v1.9.246.zip",+ "sha256": "f2d56a288f2fab8f4ab73bfcfcd131113b75a9025f9271a0e7ebe5db3a12c1f9",+ "size": 546509, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.245",+ "version": "1.9.246", "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.245"+version = "1.9.246" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.245"+version = "1.9.246" edition = "2021" [dependencies]
describe.py+1@@ -15,6 +15,7 @@ _STATUS = "fusion_get_app_state" _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_install_library_bundle", "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.", {"zipPath": "str (staged bundle zip on THIS machine) - or", "lbrPath": "str (staged .lbr, STEP models beside it)", "libraryName": "optional str (default: file stem)", "capture": "optional bool (default true: bind + prove; false: place only)"}, {"installed": "bool", "libraryPath": "str", "partsTotal": "int", "partsToBind": "int", "unbound": "[{package, why}]", "partMetadata": "{ok, summary, warnings[]}", "fusion": "{installed, running, addinReady, status, message}", "runId": "str", "statusVerb": "fusion_prove_result"}, 60, "fusion_prove_result", True, {"zipPath": "C:/Temp/MyLib.zip", "libraryName": "MyLib"}), ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. 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). EVERY PART IS PROVED, not just the first: a 10-component library streams 40 evidence frames, FOUR per component (symbol, footprint, 3d, ganged) in assembly order, each tagged with `part` so a front end can group them as they arrive, and `provedParts[]` reports per-part pass/fail. The library is proven only if EVERY part is - one good part does not vouch for nine unchecked ones. 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.", {"parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count", "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, {}),
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.245"+DASH_VERSION="1.9.246" 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.245",+ "version": "1.9.246", "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.245",+ "version": "1.9.246", "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+123@@ -11,6 +11,7 @@ Usage: import json import os+import shutil import threading import re import time as _t2@@ -12392,6 +12393,128 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: COMMAND_HANDLERS["create_library"] = _handle_create_library+++def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:+ """COMPOSITE (#68, Colby's routing call): Cloud Library bundle -> installed, bound, proven+ library in ONE call, the Fusion twin of kicad_install_library_bundle. The wiki CLI's+ `library install --format fusion` and the Export button both read this one contract, so+ the import recipe has exactly one owner (this bridge) instead of living in webcontrol.js.++ Shape (settled on #68, 2026-08-28):+ in : {zipPath | lbrPath (staged on THIS machine), libraryName?, capture? (default true)}+ out: {installed, libraryPath, partsTotal, partsToBind, unbound[], partMetadata,+ fusion{...}, runId, statusVerb:"fusion_prove_result"} - returned IMMEDIATELY.+ ASYNC by contract: a STEP bind is 3-15 minutes and cannot fit the 135s web-control+ envelope, so this returns the runId and the caller polls fusion_prove_result (measured+ progress, streamed proof frames, per-part verdicts, addin_restart_required bubbled up).+ PARTIAL SHIPS: a deviceset with no model in the bundle is a NAMED skip in `unbound[]`,+ never a silent one, and the rest still bind; partsBound < partsTotal is the signal.+ """+ import tempfile as _tf+ import library_bundle as _lb+ zip_p = (args.get("zipPath") or "").replace("\\", "/")+ lbr_in = (args.get("lbrPath") or args.get("libraryPath") or "").replace("\\", "/")+ if not zip_p and not lbr_in:+ return {"success": False, "errorCode": "no_input",+ "error": "Give me zipPath (a staged Cloud Library bundle) or lbrPath (a staged .lbr "+ "with its STEP models beside it).",+ "_hint": "Stage with write_file / write_file_chunk first; the path must exist on "+ "THIS machine. Then poll fusion_prove_result with the runId."}+ src = zip_p or lbr_in+ if not os.path.exists(src):+ return {"success": False, "errorCode": "not_found", "error": "Not on this machine: " + src,+ "_hint": "Stage it with write_file / write_file_chunk first, then pass that path."}+ lib_name = (args.get("libraryName") or "").strip()+ work = None+ try:+ if zip_p:+ if os.path.getsize(zip_p) > _lb.MAX_ZIP_BYTES:+ return {"success": False, "errorCode": "too_large",+ "error": "Bundle zip is larger than the 256 MB cap"}+ work = _tf.mkdtemp(prefix="adom-lib-bundle-")+ try:+ n = _lb.safe_extract(zip_p, work)+ except Exception as e:+ return {"success": False, "errorCode": "bad_bundle",+ "error": "Could not unpack the bundle: %s" % str(e)[:160]}+ if not n:+ return {"success": False, "errorCode": "bad_bundle", "error": "The bundle zip is empty"}+ lbr_found, models, all_lbrs = _lb.find_bundle_files(work)+ if not lbr_found:+ return {"success": False, "errorCode": "no_lbr",+ "error": "No .lbr in the bundle",+ "_hint": "Expected an Adom Cloud Library bundle for Fusion: <Lib>.lbr plus "+ "<Part>.step models (beside it or under 3dmodels/)."}+ lib_name = lib_name or os.path.splitext(os.path.basename(zip_p))[0]+ else:+ lbr_found = lbr_in+ _d = os.path.dirname(lbr_in) or "."+ _, models, all_lbrs = _lb.find_bundle_files(_d)+ lib_name = lib_name or os.path.splitext(os.path.basename(lbr_in))[0]+ if not _lb.library_name_ok(lib_name):+ return {"success": False, "errorCode": "bad_name",+ "error": "Library name not usable: %r" % lib_name,+ "_hint": "Letters, digits, space, dot, dash, underscore; up to 64 chars."}+ # The same offline gate every import runs: a corpse .lbr fails HERE, not after a launch.+ pf = _preflight_lbr(lbr_found) if "_preflight_lbr" in globals() else {"ok": True}+ if isinstance(pf, dict) and pf.get("ok") is False:+ return {"success": False, "errorCode": "lbr_preflight_failed",+ "error": "The .lbr will not open in Fusion: " + "; ".join(pf.get("errors") or [])[:300],+ "preflight": pf}+ meta = _library_part_metadata(lbr_found)+ pairing = _lb.pair_parts(lbr_found, models, meta)+ # Stable install folder: the library must outlive the temp dir the chain reads from.+ home = os.path.expanduser("~")+ dest = os.path.join(home, "Documents", "Adom Libraries", lib_name)+ placed = _lb.place_library(lbr_found, models, dest)+ lbr_path = placed["lbrPath"]+ parts = [{"package": p["package"], "lbrPath": lbr_path,+ "modelPath": placed["models"].get(p["modelPath"], p["modelPath"]).replace("\\", "/")}+ for p in pairing["parts"]]+ report = _part_metadata_report(lbr_path)+ plan = _handle_plan_library_import(fusion_info, {"parts": parts})+ out = {"success": True, "installed": True, "libraryName": lib_name, "libraryPath": lbr_path,+ "installDir": dest.replace("\\", "/"),+ "partsTotal": len(meta.get("devicesets") or []),+ "partsToBind": len(parts),+ "parts": [{"package": p["package"], "modelPath": p["modelPath"]} for p in parts],+ "unbound": pairing["unbound"], "modelsUnused": [os.path.basename(m) for m in pairing["modelsUnused"]],+ "partMetadata": report, "fusion": plan.get("fusion"), "plan": plan.get("plan"),+ "estimatedSec": plan.get("estimatedSec"),+ "otherLbrsInBundle": [os.path.basename(x) for x in all_lbrs[1:]]}+ if args.get("capture") is False:+ out.update(started=False, statusVerb=None,+ _hint="capture:false - the library is placed at libraryPath and nothing was "+ "opened in Fusion. Call fusion_create_library {parts} later to bind and prove.")+ return out+ if not parts:+ out.update(started=False, errorCode="nothing_to_bind",+ _hint="Installed, but no deviceset could be paired with a STEP, so there is "+ "nothing to bind or prove. unbound[] names each one. Add models named "+ "like the deviceset or its package and call again.")+ return out+ chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts,+ "name": lib_name})+ 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"))+ out["_hint"] = ("Installed to libraryPath. The bind+prove chain is running in the background "+ "(async by contract - a STEP bind cannot fit a relay window). Poll "+ "fusion_prove_result {runId} every ~15s: stage narrates, percent is measured, "+ "evidence[] streams four proof frames per part, provedParts[] is per-part, and "+ "an addin_restart_required payload bubbles up if Fusion must restart (the "+ "caller owns that ask). partsBound < partsTotal means named skips in unbound[].")+ return out+ finally:+ try:+ if work:+ shutil.rmtree(work, ignore_errors=True)+ except Exception:+ pass+++COMMAND_HANDLERS["install_library_bundle"] = _handle_install_library_bundle COMMAND_HANDLERS["demo_control"] = lambda info, args: _handle_demo_control(args)
skills/fusion-multipart-libraries/SKILL.md+4@@ -14,6 +14,10 @@ real chip, flat on its footprint, built entirely this way. ## ⚡ FASTEST PATH - one call: `fusion_build_library_3d` (v1.6.0+) +> **From a Cloud Library bundle (zip of .lbr + STEPs):** `fusion_install_library_bundle+> {zipPath}` derives `parts[]` itself by pairing every deviceset with a STEP by name, then+> runs this exact chain. Unpaired devicesets are named in `unbound[]`; the rest still ship.+ For a whole many-part library, you no longer loop `make_3d_package` + hand-write bindings. ONE verb does the entire pipeline - per-part footprint+chip f3d packages, **all** bindings injected in one pass, the finished library opened once, plus a BEFORE/AFTER screenshot per part:
skills/fusion-web-control/SKILL.md+28@@ -41,6 +41,34 @@ fusion_prove_result {runId} -> poll: stage, percent, etaSec, evidence `fusion_prove_result` is the single poll surface for **both** the create chain and the detached 3D build. No second poll verb to learn. +## The one-call bundle lane: `fusion_install_library_bundle` (#68)++The Fusion twin of `kicad_install_library_bundle`, so `adom-wiki library install --format+fusion` and the page's Export button read ONE contract and the recipe has one owner (this+bridge, not webcontrol.js). Stage the bundle, make one call, poll one verb:++```+write_file / write_file_chunk -> C:/Temp/MyLib.zip (<Lib>.lbr + <Part>.step models)+fusion_install_library_bundle {zipPath:"C:/Temp/MyLib.zip", libraryName?:"MyLib"}+ -> {installed:true, libraryPath, partsTotal, partsToBind, unbound[], partMetadata,+ fusion{status,message}, plan[], estimatedSec, runId, statusVerb:"fusion_prove_result"}+fusion_prove_result {runId} -> poll every ~15s until stage:done; read verdict+```++- **Async by contract.** It returns immediately. A STEP bind is 3-15 minutes and cannot fit+ the 135s web-control envelope; there is no blocking mode on purpose (one contract shape).+- **Partial ships.** A deviceset with no model in the bundle is a NAMED skip in `unbound[]`+ (with `why`), the rest still bind and prove. `partsBound < partsTotal` is the signal.+- **Pairing is by name**: deviceset name, then package name, then a normalised (alnum,+ case-folded) match, then a prefix match. A one-deviceset, one-model bundle pairs regardless+ of names. `modelsUnused[]` lists STEPs nothing claimed.+- **Placed somewhere stable**: `~/Documents/Adom Libraries/<libraryName>/`, never a temp dir.+- **The rest is the existing chain**: preflight gate, part-number lint (`partMetadata`,+ reported not blocking), the `fusion{status}` pre-flight, the bind -> prove stages, the four+ proof frames per part, the banner, and the `addin_restart_required` payload the CALLER+ turns into the two-button ask. `capture:false` only places the files.+- `lbrPath` instead of `zipPath` takes a bare staged `.lbr` with its STEPs beside it.+ ## 1. Always pre-flight before you commit a user to anything `fusion_plan_library_import {parts: [...]}` **does no work and changes nothing.** It answers