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
#133: typing fallback, chain close, latest status
2 files changed
+58−11
CHANGELOG.md+11@@ -3,6 +3,17 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.42 (2026-10-30)++Issue #133, after Andy's run on 1.13.41 failed on his PC ("the File name box could not be set"):+- When UI Automation cannot set the Open dialog's File name box, the path is typed (a fresh Open dialog has the cursor+ in that box) and Enter pressed; ab gives the window back afterwards. The reply carries `typedVia` and, per attempt,+ `typeErrors` (what ab answered), so a failure says why.+- A stuck import closes the whole chain: cancelling the .sch dialog makes the importer open its .brd dialog at once,+ which was the dialog left on Andy's screen. The Untitled documents are closed only after the last dialog is gone,+ and `dialogClosed` is true only when no Open dialog is left.+- `fusion_eagle_import_status` with no runId answers the latest import on the box, for a caller that lost the runId.+ ## 1.13.41 (2026-10-30) - `fusion_dismiss_blocking_dialogs` scans again after closing a native Open dialog: a cancelled EAGLE import opens its
rust/bridge/src/orchestrations.rs+47−11@@ -1226,7 +1226,14 @@ fn eagle_run_write(state_dir: &std::path::Path, run_id: &str, v: &Value) { let p /// `fusion_eagle_import_status`: the run record of a background EAGLE import. pub fn eagle_import_status(ctx: &Ctx, args: &Value) -> Value {- let Some(rid) = args["runId"].as_str().filter(|s| !s.is_empty()) else { return json!({"success": false, "errorCode": "run_id_required", "error": "runId is required (fusion_new_electronics_from_eagle returns it)."}) };+ // no runId: the latest import on this box (a caller that lost the runId can still read the result, #133)+ let latest = || -> Option<String> {+ std::fs::read_dir(ctx.state_dir.join("eagle-imports")).ok()?.flatten()+ .filter_map(|e| { let m = e.metadata().ok()?.modified().ok()?; Some((m, e.path().file_stem()?.to_string_lossy().to_string())) })+ .max_by_key(|(m, _)| *m).map(|(_, id)| id)+ };+ let Some(rid) = args["runId"].as_str().filter(|s| !s.is_empty()).map(str::to_string).or_else(latest) else { return json!({"success": false, "errorCode": "no_import_run", "error": "No EAGLE import has run on this box."}) };+ let rid = rid.as_str(); match std::fs::read_to_string(eagle_run_path(&ctx.state_dir, rid)).ok().and_then(|t| serde_json::from_str::<Value>(&t).ok()) { Some(mut v) => { v["success"] = json!(true); v["runId"] = json!(rid); v } None => json!({"success": false, "errorCode": "unknown_run_id", "error": format!("no EAGLE import run {rid} on this box")}),@@ -1362,13 +1369,32 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V // the user's foreground window back afterwards) // the File name Edit by its name, then by the common dialog's control id (1148), then as the // dialog's first Edit: Windows 11 24H2 named it differently and the name alone set nothing (#133)- let set_ok = |sel: Value| -> bool {- let mut a = sel; a["hwnd"] = json!(h); a["text"] = json!(full); a["submit"] = json!(true);- ab.call("desktop_ui_set", a, &ctx.caller, 20).map(|v| v["success"].as_bool() == Some(true) || v["data"]["set"].as_bool() == Some(true)).unwrap_or(false)+ let mut type_errors: Vec<Value> = Vec::new();+ let mut set_ok = |sel: Value| -> bool {+ let mut a = sel.clone(); a["hwnd"] = json!(h); a["text"] = json!(full); a["submit"] = json!(true);+ match ab.call("desktop_ui_set", a, &ctx.caller, 20) {+ Ok(v) if v["success"].as_bool() == Some(true) || v["data"]["set"].as_bool() == Some(true) => true,+ Ok(v) => { type_errors.push(json!({"via": "desktop_ui_set", "selector": sel, "errorCode": v["errorCode"], "error": v["error"]})); false }+ Err(e) => { type_errors.push(json!({"via": "desktop_ui_set", "selector": sel, "error": format!("{e}")})); false }+ } }; let mut typed = false;- for sel in [json!({"name": "File name:", "role": "Edit"}), json!({"automationId": "1148"}), json!({"role": "Edit", "nth": 0})] {- if set_ok(sel) { typed = true; std::thread::sleep(Duration::from_millis(800)); if find_open_dialog().map(|x| x != h).unwrap_or(true) { break; } }+ let mut typed_via = Value::Null;+ if args["_forceTypeFallback"].as_bool() != Some(true) {+ for sel in [json!({"name": "File name:", "role": "Edit"}), json!({"automationId": "1148"}), json!({"role": "Edit", "nth": 0})] {+ if set_ok(sel.clone()) { typed = true; typed_via = json!({"uia": sel}); std::thread::sleep(Duration::from_millis(800)); if find_open_dialog().map(|x| x != h).unwrap_or(true) { break; } }+ }+ }+ // UI Automation could not set the box (a PC where the dialog's Edit is not exposed, #133): type+ // the path. A fresh Open dialog has the cursor in its File name box; this brings the dialog in+ // front for the moment it takes (ab gives the window back afterwards)+ if find_open_dialog() == Some(h) {+ let t1 = ab.call("desktop_type", json!({"hwnd": h, "text": full}), &ctx.caller, 20);+ let t2 = ab.call("desktop_press_key", json!({"hwnd": h, "keys": ["enter"]}), &ctx.caller, 15);+ match (&t1, &t2) {+ (Ok(a), Ok(b)) if a["success"].as_bool() != Some(false) && b["success"].as_bool() != Some(false) => { typed = true; typed_via = json!("keyboard"); }+ _ => type_errors.push(json!({"via": "desktop_type", "error": t1.as_ref().map(|v| v["error"].clone()).unwrap_or_else(|e| json!(format!("{e}"))), "enter": t2.as_ref().map(|v| v["error"].clone()).unwrap_or_else(|e| json!(format!("{e}")))})),+ } } if !typed { 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@@ -1377,11 +1403,21 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V 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 { if let Some(c) = crash_dialog() { return crashed(c, Some(h), &format!("submitting the {want} path"), &picked); } } if !gone {- let (closed, how) = crate::small_verbs::close_native_dialog(&ab, &ctx.caller, h);- let cleaned = close_new_untitled(&docs_before, pass);- crate::fusion_prefs::action_ledger(&ctx.state_dir, "eagle_import_dialog_cancelled", json!({"wanted": base, "kind": want, "typed": typed, "closed": closed, "how": how, "untitledClosed": cleaned}));- return json!({"success": false, "errorCode": "open_dialog_stuck", "wanted": base, "imported": picked, "typed": typed,- "dialogClosed": closed, "dialogHwnd": h, "closedBy": how, "untitledClosed": cleaned,+ let (mut closed, how) = crate::small_verbs::close_native_dialog(&ab, &ctx.caller, h);+ // cancelling the .sch dialog makes the importer open its .brd dialog at once: close every+ // Open dialog it opens until none is left (#133: the one left on screen was that next one)+ let mut chained = 0;+ for _ in 0..3 {+ std::thread::sleep(Duration::from_millis(1500));+ let Some(next) = find_open_dialog() else { break };+ let (c, _) = crate::small_verbs::close_native_dialog(&ab, &ctx.caller, next);+ if c { chained += 1; } else { closed = false; break; }+ }+ if find_open_dialog().is_some() { closed = false; }+ let cleaned = if closed { close_new_untitled(&docs_before, pass) } else { 0 };+ crate::fusion_prefs::action_ledger(&ctx.state_dir, "eagle_import_dialog_cancelled", json!({"wanted": base, "kind": want, "typed": typed, "typedVia": typed_via, "typeErrors": type_errors, "closed": closed, "how": how, "chainedClosed": chained, "untitledClosed": cleaned}));+ return json!({"success": false, "errorCode": "open_dialog_stuck", "wanted": base, "imported": picked, "typed": typed, "typedVia": typed_via, "typeErrors": type_errors,+ "dialogClosed": closed, "dialogHwnd": h, "closedBy": how, "chainedDialogsClosed": chained, "untitledClosed": cleaned, "error": if closed { format!("Fusion's Open dialog did not take '{base}' ({}); the bridge closed the dialog ({how}) and the empty documents the import had opened.", if typed { "the path was typed and submitted, but the dialog stayed" } else { "the File name box could not be set" }) } else { format!("Fusion's Open dialog did not take '{base}', and it is STILL OPEN (hwnd {h}): Cancel and WM_CLOSE did not close it. Fusion stays blocked until it is closed.") }, "_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."});