← Commit history

v1.6.1: capture_library_views + merge-aware binding injection + retry + relay-timeout hint

John Lauer ·8e40481605 ·3mo ago ·parent f00c59e
7 files changed +166−31
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.0\ No newline at end of file+1.6.1\ No newline at end of file
CHANGELOG.md+16
@@ -3,6 +3,22 @@ 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.1 - 2026-06-29++- **NEW `fusion_capture_library_views` - the views EEs actually trust.** Given a list of packages+  (and the .lbr open), it runs `EDIT <deviceset>.sym/.pac/.dev` + `WINDOW FIT` + a background+  screenshot and returns, per part: the **SYMBOL** (full pinout), the **FOOTPRINT** (pad array ++  Copper/Silkscreen/Mask layer stack), and the **COMPONENT** view (Content Manager: symbol + the+  package table with the footprint<->package **Mapped ✓** + pin/pad counts). The 3D before/after+  shots never showed these.+- **`_inject_package3d_bindings` is now MERGE-AWARE + idempotent.** It reads bindings already in the+  .lbr and accumulates the new ones (new wins), so re-running `fusion_build_library_3d` for just the+  parts that failed NO LONGER wipes the parts that already succeeded (the "a subset re-run dropped my+  earlier bindings" trap). `fusion_build_library_3d` now reads its own `outLbrPath` when re-run, each+  part **self-retries once** on a transient first-part failure, and its `_hint` documents the+  **relay-timeout reality** (a many-part run exceeds the ~60s relay request timeout but keeps running+  server-side - verify by the bound .lbr + the PNGs, then re-run any missing parts).+ ## 1.6.0 - 2026-06-29  - **NEW `fusion_build_library_3d` - build a whole RENDERING multi-part 3D library in ONE call.** Give
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.0",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.0/adom-bridge-fusion-v1.6.0.zip",-  "sha256": "c37957abf38dc8a723df2fbb0664a131772ab74bdd6d8b8ef7f5c5cf79c074e9",-  "size": 277562,+  "version": "1.6.1",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.1/adom-bridge-fusion-v1.6.1.zip",+  "sha256": "602dd8aa07e5d3e0b376a2f0d0d87743189634b735d0a957894923cd84692e2e",+  "size": 282802,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-06-29T16:00:00Z"+  "released_at": "2026-06-29T17:30:00Z" }\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.0",+  "version": "1.6.1",   "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+3
@@ -83,6 +83,9 @@ _T = [     ("fusion_build_library_3d", "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",      {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"},      {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),+    ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",+     {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)"},+     {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}),     ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",      {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"},      {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),
server.py+106−24
@@ -1251,16 +1251,31 @@ def _capture_labeled(label) -> str | None:   def _inject_package3d_bindings(lbr_text: str, bindings: list) -> str:-    """Inject EAGLE <packages3d> + per-device <package3dinstances> into an .lbr in ONE pass.+    """Inject EAGLE <packages3d> + per-device <package3dinstances> into an .lbr - MERGE-AWARE+    and IDEMPOTENT (safe to re-run with any subset of parts). -    bindings: [{package: <pkg name>, wip_urn: <urn>}]. Adds a library-level <package3d> for-    each (between </packages> and <symbols>) and, into every <device> that uses that package,-    a <package3dinstances> right after </connects>. This is the exact XML surgery the AI used-    to hand-write per part - now one deterministic call. Returns the new .lbr text."""+    bindings: [{package: <pkg name>, wip_urn: <urn>}]. The function reads any bindings ALREADY in+    the .lbr, merges the new ones on top (new wins on conflict), strips all prior <packages3d> ++    <package3dinstances>, then re-emits the full merged set: a library-level <package3d> per part+    (between </packages> and <symbols>) and a <package3dinstances> inside every <device> that uses+    that package (right after </connects>). So calling it again with just the parts that failed last+    time ACCUMULATES instead of wiping the parts that already succeeded - the fix for the+    'a re-run dropped the earlier bindings' trap. Returns the new .lbr text."""     import re as _re-    blocks = []+    # 1. read existing bindings already in the file; new bindings override+    merged = {}+    em = _re.search(r"<packages3d>(.*?)</packages3d>", lbr_text, _re.S)+    if em:+        for pm in _re.finditer(r'<package3d name="([^"]+)"[^>]*wip_urn="([^"]+)"', em.group(1)):+            merged[pm.group(1)] = pm.group(2)     for b in bindings:-        pkg = b["package"]; urn = b["wip_urn"]+        merged[b["package"]] = b["wip_urn"]+    # 2. strip ALL prior package3d markup so re-injection is clean (idempotent)+    lbr_text = _re.sub(r"\s*<packages3d>.*?</packages3d>", "", lbr_text, flags=_re.S)+    lbr_text = _re.sub(r"\s*<package3dinstances>.*?</package3dinstances>", "", lbr_text, flags=_re.S)+    # 3. library-level <packages3d> block (all merged parts)+    blocks = []+    for pkg, urn in merged.items():         blocks.append(             f'<package3d name="{pkg}" urn="" wip_urn="{urn}" locally_modified="yes" type="model">'             f'<description>{pkg}</description>'@@ -1268,18 +1283,14 @@ def _inject_package3d_bindings(lbr_text: str, bindings: list) -> str:             f'</package3d>'         )     pkgs3d = "<packages3d>\n" + "\n".join(blocks) + "\n</packages3d>\n"-    if "<packages3d>" in lbr_text:-        lbr_text = _re.sub(r"<packages3d>.*?</packages3d>\s*", pkgs3d, lbr_text, flags=_re.S)-    else:-        lbr_text = lbr_text.replace("</packages>", "</packages>\n" + pkgs3d, 1)-    urn_by_pkg = {b["package"]: b["wip_urn"] for b in bindings}+    lbr_text = lbr_text.replace("</packages>", "</packages>\n" + pkgs3d, 1) +    # 4. per-device <package3dinstances>     def _dev_repl(m):         dev = m.group(0)         pm = _re.search(r'package="([^"]+)"', dev)-        if pm and pm.group(1) in urn_by_pkg and "<package3dinstances>" not in dev and "</connects>" in dev:-            urn = urn_by_pkg[pm.group(1)]-            inst = (f'<package3dinstances><package3dinstance package3d_urn="{urn}"/>'+        if pm and pm.group(1) in merged and "</connects>" in dev:+            inst = (f'<package3dinstances><package3dinstance package3d_urn="{merged[pm.group(1)]}"/>'                     f'</package3dinstances>')             dev = dev.replace("</connects>", "</connects>\n" + inst, 1)         return dev@@ -1450,12 +1461,19 @@ def _orchestrate_build_library_3d(args: dict) -> dict:             results.append({"package": pkg, "success": False,                             "error": "package, lbrPath (per-part .lbr) and modelPath (.step) all required"})             continue-        mk = _orchestrate_make_3d_package({+        mk_args = {             "lbrPath": per_lbr, "modelPath": step,             "projectId": project_id, "folderId": folder_id,             "fileName": f"{pkg}_3d",             "captureLabel": (pkg if capture else None),-        })+        }+        mk = _orchestrate_make_3d_package(mk_args)+        # Retry once: the FIRST part of a run often fails transiently while Fusion settles from a+        # prior view/library; a single retry recovers it (the dropped-first-part trap).+        if not mk.get("success"):+            import time as _t+            _t.sleep(2)+            mk = _orchestrate_make_3d_package(mk_args)         entry = {"package": pkg, "success": bool(mk.get("success"))}         if mk.get("success"):             entry["wip_urn"] = mk.get("wip_urn"); entry["dims_mm"] = mk.get("dims_mm")@@ -1467,11 +1485,14 @@ def _orchestrate_build_library_3d(args: dict) -> dict:         if mk.get("after"):             shots.append({"package": pkg, "stage": "after", "path": mk["after"]})         results.append(entry)-    # inject every binding in ONE pass, write the bound library+    # inject every binding in ONE pass, write the bound library. Read the ALREADY-BOUND output if it+    # exists so a re-run ACCUMULATES (merge-aware) onto prior successes instead of starting clean.     bound = None     if bindings:         try:-            with open(combined, "r", encoding="utf-8") as fh:+            import os as _os+            src = out_path if _os.path.exists(out_path) else combined+            with open(src, "r", encoding="utf-8") as fh:                 txt = fh.read()             txt = _inject_package3d_bindings(txt, bindings)             with open(out_path, "w", encoding="utf-8") as fh:@@ -1504,10 +1525,69 @@ def _orchestrate_build_library_3d(args: dict) -> dict:             "front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail "             "download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] "             "on the Windows box (C:/tmp/conduit-screenshots) - pull them with desktop_pull_file (or "-            "re-capture via desktop_screenshot_window) for the demo video. PITFALLS: needs Fusion "-            "running; on long runs Fusion can reset between parts (fusion_start, then re-run just the "-            "failed parts - each is independent); any failed part has its error in parts[]. Full recipe: "-            "the fusion-multipart-libraries skill."+            "re-capture via desktop_screenshot_window) for the demo video. "+            "AFTER (recommended): call fusion_capture_library_views per showcase part to grab the "+            "symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust "+            "the library, and the 3D-only shots miss them. "+            "PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the "+            "adom-desktop relay times out the REQUEST at ~60s - you may get 'Request timed out' even "+            "though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, "+            "then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs in "+            "C:/tmp/conduit-screenshots. If a part is missing, just re-run build_library_3d for the "+            "missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE "+            "and idempotent, so it accumulates onto the existing bindings (it no longer wipes the "+            "parts that already succeeded). Each part also self-retries once on a transient first-part "+            "failure. Full recipe: the fusion-multipart-libraries skill."+        ),+    }+++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 ++    the package table with the footprint<->package Mapped check + pin/pad counts). These are what+    developers want to see - the 3D before/after shots alone don't show them.++    Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). For each+    package it runs the EAGLE/Electron EDIT <pkg>.sym / .pac / .dev, WINDOW FIT to frame, and a+    background hwnd screenshot (never fullscreen). Returns the saved PNG paths on the Windows box+    (C:/tmp/conduit-screenshots - pull them with desktop_pull_file).++    args: {packages: [<deviceset name>, ...] (or a single 'package'),+           views?: subset of ['component','symbol','footprint'] (default all three)}+    """+    import time as _t+    pkgs = args.get("packages") or ([args["package"]] if args.get("package") else [])+    if not pkgs:+        return {"success": False,+                "error": "packages: [<deviceset name>] (or package: <name>) required.",+                "_hint": "Open the .lbr first (fusion_open_lbr). Names are the deviceset names, e.g. ESP32-S3FN8."}+    view_ext = {"component": "dev", "symbol": "sym", "footprint": "pac",+                "dev": "dev", "sym": "sym", "pac": "pac"}+    label_of = {"dev": "component", "sym": "symbol", "pac": "footprint"}+    views = args.get("views") or ["component", "symbol", "footprint"]+    out = []+    for pkg in pkgs:+        for v in views:+            ext = view_ext.get(v, v)+            er = _proxy_to_addin("electron_run", {"command": f"EDIT {pkg}.{ext}"}, timeout=30)+            _proxy_to_addin("electron_run", {"command": "WINDOW FIT"}, timeout=20)+            _t.sleep(0.4)+            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)})+    n_ok = sum(1 for o in out if o.get("ok"))+    return {+        "success": n_ok > 0,+        "captured": out,+        "_hint": (+            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."         ),     } @@ -1581,7 +1661,7 @@ def dispatch_command(command: str, args: dict) -> dict:     # The user should launch Fusion themselves; we just report the status.     # Commands that need Fusion running (add-in commands + orchestrated commands)     _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}-    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d") or command in _OPEN_WITH_SCREENSHOT:+    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views") or command in _OPEN_WITH_SCREENSHOT:         if not fusion_info.get("installed"):             return {                 "success": False,@@ -1689,6 +1769,8 @@ def dispatch_command(command: str, args: dict) -> dict:         return _orchestrate_make_3d_package(args)     if command == "build_library_3d":         return _orchestrate_build_library_3d(args)+    if command == "capture_library_views":+        return _orchestrate_capture_library_views(args)      # Add-in proxy commands     if command in ADDIN_COMMANDS:
skills/fusion-multipart-libraries/SKILL.md+34
@@ -43,6 +43,40 @@ fusion_build_library_3d {   uploads; just re-run the whole `parts[]` (re-making a few f3ds is cheap) - the injection rewrites   all bindings in one pass, so a single final call always yields a complete library. +### ⚠️ Relay timeout - a many-part run "times out" but KEEPS GOING (don't panic)++The `adom-desktop` relay times out the *request* at ~60s, but `fusion_build_library_3d` for a dozen+parts takes minutes. So you will often get **`status: timeout / Request timed out` while the build is+still running server-side and finishes fine.** Do NOT treat that as failure. Instead:++1. Wait, then **verify by the artifacts, not the response**: read the `boundLbr` and count+   `<package3d name=` (should equal your part count), and list the before/after PNGs in+   `C:/tmp/conduit-screenshots`.+2. If a part is missing (the FIRST part of a run is the usual casualty, as Fusion settles from a prior+   view), **just re-run `fusion_build_library_3d` for the missing parts with `outLbrPath` pointing at+   the SAME file.** The binding injection is **merge-aware + idempotent** (v1.6.1+): it reads the+   bindings already in the file and accumulates, so re-running a subset NO LONGER wipes the parts that+   already succeeded. (Each part also self-retries once on a transient first-part failure.)++This is the fix for the "a re-run dropped my earlier bindings" trap - earlier the injection replaced+the whole `<packages3d>` block, so a subset re-run left you with only that subset.++### 🔍 Show the library views (what makes EEs trust it) - `fusion_capture_library_views`++The 3D before/after shots prove the chip renders, but they do NOT show the **symbol, footprint, or+device** - the views developers actually want. After building, open the library and call:++```jsonc+fusion_capture_library_views { "packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT", "R_4k7"] }+```++It runs `EDIT <deviceset>.sym / .pac / .dev` + `WINDOW FIT` + a background screenshot per view and+returns the PNG paths (on the box; `desktop_pull_file` them). You get, per part: the **SYMBOL** (full+pinout), the **FOOTPRINT** (pad array + the Copper/Silkscreen/Mask layer stack), and the+**COMPONENT** view (Content Manager: the symbol + the package table with the footprint<->package+**Mapped ✓** check + pin/pad counts). Put these in the demo video alongside the 3D - they are the warm+-and-fuzzy shots. (Manual equivalent: `fusion_open_lbr` then `fusion_electron_run "EDIT X.sym"` etc.)+ Everything below is the MANUAL equivalent (what the verb does internally) - read it to understand or debug, but reach for `fusion_build_library_3d` first.