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
+137−24
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.255+1.9.256
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.255",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.255/adom-bridge-fusion-v1.9.255.zip",- "sha256": "34ec5b871505618fc58d7b72aa344de6c7ce60126bf8dde2ee6598c632c31162",- "size": 550007,+ "version": "1.9.256",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.256/adom-bridge-fusion-v1.9.256.zip",+ "sha256": "b6cf3b2da20fd7baaa0149d4093c29ef70dbeac00d174274c156ffaf0ca65a2a",+ "size": 552857, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.255",+ "version": "1.9.256", "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.255"+version = "1.9.256" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.255"+version = "1.9.256" edition = "2021" [dependencies]
describe.py+1−1@@ -15,7 +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_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. Each part is minted on its own single-deviceset library (the Package3D generator loads the FIRST package of the open library, so a merged library would put every chip on part one's footprint), with the STEP placed exactly as published (orient:false, origin trusted), then all urns are bound into the merged library, and after prove the bound library is SAVED TO THE CLOUD under <project>/Adom Libraries so it appears in Fusion's Library Manager (cloudLibrary{saved, fileId, duplicatesBefore} in fusion_prove_result). Per-part scaffolding is deleted; the user is left with one library plus its per-package f3d folder. 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.255"+DASH_VERSION="1.9.256" 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.255",+ "version": "1.9.256", "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.255",+ "version": "1.9.256", "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+93−5@@ -12017,6 +12017,47 @@ COMMAND_HANDLERS["prove_result"] = _handle_prove_result ++def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries") -> dict:+ """Save the bound library into the user's cloud project so Fusion's Library Manager lists it.++ Folder is created if missing (project root/<folder>). A same-named library already there is+ reported (Fusion allows duplicate names; this saves a NEW document and says so, update-in-+ place is a follow-up). Returns {saved, name, project, folder, fileId, versionId,+ duplicatesBefore, error}.+ """+ out = {"saved": False, "name": name, "folder": folder}+ stem = lbr_path.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+ name = (name or stem).strip()+ out["name"] = name+ try:+ r = _proxy_to_addin("create_cloud_folder", {"folderName": folder}, timeout=60)+ out["folderCreated"] = bool(r.get("success")) and not bool((r.get("data") or {}).get("existed"))+ except Exception as e:+ out["folderNote"] = str(e)[:120]+ try:+ lst = _proxy_to_addin("list_cloud_files", {"folderPath": folder}, timeout=60)+ names = [str(f.get("name") or "") for f in ((lst.get("data") or {}).get("files") or [])]+ out["duplicatesBefore"] = sum(1 for n in names if n.lower() == name.lower())+ except Exception:+ out["duplicatesBefore"] = None+ act = _ensure_library_active(lbr_path)+ if not act.get("ok"):+ out["error"] = "library document is not active, so it could not be saved: " + str(act.get("why") or "")[:120]+ return out+ r = _proxy_to_addin("save_to_cloud", {"name": name, "folderPath": folder,+ "description": "Imported by Adom (bound 3D packages)"},+ timeout=180)+ d = r.get("data") or {}+ if not r.get("success"):+ out["error"] = str(r.get("error") or "save_to_cloud failed")[:200]+ return out+ out.update(saved=True, project=d.get("project"), folder=d.get("folder") or folder,+ fileId=d.get("fileId"), versionId=d.get("versionId"), versionNumber=d.get("versionNumber"))+ _bridge_action_ledger("library_saved_to_cloud", {"name": name, "folder": folder,+ "fileId": d.get("fileId")})+ return out+ def _handle_create_library(fusion_info: dict, args: dict) -> dict: """ONE verb for the whole "create a Fusion library" task - one call, one consent. @@ -12326,6 +12367,15 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: "bind failed and returned no reason and no urn") state.update(stage="failed", success=False); _save(); return _leave("bind")+ # Per-part mint libraries were scaffolding (one deviceset each so the generator+ # loads the right footprint); nothing of them may linger on the user's disk.+ for _cd in (args.get("_cleanupAfterBind") or []):+ try:+ import tempfile as _tfc+ if str(_cd).startswith(_tfc.gettempdir()):+ shutil.rmtree(_cd, ignore_errors=True)+ except Exception:+ pass _enter("proving (four evidence frames)") # #60: publish each artifact THE MOMENT it lands, in assembly order # (symbol -> footprint -> 3d -> ganged), so a poller watches the bundle build@@ -12436,6 +12486,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: state.update(stage="done", success=bool(prove.get("proven")), verdict=("proven" if prove.get("proven") else "not_proven"), evidenceCount=_frames, prove=prove)+ # THE LIBRARY MUST END UP IN THE USER'S LIBRARY MANAGER (John, 2026-08-29: "the user+ # is just after a multipart library"). Until now the chain bound, proved, and closed+ # the local .lbr with save:false, leaving a proven FILE and no library in Fusion.+ _pub = args.get("_publishToCloud")+ if _pub and (state.get("bind") or {}).get("success"):+ try:+ state["stageLabel"] = "saving the library to your cloud Library Manager"+ _save()+ state["cloudLibrary"] = _publish_library_to_cloud(+ target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries")+ except Exception as _pe:+ state["cloudLibrary"] = {"saved": False, "error": str(_pe)[:200]}+ _save() # Resolve the in-Fusion banner to match the verdict, then let it linger briefly # so the human sees the outcome, and hide it - a stale "working" banner over an # idle Fusion would be the same lie as a stuck progress bar.@@ -12609,16 +12672,35 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict: dest = os.path.join(home, "Documents", "AdomLibraries", re.sub(r"\s+", "_", 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"]]+ # ONE LIBRARY PER PART FOR THE MINT (John, 2026-08-29). Fusion's Package3D generator+ # loads the FIRST package of the open library, so minting every part against the+ # merged file put chips two and three on part one's footprint. Each part is minted+ # on its own single-deviceset library; the urns are bound back into the merged file.+ # orient:false - the wiki publishes STEPs with the correct origin and orientation;+ # no bounding-box guess (rotation or translation) is ever right for a lopsided chip.+ _split_dir = _tf.mkdtemp(prefix="adom-lib-parts-")+ _split = _lb.split_per_part(lbr_path, _split_dir)+ parts = []+ for p in pairing["parts"]:+ _ds = p.get("deviceset") or p["package"]+ _sp = _split.get(_ds) or {}+ if not _sp.get("lbrPath") or not _sp.get("package"):+ pairing["unbound"].append({"package": _ds, "why": "could not split this deviceset "+ "into its own library (no package on its devices?)"})+ continue+ parts.append({"deviceset": _ds, "package": _sp["package"], "lbrPath": _sp["lbrPath"],+ "modelPath": placed["models"].get(p["modelPath"], p["modelPath"]).replace("\\", "/"),+ "orient": False}) 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],+ "parts": [{"deviceset": p["deviceset"], "package": p["package"], "modelPath": p["modelPath"],+ "mintedOn": p["lbrPath"]} for p in parts],+ "mint": "each part minted on its own single-deviceset library so the generator loads "+ "that part's footprint; STEP placed as published (orient:false, origin trusted)", "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"),@@ -12638,7 +12720,13 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict: # "prove ONLY this deviceset", so passing the library name made prove hunt for a # symbol called ray-dac80504 and score DAC80504's real frames not_proven. The chain # proves EVERY deviceset when name is absent, which is what a bundle wants.- chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts})+ chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts,+ # scaffolding only: deleted once the bind is done+ "_cleanupAfterBind": [_split_dir],+ # the end state the user is after: ONE library in+ # Fusion's Library Manager, not a file on disk+ "_publishToCloud": {"name": lib_name,+ "folder": "Adom Libraries"}}) 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"))
skills/fusion-multipart-libraries/SKILL.md+5@@ -17,6 +17,11 @@ real chip, flat on its footprint, built entirely this way. > **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.+> It mints EACH part on its own temporary single-deviceset library (never the merged file:+> the generator loads the merged file's first package, so three chips landed on one footprint,+> John 2026-08-29), trusts the STEP's published origin and orientation (`orient:false`, no+> bounding-box guesses), binds every urn into the merged library, proves it, and then saves+> it into the user's cloud project under `Adom Libraries` so Fusion's Library Manager lists it. 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
skills/fusion-web-control/SKILL.md+17@@ -111,6 +111,23 @@ Any verb needing the add-in now returns: - **Branch on `documentsKnown`.** The counts come from the add-in, which by definition is not answering; when it cannot be reached they are `null` rather than guessed. - **Ask before restarting when a human is present.** Then `fusion_restart_for_addin {save:true}`, which saves first and **refuses** with `unsaved_named_document` rather than discarding a never-saved document. +### The chain asks too (proven 2026-08-29, bridge 1.9.255)++With the add-in's server stopped on purpose inside a running Fusion, `fusion_create_library`+now stops at "asking you whether to save and restart it" with `errorCode:+addin_restart_required`, `userMessage`, `documentsKnown`, `remedyVerb` and `notificationId`+in the run state, fires the two-button toast, and waits (30 min). Fusion's process is+untouched while it waits (same pid before and during). Answers:+- `fusion_restart_for_addin {save:true}` (the toast's "Save and restart" or the page button):+ Fusion restarts, the add-in answers, the SAME run continues to `proven` with+ `restartChoice: "restart"`.+- `fusion_restart_for_addin {decline:true}` (the page's "I'll handle it"; the toast button+ records the same hold): the run fails `user_owns_restart`, Fusion is untouched, and+ `fusion_start` refuses with `user_owns_restart` until the user restarts Fusion themselves.+Before 1.9.255 the chain fired nothing and `_handle_launch` killed Fusion after a 30 s gate;+only direct add-in verbs ever asked. `fusion_addin_status` also said `ok` to a dead add-in;+it now returns `addin_not_answering`.+ ## 3. Reading progress honestly `fusion_prove_result` gives `stage`, `stageLabel`, `plan[]`, `stagesDone`, `percent`,