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.70: fusion_export_optimized_glb - one-call Fusion board -> wiki-grade GLB via service-step2glb molecule mode (anchor+silk+gold+join/weld/prune+Draco). Fixes 16MB/25k-drawcall dumps -> ~465KB/31.
6 files changed
+263−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.69+1.6.70
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.69",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.69/adom-bridge-fusion-v1.6.69.zip",- "sha256": "c2445f85d630be582aec04e859ab2d3b5aba6ff34a3666e2892d58f7967d03b5",- "size": 364866,+ "version": "1.6.70",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.70/adom-bridge-fusion-v1.6.70.zip",+ "sha256": "242b2a59e11c052e50cc2148d631a05d1b51ff16c2c63e05952ccc262a854d80",+ "size": 370757, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.69",+ "version": "1.6.70", "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.",
describe.py+1@@ -132,6 +132,7 @@ _T = [ ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}), ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}), ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),+ ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB in ONE call. Exports STEP (+ optional silkscreen) and runs it through service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco). ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Use for a component page's 3D viewer (component.parts.model_3d).", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium"}, {"glbPath": "str", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 300, None, True, {"outputPath": "C:/out/board.glb"}), ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}), ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}), ("fusion_export_dwg", "Export 2D DWG.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dwg"}),
server.py+164−1@@ -15,6 +15,7 @@ import sys import traceback import urllib.request import urllib.error+import urllib.parse from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler from pathlib import Path @@ -3251,6 +3252,166 @@ result = {{'type': {pkg_type!r}, 'bodies': inv, 'etched': etched, 'stepPath': st } +# ── Optimized-GLB pipeline (service-step2glb "molecule mode") ──────────────────+# A raw STEP->GLB tessellation (fusion_export_step + a plain step2glb convert) leaves+# EVERY solid/pad/via as its own draw call - a real board comes out ~16MB / ~25,000+# primitives that HALTS the viewer's GPU on rotation (John hit this live 2026-07-14).+# service-step2glb's molecule mode is the OCCT-side replacement for Colby's Blender+# molecule-converter: tessellate -> anchor to the MP machine pins -> (optional) bake+# silkscreen -> gold pins -> dedup/flatten/JOIN/weld/prune -> Draco. Same board comes+# out ~465KB / ~31 draw calls, auto-oriented. We call it straight from the bridge so+# a Fusion board becomes a wiki-grade GLB in ONE verb. Reachable from the box (it is a+# public *.adom.cloud host, same as the wiki the bridge already streams from).+STEP2GLB_URL = (os.environ.get("ADOM_STEP2GLB_URL")+ or "https://step2glb-gmdoncpxdwx0.adom.cloud").rstrip("/")+++def _multipart_body(fields: dict, files: list):+ """Build a multipart/form-data body. files = [(field, filename, bytes, content_type)]."""+ boundary = "----AdomBridgeGLB%d%d" % (int(_time.time() * 1000), os.getpid())+ out = []+ for k, v in (fields or {}).items():+ out.append(("--" + boundary).encode())+ out.append(('Content-Disposition: form-data; name="%s"' % k).encode())+ out.append(b"")+ out.append(str(v).encode())+ for field, filename, data, ctype in (files or []):+ out.append(("--" + boundary).encode())+ out.append(('Content-Disposition: form-data; name="%s"; filename="%s"'+ % (field, filename)).encode())+ out.append(("Content-Type: %s" % (ctype or "application/octet-stream")).encode())+ out.append(b"")+ out.append(data)+ out.append(("--" + boundary + "--").encode())+ out.append(b"")+ return b"\r\n".join(out), boundary+++def _service_glb_optimize(step_path: str, silk_top: str = None, silk_bottom: str = None,+ pin: str = "medium", job_name: str = "fusion-board",+ poll_max_s: int = 240) -> dict:+ """POST a STEP (+ optional silk PNGs) to service-step2glb molecule mode, poll to+ completion, return {ok, glbBytes, stats}. Pure urllib (no requests on the box)."""+ try:+ with open(step_path, "rb") as f:+ step_bytes = f.read()+ except Exception as e:+ return {"ok": False, "error": "could not read STEP: %s" % e}+ files = [("step", os.path.basename(step_path), step_bytes, "application/step")]+ for field, p in (("silk_top", silk_top), ("silk_bottom", silk_bottom)):+ if p and os.path.exists(p):+ try:+ with open(p, "rb") as f:+ files.append((field, os.path.basename(p), f.read(), "image/png"))+ except Exception:+ pass+ body, boundary = _multipart_body({}, files)+ headers = {+ "Content-Type": "multipart/form-data; boundary=" + boundary,+ "X-Client": "fusion-bridge/adom",+ "X-Job-Name": job_name,+ }+ url = "%s/convert?molecule=true&pin=%s" % (STEP2GLB_URL, urllib.parse.quote(pin))+ try:+ req = urllib.request.Request(url, data=body, headers=headers, method="POST")+ with urllib.request.urlopen(req, timeout=90) as resp:+ queued = json.loads(resp.read().decode("utf-8", "replace"))+ except Exception as e:+ return {"ok": False, "error": "service POST failed: %s" % e,+ "_hint": "Is %s reachable from this box? Override with ADOM_STEP2GLB_URL." % STEP2GLB_URL}+ job_id = queued.get("job_id")+ if not job_id:+ return {"ok": False, "error": "service did not return a job_id", "raw": queued}+ # Poll until complete (STEP tessellation of a big board can take a few minutes).+ stats, deadline = {}, _time.time() + poll_max_s+ while _time.time() < deadline:+ try:+ with urllib.request.urlopen("%s/jobs/%s" % (STEP2GLB_URL, job_id), timeout=20) as r:+ stats = json.loads(r.read().decode("utf-8", "replace"))+ except Exception:+ _time.sleep(5); continue+ st = stats.get("status")+ if st == "complete":+ break+ if st == "error":+ return {"ok": False, "error": "service job failed: %s" % (stats.get("error") or stats), "jobId": job_id}+ _time.sleep(6)+ else:+ return {"ok": False, "error": "service job did not finish within %ss" % poll_max_s, "jobId": job_id}+ try:+ with urllib.request.urlopen("%s/jobs/%s/result" % (STEP2GLB_URL, job_id), timeout=60) as r:+ glb = r.read()+ except Exception as e:+ return {"ok": False, "error": "could not download result GLB: %s" % e, "jobId": job_id}+ return {"ok": True, "glbBytes": glb, "stats": stats, "jobId": job_id}+++def _orchestrate_export_optimized_glb(args: dict) -> dict:+ """Fusion board -> wiki-grade optimized GLB in one call. Exports the active design's+ STEP, (optionally) the top/bottom silkscreen, runs it through service-step2glb's+ molecule pipeline (anchor + optional silk bake + gold pins + join/weld/prune + Draco),+ and writes the small, fast, auto-anchored GLB to outputPath. See STEP2GLB_URL above."""+ output_path = args.get("outputPath") or args.get("output_path")+ if not output_path:+ return {"success": False, "error": "No outputPath specified.",+ "_hint": 'Usage: fusion_export_optimized_glb {"outputPath":"C:/tmp/board.glb", "silkscreen":true, "pin":"medium"}'}+ if not output_path.lower().endswith(".glb"):+ output_path = output_path + ".glb"+ pin = args.get("pin", "medium")+ want_silk = args.get("silkscreen", True)+ # 1) Need a 3D Design product for STEP. Switch to the 3D board (no-op if already 3D).+ _proxy_to_addin("show_3d_board", {}, timeout=60)+ # 2) Export STEP next to the target GLB.+ step_path = output_path[:-4] + ".step"+ step_res = _proxy_to_addin("export_step", {"outputPath": step_path}, timeout=300)+ if not step_res.get("success"):+ return {"success": False, "error": "STEP export failed: %s" % (step_res.get("error") or step_res.get("message")),+ "step": step_res, "_hint": "The active design must be a 3D Design product (fusion_show_3d_board first)."}+ # 3) Optional silkscreen (best-effort; the GLB is great without it).+ silk_top = silk_bottom = None+ silk_note = None+ if want_silk:+ st = output_path[:-4] + "_silk_top.png"+ sb = output_path[:-4] + "_silk_bottom.png"+ rt = _proxy_to_addin("take_silkscreen_screenshot", {"outputPath": st, "layer": "top"}, timeout=90)+ rb = _proxy_to_addin("take_silkscreen_screenshot", {"outputPath": sb, "layer": "bottom"}, timeout=90)+ if rt.get("success"):+ silk_top = st+ if rb.get("success"):+ silk_bottom = sb+ if not (silk_top or silk_bottom):+ silk_note = "silkscreen capture unavailable on this design; GLB built without a baked silk texture."+ # 4) Run the molecule optimizer and write the GLB.+ job_name = os.path.splitext(os.path.basename(output_path))[0]+ opt = _service_glb_optimize(step_path, silk_top, silk_bottom, pin=pin, job_name=job_name)+ if not opt.get("ok"):+ return {"success": False, "error": opt.get("error"), "_hint": opt.get("_hint"),+ "stepPath": step_path, "note": "STEP exported OK; optimization failed - you can still convert the STEP another way."}+ try:+ with open(output_path, "wb") as f:+ f.write(opt["glbBytes"])+ except Exception as e:+ return {"success": False, "error": "could not write GLB to %s: %s" % (output_path, e)}+ stats = opt.get("stats") or {}+ return {+ "success": True,+ "glbPath": output_path,+ "stepPath": step_path,+ "silkTop": silk_top, "silkBottom": silk_bottom,+ "meshesBefore": stats.get("meshes_before"), "meshesAfter": stats.get("meshes_after"),+ "sizeBytes": len(opt["glbBytes"]),+ "sizeBeforeCompress": stats.get("size_before_compress"),+ "moleculeAnchored": stats.get("molecule_anchored"),+ "silkscreenApplied": stats.get("silkscreen_applied"),+ "note": silk_note,+ "message": "Optimized GLB written (%d KB, meshes %s->%s, anchored=%s, silk=%s) to %s" % (+ len(opt["glbBytes"]) // 1024, stats.get("meshes_before"), stats.get("meshes_after"),+ stats.get("molecule_anchored"), stats.get("silkscreen_applied"), output_path),+ "_hint": "Pull it with pull_file, then put it on a wiki component page as component.parts.model_3d. "+ "This is the same optimizer that produces the gorgeous molecule GLBs (Colby's pipeline).",+ }++ def dispatch_command(command: str, args: dict) -> dict: """Dispatch a command to the appropriate handler.""" # Direct handlers (don't need the add-in)@@ -3263,7 +3424,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", "generate_package") 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", "export_optimized_glb") or command in _OPEN_WITH_SCREENSHOT: if not fusion_info.get("installed"): return { "success": False,@@ -3377,6 +3538,8 @@ def dispatch_command(command: str, args: dict) -> dict: return _orchestrate_cleanup_cloud_files(args) if command == "generate_package": return _apply_failure_dialogs(_orchestrate_generate_package(args))+ if command == "export_optimized_glb":+ return _orchestrate_export_optimized_glb(args) # Add-in proxy commands if command in ADDIN_COMMANDS:
skills/fusion-molecule-export/SKILL.mdadded+92@@ -0,0 +1,92 @@+---+name: fusion-molecule-export+description: >+ Turn a Fusion 360 electronics board into a real entry in the Adom molecule system (adom-molecule /+ hydrogen) end to end. Covers the FIVE required Fusion exports the bridge already ships+ (.usdz, .f3d, EAGLE .brd, EAGLE .sch, silkscreen top .png), the exact multipart field names the+ hydrogen create API expects, and the session-cookie auth workaround for the adom-molecule CLI's auth+ gap. Trigger words: fusion board to molecule, create molecule from fusion, adom-molecule create+ fusion, upload fusion board, molecule from the board adonis made, save a molecule component page,+ export brd sch f3d from fusion, fusion molecule pipeline.+---++# Fusion 360 board -> Adom molecule (the whole pipeline)++You have an electronics design open in Fusion (a PROJECT, opened per `fusion-electronics`) and you+want it in the **adom-molecule** system so it gets a molecule datasheet page with native symbol /+footprint / 3D viewers. This skill is the durable record of doing it live (John, 2026-07-14); every+gotcha below cost real time to find.++## The bridge ALREADY exports every file the molecule system needs++Do NOT conclude "the bridge can't export .brd/.sch/.f3d" and fall back to a hand-built wiki page.+It can. The verbs exist today:++| File | Bridge verb | Which editor must be active |+|---|---|---|+| `.usdz` (3D, PBR colors) | `fusion_export_usdz {outputPath}` | 3D PCB (`fusion_show_3d_board`) |+| `.f3d` (Fusion archive) | `fusion_export_f3d {outputPath}` | 3D / a Design product (`fusion_show_3d_board` first) |+| EAGLE `.brd` (board) | `fusion_export_eagle_source {outputPath:...brd}` | Board editor (`fusion_show_2d_board`) |+| EAGLE `.sch` (schematic) | `fusion_export_eagle_source {outputPath:...sch}` | Schematic editor (`fusion_show_schematic`) |+| silkscreen top `.png` | any top-down board render, or `fusion_export_gerbers` | 3D or board |++Notes learned the hard way:+- `fusion_export_eagle_source` picks board vs schematic from the **active editor**, so switch first+ (`fusion_show_2d_board` -> export `.brd`; `fusion_show_schematic` -> export `.sch`). Tested: 840 KB+ `.brd` + 623 KB `.sch` off a ~110-part board.+- Do NOT try raw EAGLE `WRITE 'path'` via `fusion_electron_run`: the bridge BLOCKS it because on cloud+ documents `WRITE` opens a blocking "Version Description" save dialog and hangs. `fusion_export_eagle_source`+ is the supported path (it drives the export without the dialog).+- `.f3d` / `.usdz` need a **Design product** active; from the 2D board or schematic editor they error+ with "3D exports require a Design product" -> call `fusion_show_3d_board` first.+- `fusion_export_obj` is NOT supported for the electronics 3D PCB (only STL / USDZ). Use USDZ for color.++After exporting on the box, `pull_file {filePaths:[...]}` them into the container so you can upload.++## The molecule create API (what actually works)++`adom-molecule create` is the intended CLI, but **its auth is broken in the shipped build**: it sends+no credential, so every `create` returns `401 Unauthorized - provide a session_token cookie or X-Api-Key+header`. Until that's fixed (see the hand-off prompt in `fusion-molecule-export/adom-lbr-style-prompt`+below), POST to hydrogen directly with your wiki session cookie:++```bash+TOKEN=$(python3 -c "import json;print(json.load(open('$HOME/.adom/auth.json'))['token'])") # valid hydrogen session_token+curl -s -H "Cookie: session_token=$TOKEN" \+ -F "name=BQ25792_Charger" -F "username=adonis" -F "sourceType=fusion" \+ -F "[email protected]" \+ -F "[email protected]" \+ -F "[email protected]" \+ -F "[email protected]" \+ -F "silkscreenTopFile=@bq25792_top.png" \+ https://hydrogen.adom.inc/api/molecules/create+# -> {"success":true,"message":"Molecule 'BQ25792_Charger' version 'v1' created with 5 provided file(s)."}+```++Field-name gotchas (these are the exact multipart names the server parses; guessing wastes turns):+- The owner field is **`username`**, NOT `owner`.+- The source field is **`sourceType`**, NOT `source`. Value `fusion` (lowercase).+- File fields are **`usdzFile`, `f3dFile`, `brdFile`, `schFile`, `silkscreenTopFile`**+ (KiCad equivalents: `glbFile`, `kicadPcbFile`, `kicadSchFile`, `kicadProFile`; EasyEDA: `objFile`;+ optional `silkscreenBottomFile`).+- **Fusion source requires ALL FIVE** `.usdz`+`.f3d`+`.brd`+`.sch`+silk-top. The CLI's help table claims+ only usdz + silk-top are required; the SERVER rejects anything short of all five with+ `"Fusion requires .usdz, .f3d, .brd, .sch, and silkscreen top .png files"`. That message is static+ (it always lists all five), so it does NOT tell you which one is missing - supply the whole set.++The page then lives at `https://hydrogen.adom.inc/{username}/molecules/{Name}` (login-gated) and the+USDZ->GLB optimization is queued automatically (ready in a few minutes).++## A parallel, PUBLIC option: an adom-wiki `component` page++If you also want a browsable PUBLIC page with a live 3D viewer (no molecule login), scaffold an+adom-wiki component page and push a **colored GLB** made from the STEP via `adom-step2glb` (OCCT XCAF+preserves the Fusion material colors). `adom-wiki pkg init <slug> --type component`, set+`component.parts.model_3d` to the `.glb` in `page.json`, `pkg publish --org adom --public`, then+`repo push` the page.json + GLB + images (publish only ships the package layer, NOT the page repo).+The native `/viewer/3d/component/<slug>` iframe renders the GLB in Babylon. Verified live.++## Related+- `fusion-electronics` / `fusion-eagle-commands` - open the PROJECT and drive the editors first.+- `adom-molecule` (user skill) - the CLI reference (note its auth gap above).+- `fusion-bridge-dev` - if you add a one-call `fusion_export_molecule_bundle` verb, it belongs here.