← Commit history
CHANGELOG.md+11
@@ -1,5 +1,16 @@ # Changelog +## 0.2.58 (2026-10-21)+- `altium_autoroute`: Altium's Situs autorouter on the current board.+  - It picks a strategy and keeps existing routing locked by default; Situs ripped it up with the dialog's default.+  - It answers the unconnected count and DRC errors before and after, and doesn't save unless asked.+  - Measured: one ripped-up net went from 4 unconnected to 1 in 14 s, with the existing copper untouched.+- `altium_layer_stack`: copper layers in order, with copper thickness and the dielectric under each (height, Dk, material) and the total thickness.+  - It uses `Board.LayerStack_V7`; on AD 26 the classic `LayerStack.FirstLayer` halts the script.+  - Measured: the buck reads 2 layers, 36 um copper over 320 um FR-4; the Mini PC example reads 16 layers of TU-883, 2.0 mm.+- `altium_read_rules` carries each rule's `descriptor`, Altium's wording with the values (e.g. `Clearance Constraint (Gap=6mil)`), so a DRC result can be explained.+- `altium_sch_zoom`: selects parts on a sheet, frames them, and returns a frame. It is the schematic half of cross-probing.+ ## 0.2.57 (2026-10-21) **Schematic editing on an existing design, and the change order to the board.** - `altium_sch_place_part`: places a .SchLib part on an existing sheet at a position, with its library parameters and footprint links (or a named footprint).
SKILL.md+3−2
@@ -23,8 +23,9 @@ Say **altium** to reach this bridge. Every verb returns `success`, a stable `err | Number `?` designators, set the title block | `altium_annotate`, `altium_doc_params` (**altium-schematic-edit**) | | Push schematic changes to the board | `altium_update_pcb` (previews; `execute:true` runs the change order) (**altium-schematic-edit**) | | A BOM, prices, stock, JLCPCB / Mouser / DigiKey files | `altium_bom`, `altium-bom price` / `fill` (**altium-bom**) |-| Check the design | `altium_compile` (ERC), `altium_run_drc`, `altium_routing_drc`, `altium_review_board`, `altium_cross_check` |-| Place, route, pour, silkscreen a board | `altium_move_parts`, `altium_route_net`, `altium_place_vias`, `altium_apply_pours`, `altium_apply_silkscreen`, `altium_set_design_rules` (**altium-boards**) |+| Check the design | `altium_compile` (ERC), `altium_run_drc`, `altium_routing_drc`, `altium_review_board`, `altium_cross_check`, `altium_read_rules` (rule values) |+| Find a part on the sheet or board | `altium_sch_zoom`, `altium_zoom_to` |+| Place, route, pour, silkscreen a board | `altium_move_parts`, `altium_route_net`, `altium_autoroute` (Situs), `altium_place_vias`, `altium_apply_pours`, `altium_apply_silkscreen`, `altium_set_design_rules`, `altium_layer_stack` (**altium-boards**) | | Manufacturing files, STEP, PDF | `altium_export_manufacturing`, `altium_export_step`, `altium_export_pdf`, `altium_run_outjob` (**altium-outputs**) | | Make or fix library parts | `altium_import_part`, `altium_build_symbol_modes`, `altium_build_footprint`, `altium_add_body`, `altium_set_params` (**altium-libraries**, **altium-part-import**) | | Parts from the Adom wiki | `adom-wiki library export --format altium`, `altium_install_library_bundle` (**altium-wiki-import**) |
bridge-rs/src/main.rs+2−1
@@ -19,6 +19,7 @@ mod misc; mod outputs; mod progress; mod review;+mod route; mod routing; mod shot; mod support;@@ -33,7 +34,7 @@ use tiny_http::{Header, Method, Response, Server}; /// Attached to every failed reply (see the /command handler). const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend."; -pub const BRIDGE_VERSION: &str = "0.2.57";+pub const BRIDGE_VERSION: &str = "0.2.58";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/route.rsadded+85
@@ -0,0 +1,85 @@+//! 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."})+}
bridge-rs/src/sch.rs+21
@@ -107,6 +107,27 @@ pub fn place_part(args: &Value) -> Value {         "_hint": "Placed with its library parameters and footprint link. Wire it with altium_sch_connect {designator, pin, net}; a '?' designator is filled by altium_annotate; the board gets the part with altium_update_pcb."}) } +/// altium_sch_zoom {sheet, designators}: select those parts on the sheet and frame them, with a proof frame.+pub fn zoom(args: &Value) -> Value {+    if let Some(g) = support::need_licensed() { return g; }+    let sheet = match need_sheet(args) { Ok(x) => x, Err(e) => return e };+    let des: Vec<String> = match args.get("designators").or_else(|| args.get("designator")) {+        Some(Value::Array(a)) => a.iter().map(st).collect(),+        Some(Value::String(t)) => t.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(),+        _ => vec![],+    };+    if des.is_empty() { return json!({"success": false, "errorCode": "bad_args", "error": "designators is required"}); }+    if let Some(e) = support::must_open(&sheet, 60) { return e; }+    let r = agent::call("sch_zoom", &[("designators", des.iter().map(|d| clean(d)).collect::<Vec<_>>().join(","))], None, timeout_of(args, 60));+    let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+    std::thread::sleep(Duration::from_millis(600));+    let shot = if args.get("shot").and_then(|b| b.as_bool()) == Some(false) { Value::Null } else { crate::shot::shot_of_main() };+    let found: Vec<String> = d["parts"].as_array().cloned().unwrap_or_default().iter().map(|p| st(&p["designator"])).collect();+    let missing: Vec<&String> = des.iter().filter(|x| !found.iter().any(|f| f.eq_ignore_ascii_case(x))).collect();+    json!({"success": missing.is_empty(), "errorCode": if missing.is_empty() { Value::Null } else { json!("not_on_sheet") }, "sheet": sheet, "parts": d["parts"], "notOnSheet": missing, "shot": shot,+        "_hint": "The parts are selected and framed on the sheet (the board side: altium_zoom_to {designators})."})+}+ /// altium_doc_params {sheet, set?:{Title, DocumentNumber, Revision, DrawnBy, ...}, save} pub fn doc_params(args: &Value) -> Value {     if let Some(g) = support::need_licensed() { return g; }
bridge-rs/src/verbs.rs+27−5
@@ -20,7 +20,7 @@ pub const KNOWN: &[&str] = &[     "read_board", "board_summary", "rules_by_kind", "layers_in_use", "read_schematic",     "cross_check", "review_board", "list_designators", "component_data", "component_pins",     "list_nets", "net_connections", "layers", "copper_layers", "read_rules",-    "set_layer_visibility", "footprint_primitives", "schematic_data",+    "set_layer_visibility", "footprint_primitives", "schematic_data", "layer_stack",     // the live-board lane AI Flow drives     "routing_state", "route_net", "move_parts", "place_vias", "remove_items", "ripup_nets", "apply_pours", "pour_state", "remove_pours", "repour", "routing_drc", "build_board", "set_outline", "set_design_rules", "zoom_to", "add_rule", "build_schematic", "export_manufacturing", "select_net", "silk_state", "apply_silkscreen",     // surfaces@@ -28,7 +28,7 @@ pub const KNOWN: &[&str] = &[     "show_symbol", "show_footprint", "show_3d_chip", "list_library", "demo",     "animate_view", "foreground",     // outputs / project-    "compile", "run_erc", "run_drc", "run_outjob", "export_gerber", "export_bom", "export_step", "bom", "sheet_params", "sch_place_part", "sch_connect", "sch_edit", "annotate", "doc_params", "update_pcb", "export_pdf",+    "compile", "run_erc", "run_drc", "run_outjob", "export_gerber", "export_bom", "export_step", "bom", "sheet_params", "sch_place_part", "sch_connect", "sch_edit", "annotate", "doc_params", "update_pcb", "export_pdf", "sch_zoom", "autoroute",     "save", "test_board", "board_shape", "pcb_report", "import_kicad",     "install_extension", "extension_ping",     // library authoring@@ -387,10 +387,16 @@ fn describe() -> Value {             json!({"data": "{layers: [{name, id, kind, displayed}]}", "roundtripMs": "int"}), 90,             "Do not count these as the board's layers: altium_layers_in_use says which carry copper.",             &["layers", "layers_in_use"], &["Needs a .PcbDoc current.", "A capability list (the same on any board), not the stack in use."], json!({})),-        verb_doc("read_rules", "Every design rule on the current board, flat, disabled ones included: name, kind ordinal, enabled, priority, both scope expressions, comment.", json!({}),-            json!({"data": "{count, rules: [{name, kind: int, enabled, priority, scope1, scope2, comment}]}", "roundtripMs": "int"}), 90,+        verb_doc("layer_stack", "The CURRENT board's stack-up: copper layers in order with copper thickness and the dielectric under each (height, constant, material), and the total thickness. For impedance, current capacity and fab quotes.",+            json!({}),+            json!({"data": "{copperLayers, totalUm, layers: [{name, copperUm, dielectricUm (the dielectric BELOW this layer), dk1000 (dielectric constant x 1000), material}]}"}), 60,+            "Open the board first (altium_show_2d_board). dk1000 is the dielectric constant times 1000 (4200 = 4.2).",+            &["layers", "copper_layers", "read_rules"], &["Solder mask and overlay layers are not part of this list; the copper and the dielectric under each are."],+            json!({})),+        verb_doc("read_rules", "Every design rule on the current board, flat, disabled ones included: name, kind ordinal, enabled, priority, both scope expressions, comment, and Altium's descriptor with the rule's VALUES (e.g. 'Clearance Constraint (Gap=6mil) (All),(All)').", json!({}),+            json!({"data": "{count, rules: [{name, kind: int, enabled, priority, scope1, scope2, comment, descriptor}]}", "roundtripMs": "int"}), 90,             "altium_rules_by_kind groups the enabled ones under readable kind names; altium_run_drc checks the board against them.",-            &["rules_by_kind", "run_drc"], &["Needs a .PcbDoc current.", "kind is Altium's TRuleKind ORDINAL; rule values (the actual clearance/width numbers) are not read."], json!({})),+            &["rules_by_kind", "run_drc"], &["Needs a .PcbDoc current.", "kind is Altium's TRuleKind ORDINAL; the values are in descriptor, as Altium words them."], json!({})),         verb_doc("set_layer_visibility", "Show or hide one layer of the current board (by layer id) and refresh the view and the layer tabs.",             json!({"layerId": "int (required) - an id from altium_layers", "displayed": "bool, default true"}),             json!({"data": "{layerId, name, displayed}", "roundtripMs": "int"}), 60,@@ -652,6 +658,19 @@ fn describe() -> Value {             &["sch_connect", "update_pcb", "sheet_params"],             &["Every part of a multi-part component with that designator is changed together."],             json!({"sheet": "C:/proj/Main.SchDoc", "edits": [{"designator": "R5", "action": "comment", "text": "10k"}, {"designator": "C9", "action": "delete"}]})),+        verb_doc("sch_zoom", "Find parts on a sheet: select them (everything else deselected), frame the view on them and return a proof frame. The schematic half of cross-probing (the board half: altium_zoom_to).",+            json!({"sheet": "path to the .SchDoc (required)", "designators": "[string] or 'R1,U2' (required)", "shot": "bool, default true"}),+            json!({"parts": "[{designator, xMil, yMil}]", "notOnSheet": "[string]", "shot": "frame of Altium's main window"}), 60,+            "To find which sheet holds a part, altium_bom (or altium_schematic_data per sheet) lists every designator with its sheet.",+            &["zoom_to", "schematic_data", "sch_edit"], &[],+            json!({"sheet": "C:/proj/Main.SchDoc", "designators": ["U1", "L1"]})),+        verb_doc("autoroute", "Altium's own autorouter (Situs) on the current board: picks a strategy, keeps existing routing locked by default, routes, and answers the unconnected count and DRC errors before and after. Not saved unless save:true.",+            json!({"filePath": "path to the .PcbDoc - optional, else the current board", "strategy": "Default 2 Layer Board | Default Multi Layer Board | Default 2 Layer With Edge Connectors | General Orthogonal | Via Miser | Cleanup (default: the dialog's current)", "lockPreroutes": "bool, default true - existing tracks are kept", "ripupViolations": "bool, default false", "save": "bool, default false", "timeout": "seconds, default 600"}),+            json!({"before": "{unconnected, errors}", "after": "{unconnected, errors}", "steps": "[..]", "saved": "bool"}), 900,+            "Look at the result before keeping it (altium_show_2d_board) and check it with altium_routing_drc. Altium's rules drive Situs: set them first (altium_set_design_rules, altium_add_rule).",+            &["route_net", "routing_drc", "set_design_rules", "save"],+            &["Parts outside the board outline get routed to where they sit: place them first (altium_move_parts).", "With lockPreroutes:false Situs rips up and reroutes existing tracks."],+            json!({"filePath": "C:/proj/board.PcbDoc", "strategy": "Default 2 Layer Board"})),         verb_doc("annotate", "Number every '?' designator (R?, C?, U?) across the project's sheets: the lowest free number per prefix, top-left first on each sheet. dryRun shows the plan.",             json!({"projectPath": "path to the .PrjPcb (required)", "dryRun": "bool, default false", "save": "bool, default true"}),             json!({"renamed": "[{sheet, from, to, result}]", "planned": "[..] (dryRun)", "skippedMultiPart": "[{sheet, designator, libReference, partId}]"}), 300,@@ -1266,6 +1285,7 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) ->                 board_read("set_layer_visibility", vec![("layerId", l), ("displayed", if shown { "1".into() } else { "0".into() })], 60)             }         },+        "layer_stack" => board_read("layer_stack", vec![], 60),         "footprint_primitives" => match s(args, "designator") {             None => json!({"success": false, "error": "designator is required (a placed component, e.g. U1)"}),             Some(d) => board_read("footprint_primitives", vec![("designator", d)], 120),@@ -1346,6 +1366,8 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) ->         "doc_params" => crate::sch::doc_params(args),         "update_pcb" => crate::sch::update_pcb(args),         "export_pdf" => crate::sch::export_pdf(args),+        "sch_zoom" => crate::sch::zoom(args),+        "autoroute" => crate::route::autoroute(args),         "export_step" => outputs::export_step(args),         "test_board" => library::test_board(args),         "import_kicad" => outputs::import_kicad(args),
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.57",+  "version": "0.2.58",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
page.json+1−1
@@ -4,7 +4,7 @@   "type": "app",   "title": "Altium - the Altium Bridge",   "brief": "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.",-  "version": "0.2.57",+  "version": "0.2.58",   "hero": {     "type": "image",     "path": "screenshots/hero.png"
skills/altium-boards/SKILL.md+22
@@ -155,3 +155,25 @@ once per part (`partId`). `schematic_data` returns an EMPTY `params[]` unless you ask: pass `params:true` (from 0.2.38 — the agent always honoured the flag, but the bridge never sent it, so the field came back empty no matter what). `altium_read_schematic` is still the fuller parameter view.++## Autorouting, stack-up and rule values (0.2.57)++```bash+altium_autoroute '{"filePath":"C:\\proj\\board.PcbDoc","strategy":"Default 2 Layer Board","timeout":600}'+altium_layer_stack '{}'      # the current board+altium_read_rules '{}'       # every rule with its descriptor (the values)+```++- `altium_autoroute` runs Altium's Situs router with the strategy named (Default 2 Layer Board, Default+  Multi Layer Board, Default 2 Layer With Edge Connectors, General Orthogonal, Via Miser, Cleanup). It keeps+  existing routing locked (`lockPreroutes:true`, the default: Situs rips it up otherwise). It answers the+  unconnected count and DRC errors before and after (measured: one ripped-up net, 4 unconnected to 1 in+  14 s, existing copper untouched). Not saved unless `save:true`: look at it first. Parts outside the+  outline get routed out to wherever they sit, so place them first. Situs follows the board's rules: set+  them first with `altium_set_design_rules` / `altium_add_rule`.+- `altium_layer_stack` gives copper layers in order with copper thickness (um) and the dielectric under each+  (um, Dk x 1000, material) and the total thickness. Measured: the buck reads 2 layers, 36 um copper, 320 um+  FR-4, 391 um; Altium's Mini PC example reads 16 layers of TU-883, 2008 um.+- `altium_read_rules` now carries each rule's `descriptor`, Altium's own wording with the values, for+  example `Clearance Constraint (Gap=6mil) (All),(All)`: what a DRC violation is measured against.+
skills/altium-schematic-edit/SKILL.md+9
@@ -45,6 +45,15 @@ altium_update_pcb     '{"projectPath":"C:\\proj\\board.PrjPcb","execute":true,"t   top-left first. `dryRun:true` shows the plan. Multi-part components with `?` are left to Altium's own   Tools > Annotation (`skippedMultiPart`). +## Find parts on the sheet (cross-probe)++```bash+altium_sch_zoom '{"sheet":"C:\\proj\\Main.SchDoc","designators":["U1","L1"]}'+```++Selects them, frames the view on them and returns a frame (`notOnSheet` names any that are not there).+On the board, `altium_zoom_to {designators}` does the same.+ ## Change parts  ```bash
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.57+0.2.58
src/agent/AdomAgent.pas.tmpl+77−1
@@ -4571,6 +4571,40 @@ Begin         JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}'; End; +{ layer_stack: the copper layers in stack order with their copper thickness and the dielectric under each+  (height, constant x1000, material). Classic stack API (FirstLayer / NextLayer / Dielectric). }+Function CmdLayerStack(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    LS    : IPCB_LayerStack_V7;+    L     : IPCB_LayerObject_V7;+    S     : String;+    N, Tot : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    S := ''; N := 0; Tot := 0;+    LS := Board.LayerStack_V7;+    L := LS.FirstLayer;+    While (L <> Nil) And (N < 64) Do+    Begin+        If S <> '' Then S := S + ',';+        S := S + '{' + JStr('name', L.Name) + ',' + JRaw('copperUm', IntToStr(Round(CoordToMils(L.CopperThickness) * 25.4)));+        Tot := Tot + L.CopperThickness;+        { a layer's dielectric is the one below it: the last copper layer has none in the board }+        If (L.Dielectric <> Nil) And (LS.NextLayer(L) <> Nil) Then+        Begin+            S := S + ',' + JRaw('dielectricUm', IntToStr(Round(CoordToMils(L.Dielectric.DielectricHeight) * 25.4))) + ',' ++                JRaw('dk1000', IntToStr(Round(L.Dielectric.DielectricConstant * 1000))) + ',' + JStr('material', L.Dielectric.DielectricMaterial);+            Tot := Tot + L.Dielectric.DielectricHeight;+        End;+        S := S + '}';+        N := N + 1;+        L := LS.NextLayer(L);+    End;+    Result := '{' + JRaw('ok','true') + ',' + JRaw('copperLayers', IntToStr(N)) + ',' + JRaw('totalUm', IntToStr(Round(CoordToMils(Tot) * 25.4))) + ',' + JRaw('layers', '[' + S + ']') + '}';+End;+ { layers: the fixed layer set with names and visibility (stackup detail is a later verb) } Function CmdLayers(Lines : TStringList) : String; Var@@ -4922,7 +4956,7 @@ Begin         S := S + '{' + JStr('name', Rule.Name) + ',' + JRaw('kind', IntToStr(Rule.RuleKind)) + ',' +             JRaw('enabled', JBool(Rule.Enabled)) + ',' + JRaw('priority', IntToStr(Rule.Priority)) + ',' +             JStr('scope1', Rule.Scope1Expression) + ',' + JStr('scope2', Rule.Scope2Expression) + ',' +-            JStr('comment', Rule.Comment) + '}';+            JStr('comment', Rule.Comment) + ',' + JStr('descriptor', Rule.Descriptor) + '}';         N := N + 1;         Rule := BIt.NextPCBObject;     End;@@ -6930,6 +6964,46 @@ Begin     Result := '{' + JRaw('ok','true') + ',' + JRaw('results', '[' + S + ']') + '}'; End; +{ sch_zoom: select the named parts on the current sheet (everything else deselected) and zoom the view+  to them. designators=R1,U2 }+Function CmdSchZoom(Lines : TStringList) : String;+Var+    Doc  : ISch_Document;+    It   : ISch_Iterator;+    C    : ISch_Component;+    Want : TStringList;+    Found, S : String;+    N : Integer;+Begin+    Doc := SchServer.GetCurrentSchDocument;+    If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;+    Want := TStringList.Create;+    Want.Delimiter := ',';+    Want.StrictDelimiter := True;+    Want.DelimitedText := UpperCase(Param(Lines, 'designators'));+    N := 0; S := '';+    It := Doc.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eSchComponent));+    C := It.FirstSchObject;+    While C <> Nil Do+    Begin+        If Want.IndexOf(UpperCase(C.Designator.Text)) >= 0 Then+        Begin+            C.Selection := True;+            N := N + 1;+            If S <> '' Then S := S + ',';+            S := S + '{' + JStr('designator', C.Designator.Text) + ',' + JRaw('xMil', IntToStr(Round(CoordToMils(C.Location.X)))) + ',' + JRaw('yMil', IntToStr(Round(CoordToMils(C.Location.Y)))) + '}';+        End+        Else C.Selection := False;+        C := It.NextSchObject;+    End;+    Doc.SchIterator_Destroy(It);+    Want.Free;+    Doc.GraphicallyInvalidate;+    If N > 0 Then Begin ResetParameters; AddStringParameter('Object', 'Selected'); RunProcess('Sch:Zoom'); End;+    Result := '{' + JRaw('ok','true') + ',' + JRaw('selected', IntToStr(N)) + ',' + JRaw('parts', '[' + S + ']') + '}';+End;+ Function CmdBuildSchematic(Lines : TStringList; Payload : String) : String; Var     SLib   : ISch_Lib;@@ -7616,6 +7690,7 @@ Begin     Else If Cmd = 'pcb_report' Then Result := CmdPcbReport(Lines)     Else If Cmd = 'list_designators' Then Result := CmdListDesignators(Lines)     Else If Cmd = 'component_data' Then Result := CmdComponentData(Lines)+    Else If Cmd = 'layer_stack' Then Result := CmdLayerStack(Lines)     Else If Cmd = 'bom_placement' Then Result := CmdBomPlacement(Lines)     Else If Cmd = 'component_pins' Then Result := CmdComponentPins(Lines)     Else If Cmd = 'list_nets' Then Result := CmdListNets(Lines)@@ -7628,6 +7703,7 @@ Begin     Else If Cmd = 'sch_place_part' Then Result := CmdSchPlacePart(Lines)     Else If Cmd = 'doc_params' Then Result := CmdDocParams(Lines)     Else If Cmd = 'sch_rename_at' Then Result := CmdSchRenameAt(Lines, Payload)+    Else If Cmd = 'sch_zoom' Then Result := CmdSchZoom(Lines)     Else If Cmd = 'copper_layers' Then Result := CmdCopperLayers(Lines)     Else If Cmd = 'read_rules' Then Result := CmdReadRules(Lines)     Else If Cmd = 'set_layer_visibility' Then Result := CmdSetLayerVisibility(Lines)
src/bridge.json+6−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.57",+  "version": "0.2.58",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -45,6 +45,7 @@     "altium_annotate",     "altium_apply_pours",     "altium_apply_silkscreen",+    "altium_autoroute",     "altium_board_shape",     "altium_board_summary",     "altium_bom",@@ -93,6 +94,7 @@     "altium_install_receipt",     "altium_instances",     "altium_launch",+    "altium_layer_stack",     "altium_layers",     "altium_layers_in_use",     "altium_license",@@ -142,6 +144,7 @@     "altium_sch_connect",     "altium_sch_edit",     "altium_sch_place_part",+    "altium_sch_zoom",     "altium_schematic_data",     "altium_select_net",     "altium_selftest_script_error",@@ -285,7 +288,8 @@       "sch_edit": 90,       "annotate": 300,       "update_pcb": 600,-      "export_pdf": 600+      "export_pdf": 600,+      "autoroute": 900     },     "prefixes": [       {