← 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.

John Lauer ·534f33db23 ·3mo ago ·parent 7f5931e
6 files changed +263−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.69\ No newline at end of file+1.6.70\ No newline at end of file
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.