app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
Colby Knox
0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom
f82b5b5
10h ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
//! Altium's own autorouter (Situs) as a verb: the strategy dialog answered through UI Automation, existing
//! routing locked by default, and the board's connectivity before and after.
use crate::support::{self, s, s_or, timeout_of};
use crate::{ab, agent, instance};
use serde_json::{json, Value};
use std::time::Duration;
fn drc_counts() -> (Value, Value) {
let r = crate::routing::routing_drc(&json!({}));
(r.get("unconnected").cloned().unwrap_or(Value::Null), r.get("errors").cloned().unwrap_or(Value::Null))
}
fn find_window(needle: &str) -> Option<Value> {
instance::invalidate_snapshot();
instance::altium_windows().into_iter().find(|w| w.get("kind").and_then(|k| k.as_str()) != Some("main")
&& w.get("title").and_then(|t| t.as_str()).map(|t| t.to_lowercase().contains(needle)).unwrap_or(false))
}
/// The toggle state of a named checkbox in a dialog's UI tree (On / Off / None).
fn checkbox_state(tree: &Value, name: &str) -> Option<String> {
fn walk(n: &Value, name: &str) -> Option<String> {
if n.get("name").and_then(|v| v.as_str()) == Some(name) { return n.get("toggleState").and_then(|v| v.as_str()).map(str::to_string); }
for c in n.get("children").and_then(|c| c.as_array()).into_iter().flatten() { if let Some(x) = walk(c, name) { return Some(x); } }
None
}
let root = tree.get("root").cloned().or_else(|| tree.get("output").and_then(|o| o.as_str()).and_then(|t| serde_json::from_str::<Value>(t).ok()).and_then(|v| v.get("root").cloned())).unwrap_or(Value::Null);
walk(&root, name)
}
/// altium_autoroute {filePath?, strategy?, lockPreroutes?, ripupViolations?, save?, timeout?}
pub fn autoroute(args: &Value) -> Value {
if let Some(g) = support::need_licensed() { return g; }
let t0 = support::now();
if let Some(p) = s(args, "filePath") { if let Some(e) = support::must_open(&p, 60) { return e; } }
let (unc_before, err_before) = drc_counts();
agent::call("run_process", &[("process", "PCB:Autoroute".into()), ("params", "Action=All".into())], None, Duration::from_secs(6));
let mut dlg = None;
for _ in 0..40 { if let Some(w) = find_window("situs routing strategies") { dlg = Some(w); break; } std::thread::sleep(Duration::from_millis(500)); }
let Some(dlg) = dlg else {
return json!({"success": false, "errorCode": "situs_dialog_not_found", "_hint": "PCB:Autoroute did not open its strategy dialog: is a PCB the current document? (filePath opens one)"});
};
let h = dlg.get("hwnd").cloned().unwrap_or(Value::Null);
let mut steps = Vec::new();
let strategy = s_or(args, "strategy", "");
if !strategy.is_empty() {
let r = ab::call("desktop_ui_select", json!({"hwnd": h, "name": strategy, "role": "listitem"}), &[], 20);
steps.push(json!({"strategy": strategy, "selected": r.get("success")}));
if r.get("success").and_then(|b| b.as_bool()) != Some(true) {
let _ = ab::call("desktop_ui_click", json!({"hwnd": h, "name": "Cancel"}), &[], 15);
return json!({"success": false, "errorCode": "strategy_not_found", "strategy": strategy, "_hint": "Strategies Altium ships: Cleanup, Default 2 Layer Board, Default 2 Layer With Edge Connectors, Default Multi Layer Board, General Orthogonal, Via Miser."});
}
}
// existing routing stays unless lockPreroutes:false (Situs rips it up otherwise: measured)
let tree = ab::call("desktop_ui_tree", json!({"hwnd": h, "maxDepth": 10, "maxNodes": 200, "roleFilter": ["checkbox"]}), &[], 20);
for (name, want) in [("Lock All Pre-routes", args.get("lockPreroutes").and_then(|b| b.as_bool()).unwrap_or(true)),
("Rip-up Violations After Routing", args.get("ripupViolations").and_then(|b| b.as_bool()).unwrap_or(false))] {
let on = checkbox_state(&tree, name).map(|x| x.eq_ignore_ascii_case("On")).unwrap_or(false);
if on != want {
let r = ab::call("desktop_ui_toggle", json!({"hwnd": h, "name": name}), &[], 15);
steps.push(json!({"toggle": name, "to": want, "success": r.get("success")}));
}
}
let go = ab::call("desktop_ui_click", json!({"hwnd": h, "name": "Route All"}), &[], 30);
steps.push(json!({"uiClick": "Route All", "success": go.get("success")}));
// Situs routes while the script engine waits; done when the agent answers and the board stops changing
let wait = timeout_of(args, 600);
let t1 = std::time::Instant::now();
std::thread::sleep(Duration::from_secs(3));
while t1.elapsed() < wait && !agent::is_responsive() { std::thread::sleep(Duration::from_millis(700)); }
let mut last = Value::Null;
let mut stable = 0;
while t1.elapsed() < wait {
let (u, e) = drc_counts();
let now = json!([u, e]);
if now == last { stable += 1; if stable >= 2 { break; } } else { stable = 0; }
last = now;
std::thread::sleep(Duration::from_secs(2));
}
let (unc_after, err_after) = drc_counts();
let saved = if args.get("save").and_then(|b| b.as_bool()) == Some(true) { json!(crate::surfaces::save(&json!({})).get("success")) } else { json!(false) };
json!({"success": go.get("success").and_then(|b| b.as_bool()) == Some(true), "strategy": if strategy.is_empty() { json!("(the dialog's current)") } else { json!(strategy) },
"before": {"unconnected": unc_before, "errors": err_before}, "after": {"unconnected": unc_after, "errors": err_after},
"steps": steps, "saved": saved, "elapsedSec": support::elapsed_round(t0), "windowEtiquette": "background",
"_hint": "Situs routed the board in place. Look before keeping it (altium_show_2d_board) and check with altium_routing_drc; save with altium_save (or save:true), or close without saving to drop it. Parts off the board outline get routed out to wherever they sit: place them first."})
}