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
Update 102 file(s)
9 files changed
+144−24
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.287+1.9.288
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.287",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.287/adom-bridge-fusion-v1.9.287.zip",- "sha256": "5d1a0c8c8a168f5c766fe30d4a20b1cbd3b95fac28f027d5cae7af5bad8a4b06",- "size": 578906,+ "version": "1.9.288",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.288/adom-bridge-fusion-v1.9.288.zip",+ "sha256": "c603e074b5f0ac8276ea4235ec0f7262a3964779b553d87802fad08e79c65cc5",+ "size": 581068, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.287",+ "version": "1.9.288", "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/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.287"+version = "1.9.288" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.287"+version = "1.9.288" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.287"+DASH_VERSION="1.9.288" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.287",+ "version": "1.9.288", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.287",+ "version": "1.9.288", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+123−6@@ -3835,9 +3835,12 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: # body name hid nothing and 16 via rings showed on the 3D PCB). Match the occurrence # AND the body names, and switch off both the occurrence and each body. "names=[]; hidden=[]\n"+ # ALL LAYERS ON BY DEFAULT (John, 2026-08-30): with package VARIANTS, a STEP bound to a+ # package is exactly what that package should show (the -HV model IS the vias), so+ # nothing is hidden unless the caller names a group in layersOff. "def _layer(n):\n" " n=(n or '').split(':')[0]\n"- " return n.startswith('Adom.') and n!='Adom.Chip' and n not in LAYERS_ON\n"+ " return n.startswith('Adom.') and n in LAYERS_OFF\n" "def _walk(comp, under):\n" " for i in range(comp.bRepBodies.count):\n" " b=comp.bRepBodies.item(i); names.append((under or '')+'/'+b.name)\n"@@ -3852,7 +3855,7 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: " try: o.isLightBulbOn=False\n" " except Exception: pass\n" " _walk(o.component, lay)\n"- f"LAYERS_ON=set({sorted(set(str(x) for x in (args.get('layersOn') or [])))!r})\n"+ f"LAYERS_OFF=set({sorted(set(str(x) for x in (args.get('layersOff') or [])))!r})\n" "try:\n" " _walk(oc.component, '')\n" "except Exception as _e:\n"@@ -4029,6 +4032,41 @@ def _inject_package3d_bindings(lbr_text: str, bindings: list) -> str: return _re.sub(r'<device\b[^>]*>.*?</device>', _dev_repl, lbr_text, flags=_re.S) +def _pad1_xy_mm(lbr_path: str):+ """(x, y) in mm of pad/smd named "1" in the FIRST package of the .lbr, or None."""+ try:+ t = open(lbr_path, "r", encoding="utf-8", errors="replace").read()+ m = re.search(r"<package name=\"[^\"]*\">(.*?)</package>", t, re.S)+ body = m.group(1) if m else t+ pm = re.search(r'<(?:smd|pad) name="1" x="([-0-9.]+)" y="([-0-9.]+)"', body)+ if not pm:+ return None+ return (float(pm.group(1)), float(pm.group(2)))+ except Exception:+ return None+++def _step_layer_story(bodies) -> str:+ """What makes this STEP different from a chip-only model, for the palette (John,+ 2026-08-30: "tell them in the palette the nuances of what makes that step unique")."""+ try:+ groups = {}+ for b in bodies or []:+ g = str(b).split("/")[0]+ if g.startswith("Adom.") and g != "Adom.Chip":+ groups[g] = groups.get(g, 0) + 1+ if not groups:+ return "chip body only (the baseline model)"+ words = {"Adom.HeatsinkVias": "heatsink via barrels under the exposed pad",+ "Adom.SolderBalls": "solder-ball hemispheres sitting on the pads",+ "Adom.LaserEtch": "the laser-etched part number",+ "Adom.Board": "board stock", "Adom.Pads": "rendered pads"}+ parts = ["%d %s" % (n, words.get(g, g[5:])) for g, n in sorted(groups.items())]+ return "unlike the chip-only model, this one carries " + " and ".join(parts) + ", all left visible"+ except Exception:+ return ""++ def _orchestrate_make_3d_package(args: dict) -> dict: """Create a RENDERING component 3D-PACKAGE urn (footprint + chip), fully programmatically - NO GUI dialogs. A proper component 3D model contains the FOOTPRINT (pads + courtyard) AND the chip,@@ -4130,6 +4168,56 @@ def _orchestrate_make_3d_package(args: dict) -> dict: " r.setToRotation(math.pi/2, adsk.core.Vector3D.create(*axis), adsk.core.Point3D.create(0,0,0))\n" " mat.transformBy(r); oc.transform2=mat\n" ) if orient else ""+ # PIN 1 (John, 2026-08-30: "the NUMBER ONE PROBLEM ee's expect us to solve"): the LAN7800+ # footprint had pad 1 bottom-left while the chip body's pin-1 dimple sat top-left. Neither+ # file is trusted: pad 1's position is read from the .lbr, the chip's pin-1 mark is found on+ # the imported body (the small circle on the top face nearest a corner), and when the two+ # sit in different corners the chip is rotated about Z by the multiple of 90 degrees that+ # puts its mark over pad 1. The result records what was found and what was done, and the+ # palette says it, so a rotation is never silent.+ _pin1 = _pad1_xy_mm(lbr_path)+ pin1_block = (+ f"PIN1={(_pin1[0] / 10.0, _pin1[1] / 10.0) if _pin1 else None!r}\n"+ "res_pin1={'pad1_mm': " + (f"[{_pin1[0]},{_pin1[1]}]" if _pin1 else "None") + ", 'mark_mm': None, 'rotatedDeg': 0, 'status': 'unchecked'}\n"+ "def _mark(occ):\n"+ " bb2=occ.boundingBox; zmax=bb2.maxPoint.z; best=None\n"+ " def _w(comp, depth):\n"+ " nonlocal best\n"+ " if depth>6: return\n"+ " for i in range(comp.bRepBodies.count):\n"+ " b=comp.bRepBodies.item(i)\n"+ " try: vol=b.volume\n"+ " except Exception: vol=0\n"+ " if vol*1000 < 5: continue\n" # the chip body, not a via or a ball+ " for e in b.edges:\n"+ " try:\n"+ " g=e.geometry\n"+ " if g.objectType not in (adsk.core.Circle3D.classType(), adsk.core.Arc3D.classType()): continue\n"+ " c=g.center\n"+ " if abs(c.z-zmax)>0.03 or g.radius>0.1: continue\n"+ " score=abs(c.x)+abs(c.y)\n"+ " if best is None or score>best[0]: best=(score,c.x,c.y)\n"+ " except Exception: pass\n"+ " for j in range(comp.occurrences.count): _w(comp.occurrences.item(j).component, depth+1)\n"+ " _w(occ.component, 0)\n"+ " return None if best is None else (best[1],best[2])\n"+ "try:\n"+ " mk=_mark(oc)\n"+ " if mk: res_pin1['mark_mm']=[round(mk[0]*10,2), round(mk[1]*10,2)]\n"+ " if mk and PIN1:\n"+ " import math as _m\n"+ " want=_m.atan2(PIN1[1],PIN1[0]); have=_m.atan2(mk[1],mk[0])\n"+ " k=int(round(((want-have)/(_m.pi/2)))) % 4\n"+ " if k:\n"+ " mat=oc.transform2.copy(); r=adsk.core.Matrix3D.create()\n"+ " r.setToRotation(k*_m.pi/2, adsk.core.Vector3D.create(0,0,1), adsk.core.Point3D.create(0,0,0))\n"+ " mat.transformBy(r); oc.transform2=mat\n"+ " res_pin1['rotatedDeg']=k*90\n"+ " res_pin1['status']='rotated' if k else 'aligned'\n"+ " elif PIN1: res_pin1['status']='no_mark_on_chip'\n"+ " else: res_pin1['status']='no_pad1_in_lbr'\n"+ "except Exception as _e: res_pin1['status']='error: '+str(_e)[:80]\n"+ ) # 3. import the chip ONTO the footprint in the generator doc, orient, saveAs as f3d (footprint+chip) script = ( "import adsk.core, adsk.fusion, math\n"@@ -4139,7 +4227,7 @@ def _orchestrate_make_3d_package(args: dict) -> dict: 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"- + orient_block ++ + orient_block + pin1_block + "try:\n app.activeViewport.fit()\nexcept: pass\n" # FOLDER HYGIENE: saveAs into an 'Adom AI Workspace'/<task> subfolder, NEVER the project root. # activeProject is None on a fresh box until the user touches the data panel, so fall@@ -4157,9 +4245,12 @@ def _orchestrate_make_3d_package(args: dict) -> dict: # body name hid nothing and 16 via rings showed on the 3D PCB). Match the occurrence # AND the body names, and switch off both the occurrence and each body. "names=[]; hidden=[]\n"+ # ALL LAYERS ON BY DEFAULT (John, 2026-08-30): with package VARIANTS, a STEP bound to a+ # package is exactly what that package should show (the -HV model IS the vias), so+ # nothing is hidden unless the caller names a group in layersOff. "def _layer(n):\n" " n=(n or '').split(':')[0]\n"- " return n.startswith('Adom.') and n!='Adom.Chip' and n not in LAYERS_ON\n"+ " return n.startswith('Adom.') and n in LAYERS_OFF\n" "def _walk(comp, under):\n" " for i in range(comp.bRepBodies.count):\n" " b=comp.bRepBodies.item(i); names.append((under or '')+'/'+b.name)\n"@@ -4174,7 +4265,7 @@ def _orchestrate_make_3d_package(args: dict) -> dict: " try: o.isLightBulbOn=False\n" " except Exception: pass\n" " _walk(o.component, lay)\n"- f"LAYERS_ON=set({sorted(set(str(x) for x in (args.get('layersOn') or [])))!r})\n"+ f"LAYERS_OFF=set({sorted(set(str(x) for x in (args.get('layersOff') or [])))!r})\n" "try:\n" " _walk(oc.component, '')\n" "except Exception as _e:\n"@@ -4232,7 +4323,17 @@ def _orchestrate_make_3d_package(args: dict) -> dict: " if proj is None: raise RuntimeError('no usable cloud project on this account')\n" " rf=proj.rootFolder\n" f" wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n"- f" doc.saveAs('{name}', wsf, '3d package (footprint+chip)', '')\n"+ "res['pin1']=res_pin1\n"+ # A same-named f3d already in the folder (a re-run of the same package) makes saveAs+ # fail with CB_NAE ("another object with the same name already exists"); seen on the+ # LAN7800 variant run 2026-08-30. Retry once under a time-stamped name.+ " try:\n"+ f" doc.saveAs('{name}', wsf, '3d package (footprint+chip)', '')\n"+ " except Exception as _se:\n"+ " if 'CB_NAE' in str(_se) or 'same name' in str(_se):\n"+ f" import time as _t0; _nm='{name}-'+_t0.strftime('%m%d%H%M%S')\n"+ " doc.saveAs(_nm, wsf, '3d package (footprint+chip)', ''); res['renamedTo']=_nm\n"+ " else: raise\n" # A first-ever cloud save on a fresh install syncs ASYNCHRONOUSLY: right after # saveAs, dataFile.id can still be the local cache path; the lineage urn appears # once the upload lands. Wait for it (bounded well under the 120s script timeout).@@ -4252,6 +4353,8 @@ def _orchestrate_make_3d_package(args: dict) -> dict: res = (r.get("data", {}) or {}).get("result", {}) if isinstance(r, dict) else {} f3d_path = res.get("path") if isinstance(res, dict) else None dims = res.get("dims") if isinstance(res, dict) else None+ pin1 = res.get("pin1") if isinstance(res, dict) else None+ bodies = res.get("bodies") if isinstance(res, dict) else None # AFTER shot: the chip placed flat on its footprint (pads landing on pads) in the 3D viewer. after_shot = _capture_labeled(f"{cap_label}_after") if cap_label else None # doc.saveAs into a DataFolder IS a cloud save: dataFile.id returns the LINEAGE urn@@ -4267,6 +4370,7 @@ def _orchestrate_make_3d_package(args: dict) -> dict: except Exception: pass return {"success": True, "wip_urn": wip, "lineageUrn": f3d_path, "dims": dims,+ "pin1": pin1, "bodies": bodies, "renamedTo": res.get("renamedTo"), "fileName": f3d_name, "savedTo": "cloud (Adom AI Workspace)", "before": before_shot, "after": after_shot, "_hint": "Saved DIRECTLY to the cloud workspace - no upload step needed. "@@ -4715,7 +4819,20 @@ def _build_library_3d_core(args: dict) -> dict: 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")+ entry["pin1"] = mk.get("pin1"); entry["stepStory"] = _step_layer_story(mk.get("bodies")) bindings.append({"package": pkg, "wip_urn": mk["wip_urn"]})+ try:+ _p1 = mk.get("pin1") or {}+ _p1s = {"aligned": "pin 1 verified: the chip's mark sits over pad 1",+ "rotated": "pin 1: rotated the chip %s deg so its mark sits over pad 1" % _p1.get("rotatedDeg"),+ "no_mark_on_chip": "pin 1: no mark found on the chip body, alignment not checked",+ "no_pad1_in_lbr": "pin 1: no pad 1 in the footprint, alignment not checked"}.get(+ _p1.get("status") or "", "")+ _banner("update", title="%s: bound 3D, part %d of %d" % (_lib_label(combined), i, total),+ detail="%s. %s. %s" % (_named(pkg, _descs_sync), entry["stepStory"], _p1s),+ percent=100.0 * i / max(1, total) * 0.4, eta_sec=None)+ except Exception:+ pass else: entry["error"] = mk.get("error") if mk.get("before"):