main
Colby Knox 0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom f82b5b5 10h ago
//! 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."})
}