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 157 file(s)
13 files changed
+111−71
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.361+1.9.362
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.361",+ "version": "1.9.362", "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.",
contracts/describe.json+1−1@@ -665,7 +665,7 @@ "steps": "[str]" }, "statusVerb": "fusion_demo",- "summary": "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: pass {mode:'tour'} for the ASYNC guided ELECTRONICS tour (John, 2026-09-04: Adom is an electronics prototyping company, so the tour shows electronics, never a mechanical part): eight beats over the bundled sample part - the library, the symbol, the footprint, the 3D chip minted from its STEP and bound into the user's hub, a generated test project in schematic, 2D board and 3D board, and a cloud search; multipart libraries are mentioned in the narration, never shown (a bind is minutes per part). ab's glass remote (desktop_demo_panel id fusion-tour) drives it: play, pause, next, back, mute, unmute, X = stop, with an instant loopback callback plus a poll backstop, one FIFO for the panel and fusion_demo_control, an ack caption per press; captions narrate every beat and pre-rendered narration audio plays locally (mute is session-only, unmute replays the beat). Every beat is measured before it moves (front, title, painted, stable) and then moves (2D zoom through the add-in, 3D orbit and zoom through Fusion's camera, verified by capture). Returns {demoId, estimatedSec, steps} at once; fusion_demo_progress {demoId} carries step, beatIndex/beatCount, percent, paused, muted, stopping, panel, presses[], captions[], per-step verified/placed/foregrounded/motion, shots[] (one evidence frame per beat), windowsRestored and tourDocsClosed. Fusion is placed on 80% of the primary work area for the show and restored at the end. Pass legacy:true for the previous step-list tour.",+ "summary": "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: pass {mode:'tour'} for the ASYNC guided ELECTRONICS tour (John, 2026-09-04: Adom is an electronics prototyping company, so the tour shows electronics, never a mechanical part): eight beats over the bundled sample part - the library, the symbol, the footprint, the 3D chip minted from its STEP and bound into the user's hub, then a REAL board from the user's hub (BQ25792, the Adom charger; args.board or args.boardUrn override) in schematic, 2D board and 3D board, falling back to a generated solo board only when the hub has none, and a cloud search; multipart libraries are mentioned in the narration, never shown (a bind is minutes per part). Captions are 32px on one line so they never cover the remote. ab's glass remote (desktop_demo_panel id fusion-tour) drives it: play, pause, next, back, mute, unmute, X = stop, with an instant loopback callback plus a poll backstop, one FIFO for the panel and fusion_demo_control, an ack caption per press; captions narrate every beat and pre-rendered narration audio plays locally (mute is session-only, unmute replays the beat). Every beat is measured before it moves (front, title, painted, stable) and then moves (2D zoom through the add-in, 3D orbit and zoom through Fusion's camera, verified by capture). Returns {demoId, estimatedSec, steps} at once; fusion_demo_progress {demoId} carries step, beatIndex/beatCount, percent, paused, muted, stopping, panel, presses[], captions[], per-step verified/placed/foregrounded/motion, shots[] (one evidence frame per beat), windowsRestored and tourDocsClosed. Fusion is placed on 80% of the primary work area for the show and restored at the end. Pass legacy:true for the previous step-list tour.", "timeoutSeconds": 240 }, {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.361"+version = "1.9.362" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.361"+version = "1.9.362" edition = "2021" [dependencies]
describe.py+1−1@@ -73,7 +73,7 @@ _T = [ {"code": "required str (the 6 digits; non-digits are stripped)", "hwnd": "optional int (the 2FA window; auto-detected)"}, {"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: pass {mode:'tour'} for the ASYNC guided ELECTRONICS tour (John, 2026-09-04: Adom is an electronics prototyping company, so the tour shows electronics, never a mechanical part): eight beats over the bundled sample part - the library, the symbol, the footprint, the 3D chip minted from its STEP and bound into the user's hub, a generated test project in schematic, 2D board and 3D board, and a cloud search; multipart libraries are mentioned in the narration, never shown (a bind is minutes per part). ab's glass remote (desktop_demo_panel id fusion-tour) drives it: play, pause, next, back, mute, unmute, X = stop, with an instant loopback callback plus a poll backstop, one FIFO for the panel and fusion_demo_control, an ack caption per press; captions narrate every beat and pre-rendered narration audio plays locally (mute is session-only, unmute replays the beat). Every beat is measured before it moves (front, title, painted, stable) and then moves (2D zoom through the add-in, 3D orbit and zoom through Fusion's camera, verified by capture). Returns {demoId, estimatedSec, steps} at once; fusion_demo_progress {demoId} carries step, beatIndex/beatCount, percent, paused, muted, stopping, panel, presses[], captions[], per-step verified/placed/foregrounded/motion, shots[] (one evidence frame per beat), windowsRestored and tourDocsClosed. Fusion is placed on 80% of the primary work area for the show and restored at the end. Pass legacy:true for the previous step-list tour.",+ ("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: pass {mode:'tour'} for the ASYNC guided ELECTRONICS tour (John, 2026-09-04: Adom is an electronics prototyping company, so the tour shows electronics, never a mechanical part): eight beats over the bundled sample part - the library, the symbol, the footprint, the 3D chip minted from its STEP and bound into the user's hub, then a REAL board from the user's hub (BQ25792, the Adom charger; args.board or args.boardUrn override) in schematic, 2D board and 3D board, falling back to a generated solo board only when the hub has none, and a cloud search; multipart libraries are mentioned in the narration, never shown (a bind is minutes per part). Captions are 32px on one line so they never cover the remote. ab's glass remote (desktop_demo_panel id fusion-tour) drives it: play, pause, next, back, mute, unmute, X = stop, with an instant loopback callback plus a poll backstop, one FIFO for the panel and fusion_demo_control, an ack caption per press; captions narrate every beat and pre-rendered narration audio plays locally (mute is session-only, unmute replays the beat). Every beat is measured before it moves (front, title, painted, stable) and then moves (2D zoom through the add-in, 3D orbit and zoom through Fusion's camera, verified by capture). Returns {demoId, estimatedSec, steps} at once; fusion_demo_progress {demoId} carries step, beatIndex/beatCount, percent, paused, muted, stopping, panel, presses[], captions[], per-step verified/placed/foregrounded/motion, shots[] (one evidence frame per beat), windowsRestored and tourDocsClosed. Fusion is placed on 80% of the primary work area for the show and restored at the end. Pass legacy:true for the previous step-list tour.", {"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.361"+DASH_VERSION="1.9.362" 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.361",+ "version": "1.9.362", "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.361",+ "version": "1.9.362", "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]"- }-}+ ]+}
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"] # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.9.361"+version = "1.9.362" edition = "2021" license = "MIT"
rust/PORTING.md+7@@ -521,3 +521,10 @@ port learned: - **A distributed-design f3d does not import** ("cannot be created from a local file containing external references"); that killed the first mechanical draft before John redirected the content anyway.++- **The board beats show a real hub design.** John, later the same evening: the generated one-chip+ board looked lame, so beats 5 to 7 open the BQ25792 charger from the hub (exact-name search,+ Main project preferred, 56 s to open on ConfRoomROG) and fall back to the generated board only+ when the hub has none. A KiCad schematic cannot be imported into Fusion Electronics (EAGLE only),+ so the KiCad tour's board was not an option. Captions dropped to 32px on one line: ab's medium+ is 48px since v2.0.84 and covered the remote.
rust/bridge/src/tour.rs+76−32@@ -15,6 +15,13 @@ use std::time::{Duration, Instant}; pub const PANEL_ID: &str = "fusion-tour"; const CAPTION_ID: &str = "fusion-tour"; const ACK_ID: &str = "fusion-tour-ack";+/// Caption glyph size: 32px, the size ab called medium before v2.0.84 grew it to 48px.+const CAPTION_PX: u32 = 32;+/// The board the schematic, 2D and 3D beats show: a real design from the user's hub (John,+/// 2026-09-04: the generated one-chip board looked lame; show Adonis's BQ25792 charger). The+/// generated solo board stays as the fallback for a hub without it. Override with args.board.+pub const DEMO_BOARD: &str = "BQ25792";+const DEMO_BOARD_PROJECT: &str = "Main"; const PANEL_REASON: &str = "Transport for the Fusion tour the user started: play, pause, next, back, mute, stop"; /// Beats: (key, panel stop label, estimated seconds). John, 2026-09-04: Adom is an electronics@@ -26,9 +33,9 @@ const BEATS: &[(&str, &str, u64)] = &[ ("symbol", "Symbol: the full pinout", 14), ("footprint", "Footprint: pads and layers", 14), ("chip", "3D chip: minted and bound", 170),- ("schematic", "Schematic: the part wired in", 70),- ("board2d", "2D board: the part on copper", 24),- ("board3d", "3D board: the chip on it", 34),+ ("schematic", "A real board: the schematic", 90),+ ("board2d", "The board in 2D", 24),+ ("board3d", "The board in 3D", 40), ("aps", "Cloud search over your hub", 8), ]; @@ -64,13 +71,17 @@ fn top_window() -> Option<Value> { let mut ws = windows(); ws.retain(|w| w["z"]. // ── captions, acks, audio ──────────────────────────────────────────────────── fn caption(text: &str, ms: u64) { if EXIT.load(Ordering::Relaxed) { return; }- let r = ab("desktop_caption", json!({"id": CAPTION_ID, "text": text, "duration": ms, "size": "medium", "position": "bottom", "reason": "Narrate the Fusion tour the user started, one line per beat"}), 6);+ // John, 2026-09-04: ab's "medium" is 48px since v2.0.84 and the captions overlapped the glass+ // remote; 32px (the old medium) and one short line keep clear of it. ab's own warnings+ // (too_wide_for_screen, too_tall_for_screen) go into the record so a wrap never passes silently.+ let r = ab("desktop_caption", json!({"id": CAPTION_ID, "text": text, "duration": ms, "size": "medium", "fontSize": CAPTION_PX, "position": "bottom", "reason": "Narrate the Fusion tour the user started, one line per beat"}), 6); let ok = r["success"].as_bool() == Some(true) && r["error"].as_str().map(|e| e.is_empty()).unwrap_or(true);- rec(|s| push(s, "captions", json!({"text": text.chars().take(80).collect::<String>(), "ok": ok, "error": r["error"].as_str().filter(|e| !e.is_empty()).or(r["_adCallError"].as_str())})));+ let warns: Vec<String> = r["warnings"].as_array().map(|a| a.iter().filter_map(|w| w["check"].as_str().map(str::to_string)).collect()).unwrap_or_default();+ rec(|s| push(s, "captions", json!({"text": text.chars().take(80).collect::<String>(), "ok": ok, "chars": text.chars().count(), "warnings": warns, "error": r["error"].as_str().filter(|e| !e.is_empty()).or(r["_adCallError"].as_str())}))); } fn caption_clear() { let _ = ab("desktop_caption", json!({"action": "hide", "id": CAPTION_ID, "reason": "The user stopped the Fusion tour; clear its caption"}), 6); } fn ack(text: &str) {- let r = ab("desktop_caption", json!({"id": ACK_ID, "text": text, "duration": 3500, "size": "medium", "position": "top", "reason": "Acknowledge a press on the Fusion tour remote the instant it arrives"}), 6);+ let r = ab("desktop_caption", json!({"id": ACK_ID, "text": text, "duration": 3500, "size": "medium", "fontSize": CAPTION_PX, "position": "top", "reason": "Acknowledge a press on the Fusion tour remote the instant it arrives"}), 6); rec(|s| push(s, "captions", json!({"text": text, "ack": true, "ok": r["success"].as_bool() == Some(true), "error": r["error"].as_str().filter(|e| !e.is_empty())}))); } fn audio_dir() -> std::path::PathBuf { std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("resources").join("demo-audio"))).unwrap_or_default() }@@ -295,6 +306,18 @@ fn close_named_docs(pass: &dyn Fn(&str, Value) -> Value, needles: &[String]) -> let script = format!("import adsk.core\napp=adsk.core.Application.get()\nneedles={}\nclosed=0\nfor i in range(app.documents.count-1,-1,-1):\n dd=app.documents.item(i)\n nm=dd.name.lower()\n if any(n in nm for n in needles):\n try:\n dd.close(False); closed+=1\n except Exception: pass\nadsk.doEvents()\nresult={{'closed': closed}}", crate::fusion_prefs::py_str(&json!(needles))); pass("run_modeling_script", json!({"script": script}))["data"]["result"]["closed"].as_u64().unwrap_or(0) as usize }+/// The newest hub item named exactly `name`, preferring `project`; None when APS is not signed in+/// or the hub has no such design.+fn find_hub_design(name: &str, project: &str) -> Option<(String, String)> {+ let r = crate::native_aps_search(&json!({"query": name, "limit": 12}));+ if r["success"].as_bool() != Some(true) { return None; }+ let hits: Vec<Value> = r["results"].as_array().or(r["items"].as_array()).or(r["data"]["results"].as_array()).cloned().unwrap_or_default();+ let exact: Vec<&Value> = hits.iter().filter(|h| h["name"].as_str().map(|n| n.trim().eq_ignore_ascii_case(name)).unwrap_or(false) && h["id"].as_str().map(|u| u.contains("fs.file")).unwrap_or(false)).collect();+ let pick = exact.iter().filter(|h| h["projectName"].as_str().map(|p| p.eq_ignore_ascii_case(project)).unwrap_or(false)).max_by_key(|h| h["lastModified"].as_str().unwrap_or("").to_string())+ .or_else(|| exact.iter().max_by_key(|h| h["lastModified"].as_str().unwrap_or("").to_string())).cloned();+ pick.map(|h| (h["id"].as_str().unwrap_or("").to_string(), h["projectName"].as_str().unwrap_or("").to_string()))+}+fn found_none_reason(r: &Value) -> bool { r["open"].is_null() } fn active_doc(pass: &dyn Fn(&str, Value) -> Value) -> String { pass("get_app_state", json!({}))["data"]["activeDocument"].as_str().unwrap_or("").to_string() } fn wait_design(pass: &dyn Fn(&str, Value) -> Value, secs: u64) -> bool { let deadline = Instant::now() + Duration::from_secs(secs);@@ -336,6 +359,8 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E let mut bound_urn = String::new(); let mut bound = false; let mut project_base = String::new();+ let mut hub_board = false;+ let board_name = args["board"].as_str().unwrap_or(DEMO_BOARD).to_string(); let sample = match sample() { Ok(s) => s, Err(e) => { rec(|s| { s["done"] = json!(true); s["failed"] = json!(true); s["error"] = json!(e); s["stepLabel"] = json!("Tour failed: no sample library"); }); return; } }; rec(|s| s["sample"] = json!({"lbr": sample.lbr, "deviceset": sample.deviceset, "package": sample.package, "pins": sample.pins, "pads": sample.pads, "step": sample.step})); let update_pct = |done: u64, extra: u64| rec(|s| { s["percent"] = json!(((100 * (done + extra)) / total_est.max(1)).min(95)); s["elapsedSec"] = json!(t0.elapsed().as_secs()); s["etaSec"] = json!(total_est.saturating_sub(done + extra)); });@@ -363,26 +388,26 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E // ---- the beat body ---- match key { "library" => {- caption(&format!("Fusion tour 1/{n}: opening the sample library - {} with {} pins and {} pads, one file", sample.deviceset, sample.pins, sample.pads), 30000);+ caption(&format!("Fusion tour 1/{n}: the sample library - {}, {} pins, {} pads", sample.deviceset, sample.pins, sample.pads), 30000); audio_play("library"); let o = crate::orchestrations::open_lbr(ctx, &json!({"filePath": sample.lbr}), pass); r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone(); expected = "AdomDemoSample".into(); } "symbol" => {- caption(&format!("Fusion tour 2/{n}: the symbol - all {} pins, converted pin for pin from the source", sample.pins), 30000);+ caption(&format!("Fusion tour 2/{n}: the symbol - all {} pins, converted pin for pin", sample.pins), 30000); audio_play("symbol"); let o = view("sym"); r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone(); expected = "AdomDemoSample".into(); kind = "2d"; } "footprint" => {- caption(&format!("Fusion tour 3/{n}: the footprint - {} pads on real copper, mask and silk layers", sample.pads), 30000);+ caption(&format!("Fusion tour 3/{n}: the footprint - {} pads on copper, mask and silk", sample.pads), 30000); audio_play("footprint"); let o = view("pac"); r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone(); expected = "AdomDemoSample".into(); kind = "2d"; } "chip" => {- caption(&format!("Fusion tour 4/{n}: the 3D chip - importing the STEP onto the footprint, saving it to your hub, binding it into the library (about two minutes)"), 60000);+ caption(&format!("Fusion tour 4/{n}: minting the 3D chip from its STEP and binding it (about two minutes)"), 60000); audio_play("chip"); let was = active_doc(pass); match (&sample.step, sample.package.is_empty()) {@@ -397,7 +422,7 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E let o = pass("open_by_urn", json!({"urn": bound_urn})); r["open"] = json!({"success": o["success"], "error": o["error"]}); r["ok"] = json!(wait_active(pass, &|d| !d.is_empty() && d != was && !d.to_lowercase().contains("adomdemosample"), 90));- if r["ok"].as_bool() == Some(true) { caption(&format!("The minted {} - a real Fusion design in your hub, bound to the footprint", sample.package), 20000); let _ = script("vp.visualStyle=adsk.core.VisualStyles.ShadedWithVisibleEdgesOnlyVisualStyle\norient(adsk.core.ViewOrientations.IsoTopRightViewOrientation, True)\nout={'iso': True}"); }+ if r["ok"].as_bool() == Some(true) { caption(&format!("The minted {} - a real design in your hub, bound to the footprint", sample.package), 20000); let _ = script("vp.visualStyle=adsk.core.VisualStyles.ShadedWithVisibleEdgesOnlyVisualStyle\norient(adsk.core.ViewOrientations.IsoTopRightViewOrientation, True)\nout={'iso': True}"); } else { r["error"] = json!("the bound f3d did not become the active document"); } } if r["ok"].as_bool() != Some(true) && !EXIT.load(Ordering::Relaxed) {@@ -415,36 +440,55 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E kind = "3d"; } "schematic" => {- caption(&format!("Fusion tour 5/{n}: generating a project around {} - schematic and board authored from EAGLE source, instantiated by Fusion", sample.deviceset), 60000);- audio_play("schematic");- let m = crate::testproject::make_test_project(ctx, &json!({"libraryPath": sample.lbr}), pass);- r["makeProject"] = json!({"success": m["success"], "error": m["error"]});- if m["success"].as_bool() == Some(true) {- project_base = crate::testproject::test_project(&sd).and_then(|t| t["docBase"].as_str().map(str::to_string)).unwrap_or_default();- sleep_s(2.0);- let o = pass("show_schematic", json!({}));- r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone();- if r["ok"].as_bool() == Some(true) { caption(&format!("The schematic: {} wired into a real sheet", sample.deviceset), 20000); }- } else { r["error"] = m["error"].clone(); }+ // a real board from the hub first; the generated solo board only when the hub has none+ let found = match args["boardUrn"].as_str() { Some(u) => Some((u.to_string(), String::new())), None => find_hub_design(&board_name, DEMO_BOARD_PROJECT) };+ if let Some((urn, proj)) = found.filter(|_| !EXIT.load(Ordering::Relaxed)) {+ caption(&format!("Fusion tour 5/{n}: opening the {board_name} board from your hub (about a minute)"), 90000);+ audio_play("schematic");+ let o = pass("open_by_urn", json!({"urn": urn}));+ r["open"] = json!({"urn": urn, "project": proj, "success": o["success"], "error": o["error"]});+ let bn = board_name.to_lowercase();+ let opened = o["success"].as_bool() == Some(true) && wait_active(pass, &|d| d.to_lowercase().starts_with(&bn), 150);+ if opened {+ let o2 = pass("show_schematic", json!({}));+ r["ok"] = json!(o2["success"].as_bool() == Some(true)); r["error"] = o2["error"].clone();+ if r["ok"].as_bool() == Some(true) { hub_board = true; project_base = board_name.clone(); caption(&format!("The {board_name} schematic - every net and part as the designer drew it"), 20000); }+ } else { r["error"] = json!(format!("{board_name} did not become the active document")); }+ }+ if !hub_board && !EXIT.load(Ordering::Relaxed) {+ let why = if found_none_reason(&r) { "your hub has no board by that name, or APS is not signed in" } else { "the hub board did not open" };+ caption(&format!("Fusion tour 5/{n}: {why} - generating a board around {} instead", sample.deviceset), 60000);+ audio_play("schematic-solo");+ let m = crate::testproject::make_test_project(ctx, &json!({"libraryPath": sample.lbr}), pass);+ r["makeProject"] = json!({"success": m["success"], "error": m["error"], "why": why});+ if m["success"].as_bool() == Some(true) {+ project_base = crate::testproject::test_project(&sd).and_then(|t| t["docBase"].as_str().map(str::to_string)).unwrap_or_default();+ sleep_s(2.0);+ let o = pass("show_schematic", json!({}));+ r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone();+ if r["ok"].as_bool() == Some(true) { caption(&format!("The schematic: {} wired into a real sheet", sample.deviceset), 20000); }+ } else { r["error"] = m["error"].clone(); }+ }+ r["hubBoard"] = json!(hub_board); expected = project_base.clone(); kind = "2d"; } "board2d" => {- caption(&format!("Fusion tour 6/{n}: the 2D board - the same footprint, now placed on copper"), 30000);- audio_play("board2d");- let act = crate::testproject::test_project_activate(ctx, "brd", pass);+ caption(&format!("Fusion tour 6/{n}: the {} in 2D - real copper, real routing", if hub_board { board_name.clone() } else { "board".to_string() }), 30000);+ audio_play(if hub_board { "board2d" } else { "board2d-solo" });+ let act = if hub_board { pass("activate_document", json!({"name": board_name}))["success"].as_bool() == Some(true) } else { crate::testproject::test_project_activate(ctx, "brd", pass) }; let o = pass("show_2d_board", json!({})); r["activatedBoard"] = json!(act); r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone(); expected = project_base.clone(); kind = "2d"; } "board3d" => {- caption(&format!("Fusion tour 7/{n}: the 3D board{}", if bound { " - and the chip body on it is the exact cloud model minted a minute ago" } else { "" }), 30000);- audio_play("board3d");- let act = crate::testproject::test_project_activate(ctx, "brd", pass);+ caption(&format!("Fusion tour 7/{n}: the {} in 3D{}", if hub_board { board_name.clone() } else { "board".to_string() }, if !hub_board && bound { ", carrying the chip minted a minute ago" } else { "" }), 30000);+ audio_play(if hub_board { "board3d" } else { "board3d-solo" });+ let act = if hub_board { true } else { crate::testproject::test_project_activate(ctx, "brd", pass) }; let was = active_doc(pass); let o = pass("show_3d_board", json!({})); r["activatedBoard"] = json!(act); r["ok"] = json!(o["success"].as_bool() == Some(true)); r["error"] = o["error"].clone(); // the 3D board keeps the board document's NAME, so wait for a Design product, not a new name- if r["ok"].as_bool() == Some(true) { let _ = was; r["designActive"] = json!(wait_design(pass, 45)); sleep_s(1.5); let _ = script("orient(adsk.core.ViewOrientations.IsoTopRightViewOrientation, True)\nout={'iso': True}"); }+ if r["ok"].as_bool() == Some(true) { let _ = was; r["designActive"] = json!(wait_design(pass, 90)); sleep_s(1.5); let _ = script("orient(adsk.core.ViewOrientations.IsoTopRightViewOrientation, True)\nout={'iso': True}"); } expected = project_base.clone(); kind = "3d"; } "aps" => {@@ -454,10 +498,10 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E let secs = q0.elapsed().as_secs_f64(); if a["success"].as_bool() == Some(true) { let hits = a["results"].as_array().or(a["items"].as_array()).or(a["data"]["results"].as_array()).map(|a| a.len()).unwrap_or(0);- caption(&format!("Fusion tour 8/{n}: cloud search - {hits} hits for {q} across your hub in {secs:.1} s, indexed, never charged"), 20000);+ caption(&format!("Fusion tour 8/{n}: cloud search - {hits} hits for {q} in {secs:.1} s, never charged"), 20000); r["ok"] = json!(true); r["hits"] = json!(hits); r["seconds"] = json!(secs); } else {- caption(&format!("Fusion tour 8/{n}: cloud search skipped - not signed in to APS (fusion_aps_signin lights this up)"), 20000);+ caption(&format!("Fusion tour 8/{n}: cloud search skipped - not signed in to APS"), 20000); r["ok"] = json!(true); r["skipped"] = json!(a["error"].as_str().unwrap_or("not signed in").chars().take(100).collect::<String>()); } audio_play("aps");@@ -509,7 +553,7 @@ fn run_show(ctx: &Ctx, pass: &dyn Fn(&str, Value) -> Value, args: Value, env: &E audio_stop(); panel_hide(); let restored = restore_bounds(&sd, if exited { "stop" } else { "end" }); let ok_ct = results.iter().filter(|r| r["ok"].as_bool() == Some(true)).count();- if exited { caption("Fusion tour stopped", 4000); } else if failed_early { caption("Fusion tour stopped before it began: the sample library did not open", 6000); } else { audio_play("done"); caption(&format!("Fusion tour done: {ok_ct} of {n} beats - symbol, footprint, 3D chip, schematic, 2D and 3D board, cloud search; the same pipeline builds multipart libraries"), 8000); }+ if exited { caption("Fusion tour stopped", 4000); } else if failed_early { caption("Fusion tour stopped: the sample library did not open", 6000); } else { audio_play("done"); caption(&format!("Fusion tour done: {ok_ct} of {n} beats, from symbol to 3D board to cloud search"), 8000); } rec(|s| { s["done"] = json!(true); s["exited"] = json!(exited); s["failed"] = json!(failed_early || (!exited && ok_ct < n)); s["percent"] = json!(100); s["windowsRestored"] = json!(restored); s["tourDocsClosed"] = json!("closing"); s["elapsedSec"] = json!(t0.elapsed().as_secs()); s["step"] = json!("done"); s["stepLabel"] = json!(if exited { "Tour stopped" } else if failed_early { "Tour failed: the sample library did not open" } else { "Tour complete" }); s["result"] = json!({"openedBeats": ok_ct, "totalBeats": n, "exited": exited, "bound3d": bound, "failedSteps": results.iter().filter(|r| r["ok"].as_bool() != Some(true)).map(|r| r["step"].clone()).collect::<Vec<_>>()}); s["narrate"] = json!(format!("Fusion tour {}: {ok_ct} of {n} beats across the electronics library, the 3D chip and a generated board.", if exited { "stopped" } else { "complete" })); }); let s = snap(); crate::demo::persist_pub(&sd, &s);
skills/fusion-demo/SKILL.md+8−3@@ -175,14 +175,19 @@ pins), each with a caption, pre-rendered narration and on-screen motion: | 2 | Symbol: the full pinout | symbol editor, `WINDOW FIT` | 2D zoom in, in, out, fit | | 3 | Footprint: pads and layers | footprint editor | 2D zoom | | 4 | 3D chip: minted and bound | the library chain's bind core on the sample (STEP onto the footprint, saved to the hub, bound); the bound f3d is opened in the 3D canvas | iso, orbits, zoom |-| 5 | Schematic: the part wired in | `make_test_project` then `show_schematic` | 2D zoom |-| 6 | 2D board: the part on copper | board document in front, `show_2d_board` | 2D zoom |-| 7 | 3D board: the chip on it | `show_3d_board`, waits for a Design with bodies | iso, orbits, zoom |+| 5 | A real board: the schematic | the BQ25792 charger found in the hub (`fusion_aps_search`, exact name, Main project preferred), opened by urn, `show_schematic`; `args.board` / `args.boardUrn` override; a hub without it gets the generated solo board around the sample part instead (`hubBoard: false`, the caption says so) | 2D zoom |+| 6 | The board in 2D | the board document in front, `show_2d_board` | 2D zoom |+| 7 | The board in 3D | `show_3d_board`, waits for a Design with bodies | iso, orbits, zoom | | 8 | Cloud search over your hub | `fusion_aps_search` for the part; skipped honestly when not signed in | none | Multipart libraries are MENTIONED in the narration and never shown: a real 3D bind is minutes per part, so a shown multipart beat is a nightmare, not a demo (John). +**Captions are 32px on one line.** ab's "medium" grew to 48px in v2.0.84 and the tour's+captions overlapped the glass remote (John, 2026-09-04); the tour passes `fontSize: 32`, keeps+every line under about 85 characters, and records ab's caption warnings (`too_wide_for_screen`)+in `captions[].warnings`, so a wrap never passes silently.+ **Measured before it moves.** Before any gesture the tour checks the Fusion window is in front (z-order, one extra raise, 1.5 s), its title is the expected document, the canvas is painted (distinct colours in the central 60%) and stable (two captures 0.8 s apart identical).