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.5.9: auto dialog-array capture + analyze-hint after mutating ops; fusion_check_dialogs verb; upload-dialog classify rules; 4-skill restructure (driving/cloud-save/multipart)
9 files changed
+548−28
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.5.8+1.5.9
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.5.8",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.5.8/adom-bridge-fusion-v1.5.8.zip",- "sha256": "022478b11df34fee5d0aadeac92cc3f3a55cca72c939950c8f496ea0ae0167be",- "size": 253113,+ "version": "1.5.9",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.5.9/adom-bridge-fusion-v1.5.9.zip",+ "sha256": "a3c19162a7ba58e7af9858e2949964b1bf485123c032b2df8e348f0b4837ad13",+ "size": 265348, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-28T23:44:19Z"+ "released_at": "2026-06-29T01:15:20Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.5.8",+ "version": "1.5.9", "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.",
handlers/dialog_classify.py+38−12@@ -19,6 +19,23 @@ from handlers.fusion_ui import get_fusion_window_info # (category, resolution-hint) keyed by lowercase title substrings, checked in order. _RULES = [+ # ⚠️ DATA-LOSS dialog. Closing a doc while its 3D packages still upload to the Hub.+ # Title is often the bare "Fusion", so this also gets caught by the generic fallback+ # below; the rule here fires when the build surfaces descriptive text. The CORRECT+ # action is NEVER "dismiss" - it is "No, then wait for the upload to drain".+ (("uploaded to your fusion hub", "packages are being uploaded", "being uploaded",+ "lose these changes"),+ "upload_in_progress",+ "DATA LOSS RISK. Fusion is still uploading 3D package(s) to the Hub. Do NOT click Yes / do "+ "NOT close - you will LOSE the uploaded packages and any unsaved bindings. Click No, then WAIT "+ "and poll until the upload finishes before closing or saveAs. See the fusion-cloud-save skill."),++ (("cannot be saved while packages", "save was cancelled", "save cancelled"),+ "save_blocked_by_upload",+ "saveAs was refused because 3D packages are still uploading to the Hub (this also throws "+ "InternalValidationError). Not fatal: wait for the upload to drain, then retry the saveAs and "+ "poll past it. See the fusion-cloud-save skill."),+ (("needs to update", "update is available", "software update", "new update"), "update_nag", "Fusion is prompting to update itself. Do NOT auto-confirm (it can start an "@@ -57,18 +74,27 @@ def _category_for_title(title: str): for substrings, category, resolution in _RULES: if any(s in t for s in substrings): return category, resolution- # A bare "Fusion360"-titled modal appearing during an open is, in practice,- # almost always the multi-linked "Select Electronics Design File" chooser- # (its window title is just "Fusion360", not descriptive). Flag it as a- # likely picker so callers can try the headless resolver before giving up.- if t in ("fusion360", "autodesk fusion 360", "fusion 360"):- return ("possible_linked_design_picker",- "Generic Fusion modal — during a file open this is most often the "- "'Select Electronics Design File' chooser, a CEF modal that can't be "- "resolved headlessly (keyboard/Win32 don't navigate its list). "- "fusion_screenshot_fusion to confirm, then select the design on the "- "desktop.")- return "unknown", "Unrecognized modal — fusion_screenshot_fusion to inspect, then dismiss."+ # A bare "Fusion"/"Fusion360"-titled modal is generic - its body text (the part+ # that tells you what it actually IS) lives in CEF/Qt child controls that are NOT+ # Win32-enumerable, so we cannot classify it by title alone. The two it most often+ # is, depending on context: during a file OPEN, the "Select Electronics Design File"+ # chooser; during a CLOSE or SAVE, the Hub "packages are being uploaded... sure you+ # want to close?" data-loss confirm. So the only safe instruction is: READ the+ # attached screenshot and ANALYZE before acting. NEVER tell the caller to blindly+ # dismiss - clicking Yes on the upload confirm destroys the packages.+ if t in ("fusion", "fusion360", "autodesk fusion 360", "fusion 360"):+ return ("generic_modal_read_screenshot",+ "Generic Fusion modal - its body is not Win32-readable, so a screenshot "+ "is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. "+ "If it mentions uploading / 'lose these changes' / 'sure you want to "+ "close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). "+ "If it is the 'Select Electronics Design File' chooser during an open, its "+ "list is not keyboard/Win32-navigable; open the specific design by URN or "+ "select on the desktop.")+ return ("unknown",+ "Unrecognized modal - a screenshot is attached: READ it and ANALYZE before "+ "acting. Do NOT blindly dismiss (some modals lose work if confirmed wrong). "+ "Identify it from the screenshot, then take the work-preserving action.") def classify_blocking_dialogs() -> list:
server.py+115−3@@ -261,6 +261,91 @@ def _post_open_screenshot(result: dict, settle_time: float = 2.0) -> dict: return result +def _capture_dialog_array(settle: float = 0.6) -> dict | None:+ """After a mutating op, enumerate + screenshot any owned popups / modal dialogs so+ the AI can SEE and ANALYZE them. Titles alone are not enough: a dialog's body text+ lives in CEF/Qt child controls that are NOT Win32-readable, so we attach a per-dialog+ screenshot (hwnd-targeted PrintWindow, background - never foreground/fullscreen).++ Cheap when nothing is up: it enumerates windows (Win32 EnumWindows) and only+ screenshots when a real dialog is present. Returns None when no dialog is up;+ otherwise {dialogsDetected, dialogs:[{hwnd,title,category,resolution,screenshot}],+ _hint}. Never raises - best effort.+ """+ import re as _re+ import time as _t+ if settle:+ _t.sleep(settle)+ try:+ info = get_fusion_window_info()+ except Exception:+ return None+ if not info.get("success"):+ return None+ raw = info.get("dialogs") or []+ if not raw:+ return None+ try:+ classified = {c.get("hwnd"): c for c in classify_blocking_dialogs()}+ except Exception:+ classified = {}+ out = []+ for d in raw:+ rect = d.get("rect", {})+ if rect.get("width", 0) < 80 or rect.get("height", 0) < 50:+ continue # skip banners / tiny docked tool windows+ hwnd = d.get("hwnd")+ title = d.get("title", "")+ entry = {"hwnd": hwnd, "title": title}+ c = classified.get(hwnd)+ if c:+ entry["category"] = c.get("category")+ entry["resolution"] = c.get("resolution")+ try:+ label = _re.sub(r'[<>:"/\\|?*]', "", title).replace(" ", "_")[:30] or "dialog"+ ss = screenshot_hwnd(hwnd, label=f"dlg-{label}")+ if ss.get("success"):+ entry["screenshot"] = ss["savedTo"]+ except Exception:+ pass+ out.append(entry)+ if not out:+ return None+ return {+ "dialogsDetected": len(out),+ "dialogs": out,+ "_hint": (+ f"⚠️ {len(out)} Fusion dialog(s)/owned popup(s) are up after this operation. "+ "STOP and ANALYZE before the next call: READ each dialogs[].screenshot and use its "+ "category/resolution. Do NOT blind-dismiss - some confirms LOSE work (clicking Yes on a "+ "Hub-upload 'are you sure you want to close?' destroys the uploaded packages). Take the "+ "work-preserving action (often: click No, then wait for the upload to drain). See the "+ "fusion-driving skill."+ ),+ }+++def _merge_dialog_array(result: dict) -> dict:+ """Attach the post-op dialog array (if any) to a mutating verb's result, so the AI+ always sees pending dialogs WITHOUT having to remember a separate call. The hint is+ mirrored to top-level _hint AND into data.dialogArray (data is forwarded reliably by+ the relay). No-op when no dialog is up."""+ if not isinstance(result, dict):+ return result+ try:+ arr = _capture_dialog_array()+ except Exception:+ arr = None+ if not arr:+ return result+ result.setdefault("data", {})+ if isinstance(result.get("data"), dict):+ result["data"]["dialogArray"] = arr+ existing = result.get("_hint")+ result["_hint"] = arr["_hint"] + ((" | " + existing) if existing else "")+ return result++ def _handle_open_cloud_file(fusion_info: dict, args: dict) -> dict: """Open a cloud file with post-open dialog detection + auto-screenshot. @@ -480,6 +565,15 @@ COMMAND_HANDLERS = { "close_window": _handle_close_window, "window_info": _handle_window_info, "screenshot_all": _handle_screenshot_all_fusion,+ # On-demand dialog/owned-popup array: enumerate + screenshot every modal so the AI+ # can ANALYZE before acting. Use while polling a long op (e.g. a Hub upload) or any+ # time you suspect a dialog is up. Mutating verbs attach this automatically; this is+ # the manual entry point. See the fusion-driving skill.+ "check_dialogs": lambda fi, args: (+ _capture_dialog_array(settle=float(args.get("settle", 0.3)))+ or {"success": True, "dialogsDetected": 0, "dialogs": [],+ "_hint": "No Fusion dialogs/owned popups are currently up."}+ ), "addin_status": lambda fi, args: _handle_addin_status(), # APS cloud search (pure HTTPS, works with Fusion closed). Lives in # COMMAND_HANDLERS so it returns BEFORE any Fusion-running gate.@@ -677,6 +771,18 @@ _long_command = None # None or {"command": str, "startedAt": float} LONG_RUNNING_COMMANDS = {"walk_cloud_tree", "search_cloud_files"} +# Commands that change Fusion's state and can pop a modal dialog / owned popup the AI+# must see (a save confirm, the Hub "are you sure you want to close?" data-loss prompt,+# a recovery prompt, etc.). After these, the dispatcher auto-attaches the dialog array+# (_capture_dialog_array) + an analyze-this hint so the AI cannot fly blind. Read-only+# commands (get_app_state, document_info, board_info, exports) are intentionally excluded+# to avoid the per-call screenshot latency. See the fusion-driving skill.+MUTATING_COMMANDS = {+ "run_modeling_script", "execute_text_command", "electron_run",+ "close_document", "close_all_documents", "import_step",+ "set_parameter", "save_to_cloud", "delete_cloud_file",+}+ def _set_long_command(command: str): with _long_command_lock:@@ -1268,9 +1374,9 @@ def dispatch_command(command: str, args: dict) -> dict: if command == "open_lbr": return _orchestrate_open_lbr(args) if command == "save_lbr":- return _orchestrate_save_lbr(args)+ return _merge_dialog_array(_orchestrate_save_lbr(args)) if command == "attach_3d_package":- return _orchestrate_attach_3d_package(args)+ return _merge_dialog_array(_orchestrate_attach_3d_package(args)) # Add-in proxy commands if command in ADDIN_COMMANDS:@@ -1302,7 +1408,13 @@ def dispatch_command(command: str, args: dict) -> dict: return _proxy_to_addin(addin_cmd, args, timeout=proxy_timeout) finally: _clear_long_command()- return _proxy_to_addin(addin_cmd, args, timeout=proxy_timeout)+ result = _proxy_to_addin(addin_cmd, args, timeout=proxy_timeout)+ # After a state-changing op, surface any dialog/owned-popup the AI must analyze+ # (the Hub upload-close confirm, a save prompt, recovery, etc.) so it can't fly+ # blind. Read-only verbs are excluded to avoid per-call screenshot latency.+ if command in MUTATING_COMMANDS:+ result = _merge_dialog_array(result)+ return result # Unknown command — check installation/running status for helpful errors if not fusion_info.get("installed"):
skills/fusion-cloud-save/SKILL.mdadded+107@@ -0,0 +1,107 @@+---+name: fusion-cloud-save+description: How saving works in Fusion 360 when 3D packages and libraries live in the Autodesk cloud (Hub) - why FINISH/Save uploads packages ASYNCHRONOUSLY, why you must give a library a cloud home BEFORE attaching 3D so a crash cannot wipe your work, and the hard rule to NEVER close or saveAs while a Hub upload is in flight (it loses packages or throws InternalValidationError / "save was cancelled"). Includes the poll-until-uploaded saveAs recipe. Read before saving any library or document that has had 3D packages attached, or whenever you see an upload/save dialog. Trigger words - fusion save, saveAs, save library, fusion hub upload, packages being uploaded, InternalValidationError, save was cancelled, library not saved, lost attaches, lost bindings, save to cloud, wip_urn upload, poll save, save while uploading, cloud persistence fusion.+---++# Saving in Fusion 360 when everything lives in the cloud++In Fusion's Electronics library flow, a 3D package is not embedded in the `.lbr` - it is a file in+your Autodesk **Hub (cloud)**, referenced by a `wip_urn`. That one fact drives every rule here:+**saving is asynchronous and networked, so it can be in flight, refused, or lost.** Pairs with+[fusion-driving](../fusion-driving/SKILL.md) (read the dialog before acting) and+[fusion-multipart-libraries](../fusion-multipart-libraries/SKILL.md) (the build-up that depends on+this).++## ⛔ The failure this skill prevents (2026-06-28)++A 10-part library had all 10 3D packages attached into the open library doc, **but the library was+never saved.** A close loop force-closed docs mid-upload, the close-confirm dialog was blind-clicked,+and the library closed. **All 10 attaches were gone** - because the bindings lived only in an+unsaved, in-memory document while the packages were still uploading. Two root causes: (1) no cloud+home for the library before the work, (2) acting while uploads were in flight.++## Rule 1 - FINISH/Save uploads the 3D package to the Hub ASYNCHRONOUSLY++After the Package3D generator's FINISH (`Package3DStop`), Fusion saves the 3D package and **starts+uploading it to your Hub in the background.** Control returns to you immediately, but the upload is+NOT done. The binding in the library points at that package's `wip_urn`. Until the upload completes,+the package is "in flight" and the library cannot be cleanly saved or closed.++## Rule 2 - give the library a CLOUD HOME before you attach (or save incrementally)++A library that exists only as a local `.lbr` opened in Fusion, with attaches made into it, lives+**only in memory** until you `saveAs` it to the cloud. A crash, a wrong dialog click, or a force-close+wipes everything. Two safe patterns:++- **Save-first:** open the `.lbr`, `saveAs` it to the cloud once (empty of 3D) so it has a Hub+ lineage, THEN attach 3D and save again. Now a mishap costs the current attach, not all of them.+- **Save-incrementally (build-up):** save the library after each part's attach (the build-up-one-by-one+ flow in [fusion-multipart-libraries](../fusion-multipart-libraries/SKILL.md)). Slower, but every+ part is durable the moment it lands.++Either way: **do not attach 10 parts into an unsaved in-memory library and save only at the end.**+That is the exact shape that lost the work.++## Rule 3 - NEVER close or saveAs while a Hub upload is in flight++This is the load-bearing rule. While packages upload:++- **Closing the doc** pops *"Packages are being uploaded... if you close you will lose these changes.+ Are you sure you want to close?"* Clicking **Yes loses the packages.** Click **No** and wait.+- **saveAs** is refused with *"this library cannot be saved while packages are being uploaded to your+ Fusion Hub. Save was cancelled,"* and the API throws **`InternalValidationError`.**++Never `close(False)` (force-discard) a document that may be uploading. Wait for the upload to drain+first.++## Rule 4 - the poll-until-uploaded saveAs recipe (verified working)++You cannot reliably detect "upload finished" from a single flag, so **poll the saveAs itself** - it+succeeds once the upload drains. This worked end to end on the R_100R single-part library:++```python+# find the library document, then poll saveAs past the upload window+saved = False+for attempt in range(10):+ time.sleep(8) # let the Hub upload make progress+ fusion_dismiss_blocking_dialogs # only AFTER confirming it's the upload dialog (see fusion-driving)+ res = run_modeling_script("""+ lib = None+ for i in range(app.documents.count):+ d = app.documents.item(i)+ if any(d.products.item(j).productType == 'LibraryProductType'+ for j in range(d.products.count)):+ lib = d+ out = {}+ if lib:+ lib.activate()+ try:+ out['saveAs'] = lib.saveAs('MyLibName', app.data.activeProject.rootFolder, 'comment', '')+ except Exception as e:+ out['err'] = str(e)[:60] # InternalValidationError while still uploading+ result = out+ """)+ if res.get('saveAs'):+ saved = True; break+```++Notes:+- `saveAs(name, folder, comment, "")` on the `Document`; pass `app.data.activeProject.rootFolder` as+ the folder. Activate the doc first.+- An empty result (no `saveAs`, no `err`) usually means **the library doc was not found** (it got+ closed). That is the alarm bell that you lost the doc, not a transient - stop and investigate, do+ not keep polling a doc that no longer exists.+- 8s between tries with ~10 tries covers a typical few-MB package upload. Bigger models need longer.++## Rule 5 - confirm the save actually landed++After saveAs returns truthy, verify: reopen the saved library (or export it) and confirm the+`package3d` bindings are present (`package3d` / `package3dinstance` with the `wip_urn`). A truthy+saveAs plus a real binding on reopen is the only proof the work persisted. See+[fusion-libraries LIBRARY_FINDINGS](../fusion-libraries/LIBRARY_FINDINGS.md) for the on-disk markup.++## The save discipline in one line++Give the library a cloud home early, let every Hub upload drain before you touch the doc, poll the+saveAs past the upload window, and verify the binding on reopen. Saving is a network operation - treat+it like one.
skills/fusion-driving/SKILL.mdadded+123@@ -0,0 +1,123 @@+---+name: fusion-driving+description: How to SAFELY drive Fusion 360 through the Adom bridge across many steps without losing work - the discipline of reading the screenshot/owned-popup ARRAY after every mutating op and ANALYZING any dialog BEFORE the next step, never blind-dismissing, and never fullscreen-capturing (it forces foreground and disrupts the user). Includes the blocking-dialog catalog with the CORRECT response for each, and how the bridge now returns the dialog array + an analyze-this hint automatically. Read this BEFORE running any multi-step Fusion automation (attaching 3D, saving libraries, closing docs, modeling scripts). Trigger words - drive fusion, fusion dialog, blocking dialog, are you sure you want to close, packages are being uploaded, save was cancelled, ownedPopupCount, screenshot array, owned popups, fusion notification, lower right error, dismiss dialog, fusion modal, fusion_screenshot_all, fusion_window_info, fusion_check_dialogs, fullscreen capture fusion, analyze before proceeding.+---++# Driving Fusion 360 safely (read the popup array, never fly blind)++Fusion is a desktop app you are operating headlessly. **Almost any mutating operation can pop a+modal dialog or a notification you cannot see unless you look** - and if you barrel ahead (or+blind-dismiss), you destroy work. This skill is the operating discipline that keeps that from+happening. It is the foundation under [fusion-libraries](../fusion-libraries/SKILL.md),+[fusion-multipart-libraries](../fusion-multipart-libraries/SKILL.md), and+[fusion-cloud-save](../fusion-cloud-save/SKILL.md).++## ⛔ The cautionary tale (why this skill exists, 2026-06-28)++Building a 10-part library, the loop force-closed generator docs that were **still uploading 3D+packages to the Fusion Hub**. Fusion popped *"Packages are being uploaded... if you close you will+lose these changes. Are you sure you want to close?"* The automation was sleeping through a blind+`dismiss_blocking_dialogs` loop, which clicked straight through it - **closing the library before it+ever saved. All 10 attaches were lost.** Every rule below is the fix for one beat of that failure.++## Rule 1 - after EVERY mutating op, READ the popup array. Then analyze. Then proceed.++A mutating op = anything that changes Fusion's state: `fusion_run_modeling_script`,+`fusion_execute_text_command`, `fusion_close_document`, `fusion_close_all_documents`,+`fusion_save_lbr`, `fusion_attach_3d_package`, `fusion_open_lbr`, any export, any `electron_run`+that writes.++After it returns, you MUST inspect what owned popups are now up before doing the next step. Three+ways to get the array, in order of preference:++1. **The bridge does it for you (default).** Mutating `fusion_*` verbs now return+ `dialogsDetected`, a `dialogs[]` array (each with `title`, `category`, `resolution`, and a+ `screenshot` path you can Read), and a top-level `_hint`. **When `dialogsDetected > 0, STOP and+ analyze the dialogs[] before your next call.** Never ignore that hint. (See "How the bridge+ enforces this" below.)+2. **`fusion_screenshot_all`** - captures the main window PLUS every Qt dialog window as separate+ shots, and returns the list of saved paths. Read each.+3. **`desktop_screenshot_window {"hwnd": <fusion-hwnd>}`** (AD core) - returns `screenshots[]` and+ **`ownedPopupCount`**. `ownedPopupCount > 0` means dialogs are stacked on the window. Read the+ shots. Get the hwnd from `fusion_window_info` (its `hwnd` field) or `desktop_find_window+ {"titleContains":"Autodesk Fusion"}`.++**To actually SEE a shot:** the PNG/WebP is saved on the machine running the CLI. Read the file+directly with the Read tool. Do not describe it from memory - look.++## Rule 2 - NEVER fullscreen-capture. Always target the window by hwnd.++- ⛔ **Do NOT use `desktop_screenshot_screen`** (whole-desktop capture). It grabs whatever is in the+ foreground (often the user's browser, not Fusion), and screen-DC capture paths force the target+ toward the foreground - **which disrupts the user, who may be doing something else.** It also+ routinely captures the WRONG window.+- ✅ **Always capture a specific `hwnd`** (`fusion_screenshot_all`, `fusion_screenshot_fusion`,+ `desktop_screenshot_window {hwnd}`). These use `PrintWindow` / WGC and capture the **exact window+ in the BACKGROUND** without bringing it forward. Fusion keeps running behind whatever the user is+ doing. This is the only acceptable way to look at Fusion.++## Rule 3 - NEVER blind-dismiss. Classify first, then choose the RIGHT action.++A dialog has a correct answer that depends on what it says. Clicking the default can be catastrophic+(clicking "Yes" on *"sure you want to close, you'll lose changes?"* = data loss). So:++1. Identify the dialog (the `category`/`resolution` from `dialogs[]`, or Read its screenshot).+2. Choose the action that PRESERVES work, from the catalog below.+3. Only use a generic dismiss as a LAST resort, and only for dialogs you have confirmed are safe to+ cancel/escape.++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>}`.++## The blocking-dialog catalog (title substring -> what it means -> do THIS)++| Dialog (title/body contains) | Meaning | Correct response |+|---|---|---|+| **"packages are being uploaded", "lose these changes", "are you sure you want to close"** | A doc is closing while 3D packages still upload to the Hub. Closing = **lose the packages + unsaved bindings**. | Click **No**. WAIT and poll until the upload finishes (see [fusion-cloud-save](../fusion-cloud-save/SKILL.md)), THEN close/save. NEVER click Yes. |+| **"cannot be saved while packages are being uploaded", "save was cancelled"** | You tried saveAs mid-upload; Fusion refused. | Not fatal. Wait for the Hub upload to finish, then retry the saveAs (poll past it). |+| **"needs to update", "update is available", "software update"** | Fusion wants to self-update. | Do NOT auto-confirm (it can update mid-automation). Clear on the desktop or let it finish, then retry. A pending update also causes add-in/host version-drift crashes. |+| **"select electronics design", "linked to multiple"** | A file links to multiple electronics designs; CEF picker. | Its list is NOT Win32/keyboard navigable. Open the specific design by URN/API instead, or select on the desktop. See [fusion-electronics](../fusion-electronics/SKILL.md). |+| **"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"}`. |+| 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+rule there with its correct response so every future session handles it right.)++## Rule 4 - the lower-right notification tray (errors/warnings you keep missing)++Fusion shows non-modal toasts in the **lower-right** ("1 warning(s), 1 error(s)..."). They are NOT+modal dialogs, so dialog enumeration may not flag them - but they tell you an operation half-failed.+They DO render in an hwnd-targeted main-window screenshot. So after a risky op, also glance at the+main-window shot's lower-right, not just the popup array. If you see an error count, open the+notification center and read it before continuing.++## How the bridge enforces this (so you cannot miss it)++`_post_open_screenshot()` has always auto-captured the main window + every dialog after *open*+commands and told the AI to read them. That same capture now fires after **all mutating verbs** via+`_capture_dialog_array()`, which:++- cheaply enumerates dialog windows (`get_fusion_window_info`, a Win32 `EnumWindows` - no screenshot+ when nothing is up),+- on detection, classifies each (`classify_blocking_dialogs`) and screenshots it by hwnd+ (`screenshot_hwnd`, background `PrintWindow`),+- returns `dialogsDetected` + `dialogs[]` + a top-level `_hint` that says **ANALYZE these before+ proceeding; do NOT blind-dismiss**.++You can also call **`fusion_check_dialogs`** at any time to get the current array on demand (e.g.+while polling a long upload). The point: the array and the hint come back to you automatically - your+job is to actually READ them and act, every time.++## The loop that should be muscle memory++```+do mutating op+ -> read dialogsDetected / dialogs[] / ownedPopupCount in the response (or fusion_check_dialogs)+ -> if a dialog is up: Read its screenshot, look it up in the catalog, take the work-preserving action+ -> glance at the main-window shot's lower-right for an error toast+ -> only then do the next op+```++Slow is smooth, smooth is fast. The one time you skip the look is the time you lose the library.
skills/fusion-libraries/SKILL.md+20−6@@ -5,11 +5,22 @@ description: How an Adom component library (symbol + footprint + 3D chip) is bui # Fusion libraries - symbol + footprint + 3D into a placeable .lbr -> This skill is the concise entry point. The **exhaustive guides are bundled right here in this skill-> folder** - read them directly: **[MAKING_LIBRARIES.md](MAKING_LIBRARIES.md)** (the full step-by-step-> guide + screenshots) and **[LIBRARY_FINDINGS.md](LIBRARY_FINDINGS.md)** (the findings log: the exact-> `package3d` markup, the owned-popup error-capture technique, and every pitfall). (Their screenshots load-> from the wiki.)+> This skill is the concise entry point for **ONE part, end to end**. The **exhaustive guides are+> bundled right here in this skill folder** - read them directly: **[MAKING_LIBRARIES.md](MAKING_LIBRARIES.md)**+> (the full step-by-step guide + screenshots) and **[LIBRARY_FINDINGS.md](LIBRARY_FINDINGS.md)** (the+> findings log: the exact `package3d` markup, the owned-popup error-capture technique, and every+> pitfall). (Their screenshots load from the wiki.)++> **Companion skills (read these too):**+> - For a library with **MANY parts**, each needing its own 3D, use+> **[fusion-multipart-libraries](../fusion-multipart-libraries/SKILL.md)** (the reorder-file trick,+> the per-part attach loop, merging + connect/pad validation). This single-part skill is its+> building block.+> - The operating discipline this all rides on:+> **[fusion-driving](../fusion-driving/SKILL.md)** (read the dialog array after every step, never+> blind-dismiss, never fullscreen-capture) and **[fusion-cloud-save](../fusion-cloud-save/SKILL.md)**+> (saving is async/networked: give the library a cloud home early, let Hub uploads drain before you+> close/save). Skipping these is how a finished 10-part library got lost. ## 1. The pipeline (where each tool fits) @@ -82,7 +93,10 @@ One verb does steps 2-3 end to end: open the `.lbr` (library active) -> `Create3 STEP + auto-orient -> `Package3DStop` (FINISH). It returns a `_hint` with the one desktop step left: click the Fusion **Save** dialog via `desktop_ui_click {automationId:'QTApplication.QTFrameWindow.standardActions.SaveButton'}`. The 3D then binds onto the deviceset (Content Manager tree shows it; the `Package` column flips-Placeholder -> part name; the preview LAGS a beat). Fusion writes this exact markup (for a self-uploaded+Placeholder -> part name; the preview LAGS a beat). After FINISH+Save, the package **uploads to the+Hub asynchronously** - do NOT close or saveAs until it drains, or you lose it+([fusion-cloud-save](../fusion-cloud-save/SKILL.md)); and READ that Save dialog before clicking, never+blind-dismiss ([fusion-driving](../fusion-driving/SKILL.md)). Fusion writes this exact markup (for a self-uploaded model): `<package3d urn="" wip_urn="urn:adsk.wipprod:fs.file:vf.<id>?version=1" locally_modified="yes" type="model">` + a device `<package3dinstance package3d_urn="...fs.file:vf...?version=1"/>`. See [LIBRARY_FINDINGS.md](LIBRARY_FINDINGS.md) §11-12.
skills/fusion-multipart-libraries/SKILL.mdadded+138@@ -0,0 +1,138 @@+---+name: fusion-multipart-libraries+description: How to build ONE Fusion 360 .lbr library that holds MANY parts where EACH package has its real 3D model attached - the cracked recipe for per-package 3D in a multi-deviceset library. Covers the reorder-file trick (Electron.Create3DPackage always targets the FIRST package, so rewrite the .lbr to put the target package first), the build-up-one-by-one alternative, merging per-part .lbr files, the pre-merge connect/pad validation that stops one bad part poisoning the whole library, and the per-part attach loop with orient + save. Read this when attaching 3D to more than one part in a single library, or building a parts-library sampler. Trigger words - multi part library, multiple 3d packages, attach 3d to many parts, combined lbr, merge lbr, basic parts library, parts sampler, Electron.Create3DPackage first package, reorder lbr, build up library, per package 3d, package3d loop, deviceset library 3d, attach all 3d.+---++# Building a many-part Fusion library with real 3D on every package++This is the multi-part extension of [fusion-libraries](../fusion-libraries/SKILL.md) (which does ONE+part end to end). The hard part that was unsolved for years: getting Fusion to attach a **distinct 3D+model to each of many packages** inside a single `.lbr`. It is now cracked. This skill is the recipe.+It depends on [fusion-cloud-save](../fusion-cloud-save/SKILL.md) (saving is async/networked) and+[fusion-driving](../fusion-driving/SKILL.md) (read the dialog after every step).++## The core obstacle++`Electron.Create3DPackage <file.lbr>` opens the 3D Package Generator on the **FIRST footprint/package+in the file, always.** Passing a package name does not redirect it; the EAGLE `EDIT <pkg>` command+selects a package in the editor but does NOT change which one Create3DPackage targets. And re-opening+the 2D `.lbr` between parts **resets the document**, so naive looping attaches nothing that sticks.+That is why "attach 3D to part 5 of 10" had no obvious path.++## ✅ The reorder-file trick (the crack, verified 2026-06-28)++Since Create3DPackage targets the FIRST package, **make your target the first package.** Rewrite the+combined `.lbr` so the target's `<package>`, `<symbol>`, and `<deviceset>` sort first, then call+Create3DPackage on that reordered file. It opens the target's generator **while your ONE combined+library stays open**, and FINISH binds the 3D into that open library. Loop, reordering for each part.++Verified: R_100R_0402 bound as a real per-package 3D inside a 10-deviceset library (the non-attached+R_0R correctly stayed `Placeholder`), proving it is one combined library with a genuine per-package+binding, not ten standalones.++### Reorder helper (put one package first)++```python+import re+def reorder_first(xml, tag, name):+ # tag in {'packages','symbols','devicesets'}; move the child named `name` to the front+ sec = re.search(rf'<{tag}>(.*?)</{tag}>', xml, re.S); body = sec.group(1)+ item = rf'<{tag[:-1]} name="[^"]*">.*?</{tag[:-1]}>|<{tag[:-1]} name="[^"]*"/>'+ items = re.findall(f'({item})', body, re.S)+ tgt = [i for i in items if f'name="{name}"' in i[:60]]+ rest = [i for i in items if f'name="{name}"' not in i[:60]]+ return xml.replace(sec.group(0), f'<{tag}>{"".join(tgt+rest)}</{tag}>')++xml = open('AdomBasicParts.lbr').read()+for tag in ('packages','symbols','devicesets'):+ xml = reorder_first(xml, tag, 'R_100R_0402')+open('ro_R_100R_0402.lbr','w').write(xml) # R_100R now first; Create3DPackage opens IT+```++### The per-part attach loop++Open the combined library ONCE. Then for each part (do NOT re-open the library between parts):++```+Electron.Create3DPackage C:/.../ro_<part>.lbr # opens the generator on <part> (it's first)+import <part>.step + orient # importToTarget, then rotate 90 about X (below)+Package3DStop (FINISH) # binds the 3D into the open combined library+handle the Save dialog (3D package) # read it (fusion-driving), click Save+WAIT for the Hub upload to drain # fusion-cloud-save Rule 3 - do NOT race ahead+```++After all parts, `saveAs` the library with the poll-until-uploaded recipe in+[fusion-cloud-save](../fusion-cloud-save/SKILL.md).++### Orient the imported STEP++STEP imports Y-up. Rotate 90 degrees about the model's **smallest bounding-box axis** so it lies flat+(thin in Z), centered on the footprint:++```python+im = app.importManager+im.importToTarget(im.createSTEPImportOptions(step_path), root)+oc = root.occurrences.item(root.occurrences.count - 1); bb = oc.boundingBox+dx, dy, dz = (bb.maxPoint.x-bb.minPoint.x, bb.maxPoint.y-bb.minPoint.y, bb.maxPoint.z-bb.minPoint.z)+axis = (1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)+if axis:+ mat = oc.transform2.copy(); r = adsk.core.Matrix3D.create()+ r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))+ mat.transformBy(r); oc.transform2 = mat+```++Sanity-check dims (cm*10 = mm): a 0402 lands ~[1.0, 0.5, 0.4], an 0603 ~[1.6, 0.8, 0.8]. A wrong+orient shows up as the thin dimension not being Z.++## The build-up-one-by-one alternative (more durable, more re-opens)++Instead of reordering, build the library up incrementally: export a 1-part `.lbr`, open it, attach+3D, save; add part 2 via `adom-lbr`, reopen, attach 3D to the new part, save; repeat. Slower (a+reopen per part) but every part is durable the instant it saves (see fusion-cloud-save Rule 2). Use+this when durability matters more than speed, or when reordering a particular file is fiddly. The+reorder trick is cleaner for a known fixed set; build-up is safer for a long or uncertain run.++## Merging per-part .lbr files into one combined library++An EAGLE `.lbr` is a library of many devicesets, so combine the per-part files:++1. Keep ONE file's `<layers>` skeleton.+2. Concatenate every part's `<package>`, `<symbol>`, and `<deviceset>` (and `<package3d>` if already+ bound) into the single file. MPN-unique names mean no collisions.+3. `adom-lbr validate combined.lbr`.++## ⚠️ VALIDATE connect/pad consistency BEFORE merging (one bad part poisons everything)++Fusion rejects the ENTIRE merged `.lbr` with a generic "has errors and cannot be opened" if ANY+device has an invalid `<connect>` - a pin mapped to a pad its package does not define. This bit+WSL2010: chip-fetcher gave it a 3-pin symbol but a 2-pad footprint, so pin 3 connected to a+nonexistent pad 3, and the whole combined library failed to open. A single-device `.lbr` may still+open, masking the bug until merged.++**Before merging, for every device: every `<connect pad="N">` must reference an `<smd name="N">` or+`<pad name="N">` that exists in its package.** Passives are the usual offenders (symbol pin count vs+footprint pad count). `adom-lbr`'s lint did NOT catch this - validate it yourself and drop or fix any+part that fails.++## Verify the result++Export the saved library and confirm per-package bindings:++- Count `package3d` entries that are bound (have a real `wip_urn`), one per attached part.+- Each NON-attached package should read `Placeholder` (a clean negative control).+- Spot-check in Fusion's Content Manager: the 3D nests under the package under the deviceset, and the+ Packages panel's `Package` column flips from `Placeholder` to the part name.++## Capture the money shots (for the demo video)++Per part, grab three hwnd-targeted shots (never fullscreen - see fusion-driving):++1. **Before:** the footprint/deviceset with a gray placeholder 3D.+2. **Chip lands:** the Package3D generator with the real chip seated on its matched footprint+ (footprint alone in the 3D viewer, then the chip dropping in and lining up - the best shot).+3. **After:** the Content Manager / Packages view showing the 3D bound, `Package` column = part name,+ preview refreshed to the real chip.++These three per part, swapped on camera, ARE the montage. See+[CREATING_BASIC_PARTS_LIBRARIES.md](../../CREATING_BASIC_PARTS_LIBRARIES.md) for the full tool-chain+tutorial and montage recipe.