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.15: fusion_generate_package (EPG + laser etch + STEP, one call)
7 files changed
+190−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.14+1.6.15
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" }
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: