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 74 file(s)
11 files changed
+93−81
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.243+1.9.244
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.243",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.243/adom-bridge-fusion-v1.9.243.zip",- "sha256": "197960ef701c192fd57b93f366bf4466b47fa539a7586dc243533063139fd5de",- "size": 540244,+ "version": "1.9.244",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.244/adom-bridge-fusion-v1.9.244.zip",+ "sha256": "4e6d8a9e878a875bf1c8d624662a84322f1fb59506d79d97c007eb0b4794afb1",+ "size": 540459, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.243",+ "version": "1.9.244", "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.243"+version = "1.9.244" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.243"+version = "1.9.244" edition = "2021" [dependencies]
describe.py+3−3@@ -276,9 +276,9 @@ _T = [ {"parts": "optional [{package, lbrPath, modelPath}] - the parts you intend to import (drives the estimate)", "stepPath": "optional single .step when not passing parts[]", "libraryPath": "optional .lbr for context"}, {"success": "bool", "fusion": "{installed, running, addinReady, status, message}", "plan": "[{stage, label, estimatedSec, confidence, needsUserConsent?}]", "estimatedSec": "float", "estimatedSecIfAlreadyOpen": "float", "summary": "str"}, 20, None, True, {"parts": [{"package": "AL8843SP-13", "lbrPath": "C:/tmp/al8843.lbr", "modelPath": "C:/tmp/al8843.step"}]}),- ("fusion_live_feed", "A ~1 fps look INTO Fusion while the bridge drives it, published to a shotlog channel as tagged kinds: [main #n] = the Fusion window each tick (the video), [popup #n.m] = every owned dialog/panel over it tied to its tick (Fusion draws these as separate windows a main capture omits). Proof frames from the import chain are the third kind (kind:proof, with step labels) and are NOT produced here - this feed shows what happens BETWEEN proofs, e.g. during boot or a restart, so a human can see open tabs and dialogs instead of guessing. Runs in the background, never foregrounds. The wiki may choose not to render 1 fps; the bridge owns the feed regardless.",- {"action": "start|stop|status", "channel": "optional str (default fusion-live)", "intervalSec": "optional float (default 1.0)", "maxSec": "optional float (default 600)"},- {"success": "bool", "started": "bool", "channel": "str", "frames": "int"}, 15, None, True, {"action": "start", "channel": "fusion-live"}),+ ("fusion_live_feed", "A ~1 fps look INTO Fusion while the bridge drives it, kept as a BOX-SIDE ring buffer (~10 min). Each tick records the Fusion main window (kind:main - the video) and every owned popup over it (kind:popup - dialogs, Browser/tool panels, the Adom banner; Fusion draws these as separate windows a main capture omits), each a path on this machine tied to its tick. Drain from the container with {action:frames, since:<tick>}, read_file each path, and publish where a human looks - tools/live_feed_drain.py does exactly that into shotlog. Proof frames are NOT here: fusion_prove_result evidence[] carries kind:proof with step labels, so the three kinds are distinguishable at the source. Exists so a human can see open tabs and dialogs during boot or a restart instead of guessing.",+ {"action": "start|stop|status|frames", "intervalSec": "optional float (default 1.0)", "maxSec": "optional float (default 600)", "since": "frames only: return ticks newer than this"},+ {"success": "bool", "running": "bool", "tick": "int", "frames": "[{tick, kind:main|popup, path, title, at}]"}, 15, None, True, {"action": "frames", "since": 0}), ("fusion_restart_for_addin", "Finish add-in setup by restarting Fusion, SAVING the user's open documents first. Call this when any verb returns errorCode addin_restart_required (the add-in was installed/updated while Fusion was already open, and Fusion only loads add-ins at launch). ASK THE USER FIRST when a human is present - the failing verb's payload carries userMessage plus openDocuments/unsavedDocuments so you can show them what is open. Refuses with unsaved_named_document rather than discarding a never-saved document.", {"save": "optional bool (default true) - save open documents before restarting"}, {"success": "bool", "addinReady": "bool", "tookSec": "float", "steps": "[{step, ok}]"},
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.243"+DASH_VERSION="1.9.244" 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.243",+ "version": "1.9.244", "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.243",+ "version": "1.9.244", "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+66−58@@ -13529,89 +13529,97 @@ def _human_secs(v: float) -> str: return "%dm %02ds" % (m, sec) if sec else "%dm" % m -_LIVE_FEED = {"thread": None, "stop": False, "frames": 0, "channel": None}+_LIVE_FEED = {"thread": None, "stop": False, "tick": 0, "frames": [], "startedAt": None}+_LIVE_FEED_MAX = 600 # ring buffer: ~10 min at 1 fps def _handle_live_feed(fusion_info: dict, args: dict) -> dict:- """A ~1 fps look INTO Fusion while the bridge drives it, published as three KINDS.-- John, 2026-08-28, after a chain restarted Fusion under him: "had you shown me a- screenshot of fusion's interface i could have seen if there were any open tabs or not- ... i literally should be getting screenshots like every 5 seconds during boot up ...- or maybe each second, so like a 1 fps video." And: the frames need telling apart -- a Browser-panel popup, the main window, and a proof shot are three different things- and a viewer must never have to guess which it is looking at.-- kind=main the Fusion main window, the 1 fps video. One card per tick.- kind=popup every OWNED popup over it (dialogs, Browser/tool panels, the Adom banner),- each tagged with the tick it belongs to. Fusion draws these as separate- windows that a main-frame capture omits, so they ride alongside, never- instead of, the main frame.- kind=proof is NOT produced here. Proof frames are the evidence the import chain- captures (symbol / footprint / 3d / ganged) and they carry their own labels;- the feed exists so the human can see what happened BETWEEN proofs.-- The wiki may choose not to render 1 fps; that is fine. The bridge owns the feed and- publishes it to shotlog under `channel`, so a human always has somewhere to look.- args: {action: start|stop|status, channel?, intervalSec? (default 1.0), maxSec? (600)}+ """A ~1 fps look INTO Fusion while the bridge drives it, as a box-side ring buffer.++ John, 2026-08-28: "i literally should be getting screenshots like every 5 seconds+ during boot up ... or maybe each second, so like a 1 fps video" - after a chain+ restarted Fusion under him and nothing ever showed him what was in it. And: a Browser+ panel popup, the main window, and a proof shot must never be confused.++ DESIGN, and why it is shaped this way. The first version (1.9.233) ran here on the+ Windows bridge and shelled out to `adom-shotlog` while reading `localSafePath` - a+ container tool and a container-side key. It reported running:true, frames:0 forever.+ The bridge can only ever produce BOX paths, so that is what it produces: each tick+ records the main-window capture and every owned popup (dialogs, Browser/tool panels,+ the Adom banner - Fusion draws these as separate windows a main capture omits) into a+ ring buffer, tagged. Something on the container side drains it with+ {action:"frames", since:<tick>}, read_file's the paths, and publishes them wherever a+ human is looking (shotlog, or the wiki if it chooses to render 1 fps). The bridge owns+ the feed; the presenter is pluggable.++ kind=main the Fusion main window, one per tick (the video)+ kind=popup each owned popup over it, tied to its tick+ kind=proof NOT produced here: the import chain's evidence frames carry kind:"proof"+ and a step label, so the three are distinguishable at the source.++ args: {action: start|stop|status|frames, intervalSec? (default 1.0), maxSec? (600),+ since? (tick; frames newer than this)} """- import threading as _th, time as _t, subprocess as _sp+ import threading as _th, time as _t action = str(args.get("action") or "status").lower()+ alive = bool(_LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive()) if action == "status":- return {"success": True, "running": bool(_LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive()),- "frames": _LIVE_FEED["frames"], "channel": _LIVE_FEED["channel"]}+ return {"success": True, "running": alive, "tick": _LIVE_FEED["tick"],+ "buffered": len(_LIVE_FEED["frames"]), "startedAt": _LIVE_FEED["startedAt"]}+ if action == "frames":+ since = int(args.get("since") or 0)+ fr = [f for f in _LIVE_FEED["frames"] if f["tick"] > since]+ return {"success": True, "running": alive, "tick": _LIVE_FEED["tick"],+ "frames": fr[-200:], "count": len(fr),+ "_hint": ("Each frame is a BOX path (this machine); read_file it from the "+ "container and publish it. kind is main|popup; proof frames come "+ "from fusion_prove_result evidence[] with kind:'proof'. Pass "+ "since:<tick> to get only what you have not seen.")} if action == "stop": _LIVE_FEED["stop"] = True- return {"success": True, "stopped": True, "frames": _LIVE_FEED["frames"]}- if _LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive():- return {"success": True, "alreadyRunning": True, "channel": _LIVE_FEED["channel"],- "frames": _LIVE_FEED["frames"]}- channel = str(args.get("channel") or "fusion-live")+ return {"success": True, "stopped": True, "tick": _LIVE_FEED["tick"]}+ if alive:+ return {"success": True, "alreadyRunning": True, "tick": _LIVE_FEED["tick"]} interval = max(0.5, float(args.get("intervalSec") or 1.0)) max_sec = min(3600.0, float(args.get("maxSec") or 600.0))- _LIVE_FEED.update(stop=False, frames=0, channel=channel)-- def _inject(path, desc):- try:- _sp.run(["adom-shotlog", "inject", "-c", channel, "-d", desc, "-s",- "desktop_screenshot_window", "--native", path],- capture_output=True, text=True, timeout=20)- except Exception:- pass+ _LIVE_FEED.update(stop=False, tick=0, frames=[], startedAt=_t.time()) def _run():- t0 = _t.time(); tick = 0+ t0 = _t.time() while not _LIVE_FEED["stop"] and _t.time() - t0 < max_sec:- tick += 1+ _LIVE_FEED["tick"] += 1+ tick = _LIVE_FEED["tick"] try: info = get_fusion_window_info() or {} hwnd = info.get("hwnd") or (info.get("mainWindow") or {}).get("hwnd") if hwnd: shot = _ad_call("desktop_screenshot_window", {"hwnd": int(hwnd)}, timeout=20) or {}- main = shot.get("localSafePath")- pops = shot.get("screenshots") or []+ # BOX paths: fullPath (main) and fullPathHost / localFullPath per popup.+ main = shot.get("fullPath") or shot.get("fullPathHost") or shot.get("path") title = str(shot.get("title") or info.get("title") or "Fusion")[:60]+ now = _t.time() if main:- _inject(main, "[main #%d] %s | %d popup(s)" % (tick, title, len(pops)))- _LIVE_FEED["frames"] += 1- for j, sp_ in enumerate(pops, 1):- if sp_.get("localSafePath"):- _inject(sp_["localSafePath"],- "[popup #%d.%d] %s" % (tick, j, str(sp_.get("title") or "dialog")[:50]))+ _LIVE_FEED["frames"].append({"tick": tick, "kind": "main", "at": now,+ "path": main, "title": title,+ "popups": len(shot.get("screenshots") or [])})+ for k, sp_ in enumerate(shot.get("screenshots") or [], 1):+ pth = sp_.get("fullPathHost") or sp_.get("fullPath") or sp_.get("path")+ if pth:+ _LIVE_FEED["frames"].append({"tick": tick, "kind": "popup", "at": now,+ "index": k, "path": pth,+ "title": str(sp_.get("title") or "dialog")[:50]})+ if len(_LIVE_FEED["frames"]) > _LIVE_FEED_MAX:+ del _LIVE_FEED["frames"][: len(_LIVE_FEED["frames"]) - _LIVE_FEED_MAX] except Exception: pass _t.sleep(interval) _LIVE_FEED["thread"] = None _LIVE_FEED["thread"] = _th.Thread(target=_run, daemon=True); _LIVE_FEED["thread"].start()- return {"success": True, "started": True, "channel": channel, "intervalSec": interval,- "maxSec": max_sec,- "_hint": "⚠️ KNOWN LIMIT (2026-08-28): this runs on the Windows bridge, which has no "- "adom-shotlog and no container paths, so frames do NOT reach shotlog from here "- "yet - drive the capture loop from the container until the ring-buffer redesign "- "lands. Intended contract: frames land in shotlog channel %r tagged [main #n] (the 1 fps video) and "- "[popup #n.m] (dialogs/panels over it, tied to their tick). Proof frames "- "from the import chain are a third kind and carry their own step labels. "- "Open with: adom-shotlog open -c %s --skinny. Stop with action:stop." % (channel, channel)}+ return {"success": True, "started": True, "intervalSec": interval, "maxSec": max_sec,+ "_hint": ("Capturing into a box-side ring buffer. Drain it from the container with "+ "fusion_live_feed {action:'frames', since:<lastTick>}, read_file each "+ "path, and publish (e.g. adom-shotlog inject -c fusion-live -d "+ "'[main #n] ...'). tools/live_feed_drain.py does exactly that.")} def _handle_restart_for_addin(fusion_info: dict, args: dict) -> dict:
skills/fusion-ralph-test-via-wiki/SKILL.md+3−2@@ -29,7 +29,8 @@ different tests. Only the second proves anything to a user. This is the second. ## The loop ```-1. fusion_live_feed {action:start, channel:fusion-live} <- SEE Fusion before touching it+1. python3 tools/live_feed_drain.py --target <Box> --channel fusion-live & <- SEE Fusion first+ (starts fusion_live_feed on the box and drains its ring buffer into shotlog, tagged) 2. pup_open_window {sessionId, url: wiki.adom.inc/<owner>/<part>} (your own window) 3. tap the page's fetch (below) so every verb it sends is recorded 4. toggle the lane to Fusion; press Connect; confirm wc-status says "Connected"@@ -98,7 +99,7 @@ restarted - and the user owns that restart if they said so. ## Reading the three kinds of frame -`fusion_live_feed` publishes to shotlog as `[main #n]` (the window each tick - the video)+`fusion_live_feed` keeps a box-side ring buffer; `tools/live_feed_drain.py` publishes it to shotlog as `[main #n]` (the window each tick - the video) and `[popup #n.m]` (each owned dialog/panel over it, tied to its tick - Fusion draws these as separate windows a main capture omits). The chain's evidence frames are `kind:proof` with a step label. A Browser panel, the main frame, and a proof shot are three different things; a