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
Routing on Fusion 2705: classify and dismiss Fusion Electronics' own router assertion (pusher.cpp inLocator) in the envelope and sentinel, carry it in the routing reply; the routing skill asks the manufacturer and reads the rules back; release notes
7 files changed
+171−6
CHANGELOG.md+25@@ -3,6 +3,31 @@ 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.10.0 (2026-09-07)++Live per-net trace routing from the Astra work lands in the released bridge, merged from+`feature/per-net-trace-routing` and `fix/document-save-hints` (PR 7 on adom/fusion-bridge):++- `fusion_routing_state`, `fusion_route_net`, `fusion_routing_drc` and `fusion_plan_route`:+ read the live board, create explicit copper trace by trace with board and revision guards+ and exact copper read-back, run native Autodesk DRC through UL_BOARD.errors, and plan a+ bounded two-pad orthogonal path. Two-layer geometric preflight; native DRC is the final word.+- The routing skill (`skills/fusion-routing`) now asks which manufacturer the board is for,+ loads and reads back that house's rules through `fusion_set_design_rules`, and keeps the+ stricter of the rules and the user's electrical constraints.+- Every add-in reply carries read-only `documentHygiene` and a save/cleanup hint when a+ document needs saving; nothing is saved, closed or discarded automatically.+- The activity palette gains the routing panel (selected connection, calculated route,+ measured checks, recent operations) fed by the demo's event stream.+- `demo/routing/` ships the fixture, plans, recorders, audits and evidence behind the Fusion+ routing videos on adom/codex (the videos themselves stay on the wiki page).+- Also since 1.9.366: the electronics tour rebuilt around gestures Fusion renders, the+ palette parked off the drawing, slow verbs answering `_timing`, the small taskbar badge in+ every path, and the dashboard's Send to AI through Hydrogen's AI picker.++Not included: the thermal-bottleneck reroute scripts (video 12) remain unpublished on the+Codex side, and the shared EDA skillpack is not yet an installable package.+ ## 1.9.76 - 2026-08-18 (ralph, dashboard) - **"Fusion is opening right now" LED (John's ask).** A launch takes 30-90s and the board used to
handlers/dialog_classify.py+9@@ -19,6 +19,15 @@ from handlers.fusion_ui import get_fusion_window_info # (category, resolution-hint) keyed by lowercase title substrings, checked in order. _RULES = [+ # Fusion Electronics' own router assertion (pusher.cpp inLocator), seen on ConfRoomROG+ # 2026-09-07 whenever a LINE completed a net's last airwire. OK is informational; the+ # copper already landed. Checkpoint, then verify with fusion_routing_state.+ (("unexpected condition", "pusher.cpp"),+ "fusion_unexpected_condition",+ "Fusion Electronics asserted inside its own router. OK is the only button and is "+ "informational; the copper already landed. Click OK, CHECKPOINT the board "+ "(fusion_export_eagle_source), then verify the copper with fusion_routing_state."),+ # Fusion refused to open a broken .lbr and left an empty design behind (John caught it # live 2026-08-21, a 77-byte truncated file). OK is the only button; the orphan # Untitled design is bridge-attributable scratch, closed without saving by the janitor.
rust/bridge/src/etiquette.rs+46−3@@ -25,6 +25,16 @@ pub fn set_badge(caller: &Caller, hwnd: i64) -> bool { if ok { BADGED.store(true, Ordering::Relaxed); *BADGED_HWND.lock().unwrap() = hwnd; } ok }+static BADGE_AT: std::sync::Mutex<Option<Instant>> = std::sync::Mutex::new(None);+/// set_badge, but a repeat on the same window within 60 s is a no-op (per-verb callers must not+/// spam desktop_taskbar), exactly the Python bridge's throttle.+pub fn set_badge_throttled(caller: &Caller, hwnd: i64) -> bool {+ let same = *BADGED_HWND.lock().unwrap() == hwnd && BADGE_AT.lock().unwrap().map(|t| t.elapsed() < Duration::from_secs(60)).unwrap_or(false);+ if same { return true; }+ let ok = set_badge(caller, hwnd);+ if ok { *BADGE_AT.lock().unwrap() = Some(Instant::now()); }+ ok+} /// `clear_driving_badge`: remove the taskbar badge from whichever window we badged last. pub fn clear_badge(caller: &Caller) -> bool { let h = *BADGED_HWND.lock().unwrap();@@ -113,6 +123,20 @@ pub fn find_save_prompt(ab: &dyn AbClient, caller: &Caller, pids: &[u32]) -> Val json!({"found": false}) } +/// Fusion Electronics' own "unexpected condition" assertion (pusher.cpp inLocator, seen when a+/// LINE completes a net's last airwire): OK is its only button and is informational, the copper+/// has already landed. Click it and hand back the body text so the caller can checkpoint.+pub fn dismiss_unexpected_condition(ab: &dyn AbClient, caller: &Caller, pids: &[u32]) -> Option<String> {+ for d in crate::small_verbs::classify_blocking_dialogs(ab, caller, pids) {+ if d["category"].as_str() != Some("fusion_unexpected_condition") { continue; }+ let Some(h) = d["hwnd"].as_i64() else { continue };+ let body = ab.call("desktop_find_controls", json!({"hwnd": h}), caller, 10).ok().map(|r| { let dd = if r["data"].is_object() { r["data"].clone() } else { r }; dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default().iter().filter_map(|c| c["name"].as_str()).filter(|n| n.to_lowercase().contains("unexpected condition")).map(str::to_string).next().unwrap_or_default() }).unwrap_or_default();+ let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12).map(|r| r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false)).unwrap_or(false);+ eprintln!("[fusion-bridge] Fusion Electronics assertion dialog {}: {}", if clicked { "dismissed (OK)" } else { "seen, OK did not land" }, body.chars().take(160).collect::<String>());+ return Some(if body.is_empty() { "Fusion Electronics has detected an unexpected condition".into() } else { body });+ }+ None+} /// `_dismiss_save_prompt_if_bridge_doc`: Don't Save for OUR documents only; a user's prompt is never touched. pub fn dismiss_save_prompt_if_bridge_doc(state_dir: &Path, caller: &Caller, pids: &[u32], main_title: &str) -> Value { let Some(ab) = Loopback::from_env() else { return json!({"handled": false, "why": "no ab loopback"}) };@@ -170,6 +194,7 @@ impl Sentinel { std::thread::spawn(move || { while !s2.load(Ordering::Relaxed) && Instant::now() < *d2.lock().unwrap() { let _ = dismiss_save_prompt_if_bridge_doc(&state_dir, &caller, &pids, "");+ if let Some(ab) = Loopback::from_env() { let _ = dismiss_unexpected_condition(&ab, &caller, &pids); } std::thread::sleep(Duration::from_secs(1)); } });@@ -209,7 +234,10 @@ pub fn pre(host: &dyn fusion_core::HostProcess, state_dir: &Path, caller: &Calle let secs = crate::addin::timeout_secs_or(cmd, 60) as f64 + 20.0; host.background_envelope(secs, cmd == "start"); if crate::small_verbs::get_pref(state_dir, "showDrivingBadge").map(|v| v != json!(false)).unwrap_or(true) {- if let Some(ab) = Loopback::from_env() { if let Some(h) = fusion_core::windows_facts::facts_from_ab(&ab, caller).main_hwnd { if ab.call("desktop_taskbar", json!({"hwnd": h, "overlay": {"badge": "adom", "tooltip": "Adom is driving Fusion"}}), caller, 10).is_ok() { BADGED.store(true, Ordering::Relaxed); *BADGED_HWND.lock().unwrap() = h; } } }+ // the SAME small PNG badge as set_badge (John, 2026-09-07: this path painted ab's+ // built-in "adom" badge, which draws far larger than pup's overlays and than the+ // Python bridge ever did), throttled like the Python bridge's set_driving_badge+ if let Some(ab) = Loopback::from_env() { if let Some(h) = fusion_core::windows_facts::facts_from_ab(&ab, caller).main_hwnd { set_badge_throttled(caller, h); } } *LAST_DRIVE_AT.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0); } out.ran_backgrounded = true;@@ -236,8 +264,23 @@ pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res: if res.get("_ledgerHint").is_none() { res["_ledgerHint"] = json!("recentActions = every background action the bridge took on the host app while this verb ran (newest last); fusion_action_ledger {sinceSeconds} for more."); } } }+ // Fusion's own router assertion modal blocks the main thread; clear it BEFORE the document+ // snapshot below asks the add-in anything (2026-09-07: that snapshot hung 19 s per verb and+ // the routing test read as an add-in crash)+ if window_raising {+ if let Some(ab) = Loopback::from_env() {+ // the assertion lands a moment AFTER the command returns: look for it a few times+ let mut found = None;+ for i in 0..4 { found = dismiss_unexpected_condition(&ab, caller, pids); if found.is_some() { break; } if i < 3 { std::thread::sleep(Duration::from_millis(300)); } }+ if let Some(body) = found { if res.is_object() { res["fusionUnexpectedCondition"] = json!(body); let h = res["_hint"].as_str().unwrap_or("").to_string(); res["_hint"] = json!(format!("{h} Fusion Electronics raised its 'unexpected condition' assertion during this verb and the bridge clicked OK; checkpoint the board now (fusion_export_eagle_source) and verify the copper with fusion_routing_state.").trim().to_string()); } }+ }+ } if let Some(before) = &pre.docs_before {- for name in snapshot_open_docs(call) { if !before.contains(&name) && !name.is_empty() && name != "untitled" { crate::chains::note_bridge_doc_pub(state_dir, &name); eprintln!("[fusion-bridge] registered {name:?} as a bridge doc (it appeared while we drove)"); } }+ // a short, undiagnosed add-in hop: a hung main thread must cost 3 s here, not the 5 s+ // call plus a 14 s timeout diagnosis (2026-09-07, the routing test's 60 s relay window)+ let quick = |verb: &str, a: Value, _t: u64| crate::addin::addin_http(verb, &a, 3);+ let _ = call;+ for name in snapshot_open_docs(&quick) { if !before.contains(&name) && !name.is_empty() && name != "untitled" { crate::chains::note_bridge_doc_pub(state_dir, &name); eprintln!("[fusion-bridge] registered {name:?} as a bridge doc (it appeared while we drove)"); } } } if SHOW_SURFACE_VERBS.contains(&cmd) { for k in ["name", "deviceset", "libraryPath", "fileName", "query"] { if let Some(v) = args[k].as_str().filter(|s| !s.is_empty()) { crate::chains::note_bridge_doc_pub(state_dir, v); } }@@ -253,7 +296,7 @@ pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res: match cat { "upload_in_progress" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["uploadCloseDeclined"] = json!(true); } eprintln!("[fusion-bridge] answered the Hub upload-close confirm with No (work-preserving; close retried after jobs drain)"); } "create_confirm" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["createPromptDeclined"] = json!(true); } eprintln!("[fusion-bridge] declined a standing Create-new prompt at the chokepoint (raised by a raw EDIT; Yes would fabricate an empty artifact)"); }- "launch_error" | "lbr_open_error" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12); eprintln!("[fusion-bridge] dismissed a standing {cat} dialog at the chokepoint"); }+ "launch_error" | "lbr_open_error" | "fusion_unexpected_condition" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12); eprintln!("[fusion-bridge] dismissed a standing {cat} dialog at the chokepoint"); } _ => {} } }
rust/bridge/src/routing.rs+8−1@@ -11,7 +11,14 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val let r = pass("run_modeling_script", json!({"script":script,"expectDocument":args["expectDocument"]})); if r["success"].as_bool() == Some(false) { return r; } match r["data"]["result"].as_object() {- Some(_) => r["data"]["result"].clone(),+ Some(_) => {+ // the envelope's findings ride along: Fusion's own assertion (dismissed) and the+ // document hygiene state, neither of which the routing script can see+ let mut out = r["data"]["result"].clone();+ for k in ["fusionUnexpectedCondition", "documentHygiene", "_hintDocumentHygiene"] { if !r[k].is_null() { out[k] = r[k].clone(); } }+ if !r["fusionUnexpectedCondition"].is_null() { out["_hint"] = json!(format!("{} Fusion Electronics raised its 'unexpected condition' assertion after this edit and the bridge clicked OK; the copper read-back above is the truth. Checkpoint now (fusion_export_eagle_source).", out["_hint"].as_str().unwrap_or("")).trim().to_string()); }+ out+ } None => json!({"success":false,"errorCode":"invalid_routing_response","error":"Fusion did not return a structured routing result", "mutationMayHaveOccurred":cmd=="route_net"}), } }
rust/core/src/dialog_rules.json+9−1@@ -1,5 +1,13 @@ { "rules": [+ {+ "needles": [+ "unexpected condition",+ "pusher.cpp"+ ],+ "category": "fusion_unexpected_condition",+ "resolution": "Fusion Electronics asserted inside its own router (seen when a LINE completes a net's last airwire on some boxes, pusher.cpp inLocator). OK is the only button and is informational; the copper already landed. Click OK, then CHECKPOINT the board (fusion_export_eagle_source) before doing anything else, and verify the copper with fusion_routing_state; do not assume the edit was lost or applied."+ }, { "needles": [ "has errors and cannot be opened",@@ -206,4 +214,4 @@ "category": "unknown", "resolution": "Unrecognized modal - a screenshot is attached: READ it and ANALYZE before acting. Do NOT blindly dismiss (some modals lose work if confirmed wrong). Identify it from the screenshot, then take the work-preserving action." }-}+}
rust/core/src/dialogs.rs+27@@ -34,6 +34,9 @@ pub fn category_set(name: &str) -> Vec<String> { /// `_find_qt_dialog_windows` over ab's window rows: visible Qt-class windows that are not the /// main window, minus small / tall docked tool panels.+/// the title Fusion gives the bridge's activity palette (`ui.palettes.add(PALETTE_ID, "Adom", ...)`)+pub const BRIDGE_PALETTE_TITLE: &str = "Adom";+ pub fn qt_dialog_rows(rows: &[Value]) -> Vec<Value> { let mut out = Vec::new(); for w in rows {@@ -41,6 +44,10 @@ pub fn qt_dialog_rows(rows: &[Value]) -> Vec<Value> { let class = w["className"].as_str().or(w["class"].as_str()).unwrap_or(""); let title = w["title"].as_str().unwrap_or(""); if !class.contains("Qt") || title.contains("Autodesk Fusion") { continue; }+ // the bridge's own activity palette is a 760x820 Qt tool window titled "Adom": a UIA+ // control search over its HTML costs seconds, and it is never a dialog (2026-09-05:+ // every doc-switching verb paid ~13 s of post-envelope for exactly that search)+ if title.trim().eq_ignore_ascii_case(BRIDGE_PALETTE_TITLE) { continue; } let rect = rect_of(&w["rect"]); let (ww, hh) = (rect["width"].as_i64().unwrap_or(0), rect["height"].as_i64().unwrap_or(0)); if class.contains("Tool") && (ww < 400 || hh < 300 || hh > ww * 3 / 2) { continue; }@@ -62,3 +69,23 @@ pub fn rect_of(r: &Value) -> Value { let (w, h) = (r["width"].as_i64().or_else(|| r["right"].as_i64().map(|x| x - l)).unwrap_or(0), r["height"].as_i64().or_else(|| r["bottom"].as_i64().map(|y| y - t)).unwrap_or(0)); serde_json::json!({"left": l, "top": t, "right": l + w, "bottom": t + h, "width": w, "height": h}) }++#[cfg(test)]+mod tests {+ #[test]+ fn assertion_body_classifies() {+ let c = super::category_for_title("Fusion Electronics has detected an unexpected condition. Please report this error ... File: R:\\Electron\\EAGLE\\src\\pusher.cpp, Line 492. Condition: inLocator").unwrap();+ assert_eq!(c.0, "fusion_unexpected_condition");+ assert_eq!(super::category_for_title("Fusion").unwrap().0, "generic_modal_read_screenshot");+ }+ #[test]+ fn own_palette_is_never_a_dialog() {+ let rows = vec![+ serde_json::json!({"className": "Qt683QWindowToolSaveBits", "hwnd": 1379664, "pid": 16004, "rect": [1900, 300, 2660, 1120], "title": "ADOM", "visible": true}),+ serde_json::json!({"className": "Qt683QWindow", "hwnd": 42, "pid": 16004, "rect": [100, 100, 700, 400], "title": "Save changes?", "visible": true}),+ ];+ let out = super::qt_dialog_rows(&rows);+ assert_eq!(out.len(), 1, "{out:?}");+ assert_eq!(out[0]["hwnd"], 42);+ }+}
skills/fusion-routing/SKILL.md+47−1@@ -1,6 +1,6 @@ --- name: fusion-routing-description: Use when asked to route explicit net paths live in Fusion or record trace creation using fusion_routing_state, fusion_route_net and fusion_routing_drc. Use fusion_plan_route for bounded obstacle-aware path calculation. Development feature with two-layer geometric preflight and native final DRC.+description: Use when asked to route a net, draw traces, add vias, pour copper or reproduce the Astra routing demos live in Fusion with fusion_routing_state, fusion_route_net, fusion_routing_drc and fusion_plan_route. Two-layer geometric preflight, native Autodesk DRC before and after, manufacturer rules loaded from adom/pcb-design-rules first. Trigger words - route this net, draw a trace, add a via, copper pour, refill, ratsnest, airwires, DRC, design rules, JLCPCB rules, reproduce the routing demo. --- Parent skill: [fusion-bridge](../../SKILL.md).@@ -56,3 +56,49 @@ Fusion palettes can be owned popup windows omitted by parent-window capture. Inspect the popup screenshots and capture its HWND separately if necessary. Check that both windows are visible, unminimized and large enough before recording. Preserve raw takes and record synchronization limits when composing two captures.++## Before the first trace: which manufacturer, and whose rules win++Ask the user which board house will build the board before routing anything, unless they+already said. Load that house's rules into the open board and read them back, then keep the+stricter of the rules and the electrical constraints the user states:++```jsonc+fusion_set_design_rules {"action": "list"} // the vendors and tiers the wiki page adom/pcb-design-rules carries+fusion_set_design_rules {"fab": "jlcpcb", "layers": "2"} // the user's answer; process defaults to standard+fusion_set_design_rules {"action": "export", "outputPath": "C:/Users/<you>/Documents/<task>/rules-loaded.edru"}+```++- Read the exported file and quote `mdWireWire` (copper clearance), `msWidth` (minimum width)+ and `msDrill` back to the user before routing; `fusion_alerts {sinceSec:60}` must be empty,+ a refused rule file is an in-canvas toast, not an error.+- Route with `clearance` and `width` no smaller than the loaded rules. A user constraint that+ is stricter (a wider power trace, a larger creepage gap, a keep-out) wins over the vendor+ minimum; the vendor minimum wins over a request to go below it. Say which one you applied.+- `fusion_set_design_rules {}` with no arguments loads the bundled Adom InstaPCB rules. The+ published BQ25792 demos ran on a DIAGNOSTIC copy of those rules (one inherited all-object+ same-net spacing rule disabled on disposable copies); they do not establish compliance with+ the unmodified InstaPCB profile, and neither does a clean `fusion_routing_drc` under any+ profile other than the one the user asked for.++## Checkpoints, and what a save hint is not++Every add-in reply after 1.9.371 carries `documentHygiene` (which open documents need saving)+and, when something does, `_hintDocumentHygiene`. The bridge never saves, closes or discards+on its own. Checkpoint with `fusion_export_eagle_source {outputPath}` (a `.brd` beside your+task folder) or `fusion_save_to_cloud` at milestones and before ending; verify the file exists+and re-read the hint, because an export leaves Fusion's own modified state as it was. Close+only the scratch document this task opened, by its exact name, with+`fusion_close_document {name, save:false}`, after the checkpoint is verified. Never bulk-close.++## Reproducing the published demos from the installed package++The installed package carries these verbs in the bridge runtime and the fixture, plans and+recorders under `demo/routing/` in the page source (`adom-wiki repo clone adom/fusion-bridge`).+`demo/routing/README.md` is the fixture walkthrough; `demo/routing/bq25792/` holds the BQ25792+plans, replay scripts and audits for the videos on adom/codex. `tools/install-routing-dev.ps1`+is a developer helper for hand-installing an exe over a dev-pinned cache; an installed release+never needs it. Two things are not in this package: the thermal-bottleneck reroute scripts+(video 12, `thermal-reroute/`), whose public source release is still pending on the Codex side,+and the shared EDA skillpack (adom/eda-engineering), which is browseable source, not an+installable package yet. A reproduction that needs them must say so.