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.6.4: saveAs folder hygiene + background dialog dismiss + skills/hints
10 files changed
+177−21
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.3+1.6.4
CHANGELOG.md+14@@ -3,6 +3,20 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.4 - 2026-06-29++- **Folder hygiene, part 2: `make_3d_package` no longer saveAs-es to the project ROOT either.** v1.6.3+ fixed the upload target but the f3d `saveAs` still landed in the shared root - now it saves into+ `Adom AI Workspace/<task>` (found/created in the active project), so a build leaves ZERO loose files+ in root.+- **Dialogs are dismissed in the BACKGROUND now (no focus steal).** New `_dismiss_dialogs_bg` uses+ `close_window` (WM_CLOSE via PostMessage = Cancel/No), NEVER `send_key`/Escape (which calls+ SetForegroundWindow and yanks Fusion to the foreground). `fusion_capture_library_views` now+ auto-dismisses the "Create new symbol/footprint '<name>'?" prompt that pops when a DEVICESET name is+ passed to `.sym`/`.pac` in a shared-footprint library (the symbol/package is named by the combo, e.g.+ `R-0603`, not `R-0603-10K`) - and its `_hint` + the `fusion-driving` skill now teach the right names+ + the background-dismiss rule.+ ## 1.6.3 - 2026-06-29 - **Cloud folder hygiene - the bridge NEVER writes loose files to a project ROOT anymore.** Root cause
CLAUDE.md+12−3@@ -49,9 +49,18 @@ there. The exact, watched-it-happen release sequence (AFTER publishing to the wi 2. **`fusion_stop`** (graceful; `fusion_kill` if it is wedged) - REQUIRED. Fusion holds the add-in files OPEN, so the sync canNOT overwrite them while Fusion runs. (This is why a bridge-server respawn alone does NOT update the add-in, and why `fusion_stop`+`fusion_start` by itself deploys nothing.)-3. Run the cache's **`install_addin.py`**: `shell_execute` -> `cd <cache>\fusion360 && python install_addin.py`.- It prints `Updated: commands\cloud_documents.py ...` and removes stale `.bak`s - that is the add-in- actually landing in Roaming.+3. Deploy the add-in into Roaming. Two ways:+ - **(intended)** Run the cache's `install_addin.py`: `shell_execute` -> `cd <cache>\fusion360 &&+ python install_addin.py`. It prints `Updated: commands\cloud_documents.py ...`.+ - **⚠️ TRAP (hit 2026-06-29): `python`/`py` are NOT on the box's shell PATH.** AD spawns the bridge+ with its OWN python, so `python install_addin.py` via `shell_execute` returns "Python was not+ found" / errors with empty output (same Windows-Store-alias stub that makes a bridge fail to+ respawn). `cmd /c copy` and `robocopy` also fight you on the spaces in `Adom Desktop` / `Autodesk+ Fusion 360` (robocopy exit 16). **The reliable deploy that needs NO shell + NO python:** copy the+ changed add-in file(s) with the BRIDGE's own file verbs - `read_file {path:<cache>/addin/AdomBridge/+ commands/<f>.py}` then `write_file {path:<Roaming>/.../AddIns/AdomBridge/commands/<f>.py, content}`+ (Fusion CLOSED so the file is not locked). Verify with another `read_file` (grep a marker from your+ change). The Roaming AddIns path is `%APPDATA%/Roaming/Autodesk/Autodesk Fusion 360/API/AddIns/AdomBridge`. 4. **Verify by SHA256, never findstr.** `(Get-FileHash <Roaming>\commands\cloud_documents.py).Hash` must equal `sha256sum` of that same file unzipped from the published `v<ver>.zip`. findstr gives FALSE NEGATIVES on these files (UTF-8 box-drawing / dash chars break it) - it told me the reprimand
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.3",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.3/adom-bridge-fusion-v1.6.3.zip",- "sha256": "27aab4981320faddb6828e69d70f4ebcb0b082ab2d38f2a0a5af5b855a1ee363",- "size": 288303,+ "version": "1.6.4",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.4/adom-bridge-fusion-v1.6.4.zip",+ "sha256": "7d12ea97123991de94de6a76983391cba3589cdcfe4be025597cb54e6db4335a",+ "size": 290714, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-29T20:30:00Z"+ "released_at": "2026-06-29T21:30:00Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.3",+ "version": "1.6.4", "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://github.com/adom-inc/adom-desktop-fusion-bridge", "author": "Adom Inc.",
describe.py+4−4@@ -49,11 +49,11 @@ _T = [ {"query": "str OR projectId+urn"}, {"versionCount": "int", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}), ("fusion_aps_download", "Download a cloud file to the local machine (no Fusion needed).", {"query": "str OR projectId+urn", "saveDir": "optional str", "fileName": "optional str"}, {"path": "str", "bytes": "int"}, 90, None, False, {"query": "BME690 Molecule"}),- ("fusion_aps_upload", "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope).",- {"projectId": "required str", "folderId": "required str", "localPath": "required str", "fileName": "optional str"},+ ("fusion_aps_upload", "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). ⛔ FOLDER HYGIENE: NEVER pass a project ROOT folderId - the bridge REFUSES it (refused_root_upload). Loose files in a shared root clutter the team's cloud. Create/choose a real subfolder first (fusion_aps_create_folder under an 'Adom AI Workspace' folder), or just use fusion_make_3d_package / fusion_build_library_3d which auto-target a non-root workspace. To RELOCATE an existing file, MOVE it (preserves its urn) - do NOT delete + re-upload (mints a new urn, breaks 3D bindings). See the fusion-cloud-hygiene skill.",+ {"projectId": "required str", "folderId": "required str (NON-ROOT subfolder)", "localPath": "required str", "fileName": "optional str"}, {"itemUrn": "str", "bytes": "int"}, 90, None, False, {"projectId": "a.YnVz...", "folderId": "urn:...", "localPath": "C:/x.f3d"}),- ("fusion_aps_create_folder", "Create a subfolder in a project (free; needs data:create scope).",- {"projectId": "required str", "parentFolderId": "required str", "name": "required str"}, {"folderId": "str"}, 50, None, False, {"projectId": "a.YnVz...", "parentFolderId": "urn:...", "name": "new-folder"}),+ ("fusion_aps_create_folder", "Create a subfolder in a project (free; needs data:create scope). Use this to make an 'Adom AI Workspace' folder + per-task subfolders so the bridge never dumps loose files in the shared project root. See fusion-cloud-hygiene.",+ {"projectId": "required str", "parentFolderId": "required str", "name": "required str"}, {"folderId": "str"}, 50, None, False, {"projectId": "a.YnVz...", "parentFolderId": "urn:...", "name": "Adom AI Workspace"}), ("fusion_aps_get", "Raw authenticated GET against a free APS path (dev/verify helper).", {"path": "required str"}, {"status": "int", "body": "obj"}, 40, None, False, {"path": "/project/v1/hubs"}), ("fusion_walk_cloud_tree", "DO NOT USE: slow in-app add-in walk (30+ min) that CAN CRASH Fusion (WinError 10054). Use fusion_aps_search instead; see the 'fusion-aps-search' skill.",
server.py+48−7@@ -1389,6 +1389,7 @@ def _orchestrate_make_3d_package(args: dict) -> dict: "so it can create 'Adom AI Workspace', or pass a real (non-root) folderId. NEVER " "pass a project root folderId. See the fusion-cloud-hygiene skill."} cap_label = args.get("captureLabel") # when set, capture BEFORE (footprint) + AFTER (chip placed)+ task = _safe_folder_name(args.get("task") or "AI 3D packages") # saveAs subfolder (never root) orient = args.get("orient", True) base = _os.path.basename(model_path).rsplit(".", 1)[0] name = (args.get("fileName") or (base + "_3d")).replace("'", "").replace(".f3d", "")@@ -1430,9 +1431,16 @@ def _orchestrate_make_3d_package(args: dict) -> dict: "dx=bb.maxPoint.x-bb.minPoint.x; dy=bb.maxPoint.y-bb.minPoint.y; dz=bb.maxPoint.z-bb.minPoint.z\n" + orient_block + "try:\n app.activeViewport.fit()\nexcept: pass\n"+ # FOLDER HYGIENE: saveAs into an 'Adom AI Workspace'/<task> subfolder, NEVER the project root.+ "proj=app.data.activeProject; rf=proj.rootFolder\n"+ "def _sub(p,nm):\n"+ " for i in range(p.dataFolders.count):\n"+ " if p.dataFolders.item(i).name==nm: return p.dataFolders.item(i)\n"+ " return p.dataFolders.add(nm)\n"+ f"wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n" "res={}\n" "try:\n"- f" doc.saveAs('{name}', app.data.activeProject.rootFolder, '3d package (footprint+chip)', '')\n"+ f" doc.saveAs('{name}', wsf, '3d package (footprint+chip)', '')\n" " res['path']=doc.dataFile.id if doc.dataFile else None\n" " res['dims']=[round(dx*10,2),round(dy*10,2),round(dz*10,2)]\n" "except Exception as e: res['err']=str(e)[:80]\n"@@ -1538,7 +1546,7 @@ def _orchestrate_build_library_3d(args: dict) -> dict: mk_args = { "lbrPath": per_lbr, "modelPath": step, "projectId": project_id, "folderId": folder_id,- "fileName": f"{pkg}_3d",+ "fileName": f"{pkg}_3d", "task": task, "captureLabel": (pkg if capture else None), } mk = _orchestrate_make_3d_package(mk_args)@@ -1616,6 +1624,29 @@ def _orchestrate_build_library_3d(args: dict) -> dict: } +def _dismiss_dialogs_bg() -> list:+ """Dismiss any blocking Fusion dialog in the BACKGROUND, without stealing focus.++ Uses close_window (WM_CLOSE via PostMessage) = Cancel/No on the dialog - the background-safe+ dismiss. ⛔ NEVER use send_key/Escape for this: send_key calls SetForegroundWindow and YANKS+ Fusion to the foreground, disrupting the user (learned 2026-06-29). WM_CLOSE is also more reliable+ than Escape on Qt dialogs. Returns the titles dismissed."""+ try:+ info = get_fusion_window_info()+ dismissed = []+ for d in info.get("dialogs", []) or []:+ hwnd = d.get("hwnd")+ if hwnd:+ try:+ close_window(hwnd) # background WM_CLOSE = Cancel/No (never creates a stray)+ dismissed.append(d.get("title") or "")+ except Exception:+ pass+ return dismissed+ except Exception:+ return []++ def _orchestrate_capture_library_views(args: dict) -> dict: """Capture the LIBRARY-EDITOR views that make EEs trust a part: the schematic SYMBOL, the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: the symbol +@@ -1653,11 +1684,16 @@ def _orchestrate_capture_library_views(args: dict) -> dict: ext = view_ext.get(v, v) er = _proxy_to_addin("electron_run", {"command": f"EDIT {pkg}.{ext}"}, timeout=30) _t.sleep(settle) # let the editor LOAD the new view before framing/capturing it+ # ALWAYS catch + dismiss any dialog the EDIT raised - in the BACKGROUND (no focus steal).+ # Common one: "Create new symbol/footprint '<name>'?" when <name> is a DEVICESET name but+ # the symbol/package is named by the shared combo. Dismissing (Cancel/No) avoids a stray.+ dismissed = _dismiss_dialogs_bg() _proxy_to_addin("electron_run", {"command": "WINDOW FIT"}, timeout=20) _t.sleep(0.6) shot = _capture_labeled(f"{pkg}_{label_of.get(ext, ext)}") out.append({"package": pkg, "view": label_of.get(ext, ext), "path": shot,- "ok": bool(er.get("success", True)) and bool(shot)})+ "dialogDismissed": dismissed or None,+ "ok": bool(er.get("success", True)) and bool(shot) and not dismissed}) n_ok = sum(1 for o in out if o.get("ok")) return { "success": n_ok > 0,@@ -1666,10 +1702,15 @@ def _orchestrate_capture_library_views(args: dict) -> dict: f"Captured {n_ok}/{len(out)} library-editor views to C:/tmp/conduit-screenshots (pull with " "desktop_pull_file). SYMBOL = full pinout; FOOTPRINT = pads + layer stack; COMPONENT = the " "Content Manager device view with the footprint<->package Mapped check + pin/pad counts. "- "PITFALLS: the .lbr must be OPEN in the Electronics Library workspace (fusion_open_lbr first; "- "fusion_get_app_state activeWorkspace == 'Electronics Library'); EDIT uses the DEVICESET "- "name (e.g. ESP32-S3FN8); a blank capture usually means a wrong name or the library is not "- "the active tab."+ "⚠️ NAMES: the COMPONENT (.dev) view opens by DEVICESET name (e.g. R-0603-10K). But SYMBOL "+ "(.sym) and FOOTPRINT (.pac) open by the SYMBOL / PACKAGE name - in a SHARED-FOOTPRINT "+ "library (one symbol/footprint per package, many value devicesets) that is the COMBO name "+ "(e.g. 'R-0603'), NOT the deviceset name ('R-0603-10K'). Passing a deviceset name to .sym/"+ ".pac makes Fusion pop 'Create new symbol/footprint?'; the bridge now auto-dismisses that in "+ "the BACKGROUND (Cancel/No, no focus steal - see entries with dialogDismissed) and marks the "+ "view not-ok, but you should pass the right name. For shared libraries the COMPONENT view "+ "alone shows symbol + footprint + 3D previews + Mapped, so it is usually enough. "+ "PITFALLS: the .lbr must be OPEN in the Electronics Library workspace (fusion_open_lbr first)." ), }
skills/fusion-build-parts-library/SKILL.mdadded+71@@ -0,0 +1,71 @@+---+name: fusion-build-parts-library+description: Reproducible recipe for building a WHOLE parts catalog (e.g. the 138-part Adom basic parts library) into ONE Fusion 360 .lbr where every value is placeable with its real footprint + a rendering 3D model - the SHARED-FOOTPRINT method, end to end. Collapse the catalog to its unique (type,package) footprints, build a 3D package per footprint with fusion_build_library_3d, then clone the base deviceset per catalog value so all values share the footprint + symbol + 3D binding. Covers sourcing the footprints/3D (wiki per-MPN pages, the static footprints library, generic KiCad models), the deviceset-cloning assembly, validation, verification, and publishing. Read this to reproduce or extend the Adom basic parts Fusion library, or to turn any parts list into a Fusion library. Trigger words - build a parts library, adom basic parts library, basic parts fusion lbr, whole catalog library, shared footprint library, clone devicesets, 138 part library, reproduce the basic parts library, parts table to lbr, resistor capacitor library fusion.+---++# Build a whole parts catalog into one Fusion library (the shared-footprint method)++How the **138-part Adom basic parts** Fusion library was built, reproducibly. The key idea (see also+[fusion-multipart-libraries](../fusion-multipart-libraries/SKILL.md)): a real library has FAR fewer+footprints than values - one `R-0402` footprint serves all 41 of the 0402 resistor values. So you+build a 3D package per *footprint* (~10), then clone a deviceset per *value* (138) that all share it.++## Step 1 - get the catalog + collapse it to unique footprints++The Adom basic parts catalog lives in the **[basic-parts-library](https://wiki.adom.inc/basic-parts-library)**+wiki page (the parts array is embedded in its viewer html). Each part has `type`, `value`, `package`,+`mfrPN`, `manufacturer`. Collapse to the unique **`(type, package)`** combos - the 138 parts are just+**10 footprints**: R-0402, R-0603, C-0402, C-0603, C-0805, L, LED-0402, LED-0603, LED-2020, FB-0402.++## Step 2 - source ONE footprint + STEP per combo++Passive land patterns are standard per package (IPC-7351), so one representative part per combo is+enough; a 0603 cap shares the 0603 resistor land, etc. Sources, in order of preference:+- **Per-MPN wiki pages** (chip-fetcher output): `/api/v1/pages/<mpn-lowercased>/files/<MPN>.kicad_mod`+ + `.step` (+ `.kicad_sym`). Pick a representative `mfrPN` from the catalog for the combo.+- **The wiki static footprints library**: `static/footprints/<pkg>/<Name>.kicad_mod` (e.g. `c-0603`,+ `r-0805`).+- **Generic KiCad models**: `C_0603_1608Metric.step`, `R_0402_1005Metric.step`, etc.++## Step 3 - `adom-lbr generate` a base .lbr per combo++```bash+adom-lbr generate --sym <combo>.kicad_sym --fp <combo>.kicad_mod --name <combo> \+ --model <combo>.step -o <combo>.lbr+```+Gives, per combo, a `.lbr` with one `<package>` + `<symbol>` + `<deviceset>` (named `<combo>`).++## Step 4 - build the 3D once per footprint (~10 calls, not 138)++Merge the ~10 base `.lbr` into a combined `base10.lbr` (concatenate `<packages>`/`<symbols>`/+`<devicesets>`), stage to Windows, then:+```jsonc+fusion_build_library_3d { "lbrPath":"C:/.../base10.lbr",+ "parts":[{package, lbrPath (per-combo .lbr), modelPath (.step)}, ...],+ "outLbrPath":"C:/.../base10_bound.lbr", "task":"AdomBasicParts-3D" }+```+This makes a footprint+chip `.f3d` per combo, uploads it **into `Adom AI Workspace/<task>`** (never the+project root - see [fusion-cloud-hygiene](../fusion-cloud-hygiene/SKILL.md)), and injects the+`<package3d>` bindings. Result: 10 packages, each Mapped to a rendering 3D model.++## Step 5 - clone a deviceset per catalog value (138 from 10)++For each catalog part, copy its combo's bound `<deviceset>` (which already carries the shared+`<gate symbol=…>`, `<device package=…>`, and `<package3dinstances>` 3D binding) and change only:+- `<deviceset name="…">` to an EAGLE-style value name (`R-0402-4K7`, `C-0603-100N`, `LED-0603-BLUE`,+ `FB-0402-600R`), and+- the `<technology>` attributes: `VALUE` (the human value), `MPN`, `MF`, `MOUSER`.++Replace the ~10 base devicesets with the 138 clones. Final `.lbr` = 10 packages + 10 symbols + 10+`<package3d>` + **138 devicesets**, every one Mapped + rendering. `adom-lbr validate` (the only errors+should be the harmless `duplicate-pin-name` warnings).++## Step 6 - open, verify, capture, publish++- `fusion_open_lbr` the full `.lbr`; `fusion_check_dialogs` == 0 (no broken refs); each device shows+ Mapped ✓ + a rendering 3D preview.+- `fusion_capture_library_views {packages:[...]}` for symbol/footprint/component screenshots; pull the+ `before/after` 3D shots; make a montage.+- Publish the `.lbr` + README (a table of all values) + screenshots + video to the wiki.++The whole thing is ~10 3D builds + a deterministic clone-and-assemble - not 138 separate builds.
skills/fusion-cloud-hygiene/SKILL.md+10@@ -50,6 +50,16 @@ fusion_aps_browse { "projectId": "...", "folderId": "<the folder>" } // items[ fusion_cleanup_cloud_files { "fileIds": ["urn:adsk.wipprod:dm.lineage:...", ...], "projectName": "Adom" } ``` +## Rule 4 - to RELOCATE a file, MOVE it (preserve the urn) - don't delete + rebuild++If a file is in the wrong folder (e.g. you need to get a 3D `.f3d` out of the root and into the+workspace), **MOVE it** - Autodesk's cloud keeps the file's **lineage urn** across a move, so anything+that references it (a library's `<package3d wip_urn=...>` 3D binding) **still resolves**. Deleting it+and re-uploading mints a NEW urn, which breaks every binding and forces a full rebuild. (Learned the+hard way 2026-06-29: a library's 10 base f3d were deleted during cleanup instead of moved, so the+library's 3D had to be rebuilt + re-bound. Moving them would have kept the library intact.) So:+clean up only the JUNK by urn; **move** the files you still need.+ ## The discipline, in one line Pick (or make) a real, non-root, AI-owned subfolder before you write anything to the cloud; clean up
skills/fusion-driving/SKILL.md+11@@ -75,6 +75,16 @@ A dialog has a correct answer that depends on what it says. Clicking the default To act on a specific dialog, target its `hwnd`: `fusion_click_fusion {"hwnd":<dlg>, ...}`, `fusion_send_key {"hwnd":<dlg>, "key":"escape"}`, or `fusion_close_window {"hwnd":<dlg>}`. +### ⛔ Dismiss in the BACKGROUND - `close_window` (WM_CLOSE), NEVER `send_key`/Escape++When you DO dismiss a dialog, use **`fusion_close_window {"hwnd":<dlg>}`** (WM_CLOSE via PostMessage =+Cancel/No). It dismisses **without stealing focus**. ⛔ **Do NOT use `fusion_send_key`/Escape to expire+a dialog** - `send_key` calls `SetForegroundWindow` and **YANKS Fusion to the foreground**, disrupting+the user (caught 2026-06-29: an Escape pulled Fusion to the front mid-task). WM_CLOSE is also more+reliable than Escape on Qt dialogs. The bridge's own auto-dismiss paths (`_dismiss_dialogs_bg`, the+long-command pre-dismiss) all use `close_window` for exactly this reason. Background screenshots ++background dismiss = the user never sees Fusion jump around.+ ## The blocking-dialog catalog (title substring -> what it means -> do THIS) | Dialog (title/body contains) | Meaning | Correct response |@@ -86,6 +96,7 @@ To act on a specific dialog, target its `hwnd`: `fusion_click_fusion {"hwnd":<dl | **"recovery", "recover unsaved", "document recovery"** | Document Recovery prompt. | `fusion_dismiss_recovery`, or `fusion_relocate_recovery` BEFORE `fusion_start` to prevent it. | | **"save changes", "unsaved changes", "do you want to save"** | Unsaved-changes prompt on close. | Use `fusion_close_document` (closes without the save modal). Only Escape/Cancel if you mean to keep editing. | | **"what do you want to design"** | Fusion's start picker (no doc open). | `fusion_send_key {"key":"escape"}`. |+| **"create new symbol/footprint/device '<name>'?"** | An `EDIT <name>.sym/.pac/.dev` was run with a name Fusion can't find as that object - usually a DEVICESET name passed to `.sym`/`.pac` whose symbol/package is named by the shared combo (e.g. `R-0603`, not `R-0603-10K`). | Dismiss in the BACKGROUND (`fusion_close_window {hwnd}` = No - never create a stray). Then re-`EDIT` with the correct symbol/package name. `fusion_capture_library_views` now auto-handles this. | | bare **"Fusion360"** modal during an open | Usually the linked-design picker. | `fusion_screenshot_fusion` to confirm, then handle as the picker. | (The bridge classifies these in `handlers/dialog_classify.py`. When you discover a new one, add a