← Commit history

Add fusion_attach_3d_package orchestrator verb (open -> Create3DPackage -> import+orient -> FINISH, returns the Save-click hint); correct the open_lbr 3D hints to point at it

John Lauer ·2a95341173 ·4mo ago ·parent 42dd2a4
2 files changed +84−3
describe.py+3
@@ -74,6 +74,9 @@ _T = [     ("fusion_open_lbr", "Import an EAGLE .lbr library into Fusion's Electronics LIBRARY editor (Content Manager / Electronics Library workspace), not a PCB design. PITFALL: filePath MUST be a WINDOWS-local path (C:/...). The chip-fetcher/adom-lbr pipeline runs in the cloud container, and there is NO container-to-Windows push verb, so STAGE the .lbr onto Windows first (serve it on a proxied port + Invoke-WebRequest via shell_execute), then pass the C:/ path. PITFALL: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package; the real 3D chip is NOT attached (that needs a Fusion cloud URN). To attach the 3D, see the 'fusion-libraries' skill. Pass verify:true to round-trip-confirm Fusion parsed the deviceset, then check fusion_get_app_state (activeWorkspace 'Electronics Library').",      {"filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)", "symbolName": "optional str", "verify": "optional bool"},      {"success": "bool", "postOpenScreenshot": "{savedTo}"}, 50, None, False, {"filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"}),+    ("fusion_attach_3d_package", "Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",+     {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},+     {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),     ("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+81−3
@@ -1009,8 +1009,8 @@ def _orchestrate_open_lbr(args: dict) -> dict:             "_screenshots[] array (AD v1.8.177+ captures owned dialogs invisible to a plain capture); "             "ownedPopupCount>0 means an error/confirm dialog is up. "             "NOTE: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package. "-            "The real 3D chip is NOT attached; that needs an adsk.eagle:package URN (a wipprod URN does NOT "-            "work in package3d). See the 'fusion-libraries' skill / FUSION_3D_FINDINGS.md."+            "To attach the real 3D chip, use fusion_attach_3d_package (it runs the Package3D generator + "+            "FINISH, which binds the 3D onto the deviceset). See the 'fusion-libraries' skill / FUSION_3D_FINDINGS.md."         ),     }     if symbol_name:@@ -1022,6 +1022,82 @@ def _orchestrate_open_lbr(args: dict) -> dict:     return _post_open_screenshot(response)  +def _orchestrate_attach_3d_package(args: dict) -> dict:+    """Attach a real 3D model to a library package, end to end.++    Opens the .lbr (library active), runs Electron.Create3DPackage to enter the+    Package3DEnvironment showing the footprint, imports the STEP model and+    auto-orients it flat on the footprint, then executes Package3DStop (FINISH).+    Fusion then shows a modal Save dialog (an OWNED popup) — that single click is+    the only desktop-side step; this returns the exact instruction for it.++    args: {filePath: Windows path to the .lbr, modelPath: Windows path to the+           STEP, packageName: optional str}+    """+    import time+    file_path = (args.get("filePath") or "").replace("\\", "/")+    model_path = (args.get("modelPath") or "").replace("\\", "/")+    package = args.get("packageName", "")+    if not file_path or not model_path:+        return {"success": False,+                "error": "filePath (.lbr) and modelPath (.step) are required (Windows paths, e.g. C:/...).",+                "_hint": "Stage both onto Windows first (no container->Windows push verb). See the fusion-libraries skill."}+    steps = []+    open_res = _orchestrate_open_lbr({"filePath": file_path})+    if not open_res.get("success"):+        return {"success": False, "error": "open_lbr failed: " + str(open_res.get("error")), "data": {"steps": steps}}+    steps.append("open_lbr: ok (library active)")+    time.sleep(1)+    cp = _proxy_to_addin("execute_text_command", {"command": f"Electron.Create3DPackage {file_path}"}, timeout=40)+    if not cp.get("success"):+        return {"success": False, "error": "Create3DPackage failed: " + str(cp.get("error")),+                "_hint": "The library document must be ACTIVE, and the .lbr must be adom-lbr-generated "+                         "(a raw vendor EAGLE .lbr fails Fusion's XML parser).", "data": {"steps": steps}}+    steps.append("Create3DPackage: ok (Package3DEnvironment)")+    time.sleep(2)+    import_script = (+        "import adsk.core, adsk.fusion, math\n"+        "res={}\n"+        "d=adsk.fusion.Design.cast(app.activeProduct); root=d.rootComponent\n"+        "im=app.importManager\n"+        f"im.importToTarget(im.createSTEPImportOptions('{model_path}'), root)\n"+        "oc=root.occurrences.item(root.occurrences.count-1); bb=oc.boundingBox\n"+        "dx=bb.maxPoint.x-bb.minPoint.x; dy=bb.maxPoint.y-bb.minPoint.y; dz=bb.maxPoint.z-bb.minPoint.z\n"+        "axis=(1,0,0) if (dy<=dx and dy<=dz) else ((0,1,0) if (dx<=dy and dx<=dz) else None)\n"+        "if axis:\n"+        "    mat=oc.transform2.copy(); rot=adsk.core.Matrix3D.create()\n"+        "    rot.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))\n"+        "    mat.transformBy(rot); oc.transform2=mat\n"+        "    d.snapshots.add() if d.snapshots.hasPendingSnapshot else None\n"+        "res['dims_mm']=[round(dx*10,2),round(dy*10,2),round(dz*10,2)]\n"+        "result=res\n"+    )+    imp = _proxy_to_addin("run_modeling_script", {"script": import_script}, timeout=120)+    if not imp.get("success"):+        return {"success": False, "error": "import/orient failed: " + str(imp.get("error")), "data": {"steps": steps}}+    dims = (imp.get("data", {}) or {}).get("result", {})+    steps.append(f"import+orient: ok ({dims.get('dims_mm') if isinstance(dims, dict) else dims})")+    time.sleep(1)+    _proxy_to_addin("run_modeling_script",+                    {"script": "import adsk.core\ncd=ui.commandDefinitions.itemById('Package3DStop')\nresult={'finished': bool(cd) and cd.execute()}\n"},+                    timeout=30)+    steps.append("FINISH (Package3DStop): executed")+    return {+        "success": True,+        "output": f"3D model placed + oriented on the footprint and FINISH executed for '{package or file_path}'. A Save dialog is now up.",+        "data": {"steps": steps, "package": package},+        "savePending": True,+        "_hint": (+            "FINAL STEP (desktop-side, one click): a Fusion 'Save' dialog is now up to save the 3D package. "+            "Click it: desktop_find_window {titleContains:'Save'} -> desktop_ui_click "+            "{hwnd, automationId:'QTApplication.QTFrameWindow.standardActions.SaveButton'}. "+            "THEN VERIFY with desktop_screenshot_window on the Fusion hwnd: check ownedPopupCount (errors are owned "+            "popups), and the deviceset's Package column should flip Placeholder->part-name + the 3D preview becomes "+            "the real chip (the preview LAGS a beat - re-grab). Full flow: fusion-libraries skill / FUSION_3D_FINDINGS.md sect 11."+        ),+    }++ def _orchestrate_save_lbr(args: dict) -> dict:     """Save the currently open Electronics library as a .flbr file. @@ -1091,7 +1167,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") or command in _OPEN_WITH_SCREENSHOT:+    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package") or command in _OPEN_WITH_SCREENSHOT:         if not fusion_info.get("installed"):             return {                 "success": False,@@ -1193,6 +1269,8 @@ def dispatch_command(command: str, args: dict) -> dict:         return _orchestrate_open_lbr(args)     if command == "save_lbr":         return _orchestrate_save_lbr(args)+    if command == "attach_3d_package":+        return _orchestrate_attach_3d_package(args)      # Add-in proxy commands     if command in ADDIN_COMMANDS: