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 163 file(s)
18 files changed
+202−45
BRIDGE_VERSION+1−1bridge.json+3−2contracts/describe.json+33−2dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+4−1install.sh+1−1package.json+1−1page.json+11−27rust/Cargo.toml+1−1rust/bridge/src/lifecycle.rs+24−5rust/bridge/src/main.rs+1−1rust/bridge/src/orchestrations.rs+113−1skills/fusion-electronics/SKILL.md+7tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.shBRIDGE_VERSION+1−1@@ -1 +1 @@-1.10.19+1.10.20
bridge.json+3−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.10.19",+ "version": "1.10.20", "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.",@@ -81,7 +81,8 @@ "fusion_route_net", "fusion_routing_drc", "fusion_routing_state",- "fusion_plan_route"+ "fusion_plan_route",+ "fusion_schematic" ], "dependencies": {}, "category": "CAD",
contracts/describe.json+33−2@@ -1,6 +1,6 @@ { "contract": "fusion-bridge verb catalog",- "frozen": "2026-09-19",+ "frozen": "2026-09-20", "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.", "source": "describe.py", "verbs": [@@ -1966,7 +1966,7 @@ "success": "bool" }, "statusVerb": null,- "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+ "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.", "timeoutSeconds": 200 }, {@@ -2470,6 +2470,37 @@ "summary": "Save the ACTIVE document to Autodesk cloud storage with an explicit name and destination. Dialog-free precisely because you supply what the Save box would ask for - use it before closing or restarting when a document has never been saved (fusion_close_all_documents refuses such a document with unsaved_named_document rather than discarding it).", "timeoutSeconds": 180 },+ {+ "example": {+ "action": "open",+ "fileName": "BQ25792 Assembly",+ "projectName": "Main"+ },+ "input": {+ "action": "required str: open | create | show",+ "brdPath": "optional local .brd (open; defaults to the sibling)",+ "capture": "optional bool: inline screenshot proof after the switch",+ "fileName": "cloud design name (open)",+ "filePath": "local .fsch (open)",+ "name": "optional str (create)",+ "projectName": "optional str (open, with fileName)",+ "query": "find-and-open by name (open)",+ "schPath": "local EAGLE .sch on the Fusion machine (open)",+ "urn": "cloud lineage urn (open)",+ "withBoard": "optional bool (open; false = schematic only, dialog-free)"+ },+ "longRunning": true,+ "name": "fusion_schematic",+ "output": {+ "documentName": "str (create)",+ "schPath": "str (create)",+ "success": "bool",+ "via": "str: the verb that did the work"+ },+ "statusVerb": "fusion_get_app_state",+ "summary": "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+ "timeoutSeconds": 240+ }, { "example": {}, "input": {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.10.19"+version = "1.10.20" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.19"+version = "1.10.20" edition = "2021" [dependencies]
describe.py+4−1@@ -43,7 +43,7 @@ _T = [ ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.", {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}), - ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+ ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.", {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"}, {"success": "bool", "imported": "list"}, 200, None, False, {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),@@ -357,6 +357,9 @@ _T = [ {"filePath": "required str (WINDOWS path to the .brd, C:/...; must exist on the Fusion machine)"}, {"success": "bool", "filePath": "str", "fileType": "str", "activeWorkspace": "str", "activeDocument": "str", "nextSteps": "[str]", "postOpenCheck": "{required, steps}", "screenshots": "[{type,savedTo,sizeKB}] (sidecar shell) or postOpenScreenshot (exe shell)"}, 60, None, True, {"filePath": "C:/Temp/TJA1050.brd"}),+ ("fusion_schematic", "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+ {"action": "required str: open | create | show", "fileName": "cloud design name (open)", "projectName": "optional str (open, with fileName)", "urn": "cloud lineage urn (open)", "query": "find-and-open by name (open)", "schPath": "local EAGLE .sch on the Fusion machine (open)", "brdPath": "optional local .brd (open; defaults to the sibling)", "withBoard": "optional bool (open; false = schematic only, dialog-free)", "filePath": "local .fsch (open)", "name": "optional str (create)", "capture": "optional bool: inline screenshot proof after the switch"},+ {"success": "bool", "via": "str: the verb that did the work", "documentName": "str (create)", "schPath": "str (create)"}, 240, _STATUS, True, {"action": "open", "fileName": "BQ25792 Assembly", "projectName": "Main"}), ("fusion_open_schematic", "Open a LOCAL legacy EAGLE .sch in Fusion's Schematic Editor (dialog-free Document.newDesignFromLocal; a background open). NAV RULE: the schematic, 2D board and 3D board are VIEWS of ONE electronics design, not standalone files. For a team design open the DESIGN document (fusion_open_cloud_file / fusion_open_by_urn) and switch views with fusion_show_schematic; this verb is for a .sch that exists on the Fusion machine (push it there first). Drive the opened sheet with fusion_electron_run (WINDOW FIT, DISPLAY ALL). The bridge captures a post-open screenshot because Fusion can raise a blocking dialog the API cannot see.", {"filePath": "required str (WINDOWS path to the .sch, C:/...; must exist on the Fusion machine)"}, {"success": "bool", "filePath": "str", "fileType": "str", "activeWorkspace": "str", "activeDocument": "str", "nextSteps": "[str]", "postOpenCheck": "{required, steps}", "screenshots": "[{type,savedTo,sizeKB}] (sidecar shell) or postOpenScreenshot (exe shell)"},
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.10.19"+DASH_VERSION="1.10.20" 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.10.19",+ "version": "1.10.20", "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.10.19",+ "version": "1.10.20", "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.10.19"+version = "1.10.20" edition = "2021" license = "MIT"
rust/bridge/src/lifecycle.rs+24−5@@ -191,6 +191,24 @@ pub fn dialog_inventory(ctx: &Ctx) -> Vec<Value> { let rows = match ab.call("desktop_enum_windows", args, &ctx.caller, 10) { Ok(v) => v["windows"].as_array().or_else(|| v["data"]["windows"].as_array()).cloned().unwrap_or_default(), Err(_) => return vec![] }; let classified = crate::small_verbs::classify_blocking_dialogs(&ab, &ctx.caller, &ctx.pids); let mut out = Vec::new();+ // native Windows file dialogs (class #32770: Open, Save As) are not Qt dialogs; a+ // dialog-driven import (ImportSCHAndBRDCmd) can leave one up, and it blocks Fusion just the same+ for w in &rows {+ let class = w["className"].as_str().or(w["class"].as_str()).unwrap_or("");+ if !class.contains("#32770") || w["visible"].as_bool() == Some(false) { continue; }+ let h = w["hwnd"].as_i64().unwrap_or(0);+ let (mut text, mut buttons): (Vec<String>, Vec<String>) = (vec![], vec![]);+ if let Ok(r) = ab.call("desktop_find_controls", json!({"hwnd": h}), &ctx.caller, 10) {+ let dd = if r["data"].is_object() { r["data"].clone() } else { r };+ for c in dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default() {+ let name = c["name"].as_str().unwrap_or("").trim().to_string(); if name.is_empty() { continue; }+ let ty = c["controlType"].as_str().or(c["type"].as_str()).or(c["role"].as_str()).unwrap_or("").to_lowercase();+ if ty.contains("button") { buttons.push(name); } else if text.len() < 12 { text.push(name); }+ }+ }+ out.push(json!({"hwnd": h, "title": w["title"], "category": "file_dialog", "resolution": "a native Windows Open/Save dialog (left by a dialog-driven import or a manual action): cancelled safely with WM_CLOSE, nothing is saved or discarded",+ "text": text.join(" ").chars().take(300).collect::<String>(), "buttons": buttons}));+ } for d in fusion_core::dialogs::qt_dialog_rows(&rows) { let h = d["hwnd"].as_i64().unwrap_or(0); let (mut text, mut buttons): (Vec<String>, Vec<String>) = (vec![], vec![]);@@ -222,7 +240,7 @@ pub fn expire_dialogs(ctx: &Ctx, before: &[Value]) -> (Vec<Value>, Vec<Value>) { let r = crate::small_verbs::dismiss_blocking_dialogs(&ctx.state_dir, &json!({}), &ctx.caller, &ctx.pids, &|_| {}); tried.push(json!({"step": "fusion_dismiss_blocking_dialogs (known rules)", "dismissed": r["dismissed"]})); }- for d in before.iter().filter(|d| d["category"].as_str() == Some("unknown")) {+ for d in before.iter().filter(|d| matches!(d["category"].as_str(), Some("unknown") | Some("file_dialog"))) { if let Some(h) = d["hwnd"].as_i64() { let r = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10); tried.push(json!({"step": "WM_CLOSE (Cancel/X semantics)", "hwnd": h, "title": d["title"], "ok": r.is_ok()}));@@ -243,13 +261,14 @@ pub enum DialogGate { Clear, Expired(Vec<Value>, Vec<Value>), Blocked(Value) } pub fn blocked_by_dialog_reply(ctx: &Ctx, cmd: &str) -> DialogGate { let before = dialog_inventory(ctx);- let shot0 = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);- if before.is_empty() && shot0["ownedPopupCount"].as_u64().unwrap_or(0) == 0 { return DialogGate::Clear; }+ // only real dialogs count: Fusion's own tool windows (Data panel, message tray) are owned+ // popups too, and counting them refused every verb for hours on 2026-09-19+ if before.is_empty() { return DialogGate::Clear; } let (tried, after) = expire_dialogs(ctx, &before); let shot = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller); let popups = shot["ownedPopupCount"].as_u64().unwrap_or(0); crate::fusion_prefs::action_ledger(&ctx.state_dir, "blocked_by_dialog", json!({"forCommand": cmd, "before": before.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "tried": tried, "after": after.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "ownedPopups": popups}));- if after.is_empty() && popups == 0 {+ if after.is_empty() { eprintln!("[fusion-bridge] {cmd}: expired {} dialog(s) programmatically before running", before.len()); return DialogGate::Expired(before, tried); }@@ -397,7 +416,7 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val let inv = dialog_inventory(ctx); let (tried, after) = expire_dialogs(ctx, &inv); let popups = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);- if !inv.is_empty() && after.is_empty() && popups["ownedPopupCount"].as_u64().unwrap_or(0) == 0 {+ if !inv.is_empty() && after.is_empty() { crate::fusion_prefs::action_ledger(&ctx.state_dir, "dialog_expired_after_timeout", json!({"forCommand": command, "dialogs": inv.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "tried": tried})); return json!({"success": false, "errorCode": "dialog_expired_retry", "dialogsExpired": inv, "expiryTried": tried, "error": format!("'{command}' timed out behind a dialog ({}); the bridge expired it programmatically (rules, then WM_CLOSE). Re-run the verb.", inv.iter().map(|d| d["title"].as_str().unwrap_or("").to_string()).collect::<Vec<_>>().join(", ")),
rust/bridge/src/main.rs+1−1@@ -408,7 +408,7 @@ fn main() { r["shell"] = json!("rust"); r }, "fusion_start" | "start" | "launch" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "start", &caller); let mut r = launch::start(&ctx, &parsed["args"]); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "start", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r }, "fusion_open_lbr" | "open_lbr" if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "open_lbr", &caller); let mut r = orchestrations::open_lbr(&ctx, &parsed["args"], &|v, a| addin::pass_through(v, &a, &ctx)); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "open_lbr", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },- v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },+ v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r }, "fusion_state" | "state" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let mut r = orchestrations::fusion_state(&ctx, &parsed["args"]); r["shell"] = json!("rust"); r }, v @ ("fusion_make_3d_package" | "make_3d_package" | "fusion_build_library_3d" | "build_library_3d" | "fusion_bind_library_3d" | "bind_library_3d" | "fusion_prove_library" | "prove_library" | "fusion_attach_3d_package" | "attach_3d_package" | "fusion_show_3d_package" | "show_3d_package") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); *LONG_OP.lock().unwrap() = Some(cmd.to_string()); *LONG_OP_SINCE.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "make_3d_package" => mint::make_3d_package(&ctx, &parsed["args"], &pass), "bind_library_3d" => mint::bind_library_3d(&ctx, &parsed["args"], &pass), "prove_library" => prove::prove_library(&ctx, &parsed["args"], &pass), "attach_3d_package" => mint::attach_3d_package(&ctx, &parsed["args"], &pass), "show_3d_package" => mint::show_3d_package(&ctx, &parsed["args"], &pass), _ => mint::build_library_3d(&ctx, &parsed["args"], &pass) }; *LONG_OP.lock().unwrap() = None; let mut r = small_verbs::apply_failure_dialogs(r0, &caller, &pids); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r }, v @ ("fusion_apply_fusion_update" | "apply_fusion_update" | "fusion_install_fusion" | "install_fusion" | "fusion_uninstall" | "uninstall" | "fusion_prefs_open" | "prefs_open" | "fusion_prefs_close" | "prefs_close" | "fusion_generate_package" | "generate_package" | "fusion_mcp_status" | "mcp_status" | "fusion_mcp_tools" | "mcp_tools" | "fusion_mcp_call" | "mcp_call" | "fusion_mcp_resources" | "mcp_resources" | "fusion_mcp_enable" | "mcp_enable" | "fusion_library_manager" | "library_manager" | "fusion_managed_library_create" | "managed_library_create" | "fusion_managed_library_link" | "managed_library_link" | "fusion_managed_library_pull" | "managed_library_pull" | "fusion_managed_library_push" | "managed_library_push" | "fusion_managed_library_unlink" | "managed_library_unlink" | "fusion_signin" | "signin" | "fusion_signin_2fa" | "signin_2fa" | "fusion_web_signin" | "web_signin" | "fusion_aps_signin" | "aps_signin" | "fusion_demo" | "demo" | "fusion_demo_progress" | "demo_progress" | "fusion_demo_control" | "demo_control" | "fusion_electronics_query" | "electronics_query" | "fusion_view" | "view" | "fusion_interactive" | "interactive" | "fusion_narrate" | "narrate") if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let cmd = v.strip_prefix("fusion_").unwrap_or(v); let mut r = match cmd { "apply_fusion_update" => maintenance::apply_fusion_update(&ctx, &pass), "install_fusion" => maintenance::install_fusion(&ctx), "prefs_open" => maintenance::prefs_open(&ctx, &parsed["args"], &pass), "prefs_close" => maintenance::prefs_close(&ctx, &parsed["args"]), "generate_package" => epg::generate_package(&parsed["args"], VERSION, &pass),
rust/bridge/src/orchestrations.rs+113−1@@ -844,10 +844,109 @@ pub fn open_design(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) } /// `fusion_new_electronics_from_eagle`: Fusion's own EAGLE importer, its two Open dialogs driven through ab.+/// A blank EAGLE schematic: one empty sheet, the schematic layers, nothing else. Opened through+/// Document.newDesignFromLocal it becomes a new local electronics design with the schematic+/// editor active, with no dialog anywhere (the create path of `fusion_schematic`).+const BLANK_SCH: &str = r#"<?xml version="1.0" encoding="utf-8"?>+<!DOCTYPE eagle SYSTEM "eagle.dtd">+<eagle version="9.6.2">+<drawing>+<settings><setting alwaysvectorfont="no"/><setting verticaltext="up"/></settings>+<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>+<layers>+<layer number="88" name="SimResults" color="9" fill="1" visible="yes" active="yes"/>+<layer number="89" name="SimProbes" color="9" fill="1" visible="yes" active="yes"/>+<layer number="90" name="Modules" color="5" fill="1" visible="yes" active="yes"/>+<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes"/>+<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes"/>+<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes"/>+<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>+<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>+<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>+<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes"/>+<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes"/>+</layers>+<schematic xreflabel="%F%N/%S.%C%R" xrefpart="/%S.%C%R">+<libraries/>+<attributes/>+<variantdefs/>+<classes><class number="0" name="default" width="0" drill="0"/></classes>+<parts/>+<sheets><sheet><plain/><instances/><busses/><nets/></sheet></sheets>+</schematic>+</drawing>+</eagle>+"#;++/// `fusion_schematic`: THE front door for a schematic. `action:"open"` takes whatever names+/// the design (a cloud fileName / urn / query, a local .sch with or without its .brd, a+/// Fusion-native .fsch) and lands in the schematic editor; `action:"create"` writes a blank+/// EAGLE schematic and opens it as a new local design, dialog-free; `action:"show"` switches+/// the open design to its schematic view. Every path says which verb it used (`via`).+pub fn schematic(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+ let action = args["action"].as_str().unwrap_or("").trim().to_lowercase();+ let a = |k: &str| args[k].as_str().map(|s| s.trim().replace('\\', "/")).filter(|s| !s.is_empty());+ let capture = args["capture"].clone();+ let menu = "fusion_schematic takes action:\"open\" with ONE of: fileName (+projectName) for a cloud design, urn for a cloud design by lineage, query to find-and-open by name, schPath for a local EAGLE .sch (its sibling .brd is imported too unless withBoard:false), or filePath for a Fusion-native .fsch; action:\"create\" with an optional name for a new blank schematic (dialog-free); action:\"show\" to switch the open design to its schematic view. A schematic is a VIEW of an electronics design, never a file on its own.";+ match action.as_str() {+ "show" => { let mut r = pass("show_schematic", json!({"capture": capture})); r["via"] = json!("show_schematic"); r }+ "create" => {+ let name: String = a("name").unwrap_or_else(|| format!("adom-schematic-{}", (now_f() as u64) % 100000)).chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect();+ let root = match crate::fusionpath::stage_root(&ctx.state_dir) { Ok(r) => r.join("new"), Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+ let _ = std::fs::create_dir_all(&root);+ let path = root.join(format!("{name}.sch")).to_string_lossy().replace('\\', "/");+ if let Err(e) = std::fs::write(&path, BLANK_SCH) { return json!({"success": false, "errorCode": "write_failed", "error": format!("cannot write {path}: {e}")}); }+ let mut r = pass("open_schematic", json!({"filePath": path}));+ r["via"] = json!("blank EAGLE .sch written on the box, then open_schematic (Document.newDesignFromLocal, no dialog)");+ r["schPath"] = json!(path); r["documentName"] = json!(name);+ if r["success"].as_bool() == Some(true) {+ r["_hint"] = json!(format!("A new electronics design named {name} is open with its (empty) schematic editor active. NEXT: put circuitry on it. Interactive editor commands (ADD, MOVE) are blocked because they wait for a mouse; author the schematic as an EAGLE .sch offline (adom-schematic / adom-lbr generate real parts and nets), then fusion_schematic {{action:\"open\", schPath}} re-opens it, or fusion_new_electronics_from_eagle {{schPath, brdPath}} when a placed board should come with it. fusion_export_eagle_source saves what is open; fusion_save_to_cloud puts it in the hub."));+ }+ r+ }+ "open" => {+ if let Some(p) = a("schPath").or_else(|| a("filePath")).filter(|p| p.to_lowercase().ends_with(".sch")) {+ let sibling = format!("{}.brd", &p[..p.len() - 4]);+ let brd = a("brdPath").or_else(|| if std::path::Path::new(&sibling).is_file() { Some(sibling.clone()) } else { None });+ if args["withBoard"].as_bool() != Some(false) {+ if let Some(b) = brd {+ let mut r = new_electronics_from_eagle(ctx, &json!({"schPath": p, "brdPath": b}), pass);+ r["via"] = json!("new_electronics_from_eagle (schematic + board, parts instantiated; this one drives Fusion's own Open dialogs in the background because Autodesk offers no API for it)");+ return r;+ }+ }+ let mut r = pass("open_schematic", json!({"filePath": p})); r["via"] = json!("open_schematic (Document.newDesignFromLocal, dialog-free, schematic only)"); return r;+ }+ if let Some(p) = a("filePath").filter(|p| p.to_lowercase().ends_with(".fsch")) { let mut r = pass("import_electronics", json!({"filePath": p})); r["via"] = json!("import_electronics (Fusion-native .fsch)"); return r; }+ let opened = if let Some(u) = a("urn") { Some(("open_by_urn", pass("open_by_urn", json!({"urn": u, "intent": "electronics"})))) }+ else if let Some(f) = a("fileName") { Some(("open_cloud_file", open_cloud_file(ctx, &json!({"fileName": f, "projectName": args["projectName"], "folderPath": args["folderPath"]}), pass))) }+ else if let Some(q) = a("query") { Some(("aps_open", aps_open(ctx, &json!({"query": q, "wait": true}), pass))) }+ else { None };+ let Some((how, r)) = opened else { return json!({"success": false, "errorCode": "schematic_source_required", "error": "Nothing names the design to open.", "_hint": menu}); };+ if r["success"].as_bool() != Some(true) { let mut r = r; r["via"] = json!(how); return r; }+ std::thread::sleep(Duration::from_secs(2));+ let mut s = pass("show_schematic", json!({"capture": capture}));+ s["opened"] = json!({"via": how, "output": r["output"], "hint": r["_hint"]}); s["via"] = json!(format!("{how} + show_schematic"));+ if s["success"].as_bool() != Some(true) { s["_hint"] = json!(format!("The design opened ({how}) but the switch to its schematic view failed: {}. If the open landed on a mechanical design (DesignProductType) it has no schematic; check fusion_get_app_state and open the electronics DESIGN document instead.", s["error"].as_str().unwrap_or("unknown"))); }+ s+ }+ _ => json!({"success": false, "errorCode": "schematic_action_required", "error": format!("action must be open, create or show (got '{action}')."), "_hint": menu}),+ }+}+ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value { let sch = args["schPath"].as_str().or(args["filePath"].as_str()).unwrap_or("").replace('\\', "/"); if sch.is_empty() || !sch.to_lowercase().ends_with(".sch") { return json!({"success": false, "error": "schPath (a .sch file on the Windows host) is required.", "_hint": "Author the EAGLE .sch + paired .brd (same basename, same folder), stage both to Windows (send_files), then call with {schPath, brdPath}."}); } let brd = args["brdPath"].as_str().map(|s| s.replace('\\', "/")).filter(|s| !s.is_empty()).unwrap_or_else(|| format!("{}.brd", &sch[..sch.len() - 4]));+ // Preflight (John, 2026-09-20): this verb drives Fusion's own Open dialogs, so a wrong path+ // used to leave a real Open dialog on the user's screen. Both files must exist on this box+ // before any dialog is touched.+ let missing: Vec<&str> = [(&sch, "schPath"), (&brd, "brdPath")].iter().filter(|(p, _)| !std::path::Path::new(p.as_str()).is_file()).map(|(_, k)| *k).collect();+ if !missing.is_empty() {+ return json!({"success": false, "errorCode": "eagle_files_missing", "schPath": sch, "brdPath": brd, "missing": missing,+ "error": format!("Not started: {} not found on the Fusion machine. This verb drives Fusion's Open dialogs, so it refuses before touching them.", missing.join(" and ")),+ "_hint": "Push both files to the box first (send_files, a space-free folder is best) and pass their absolute Windows paths. No dialog was opened. If you only have a .sch, fusion_schematic {action:\"open\", schPath, withBoard:false} opens it dialog-free (schematic only); if the .brd already carries placed parts, fusion_open_board {filePath} instantiates them with no dialog either."});+ } let Some(ab) = fusion_core::ab::Loopback::from_env() else { return json!({"success": false, "errorCode": "use_open_board_instead", "error": "ab direct API unavailable - and this dialog-driven import FOREGROUNDS Fusion anyway.", "_hint": format!("Don't drive the Open dialogs. If the parts are placed in the .brd (elements + embedded library), call `fusion_open_board {{filePath:\"{brd}\"}}` - it instantiates them via newDesignFromLocal with NO dialogs and NO foreground (the background way). For 3D bodies, embed <packages3d> + per-element package3d_urn in the .brd (see this verb's docstring), then fusion_show_3d_board. Only if you truly have a bare .sch that Fusion must import: fire 'Commands.Start ImportSCHAndBRDCmd' via fusion_execute_text_command and drive the two Open dialogs (foreground; last resort).")});@@ -855,6 +954,8 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V use fusion_core::ab::AbClient; let find_open_dialog = || -> Option<i64> { ab.call("desktop_list_windows", json!({}), &ctx.caller, 10).ok().and_then(|v| v["data"]["windows"].as_array().or_else(|| v["windows"].as_array()).cloned()).unwrap_or_default().iter().find(|w| w["title"].as_str().unwrap_or("").trim().to_lowercase() == "open").and_then(|w| w["hwnd"].as_i64()) }; let (sch_base, brd_base) = (sch.rsplit('/').next().unwrap_or("").to_string(), brd.rsplit('/').next().unwrap_or("").to_string());+ let mut stale_cancelled = false;+ if let Some(h) = find_open_dialog() { let _ = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10); stale_cancelled = true; std::thread::sleep(Duration::from_secs(1)); } let _ = pass("execute_text_command", json!({"command": "Commands.Start ImportSCHAndBRDCmd"})); let mut picked: Vec<String> = Vec::new(); for (want, base) in [("sch", &sch_base), ("brd", &brd_base)] {@@ -865,6 +966,17 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V return json!({"success": false, "error": format!("The '{want}' Open dialog never appeared."), "_hint": "Screenshot the Fusion hwnd + READ screenshots[] for a blocking modal; the importer may have errored on the EAGLE source. Verify the .sch/.brd are valid EAGLE.", "data": {"picked": picked}}); }; let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": base}), &ctx.caller, 15); std::thread::sleep(Duration::from_secs(1)); let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "Open"}), &ctx.caller, 15);+ // the dialog must be gone now; if the file was not in the folder the dialog showed, the+ // click missed and the dialog stays. Never leave it on the user's screen.+ let mut gone = false;+ for _ in 0..8 { std::thread::sleep(Duration::from_secs(1)); if find_open_dialog().map(|x| x != h).unwrap_or(true) { gone = true; break; } }+ if !gone {+ let _ = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10);+ crate::fusion_prefs::action_ledger(&ctx.state_dir, "eagle_import_dialog_cancelled", json!({"wanted": base, "kind": want}));+ return json!({"success": false, "errorCode": "open_dialog_stuck", "wanted": base, "imported": picked,+ "error": format!("Fusion's Open dialog did not accept '{base}' (it was not in the folder the dialog showed, or the name did not match); the bridge cancelled the dialog so nothing is left on screen."),+ "_hint": "Nothing was imported. Put the .sch and .brd together in one space-free folder, pass both absolute paths, and retry; or use the dialog-free routes: fusion_schematic {action:\"open\", schPath, withBoard:false} for the schematic alone, fusion_open_board {filePath} for a .brd with placed parts."});+ } picked.push(base.clone()); std::thread::sleep(Duration::from_secs(2)); }@@ -875,7 +987,7 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V let d = &st["data"]; if d["isElectronics"].as_bool() == Some(true) && ["pcb editor", "schematic editor", "board layout", "3d pcb"].contains(&d["activeWorkspace"].as_str().unwrap_or("").to_lowercase().as_str()) { ready = true; break; } }- json!({"success": ready, "imported": picked, "statusVerb": "fusion_board_info",+ json!({"success": ready, "imported": picked, "staleDialogCancelled": stale_cancelled, "dialogsDriven": "Fusion's own Open dialogs (sch, then brd), driven in the background by accessible name; this is Autodesk's only route to a populated board from EAGLE source", "statusVerb": "fusion_board_info", "_hint": if ready { "EAGLE design imported via Fusion's own ImportSCHAndBRDCmd (both Open dialogs driven in the background). It is now an electronics design in the PCB editor. NEXT: fusion_show_2d_board, then RATSNEST + 'AUTO ;' (fusion_electron_run) to autoroute, fusion_show_3d_board for the 3D. NOTE: board_info can read 0 right after import (wrong-view query) even though the board is populated - verify by screenshotting the Fusion hwnd (read screenshots[]) or fusion_show_2d_board + WINDOW FIT. If 3D bodies are missing, the placed parts' package3d urns must resolve in THIS project - build the library's 3D into this project with fusion_build_library_3d (do NOT reuse another library's urns; they don't resolve cross-project)." } else { "Import fired + both Open dialogs driven, but the design did not confirm as electronics within the budget (a slow VM import can run longer). Poll fusion_get_app_state, and screenshot the Fusion hwnd reading screenshots[] for a blocking import dialog." }}) }
skills/fusion-electronics/SKILL.md+7@@ -5,6 +5,13 @@ description: How to drive Fusion 360 Electronics (the EAGLE-based schematic + PC # Fusion Electronics - drive the schematic, board, and 3D PCB +> **Opening or creating a schematic: `fusion_schematic`.** `{action:"open", fileName|urn|query}` opens a+> cloud design and switches to its schematic; `{action:"open", schPath}` opens a local EAGLE .sch (with+> its .brd when one sits beside it, so parts instantiate); `{action:"create", name}` writes a blank+> schematic and opens it as a new local design with no dialog; `{action:"show"}` switches the open+> design to the schematic view. The reply's `via` names the verb it used. Everything below is the+> detail behind that front door.+ ## 1. ALWAYS open the PROJECT, never a child file A Fusion electronics design is NOT one file. It is a parent/child chain of separate files, each a
tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.sh