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 104 file(s)
10 files changed
+112−69
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.342+1.9.343
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.342",+ "version": "1.9.343", "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.342"+version = "1.9.343" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.342"+version = "1.9.343" edition = "2021" [dependencies]
dashboard/src/main.rs+10−34@@ -283,40 +283,16 @@ fn action(ai_thread: &str, target: &str, name: &str, args: &serde_json::Value) - } "signin" => bridge(ai_thread, target, "fusion_signin", &a), "tour" => {- let r = bridge(ai_thread, target, "fusion_demo", "{\"mode\":\"tour\"}");- // FLOATY TOUR REMOTE (John, 2026-08-24: "a play/pause/next/prev/exit window or- // dialog or floaty giving me an easy way to stop the demo"): a compact pup- // window on the user's desktop loading this dashboard's own /?remote=<demoId>- // view - transport buttons + the live caption, clickable while they watch- // Fusion. Best-effort: no pup, no problem, the dashboard page has the same- // controls. Opened FOREGROUND deliberately - the user just pressed Run.- let dug = {- let d = unwrap_data(&r);- if d["demoId"].is_string() { d }- else if let Some(o) = d["output"].as_str() {- serde_json::from_str::<serde_json::Value>(o).unwrap_or(d)- } else { d }- };- if let Some(id) = dug["demoId"].as_str() {- let url = format!("{}?remote={}", url_for(current_port()), id);- let args = serde_json::json!({"url": url, "sessionId": format!("fusion-tour-remote-{}", id),- "width": 320, "height": 420, "left": 40, "top": 120,- "foreground": true, "userRequestedForeground": true,- "foregroundReason": "the user just started the Fusion tour; this is its stop/pause remote"}).to_string();- let ai2 = ai_thread.to_string(); let t2 = target.to_string();- std::thread::spawn(move || {- match Command::new("adom-bridge")- .args(["--ai-thread", &ai2, "--target", &t2, "pup_open_window", &args])- .output() {- Ok(o) if o.status.success() =>- eprintln!("[dash] tour remote floaty opened"),- Ok(o) => eprintln!("[dash] tour remote floaty FAILED: {}",- String::from_utf8_lossy(&o.stderr).chars().take(200).collect::<String>()),- Err(e) => eprintln!("[dash] tour remote floaty spawn error: {}", e),- }- });- }- r+ // THE REMOTE IS AB'S GLASS PANEL NOW (John, 2026-09-01: "ab's new demo control+ // panel that has play/pause/next/prev and is a floaty glass like interface").+ // remote:true makes the tour show desktop_demo_panel itself and drain its+ // presses onto the same transport flags this page's buttons set, so both work+ // mid-tour. The earlier pup floaty (a 320x420 window of this page's ?remote view)+ // is retired: two remotes on screen confused the story, and the glass panel+ // never steals focus. preset:electronics is the eight-stop showcase: library in,+ // symbol, footprint, 3D chip, ganged view, schematic, 2D board, 3D board.+ bridge(ai_thread, target, "fusion_demo",+ "{\"mode\":\"tour\",\"preset\":\"electronics\",\"remote\":true}") } "tour_control" => bridge(ai_thread, target, "fusion_demo_control", &a), "tour_progress" => bridge(ai_thread, target, "fusion_demo_progress", &a),
describe.py+1−1@@ -74,7 +74,7 @@ _T = [ {"submitted": "bool", "steps": "list", "narrate": "str"}, 90, "fusion_readiness", True, {"code": "123456"}), ("fusion_demo", "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill. TOUR MODE (issue #29): pass {mode:'tour'} for the ASYNC six-content-type showcase - it stages the bundled sample library and walks symbol -> footprint -> device -> 3D package (plus schematic/2D/3D board when a query is given), returns {demoId, estimatedSec, steps} immediately, reports structured progress via fusion_demo_progress for a real progress bar, runs in the BACKGROUND by default (foreground:true only when the user wants to watch), and its final progress poll carries the screenshot array.",- {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},+ {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)", "mode": "optional str ('tour' = the async showcase; poll fusion_demo_progress {demoId})", "preset": "optional str ('electronics' = library import, symbol, footprint, 3D chip, ganged component view, schematic, 2D board, 3D board - the eight-stop showcase; omit for the full tour incl. Data Hub, APS, MCP, exports)", "remote": "optional bool (tour mode: show ab's floaty glass PLAY/PAUSE/NEXT/BACK/STOP panel on the desktop; its presses drive the same transport flags as fusion_demo_control, so the dashboard buttons and the remote can be mixed)", "captions": "optional bool (default true: one desktop caption per stop)", "foreground": "optional bool (default false: background envelope, never steals focus)"}, {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"}, 240, "fusion_demo", True, {}),
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.342"+DASH_VERSION="1.9.343" 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.342",+ "version": "1.9.343", "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−27@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.342",+ "version": "1.9.343", "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,28 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "agent_permissions": {- "allow_bash": [],- "allow": [- "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",- "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",- "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",- "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."- ],- "soft_deny": [- "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",- "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",- "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",- "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."- ],- "environment": [- "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",- "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."- ]- },- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+84−1@@ -9602,6 +9602,72 @@ _TOUR_EXPLAIN = { } +# ── The on-screen remote (John, 2026-09-01): ab's floaty glass PLAY/PAUSE/NEXT panel ──+# desktop_demo_panel is a shared transport: its buttons only QUEUE presses for the driver+# (us) to poll, they never run anything, so it adds no permission surface. We map presses+# onto the tour's existing transport flags (paused / skipNext / replayPrev / abort), the+# same ones fusion_demo_control sets, so the dashboard's buttons and the desktop remote+# drive one mechanism and can be used interchangeably mid-tour.+_TOUR_PANEL_ID = "fusion-tour"+++def _tour_panel_show(state, stops, stage, playing=True, reason=""):+ """Create/update the remote (idempotent by id). Best-effort, never raises."""+ try:+ _ad_call("desktop_demo_panel", {+ "action": "show", "id": _TOUR_PANEL_ID, "title": "Fusion bridge tour",+ "stops": stops, "stage": int(stage), "state": "playing" if playing else "paused",+ "position": "bottom-right",+ "reason": reason or "Guided Fusion tour: this remote pauses, skips, or replays the tour steps"},+ timeout=10)+ state["remote"] = {"shown": True, "stage": int(stage)}+ except Exception:+ pass+++def _tour_panel_pump(state):+ """Drain the remote's presses into the tour's transport flags. Returns the list of+ button names seen (for the progress stream). Never raises."""+ seen = []+ try:+ r = _ad_call("desktop_demo_panel", {"action": "poll", "id": _TOUR_PANEL_ID}, timeout=8) or {}+ events = _find_key(r, "events") or []+ for ev in events:+ b = str((ev or {}).get("button") or "").lower()+ seen.append(b)+ if b == "pause":+ state["paused"] = True+ elif b == "play":+ state["paused"] = False+ elif b == "next":+ state["skipNext"] = True; state["paused"] = False+ elif b == "back":+ state["replayPrev"] = True; state["paused"] = False+ elif b == "stop":+ state["abort"] = True; state["paused"] = False+ if seen:+ state.setdefault("remotePresses", []).extend(seen[-8:])+ except Exception:+ pass+ return seen+++def _tour_panel_hide():+ try:+ _ad_call("desktop_demo_panel", {"action": "hide", "id": _TOUR_PANEL_ID}, timeout=8)+ except Exception:+ pass+++# The showcase John asked for (2026-09-01): library in, then symbol, footprint, 3D chip,+# the ganged component view, a schematic with that part, the 2D board, the 3D board.+# Drops the platform stops (datahub / aps / mcp / exports) which belong to the full tour.+_TOUR_PRESETS = {+ "electronics": ["launch", "stage", "library", "symbol", "footprint", "3d-package", "device",+ "make-project", "open-design", "schematic", "2d-board", "3d-board"],+}++ def _tour_caption(text: str, seconds: int = 45): """One desktop caption per tour step (id-replace: each step's caption replaces the last, so the screen always narrates the CURRENT step). Best-effort, never raises."""@@ -9662,6 +9728,11 @@ def _orchestrate_demo_tour(args: dict) -> dict: ("3d-board", "3D board", _est("3d-board", 20)), ("exports", "Exporting: STEP + STL (of a dozen formats)", _est("exports", 25)), ]+ preset = (args.get("preset") or "").lower()+ if preset in _TOUR_PRESETS:+ keep = _TOUR_PRESETS[preset]+ steps = sorted([s for s in steps if s[0] in keep], key=lambda s: keep.index(s[0]))+ remote = bool(args.get("remote")) est = sum(s[2] for s in steps) state = {"demoId": demo_id, "percent": 0, "elapsedSec": 0, "estimatedSec": est, "step": steps[0][0], "stepLabel": steps[0][1], "done": False, "failed": False,@@ -9702,6 +9773,9 @@ def _orchestrate_demo_tour(args: dict) -> dict: begin_background_envelope(float(est + 60)) done_est = 0 _docs_before_tour = _open_doc_names() # tour hygiene: close only what WE open+ _stop_labels = [s[1] for s in steps]+ if remote:+ _tour_panel_show(state, _stop_labels, 0, playing=True) try: _VIEW_STEPS = {"symbol", "footprint", "device", "schematic", "2d-board", "3d-board", "library"} _last_view_step = None@@ -9709,11 +9783,18 @@ def _orchestrate_demo_tour(args: dict) -> dict: while _step_i < len(steps): key, label, dur = steps[_step_i] _step_i += 1- # ── TRANSPORT (fusion_demo_control): boundary checks, never mid-verb ──+ # ── TRANSPORT (fusion_demo_control + the on-screen remote): boundary+ # checks, never mid-verb. The remote's presses are drained here and while+ # paused, onto the same flags the dashboard's buttons set. ──+ if remote:+ _tour_panel_pump(state)+ _tour_panel_show(state, _stop_labels, _step_i - 1, playing=not state.get("paused")) if state.pop("abort", None): raise RuntimeError("tour exited by the user (transport control)") while state.get("paused") and not state.get("abort"): state.update(stepLabel="Paused - press play to continue", stepCaption="Paused.")+ if remote:+ _tour_panel_pump(state) time.sleep(1.5) if state.pop("abort", None): raise RuntimeError("tour exited by the user (transport control)")@@ -10009,6 +10090,8 @@ def _orchestrate_demo_tour(args: dict) -> dict: _ad_call("desktop_caption", {"action": "hide", "id": "fusion-tour"}, timeout=8) except Exception: pass+ if remote:+ _tour_panel_hide() _demo_tour_persist(state) threading.Thread(target=_run, daemon=True, name="fusion-demo-tour").start()