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.
← Commit history
0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom
13 files changed
+266−14
CHANGELOG.md+11SKILL.md+3−2bridge-rs/src/main.rs+2−1bridge-rs/src/route.rs+85bridge-rs/src/sch.rs+21bridge-rs/src/verbs.rs+27−5package.json+1−1page.json+1−1skills/altium-boards/SKILL.md+22skills/altium-schematic-edit/SKILL.md+9src/BRIDGE_VERSION+1−1src/agent/AdomAgent.pas.tmpl+77−1src/bridge.json+6−2CHANGELOG.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": [ {