← Commit history

v1.6.15: fusion_generate_package (EPG + laser etch + STEP, one call)

John Lauer ·82ed230f05 ·3mo ago ·parent 3ad51a4
7 files changed +190−9
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.14\ No newline at end of file+1.6.15\ No newline at end of file
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.15 - 2026-07-03++- **NEW `fusion_generate_package` - parametric IPC packages from Fusion's BUILT-IN+  ElectronicsPackageGenerator, with laser-etched markings, in one headless call.** Give it a `type`+  (~50 EPG families: chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...) + `params` (mm,+  auto-converted to EPG's cm) and it drives EPG's generator modules directly (no GUI), optionally+  **laser-etches** a marking (`etch:"103"` / an MPN) into the body top as REAL cut geometry, and+  exports STEP (`outputStep`) - the etch survives into the STEP for KiCad. Proven live on a fresh VM:+  0603 + '103' etch + valid 233KB STEP (121 faces) in ~2s. EPG is located dynamically across+  webdeploy hashes + install roots (never hardcoded); missing EPG returns `errorCode:epg_not_found`.+  Etch targets the LARGEST body's top face (terminals tie the molded body on max-Z - learned live).+  This is the royalty-free path to whole component libraries: our own generated output, not+  license-restricted vendor models.+ ## 1.6.14 - 2026-07-03  - **#63 ROOT-CAUSED + FIXED: Fusion MOVED its add-in directory; we now install to ALL of them.**
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.14",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.14/adom-bridge-fusion-v1.6.14.zip",-  "sha256": "cba2e918b3e4d90743675d1de369e82f781ddb49518af64ddedbf5ba24e324ad",-  "size": 304852,+  "version": "1.6.15",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.15/adom-bridge-fusion-v1.6.15.zip",+  "sha256": "9b60c2a79f8b2e346d6d3a67125b8ec358af1edfc94a23fb36c34a3c21eb2302",+  "size": 308840,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-03T19:00:00Z"+  "released_at": "2026-07-04T15:30:00Z" }\ No newline at end of file
bridge.json+2−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.14",+  "version": "1.6.15",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",@@ -57,6 +57,7 @@     "fusion_export_step",     "fusion_export_stl",     "fusion_export_usdz",+    "fusion_generate_package",     "fusion_get_app_state",     "fusion_kill",     "fusion_load_design_rules",
describe.py+3
@@ -79,6 +79,9 @@ _T = [     ("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_generate_package", "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",+     {"type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)", "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)", "unitsCm": "optional bool (params already EPG-native cm)", "etch": "optional str (marking text lasered into the body top)", "etchDepthMm": "optional float (default 0.03)", "etchHeightMm": "optional float (default auto-fit to the face)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the etch)"},+     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),     ("fusion_make_3d_package", "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",      {"lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true)"},      {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-fusion-bridge",-  "version": "1.6.15",+  "version": "1.6.16",   "type": "app",   "description": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: 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 with browse/recent/versions, cloud download/upload and folder creation, and in-app parametric modeling. Never-charge: APS calls are capped to the free tier.",   "dependencies": {},
server.py+164−1
@@ -1963,6 +1963,167 @@ def _orchestrate_save_lbr(args: dict) -> dict:     return _post_open_screenshot(result, settle_time=1.0)  +# Package types = EPG's Scripts3d module names (each has runWithInput(params)).+# Kept in sync with Autodesk's ElectronicsPackageGenerator internal add-in.+_EPG_TYPES = [+    "axial_diode", "axial_fuse", "axial_polarized_capacitor", "axial_resistor",+    "bga", "chip", "chiparray2sideconvex", "chiparray2sideflat", "chiparray4sideflat",+    "chip_led", "cornerconcave", "crystal", "dfn2", "dfn3", "dfn4", "dip",+    "dip_socket", "dip_socket_dual_leaf", "dpak", "ecap", "female_standoff", "hc49",+    "header_right_angle", "header_right_angle_socket", "header_straight",+    "header_straight_socket", "male_female_standoff", "melf", "molded",+    "oscillator_j", "oscillator_l", "plcc", "qfn", "qfp", "radial_dipped_rect",+    "radial_ecap", "radial_inductor", "radial_round_led", "snap_lock", "sod",+    "sodfl", "soic", "soj", "son", "sot143", "sot223", "sot23", "sotfl",+    "surface_mount_header_female", "surface_mount_pin_header_right_angle",+    "surface_mount_pin_header_straight",+]++# EPG param keys that are NOT lengths (never mm->cm converted).+_EPG_NON_DIMENSION_KEYS = {"DPins", "EPins", "pins", "thermal", "color_r", "color_g", "color_b"}+++def _orchestrate_generate_package(args: dict) -> dict:+    """Generate an IPC-compliant 3D package via Fusion's built-in+    ElectronicsPackageGenerator (EPG), optionally laser-etch a marking into the+    body top, and optionally export STEP - all in ONE headless add-in call.++    Proven live 2026-07-03 (0603 + '103' etch + 233KB STEP, 1.9s generation).+    """+    pkg_type = (args.get("type") or "").strip().lower()+    if pkg_type not in _EPG_TYPES:+        return {+            "success": False,+            "error": f"Unknown package type: {pkg_type!r}",+            "errorCode": "unknown_package_type",+            "supportedTypes": _EPG_TYPES,+            "_hint": "Pass type as one of supportedTypes (EPG Scripts3d module names). "+                     "Common: chip (0402/0603/0805 passives), soic, qfn, qfp, bga, sot23, "+                     "dfn2, melf, ecap, crystal, header_straight.",+        }++    raw_params = args.get("params") or {}+    units_cm = bool(args.get("unitsCm"))+    params = {}+    for k, v in raw_params.items():+        if (not units_cm) and isinstance(v, (int, float)) and k not in _EPG_NON_DIMENSION_KEYS:+            params[k] = v / 10.0  # mm (datasheet-native) -> cm (Fusion/EPG-native)+        else:+            params[k] = v++    etch = args.get("etch") or ""+    etch_depth_cm = float(args.get("etchDepthMm") or 0.03) / 10.0+    etch_height_mm = args.get("etchHeightMm")  # None = auto-fit+    output_step = args.get("outputStep") or ""++    script = f"""+import sys, glob, os, importlib+import adsk.core, adsk.fusion++# Locate EPG across install conventions + versions (NEVER hardcode the webdeploy hash).+_bases = []+for env in ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'LOCALAPPDATA'):+    root = os.environ.get(env)+    if root:+        _bases += glob.glob(os.path.join(root, 'Autodesk', 'webdeploy', 'production', '*',+                                         'Api', 'InternalAddins', 'ElectronicsPackageGenerator'))+if not _bases:+    raise RuntimeError('EPG_NOT_FOUND: ElectronicsPackageGenerator not present in this Fusion install')+epg_dir = max(_bases, key=os.path.getmtime)+parent = os.path.dirname(epg_dir)+if parent not in sys.path:+    sys.path.insert(0, parent)++mod = importlib.import_module('ElectronicsPackageGenerator.Scripts3d.{pkg_type}')++doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)+design = adsk.fusion.Design.cast(app.activeProduct)+mod.runWithInput({params!r}, design)+root = design.rootComponent++inv = [{{'name': b.name, 'vol': round(b.volume, 6)}} for b in root.bRepBodies]+etched = None+if {etch!r}:+    # Etch the LARGEST body (the molded body): terminals can TIE the body on+    # max-Z, so never pick the top face globally (learned live - first attempt+    # etched a terminal).+    body = max(root.bRepBodies, key=lambda b: b.volume)+    top_face, top_z = None, -1e9+    for f in body.faces:+        if isinstance(f.geometry, adsk.core.Plane) and f.geometry.normal.z > 0.9 and f.centroid.z > top_z:+            top_face, top_z = f, f.centroid.z+    if top_face is None:+        raise RuntimeError('ETCH_NO_TOP_FACE: largest body has no upward planar face')+    sk = root.sketches.add(top_face)+    center = sk.modelToSketchSpace(top_face.centroid)+    # auto-fit text box to the face bounding box (80% wide, 55% tall) unless overridden+    bb = top_face.boundingBox+    face_w = abs(bb.maxPoint.x - bb.minPoint.x)+    face_h = abs(bb.maxPoint.y - bb.minPoint.y)+    span = min(face_w, face_h)+    h = ({etch_height_mm!r} / 10.0) if {etch_height_mm!r} else max(0.02, span * 0.45)+    w = max(face_w, face_h) * 0.8+    c1 = adsk.core.Point3D.create(center.x - w/2, center.y - h*0.75, 0)+    c2 = adsk.core.Point3D.create(center.x + w/2, center.y + h*0.75, 0)+    ti = sk.sketchTexts.createInput2({etch!r}, h)+    ti.setAsMultiLine(c1, c2,+                      adsk.core.HorizontalAlignments.CenterHorizontalAlignment,+                      adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)+    txt = sk.sketchTexts.add(ti)+    ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)+    ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(-{etch_depth_cm!r}))+    ext_in.participantBodies = [body]+    root.features.extrudeFeatures.add(ext_in)+    etched = {etch!r}++step_path = None+if {output_step!r}:+    em = design.exportManager+    opts = em.createSTEPExportOptions({output_step!r})+    em.execute(opts)+    step_path = {output_step!r}++vp = app.activeViewport+cam = vp.camera+cam.isSmoothTransition = False+cam.target = adsk.core.Point3D.create(0, 0, 0)+cam.eye = adsk.core.Point3D.create(0.25, -0.35, 0.45)+cam.upVector = adsk.core.Vector3D.create(0, 0, 1)+cam.isFitView = True+vp.camera = cam+vp.refresh()++result = {{'type': {pkg_type!r}, 'bodies': inv, 'etched': etched, 'stepPath': step_path,+           'epgDir': epg_dir, 'doc': app.activeDocument.name}}+"""++    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=110)+    if not res.get("success"):+        err = (res.get("error") or "")+        if "EPG_NOT_FOUND" in err:+            res["errorCode"] = "epg_not_found"+            res["_hint"] = ("Fusion's built-in ElectronicsPackageGenerator add-in was not found in "+                            "this install (ships with 2024+ Fusion under webdeploy .../Api/"+                            "InternalAddins). Update Fusion, or build the part with "+                            "fusion_make_3d_package from a STEP instead.")+        return res++    data = res.get("data") or {}+    inner = data.get("result") or {}+    return {+        "success": True,+        "output": json.dumps(inner),+        **inner,+        "_hint": ("Generated an IPC-compliant parametric package via Fusion's built-in EPG"+                  + (f", laser-etched '{etch}' into the body top (real cut geometry - survives STEP)" if etch else "")+                  + (f", exported STEP to {output_step}" if output_step else "")+                  + ". NEXT: fusion_screenshot_fusion for a visual check; pull_file the STEP for "+                    "KiCad. PITFALLS: params are mm by default (unitsCm:true to pass EPG-native cm); "+                    "dimension names follow each EPG generator (chip: D/E/A/L/L1; soic: A/A1/b/D/E/E1/e/L/DPins); "+                    "the etch targets the LARGEST body's top face (terminals tie the body on max-Z)."),+    }++ def dispatch_command(command: str, args: dict) -> dict:     """Dispatch a command to the appropriate handler."""     # Direct handlers (don't need the add-in)@@ -1975,7 +2136,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", "capture_library_views", "cleanup_cloud_files") 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", "cleanup_cloud_files", "generate_package") or command in _OPEN_WITH_SCREENSHOT:         if not fusion_info.get("installed"):             return {                 "success": False,@@ -2087,6 +2248,8 @@ def dispatch_command(command: str, args: dict) -> dict:         return _orchestrate_capture_library_views(args)     if command == "cleanup_cloud_files":         return _orchestrate_cleanup_cloud_files(args)+    if command == "generate_package":+        return _orchestrate_generate_package(args)      # Add-in proxy commands     if command in ADDIN_COMMANDS: