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.57 source (not yet released, needs signing): schematic editing (sch_place_part, sch_connect, sch_edit, annotate, doc_params), update_pcb, export_pdf, the health-check answer
14 files changed
+1159−10
CHANGELOG.md+18SKILL.md+25−1bridge-rs/src/main.rs+2−1bridge-rs/src/outputs.rs+34−1bridge-rs/src/sch.rs+463bridge-rs/src/verbs.rs+55−1install.sh+1package.json+2−1page.json+1−1skills/altium-schematic-edit/SKILL.md+98src/BRIDGE_VERSION+1−1src/agent/AdomAgent.pas.tmpl+443src/bridge.json+15−2uninstall.sh+1−1CHANGELOG.md+18@@ -1,5 +1,23 @@ # Changelog +## 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).+- `altium_sch_connect`: wire stubs from pins' outer ends, with a net label (`kind:"label"`), a power port (`kind:"power"`; GND-like nets get a ground symbol) or nothing. Each connection reports ok or why not.+- `altium_sch_edit`: move, moveBy, rotate, rename, re-comment or delete placed parts, in one undo step.+- `altium_annotate`: numbers every `R?`, `C?`, `U?` with the lowest free number per prefix across the project. Each part is named by its position, so several `R?` on one sheet are told apart. `dryRun` shows the plan.+- `altium_doc_params`: reads or sets a sheet's title-block parameters.+- `altium_update_pcb`: Design > Update PCB Document.+ - It previews by default: parts to add, and board-only parts the change order would delete.+ - `execute:true` runs it, and refuses deletions unless `allowRemovals:true`.+ - It answers the one-time "link parts by designator" prompt on a board built by script (a real click: that dialog's buttons ignore Invoke) and the Information box after it.+ - It presses Execute Changes, waits until every row is done (the agent is parked while the modal is up, so it cannot be the signal), closes, and **saves the board**. The change order edits it in memory only; a first test lost every change by closing it.+ - Verified live on AD 26.11.1: R9 and R10 placed, annotated and connected on a sheet reached the board with their nets (R9: VOUT/GND, R10: VIN/GND) and survived a close and reopen. A comment-only change on an existing part opens no change order on that build; documented.+- `altium_export_pdf`: every sheet as one PDF, plus the board's composite print with `board:true`. It runs through an OutJob in a throwaway project, so the user's project is untouched. Verified: a 2-page PDF in 14 s.+- `altium_run_outjob` (and every verb built on it: manufacturing, STEP, PDF) answers Altium's "PCB Health Check Issues Detected" modal, which otherwise stopped the job indefinitely. It presses Generate Outputs (`healthCheck:"stop"` cancels) and returns the dialog's text in `healthCheck`.+- `schematic_data` also reports each part's `uniqueId`.+- New skill **altium-schematic-edit**. The main skill opens with an "I want to..." map of the whole bridge, and notes that `altium_eval` does nothing while the agent is resident.+ ## 0.2.56 (2026-10-21) - **`altium_bom`: the project's BOM, read straight from Altium.** - Reads every schematic sheet the `.PrjPcb` lists, managed device sheets included, with each part's parameters.
SKILL.md+25−1@@ -13,6 +13,26 @@ adom-bridge --ai-thread "<your thread name>" altium_<verb> '<json args>' Say **altium** to reach this bridge. Every verb returns `success`, a stable `errorCode` on failure, and a `_hint` that tells you the next move — **relay the `_hint` verbatim and branch on `errorCode`, never on prose.** +## I want to... (start here)++| Goal | Verbs (skill) |+|---|---|+| See a design | `altium_show_project` / `_schematic` / `_2d_board` / `_3d_board` (frames come back as images), `altium_state` |+| Read a design as data | `altium_schematic_data` (parts, pins, parameters, kind, workspace ids), `altium_routing_state` (the whole board in mm), `altium_read_board`, `altium_net_connections` |+| Add a part / wire pins / change values / delete | `altium_sch_place_part`, `altium_sch_connect`, `altium_sch_edit`, `altium_sheet_params` (**altium-schematic-edit**) |+| 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**) |+| 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**) |+| Altium 365 workspace | `altium_signin_status`, `altium_a365_*` (needs API access) (**altium-a365**) |+| Anything else Altium can do | `altium_list_commands {query}` then `altium_run_command {command, params}` (1,100 processes) |++Long verbs: pass `timeout` (seconds) in the args; ab waits for the call's own `timeout`.+ ## FIRST-TIME / COLD-START — read before you panic Altium is a licensed host app. The bridge never installs it silently; it detects it, launches it, and gets the script engine ready.@@ -77,6 +97,9 @@ Altium is a licensed host app. The bridge never installs it silently; it detects | `altium_close_document` | close one open document through its handle; never saves | `path` | | `altium_read_board` / `altium_board_summary` / `altium_rules_by_kind` / `altium_layers_in_use` | ONE agent round trip for the whole board (components, nets, layers, copper, rules, occupancy, outline) and three views shaped from it | `sections`, `limit`, `raw:true` for the unshaped payload | | `altium_read_schematic` | components on the CURRENT sheet with their parameters | `sections`, `params:false`, `limit` |+| `altium_sch_place_part` / `altium_sch_connect` / `altium_sch_edit` (from 0.2.57) | place a library part on an existing sheet / wire stubs with net labels or power ports at pins / move, rotate, rename, re-comment, delete | `sheet`, `library`, `symbol` / `connections[]` / `edits[]` |+| `altium_annotate` / `altium_doc_params` / `altium_update_pcb` / `altium_export_pdf` (from 0.2.57) | number `?` designators / title block / the change order to the board (preview by default) / every sheet as one PDF | `projectPath` / `sheet`, `set` / `execute`, `allowRemovals` / `board` |+| `altium_bom` / `altium_sheet_params` (from 0.2.56) | the project BOM with order checks and distributor files / write parameters onto placed parts | `projectPath`, `variant`, `boards` / `designators`, `params` | | `altium_cross_check` | designators on the sheet but not the board and the reverse (`matched`, `onSheetNotOnBoard`, `onBoardNotOnSheet`, `sameProject`) | `board`, `sheet` (paths, optional; opened in turn) | | `altium_review_board` | 14 design-review checks over one board read, schematic-side checks with `includeSchematic`; partial by design (`skipped`, `notImplemented`) | `board`, `sheet`, `includeSchematic`, `checks[]` | | `altium_license_status` / `altium_ensure_licensed` / `altium_setup_trial` | licence state in words + page frame / launch, claim a seat or name the human step / the one next onboarding action (see `altium-licensing`) | — / `launchWaitSec`, `allowSignInReset`, `allowTrial` / — |@@ -107,11 +130,12 @@ adom-bridge --ai-thread "board review" altium_export_gerber '{"outjobPath":"C:/d - `altium_import_kicad` / `altium_install_extension` (and `altium_export_step {lane:"extension"}`) ship Barrett's AdomBridge .NET extension, but on AD 26.9.1 the server never loads into the running instance (measured; see the plan skill). They fail loudly with `extension_timeout` until it is rebuilt for AD 26. - Headless STEP IS real: `altium_export_step {filePath, output}` (default lane `outjob`) runs Altium's own ExportSTEP output from a throwaway project in %TEMP%, with no dialog and no click (measured on AD 26.9.1, 2026-10-09). Altium names every part by its DESIGNATOR; `suffixType` none/board/custom/id only appends a suffix, and `products[]` in the reply shows the names.-- `altium_a365_*` need a token; the Develop trial workspace has no Admin → Developer page.+- `altium_a365_*` need API access: Altium's 365 API is a closed beta (enroll at developer.altium.com); a workspace on the Altium Develop plan shows no Admin → Developer page. The signed-in Altium Designer itself reaches the workspace: its Components panel searches it (driven through UI Automation; a verb is in progress). ## DelphiScript facts you need for `altium_eval` - Statements only — `Begin … End;` — no `Var` block (use `altium_run_script` with a `.PrjScr` for real units, and only when the agent is stopped).+- While the resident agent runs, `altium_eval` hands the text to RunScriptText from inside the agent and Altium ignores the nested script (measured on AD 26.11.1: `success:true`, nothing ran). Stop the agent first, or use a structured verb. - Single quotes for strings; no double quotes anywhere in the text. - Unknown identifiers fail at **runtime** with an `Error` modal and a halted engine → `altium_agent_recover`. - To get data OUT, write a file from the script and read it back; `altium_run_command` / the structured verbs already do this for the common cases.
bridge-rs/src/main.rs+2−1@@ -6,6 +6,7 @@ mod ab; mod agent; mod board; mod bom;+mod sch; mod dialogs; mod health; mod install;@@ -32,7 +33,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.56";+pub const BRIDGE_VERSION: &str = "0.2.57"; 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/outputs.rs+34−1@@ -61,9 +61,42 @@ pub fn run_outjob(args: &Value) -> Value { return r; } }+ // Altium stops an OutJob on a board with health-check findings (shelved or modified polygons, ...) with a+ // modal the agent cannot answer while it waits: a watcher answers it (Generate Outputs, or Cancel with+ // healthCheck:"stop") and the reply says what it found (measured 2026-10-21 on a board after a change order)+ let stop = s_or(args, "healthCheck", "continue") == "stop";+ let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));+ let seen: std::sync::Arc<std::sync::Mutex<Vec<Value>>> = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));+ let watcher = {+ let (done, seen) = (done.clone(), seen.clone());+ std::thread::spawn(move || {+ while !done.load(std::sync::atomic::Ordering::Relaxed) {+ instance::invalidate_snapshot();+ if let Some(w) = instance::altium_windows().into_iter().find(|w| w.get("title").and_then(|t| t.as_str()).map(|t| t.to_lowercase().contains("health check")).unwrap_or(false)) {+ let h = w.get("hwnd").cloned().unwrap_or(Value::Null);+ let tree = ab::call("desktop_ui_tree", json!({"hwnd": h, "maxDepth": 12, "maxNodes": 200, "roleFilter": ["text"]}), &[], 20);+ let mut texts = Vec::new();+ fn walk(n: &Value, out: &mut Vec<String>) {+ if let Some(s) = n.get("name").and_then(|v| v.as_str()) { if !s.trim().is_empty() { out.push(s.trim().to_string()); } }+ for c in n.get("children").and_then(|c| c.as_array()).into_iter().flatten() { walk(c, out); }+ }+ 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, &mut texts);+ let button = if stop { "Cancel" } else { "Generate Outputs" };+ let c = ab::call("desktop_ui_click", json!({"hwnd": h, "contains": button, "role": "button"}), &[], 20);+ seen.lock().unwrap().push(json!({"dialog": w.get("title"), "text": texts, "answered": button, "clicked": c.get("success")}));+ std::thread::sleep(Duration::from_secs(2));+ }+ std::thread::sleep(Duration::from_secs(1));+ }+ })+ }; let o = agent::call("outjob", &[("path", outjob.clone()), ("medium", medium.clone()), ("objectKind", s_or(args, "objectKind", "OutputBatch"))], None, timeout_of(args, 300));+ done.store(true, std::sync::atomic::Ordering::Relaxed);+ let _ = watcher.join();+ let health: Vec<Value> = seen.lock().unwrap().clone(); let mut dl = dialogs::scan_scoped(true); shot::attach_dialog_shots(&mut dl); // what the OutJob wrote under "Project Outputs for <project>"@@ -94,7 +127,7 @@ pub fn run_outjob(args: &Value) -> Value { }; let ok = o.get("success").and_then(|b| b.as_bool()).unwrap_or(false) && dl.is_empty(); json!({"success": ok, "outjob": outjob, "medium": medium, "agent": o.get("data"),- "outputDir": out_dir, "files": files, "dialogs": dl,+ "outputDir": out_dir, "files": files, "dialogs": dl, "healthCheck": health, "elapsedSec": support::elapsed_round(t0), "windowEtiquette": "background", "_hint": "files[] is what the OutJob wrote under 'Project Outputs for <project>'. A dialog means a medium needed a decision (printer/PDF path) — read dialogs[].shot."}) }
bridge-rs/src/sch.rsadded+463@@ -0,0 +1,463 @@+//! Editing an existing design's schematic, the way a person does at the sheet: place a library part,+//! connect pins (wire stub + net label or power port), move/rotate/rename/re-comment/delete parts,+//! annotate `?` designators, the title block, the change order to the board, and a schematic PDF.+use crate::support::{self, s, timeout_of};+use crate::{ab, agent, instance};+use serde_json::{json, Value};+use std::collections::{BTreeMap, HashMap, HashSet};+use std::path::Path;+use std::time::Duration;++fn st(v: &Value) -> String { v.as_str().map(str::to_string).unwrap_or_else(|| if v.is_null() { String::new() } else { v.to_string() }) }++fn clean(v: &str) -> String { v.replace(['|', '\n', '\r'], " ") }++fn need_sheet(args: &Value) -> Result<String, Value> {+ let Some(sheet) = s(args, "sheet").or_else(|| s(args, "filePath")) else {+ return Err(json!({"success": false, "errorCode": "bad_args", "error": "sheet (.SchDoc path) is required"}));+ };+ if !sheet.to_lowercase().ends_with(".schdoc") {+ return Err(json!({"success": false, "errorCode": "not_a_sheet", "error": format!("{sheet} is not a .SchDoc")}));+ }+ Ok(sheet)+}++/// Save the sheet unless save:false; the reply says which.+fn maybe_save(args: &Value, sheet: &str) -> Value {+ if args.get("save").and_then(|b| b.as_bool()) == Some(false) { return json!(false); }+ let r = crate::surfaces::save(&json!({"filePath": sheet}));+ json!(r.get("success").and_then(|b| b.as_bool()) != Some(false))+}++fn agent_data(r: &Value) -> Result<Value, Value> {+ match r.get("data") {+ Some(d) if d["ok"].as_bool() == Some(true) => Ok(d.clone()),+ Some(d) => Err(json!({"success": false, "errorCode": d.get("errorCode").cloned().unwrap_or(json!("agent_refused")), "error": d.get("error"), "reply": r})),+ None => Err(json!({"success": false, "errorCode": r.get("errorCode").cloned().unwrap_or(json!("agent_failed")), "error": r.get("error"), "reply": r})),+ }+}++/// altium_sch_connect {sheet, connections:[{designator, pin, net, kind: label|power|wire, stubMil}], save}+pub fn connect(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 Some(list) = args.get("connections").and_then(|c| c.as_array()).filter(|a| !a.is_empty()) else {+ return json!({"success": false, "errorCode": "bad_args", "error": "connections is required: [{designator, pin, net, kind?}]", "_hint": "kind: label (a net label, default), power (a power port; GND-like nets get a ground symbol), wire (a stub only)"});+ };+ let lines: Vec<String> = list.iter().map(|c| format!("{}|{}|{}|{}|{}", clean(&st(&c["designator"])), clean(&st(&c["pin"])), clean(&st(&c["net"])),+ clean(&st(&c["kind"])), c.get("stubMil").and_then(|v| v.as_i64()).unwrap_or(200))).collect();+ if let Some(e) = support::must_open(&sheet, 60) { return e; }+ let r = agent::call("sch_connect", &[], Some(&lines.join("\n")), timeout_of(args, 90));+ let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+ let failed: Vec<Value> = d["results"].as_array().cloned().unwrap_or_default().into_iter().filter(|x| x["result"] != json!("ok")).collect();+ let saved = maybe_save(args, &sheet);+ json!({"success": failed.is_empty(), "errorCode": if failed.is_empty() { Value::Null } else { json!("some_not_connected") },+ "sheet": sheet, "connected": d["connected"], "results": d["results"], "failed": failed, "saved": saved,+ "_hint": "Each pin got a wire stub from its outer end and a net label or power port on it. Check with altium_compile (ERC) or altium_schematic_data; a net label is matched by its text across the project, a power port by its net name globally."})+}++/// altium_sch_edit {sheet, edits:[{designator, action: move|moveBy|rotate|designator|comment|delete, x,y | dx,dy | degrees | to | text}], save}+pub fn edit(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 Some(list) = args.get("edits").and_then(|c| c.as_array()).filter(|a| !a.is_empty()) else {+ return json!({"success": false, "errorCode": "bad_args", "error": "edits is required: [{designator, action, ...}]"});+ };+ let mut lines = Vec::new();+ for e in list {+ let des = clean(&st(&e["designator"]));+ let action = st(&e["action"]).to_lowercase();+ let n = |k: &str| e.get(k).and_then(|v| v.as_f64()).map(|v| (v.round() as i64).to_string()).unwrap_or_default();+ let (a, b) = match action.as_str() {+ "move" => (n("x"), n("y")),+ "moveby" => (n("dx"), n("dy")),+ "rotate" => (n("degrees"), String::new()),+ "designator" => (clean(&st(&e["to"])), String::new()),+ "comment" => (clean(&st(&e["text"])), String::new()),+ "delete" => (String::new(), String::new()),+ other => return json!({"success": false, "errorCode": "bad_args", "error": format!("unknown action {other}: move, moveBy, rotate, designator, comment, delete")}),+ };+ lines.push(format!("{des}|{action}|{a}|{b}"));+ }+ if let Some(e) = support::must_open(&sheet, 60) { return e; }+ let r = agent::call("sch_edit", &[], Some(&lines.join("\n")), timeout_of(args, 90));+ let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+ let failed: Vec<Value> = d["results"].as_array().cloned().unwrap_or_default().into_iter().filter(|x| x["result"] != json!("ok")).collect();+ let saved = maybe_save(args, &sheet);+ json!({"success": failed.is_empty(), "errorCode": if failed.is_empty() { Value::Null } else { json!("some_not_applied") },+ "sheet": sheet, "applied": d["applied"], "results": d["results"], "failed": failed, "saved": saved,+ "_hint": "Positions are mils (the part's origin). A moved part's wires do not follow: reconnect with altium_sch_connect. The board learns about renames and deletions with altium_update_pcb."})+}++/// altium_sch_place_part {sheet, library, symbol, designator?, x?, y?, rotation?, comment?, partId?, footprint?, pcbLib?, save}+pub fn place_part(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 (Some(lib), Some(sym)) = (s(args, "library"), s(args, "symbol").or_else(|| s(args, "libref"))) else {+ return json!({"success": false, "errorCode": "bad_args", "error": "library (.SchLib path) and symbol (its component name) are required", "_hint": "altium_list_library {filePath} names a library's components; altium_list_installed_libraries lists the installed ones."});+ };+ if let Some(e) = support::must_open(&lib, 60) { return e; }+ let p = |k: &str| s(args, k).map(|v| clean(&v)).unwrap_or_default();+ let n = |k: &str| args.get(k).and_then(|v| v.as_f64()).map(|v| (v.round() as i64).to_string()).unwrap_or_default();+ let r = agent::call("sch_place_part", &[("libref", sym.clone()), ("sch", sheet.clone()), ("designator", p("designator")), ("x", n("x")), ("y", n("y")),+ ("rotation", n("rotation")), ("comment", p("comment")), ("partId", n("partId")), ("footprint", p("footprint")), ("pcblib", p("pcbLib"))], None, timeout_of(args, 90));+ let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+ let saved = maybe_save(args, &sheet);+ json!({"success": true, "sheet": sheet, "designator": d["designator"], "libref": d["libref"], "pins": d["pins"], "footprints": d["footprints"], "atMil": d["atMil"], "saved": saved,+ "_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_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; }+ let sheet = match need_sheet(args) { Ok(x) => x, Err(e) => return e };+ let set: Vec<String> = args.get("set").and_then(|o| o.as_object()).map(|o| o.iter().map(|(k, v)| format!("{}={}", clean(k).replace(['=', ';'], " "), clean(&st(v)).replace(';', ","))).collect()).unwrap_or_default();+ if let Some(e) = support::must_open(&sheet, 60) { return e; }+ let r = agent::call("doc_params", &[("params", set.join(";"))], None, timeout_of(args, 60));+ let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+ let saved = if set.is_empty() { json!(false) } else { maybe_save(args, &sheet) };+ json!({"success": true, "sheet": sheet, "set": d["set"], "params": d["params"], "saved": saved,+ "_hint": "The title block shows these through its =Title / =DocumentNumber / =Revision special strings. Set them on every sheet of a project to keep the drawings consistent."})+}++/// The `?` designators of a project and the numbers they get: the lowest unused per prefix, in sheet+/// order, then left to right, top to bottom. Pure, for the tests.+pub fn plan_annotation(sheets: &[(String, Vec<(String, i64, i64)>)]) -> Vec<(String, String, String, i64, i64)> {+ let mut used: HashMap<String, HashSet<u64>> = HashMap::new();+ for (_, comps) in sheets {+ for (d, _, _) in comps {+ let pre: String = d.chars().take_while(|c| !c.is_ascii_digit()).collect();+ if let Ok(n) = d[pre.len()..].parse::<u64>() { used.entry(pre.to_ascii_uppercase()).or_default().insert(n); }+ }+ }+ let mut out = Vec::new();+ for (sheet, comps) in sheets {+ let mut q: Vec<&(String, i64, i64)> = comps.iter().filter(|(d, _, _)| d.ends_with('?')).collect();+ q.sort_by_key(|(_, x, y)| (-*y / 100, *x));+ for (d, x, y) in q {+ let pre = d.trim_end_matches('?').to_string();+ let set = used.entry(pre.to_ascii_uppercase()).or_default();+ let mut n = 1;+ while set.contains(&n) { n += 1; }+ set.insert(n);+ out.push((sheet.clone(), d.clone(), format!("{pre}{n}"), *x, *y));+ }+ }+ out+}++/// altium_annotate {projectPath, dryRun?}: number every `?` designator across the project's sheets.+pub fn annotate(args: &Value) -> Value {+ if let Some(g) = support::need_licensed() { return g; }+ let Some(prj) = s(args, "projectPath") else { return json!({"success": false, "errorCode": "bad_args", "error": "projectPath (.PrjPcb) is required"}) };+ let text = match std::fs::read(&prj) { Ok(b) => String::from_utf8_lossy(&b).to_string(), Err(e) => return json!({"success": false, "errorCode": "project_not_found", "error": format!("{prj}: {e}")}) };+ let project = crate::bom::parse_project(&text, Path::new(&prj).parent().unwrap_or(Path::new(".")));+ let mut sheets = Vec::new();+ let mut skipped = Vec::new();+ for sheet in &project.sheets {+ if let Some(e) = support::must_open(sheet, 60) { return json!({"success": false, "errorCode": "sheet_open_failed", "sheet": sheet, "reply": e}); }+ let r = agent::call("schematic_data", &[("pins", "0".into()), ("params", "0".into()), ("limit", "5000".into())], None, timeout_of(args, 120));+ let d = match agent_data(&r) { Ok(d) => d, Err(e) => return e };+ let all = d["components"].as_array().cloned().unwrap_or_default();+ // a multi-part component's parts share one designator and cannot be told apart here: left to+ // Altium's own Tools > Annotation, and said so+ for c in all.iter().filter(|c| st(&c["designator"]).ends_with('?') && c["partId"].as_i64().unwrap_or(1) > 1) {+ skipped.push(json!({"sheet": sheet, "designator": st(&c["designator"]), "libReference": st(&c["libReference"]), "partId": c["partId"]}));+ }+ let comps: Vec<(String, i64, i64)> = all.iter().filter(|c| c["partId"].as_i64().unwrap_or(1) <= 1)+ .map(|c| (st(&c["designator"]), c["xMil"].as_i64().unwrap_or(0), c["yMil"].as_i64().unwrap_or(0))).collect();+ sheets.push((sheet.clone(), comps));+ }+ let plan = plan_annotation(&sheets);+ let planned: Vec<Value> = plan.iter().map(|(sh, from, to, x, y)| json!({"sheet": sh, "from": from, "to": to, "atMil": [x, y]})).collect();+ if plan.is_empty() { return json!({"success": true, "renamed": [], "skippedMultiPart": skipped, "_hint": "Every single-part designator already has a number."}); }+ if args.get("dryRun").and_then(|b| b.as_bool()) == Some(true) { return json!({"success": true, "dryRun": true, "planned": planned, "skippedMultiPart": skipped}); }+ // each rename names its part by position: several parts on a sheet can all read 'R?'+ let mut by_sheet: BTreeMap<String, Vec<String>> = BTreeMap::new();+ for (sh, from, to, x, y) in &plan { by_sheet.entry(sh.clone()).or_default().push(format!("{}|{}|{}|{}", clean(from), clean(to), x, y)); }+ let mut done = Vec::new();+ for (sheet, lines) in by_sheet {+ if let Some(e) = support::must_open(&sheet, 60) { return e; }+ let r = agent::call("sch_rename_at", &[], Some(&lines.join("\n")), Duration::from_secs(90));+ match agent_data(&r) {+ Ok(d) => done.extend(d["results"].as_array().cloned().unwrap_or_default().into_iter().map(|mut x| { x["sheet"] = json!(sheet); x })),+ Err(e) => return json!({"success": false, "errorCode": "rename_failed", "renamed": done, "sheet": sheet, "reply": e}),+ }+ let _ = maybe_save(args, &sheet);+ }+ let failed: Vec<&Value> = done.iter().filter(|x| x["result"] != json!("ok")).collect();+ json!({"success": failed.is_empty(), "renamed": done, "skippedMultiPart": skipped,+ "_hint": "Numbers are the lowest free per prefix across the project, top-left first on each sheet. Multi-part components with '?' are left to Altium's Tools > Annotation (skippedMultiPart). The board gets the new designators with altium_update_pcb."})+}++/// The designators on the project's sheets (graphical parts left out: they never reach a board) and on its board.+fn sides(project: &crate::bom::Project, board: &str, args: &Value) -> Result<(Vec<String>, Vec<String>), Value> {+ let mut sheet_refs = Vec::new();+ for sheet in &project.sheets {+ if let Some(e) = support::must_open(sheet, 60) { return Err(e); }+ let r = agent::call("schematic_data", &[("pins", "0".into()), ("params", "0".into()), ("limit", "5000".into())], None, timeout_of(args, 120));+ let d = agent_data(&r)?;+ for c in d["components"].as_array().cloned().unwrap_or_default() {+ let des = st(&c["designator"]);+ if c["kind"].as_i64() != Some(2) && !des.is_empty() && !sheet_refs.contains(&des) { sheet_refs.push(des); }+ }+ }+ if let Some(e) = support::must_open(board, 60) { return Err(e); }+ let r = agent::call("bom_placement", &[], None, timeout_of(args, 120));+ let d = agent_data(&r)?;+ let board_refs: Vec<String> = d["comps"].as_array().cloned().unwrap_or_default().iter().map(|c| st(&c[0])).collect();+ Ok((sheet_refs, board_refs))+}++/// What the change order will do to the parts, from the two sides: (add, remove). Pure, for the tests.+pub fn part_diff(sheet: &[String], board: &[String]) -> (Vec<String>, Vec<String>) {+ let up = |v: &[String]| v.iter().map(|x| x.to_ascii_uppercase()).collect::<HashSet<String>>();+ let (s, b) = (up(sheet), up(board));+ let mut add: Vec<String> = sheet.iter().filter(|x| !b.contains(&x.to_ascii_uppercase())).cloned().collect();+ let mut remove: Vec<String> = board.iter().filter(|x| !s.contains(&x.to_ascii_uppercase())).cloned().collect();+ add.sort_by_key(|d| crate::bom::natural_key(d)); remove.sort_by_key(|d| crate::bom::natural_key(d));+ add.dedup(); remove.dedup();+ (add, remove)+}++/// altium_update_pcb {projectPath, execute?, allowRemovals?}: the schematic's changes to the board (Altium's+/// engineering change order). Without execute it only previews; it never removes board parts unless told.+pub fn update_pcb(args: &Value) -> Value {+ if let Some(g) = support::need_licensed() { return g; }+ let Some(prj) = s(args, "projectPath") else { return json!({"success": false, "errorCode": "bad_args", "error": "projectPath (.PrjPcb) is required"}) };+ let text = std::fs::read(&prj).map(|b| String::from_utf8_lossy(&b).to_string()).unwrap_or_default();+ let project = crate::bom::parse_project(&text, Path::new(&prj).parent().unwrap_or(Path::new(".")));+ if project.sheets.is_empty() { return json!({"success": false, "errorCode": "no_sheets", "error": "the project lists no sheet"}); }+ let Some(board) = project.boards.first().cloned() else { return json!({"success": false, "errorCode": "no_board", "error": "the project has no .PcbDoc to update", "_hint": "Add a board to the project first (altium_build_board makes one)."}) };+ let t0 = support::now();+ for sh in &project.sheets { let _ = crate::surfaces::save(&json!({"filePath": sh})); }+ let (sheet_refs, board_refs) = match sides(&project, &board, args) { Ok(x) => x, Err(e) => return json!({"success": false, "errorCode": "read_failed", "reply": e}) };+ let (add, remove) = part_diff(&sheet_refs, &board_refs);+ let preview = json!({"addParts": add, "removeParts": remove, "sheetParts": sheet_refs.len(), "boardParts": board_refs.len()});+ let execute = args.get("execute").and_then(|b| b.as_bool()) == Some(true);+ if !execute {+ return json!({"success": true, "executed": false, "preview": preview, "elapsedSec": support::elapsed_round(t0),+ "_hint": if remove.is_empty() { "Preview only. Parameter, footprint and net changes are not listed here; altium_update_pcb {execute:true} runs the change order.".to_string() }+ else { format!("Preview only. The change order would REMOVE {} part(s) from the board that are on no sheet ({}): board-only test pads, fiducials or mounting holes usually. Put them on a sheet, or run with allowRemovals:true if they really should go.", remove.len(), remove.iter().take(10).cloned().collect::<Vec<_>>().join(", ")) }});+ }+ if !remove.is_empty() && args.get("allowRemovals").and_then(|b| b.as_bool()) != Some(true) {+ return json!({"success": false, "errorCode": "would_remove_parts", "executed": false, "preview": preview,+ "_hint": format!("Not run: the change order would delete {} board part(s) that are on no sheet ({}). Add them to a sheet (altium_sch_place_part), or pass allowRemovals:true to delete them.", remove.len(), remove.iter().take(10).cloned().collect::<Vec<_>>().join(", "))});+ }+ // from the board, Import Changes (Action=UpdateMe) is the change order; from a sheet, Action=UpdateOther+ // opened nothing on AD 26.11.1 (2026-10-21)+ if let Some(e) = support::must_open(&board, 60) { return e; }+ agent::call("run_process", &[("process", "WorkspaceManager:Compare".into()), ("params", "ObjectKind=Project|Action=UpdateMe".into())], None, Duration::from_secs(6));+ let find = |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))+ };+ let mut steps = Vec::new();+ let mut eco = None;+ // a change order opens within seconds (the link prompt and its Information box come first on a new+ // board); with no differences Altium opens nothing at all, so the wait is short+ let t_wait = std::time::Instant::now();+ while t_wait.elapsed() < Duration::from_secs(35) {+ if let Some(w) = find("engineering change order") { eco = Some(w); break; }+ // a board built by script has no component links yet: Altium asks to match by designator, once+ if let Some(w) = find("component links") {+ let h = w.get("hwnd").cloned().unwrap_or(Value::Null);+ let c = ab::call("desktop_ui_click", json!({"hwnd": h, "contains": "Automatically Create Component Links"}), &[], 20);+ std::thread::sleep(Duration::from_secs(2));+ if find("component links").is_some() {+ // its buttons answer Invoke without acting (measured): a real click on the button's centre+ if let Some(r) = c.get("control").and_then(|c| c.get("rect")) {+ let _ = ab::call("desktop_click", json!({"hwnd": h, "x": r["centerX"], "y": r["centerY"], "reason": "link schematic and board parts by designator for the change order"}), &[], 20);+ }+ }+ steps.push(json!({"componentLinks": "matched by designator"}));+ std::thread::sleep(Duration::from_secs(2));+ continue;+ }+ // an Information box comes after the links are made (and alone when nothing differs): read it,+ // press OK and keep waiting; the change order follows it when there is one+ if let Some(w) = find("information").or_else(|| find("confirm")) {+ let dl = crate::dialogs::scan_scoped(true);+ steps.push(json!({"dialog": w.get("title"), "text": dl.iter().filter_map(|d| d.get("text").or_else(|| d.get("body")).cloned()).collect::<Vec<_>>()}));+ if let Some(h) = w.get("hwnd") { let _ = ab::call("desktop_ui_click", json!({"hwnd": h, "contains": "OK", "role": "button"}), &[], 15); }+ std::thread::sleep(Duration::from_secs(3));+ continue;+ }+ std::thread::sleep(Duration::from_millis(500));+ }+ let Some(eco) = eco else {+ let dl = crate::dialogs::scan_scoped(true);+ let boxed = steps.iter().any(|s| s.get("dialog").is_some());+ return json!({"success": boxed, "errorCode": if boxed { Value::Null } else { json!("eco_dialog_not_found") }, "executed": false, "preview": preview,+ "steps": steps, "dialogs": dl, "elapsedSec": support::elapsed_round(t0),+ "_hint": "No Engineering Change Order opened: Altium found no differences between the sheets and the board that its change order carries. On AD 26.11.1 a comment-only change on an existing part is one of those (measured). Parts, nets and parameters are carried. If you expected changes, check the sheets are saved and that no second project with the same file names is open."});+ };+ let hwnd = eco.get("hwnd").and_then(|h| h.as_i64()).unwrap_or(0);+ // the change order's first page, as Altium lists it (group rows carry counts: 'Add Components(1)')+ let tree = ab::call("desktop_ui_tree", json!({"hwnd": hwnd, "maxDepth": 14, "maxNodes": 400}), &[], 30);+ let rows = eco_rows(&tree);+ let execute_r = ab::call("desktop_ui_click", json!({"hwnd": hwnd, "contains": "Execute Changes", "role": "button"}), &[], 60);+ steps.push(json!({"uiClick": "Execute Changes", "success": execute_r.get("success")}));+ // done when the list stops changing (Altium marks each row Done and scrolls to the end); the agent+ // stays unresponsive while the modal is up, so it cannot be the signal+ let wait = timeout_of(args, 300);+ let t1 = std::time::Instant::now();+ std::thread::sleep(Duration::from_secs(3));+ let mut rows_after = Vec::new();+ let mut stable = 0;+ while t1.elapsed() < wait {+ let now = eco_rows(&ab::call("desktop_ui_tree", json!({"hwnd": hwnd, "maxDepth": 14, "maxNodes": 400}), &[], 30));+ if !now.is_empty() && now == rows_after { stable += 1; if stable >= 2 { break; } } else { stable = 0; }+ rows_after = now;+ std::thread::sleep(Duration::from_secs(2));+ }+ let close = ab::call("desktop_ui_click", json!({"hwnd": hwnd, "contains": "Close", "role": "button"}), &[], 30);+ steps.push(json!({"uiClick": "Close", "success": close.get("success")}));+ // the agent loop parked while the modal was up: bring it back for the verbs that follow+ for _ in 0..20 { if agent::is_responsive() { break; } std::thread::sleep(Duration::from_millis(500)); }+ if !agent::is_responsive() { let _ = crate::lifecycle::recover("after the change order"); }+ // the change order edits the board in memory: save it, or closing it loses every change (measured)+ let saved = crate::surfaces::save(&json!({"filePath": board}));+ steps.push(json!({"saveBoard": saved.get("success")}));+ let dl = crate::dialogs::scan_scoped(true);+ json!({"success": execute_r.get("success").and_then(|b| b.as_bool()) == Some(true), "executed": true, "projectPath": prj, "board": board, "preview": preview,+ "changeOrder": rows, "afterExecute": rows_after, "steps": steps, "dialogs": dl, "elapsedSec": support::elapsed_round(t0), "windowEtiquette": "background",+ "_hint": "The change order ran. New parts land beside the board outline: place them (altium_move_parts) and check with altium_routing_drc. changeOrder lists the dialog's first page (groups carry counts)."})+}++/// The change order's visible rows: their text, as the grid shows it.+fn eco_rows(tree: &Value) -> Vec<String> {+ fn walk(n: &Value, out: &mut Vec<String>) {+ if n.get("role").and_then(|r| r.as_str()) == Some("ListItem") {+ if let Some(v) = n.get("value").and_then(|v| v.as_str()) { let v = v.split_whitespace().collect::<Vec<_>>().join(" "); if !v.is_empty() { out.push(v); } }+ }+ for c in n.get("children").and_then(|c| c.as_array()).into_iter().flatten() { walk(c, out); }+ }+ 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);+ let mut out = Vec::new();+ walk(&root, &mut out);+ out+}++/// The OutJob for a schematic PDF (all the project's sheets, optionally a board composite print).+pub fn pdf_outjob(file_name: &str, board: bool) -> String {+ let page = "Record=PageOptions|CenterHorizontal=True|CenterVertical=True|PrintScale=1.00|XCorrection=1.00|YCorrection=1.00|PrintKind=0|BorderSize=5000000|LeftOffset=0|BottomOffset=0|Orientation=2|PaperLength=1000|PaperWidth=1000|Scale=100|PaperSource=7|PrintQuality=-4|MediaType=1|DitherType=10|PaperKind=A4|PrintScaleMode=1";+ let sch_view = "Record=SchPrintView|ShowNoERC=True|ShowParamSet=True|ShowProbe=True|ShowBlanket=True|ShowNote=True|ShowNoteCollapsed=True|ExpandDesignator=True|ExpandNetLabel=False|ExpandPort=False|ExpandSheetNum=False|ExpandDocNum=False|PrintArea=0";+ let mut l: Vec<String> = ["[OutputJobFile]", "Version=1.0", "Caption=", "Description=Adom altium_export_pdf", "",+ "[OutputGroup1]", "Name=AdomPdf.OutJob", "Description=", "TargetOutputMedium=PDF", "VariantName=[No Variations]", "VariantScope=0",+ "CurrentConfigurationName=", "TargetPrinter=", "PrinterOptions=Record=PrinterOptions|Copies=1|Duplex=1|TrueTypeOptions=3|Collate=1|PrintWhat=1",+ "OutputMedium1=PDF", "OutputMedium1_Type=Publish",+ "OutputType1=Schematic Print", "OutputName1=Schematic Prints", "OutputCategory1=Documentation", "OutputDocumentPath1=[Project Physical Documents]",+ "OutputVariantName1=", "OutputEnabled1=1", "OutputEnabled1_OutputMedium1=1", "OutputDefault1=0"].iter().map(|x| x.to_string()).collect();+ l.push(format!("PageOptions1={page}"));+ l.push("Configuration1_Name1=OutputConfigurationParameter1".into());+ l.push(format!("Configuration1_Item1={sch_view}"));+ if board {+ l.extend(["OutputType2=Composite", "OutputName2=Composite Drawing", "OutputCategory2=Documentation", "OutputDocumentPath2=", "OutputVariantName2=",+ "OutputEnabled2=1", "OutputEnabled2_OutputMedium1=2", "OutputDefault2=0"].iter().map(|x| x.to_string()));+ l.push(format!("PageOptions2={page}"));+ l.push("Configuration2_Name1=OutputConfigurationParameter1".into());+ l.push("Configuration2_Item1=DesignatorDisplayMode=Physical|PrintArea=DesignExtent|PrintAreaLowerLeftCornerX=0|PrintAreaLowerLeftCornerY=0|PrintAreaUpperRightCornerX=0|PrintAreaUpperRightCornerY=0|Record=PcbPrintView".into());+ l.push("Configuration2_Name2=OutputConfigurationParameter2".into());+ l.push("Configuration2_Item2=IncludeBottomLayerComponents=True|IncludeMultiLayerComponents=True|IncludeTopLayerComponents=True|IncludeViewports=True|Index=0|Mirror=False|Name=Multilayer Composite Print|PadNumberFontSize=14|Record=PcbPrintOut|ShowHoles=False|ShowPadNets=False|ShowPadNumbers=False|SubstituteFonts=False".into());+ for (i, layer) in ["TopOverlay", "TopLayer", "BottomLayer", "MultiLayer", "Mechanical1"].iter().enumerate() {+ l.push(format!("Configuration2_Name{}=OutputConfigurationParameter{}", i + 3, i + 3));+ l.push(format!("Configuration2_Item{}=CArc=Full|CFill=Full|Comment=Full|Coordinate=Full|CPad=Full|CRegion=Full|CText=Full|CTrack=Full|CVia=Full|DDSymbolKind=0|DDSymbolSize=500000|DDSymbolSortKind=0|Designator=Full|Dimension=Full|DLayer1=TopLayer|DLayer2=BottomLayer|FArc=Full|FFill=Full|FPad=Full|FRegion=Full|FText=Full|FTrack=Full|FVia=Full|Layer={layer}|Polygon=Full|PrintOutIndex=0|Record=PcbPrintLayer", i + 3));+ }+ }+ l.extend(["".into(), "[PublishSettings]".into(), format!("OutputFilePath1=\\{file_name}"), "ReleaseManaged1=0".into(), "OutputBasePath1=".into(),+ "OutputPathMedia1=".into(), "OutputPathMediaValue1=".into(), "OutputPathOutputer1=".into(), "OutputPathOutputerPrefix1=".into(), "OutputPathOutputerValue1=".into(),+ format!("OutputFileName1={file_name}"), "OutputFileNameMulti1=".into(), "UseOutputNameForMulti1=0".into(), "OutputFileNameSpecial1=".into(),+ "OpenOutput1=0".into(), "PromptOverwrite1=0".into(), "PublishMethod1=0".into(), "ZoomLevel1=50".into(), "FitSCHPrintSizeToDoc1=1".into(), "FitPCBPrintSizeToDoc1=1".into(),+ "GenerateNetsInfo1=1".into(), "MarkPins1=1".into(), "MarkNetLabels1=1".into(), "MarkPortsId1=1".into(), "GenerateTOC=1".into(), "ShowComponentParameters1=1".into(),+ "".into(), "[GeneratedFilesSettings]".into(), "RelativeOutputPath1=".into(), "OpenOutputs1=0".into(), "".into()]);+ l.join("\r\n")+}++/// altium_export_pdf {projectPath, output?, board?}: the schematic (and optionally the board) as one PDF,+/// from an OutJob in a throwaway project that lists the project's documents; the user's project is not touched.+pub fn export_pdf(args: &Value) -> Value {+ if let Some(g) = support::need_licensed() { return g; }+ let t0 = support::now();+ let Some(prj) = s(args, "projectPath") else { return json!({"success": false, "errorCode": "bad_args", "error": "projectPath (.PrjPcb) is required"}) };+ let text = match std::fs::read(&prj) { Ok(b) => String::from_utf8_lossy(&b).to_string(), Err(e) => return json!({"success": false, "errorCode": "project_not_found", "error": format!("{prj}: {e}")}) };+ let dir = Path::new(&prj).parent().unwrap_or(Path::new(".")).to_path_buf();+ let project = crate::bom::parse_project(&text, &dir);+ if project.sheets.is_empty() { return json!({"success": false, "errorCode": "no_sheets", "error": "the project lists no sheet"}); }+ let stem = Path::new(&prj).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or("schematic".into());+ let out = s(args, "output").unwrap_or_else(|| dir.join(format!("{stem}.pdf")).to_string_lossy().to_string());+ let with_board = args.get("board").and_then(|b| b.as_bool()) == Some(true) && !project.boards.is_empty();+ for sh in &project.sheets { let _ = crate::surfaces::save(&json!({"filePath": sh})); }+ let tmp = std::env::temp_dir().join("adom-altium-pdf").join(format!("{}", (support::now() * 1000.0) as u64));+ let proj = tmp.join("AdomPdfExport.PrjPcb");+ let oj = tmp.join("AdomPdf.OutJob");+ let mut prj_text = String::from("[Design]\r\nVersion=1.0\r\nHierarchyMode=0\r\n\r\n");+ let mut docs: Vec<String> = project.sheets.clone();+ if with_board { docs.extend(project.boards.iter().take(1).cloned()); }+ for (i, d) in docs.iter().enumerate() { prj_text.push_str(&format!("[Document{}]\r\nDocumentPath={}\r\nAnnotationEnabled=1\r\n\r\n", i + 1, d)); }+ prj_text.push_str(&format!("[Document{}]\r\nDocumentPath=AdomPdf.OutJob\r\n\r\n", docs.len() + 1));+ let file_name = "AdomExport.pdf";+ if let Err(e) = std::fs::create_dir_all(&tmp).and_then(|_| std::fs::write(&oj, pdf_outjob(file_name, with_board))).and_then(|_| std::fs::write(&proj, prj_text)) {+ return json!({"success": false, "errorCode": "temp_write_failed", "error": e.to_string()});+ }+ let proj_s = proj.to_string_lossy().to_string();+ let r = crate::outputs::run_outjob(&json!({"outjobPath": oj.to_string_lossy(), "medium": "PDF", "projectPath": proj_s, "timeout": timeout_of(args, 300).as_secs()}));+ let found = walk_pdf(&tmp);+ let _ = agent::call("close_document", &[("path", oj.to_string_lossy().to_string())], None, Duration::from_secs(30));+ let _ = agent::call("close_document", &[("path", proj_s.clone())], None, Duration::from_secs(30));+ let Some(src) = found else {+ return json!({"success": false, "errorCode": "pdf_not_written", "outjob": r, "tempDir": tmp.to_string_lossy(), "elapsedSec": support::elapsed_round(t0),+ "_hint": "Altium ran the PDF output but wrote no .pdf. Read outjob.dialogs[]; altium_check_dialogs shows any modal."});+ };+ if let Some(parent) = Path::new(&out).parent() { let _ = std::fs::create_dir_all(parent); }+ if let Err(e) = std::fs::copy(&src, &out) { return json!({"success": false, "errorCode": "copy_failed", "error": e.to_string(), "produced": src.to_string_lossy()}); }+ let bytes = std::fs::metadata(&out).map(|m| m.len()).unwrap_or(0);+ let _ = std::fs::remove_dir_all(&tmp);+ json!({"success": bytes > 0, "output": out, "outputBytes": bytes, "sheets": project.sheets.len(), "board": with_board, "healthCheck": r.get("healthCheck"), "elapsedSec": support::elapsed_round(t0),+ "windowEtiquette": "background", "_hint": "One PDF of every sheet (and the board's composite print with board:true). Pull it with ab's pull_file."})+}++fn walk_pdf(dir: &Path) -> Option<std::path::PathBuf> {+ let rd = std::fs::read_dir(dir).ok()?;+ for e in rd.flatten() {+ let p = e.path();+ if p.is_dir() { if let Some(f) = walk_pdf(&p) { return Some(f); } }+ else if p.extension().map(|x| x.eq_ignore_ascii_case("pdf")).unwrap_or(false) { return Some(p); }+ }+ None+}++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn question_marks_get_the_lowest_free_numbers() {+ let sheets = vec![+ ("A.SchDoc".to_string(), vec![("R1".to_string(), 0, 0), ("R?".to_string(), 500, 1000), ("R?".to_string(), 100, 1000), ("C?".to_string(), 0, 0)]),+ ("B.SchDoc".to_string(), vec![("R3".to_string(), 0, 0), ("R?".to_string(), 0, 0)]),+ ];+ let plan = plan_annotation(&sheets);+ let names: Vec<(String, String)> = plan.iter().map(|(_, f, t, _, _)| (f.clone(), t.clone())).collect();+ assert_eq!(names, vec![("R?".into(), "R2".into()), ("R?".into(), "R4".into()), ("C?".into(), "C1".into()), ("R?".into(), "R5".into())]);+ }++ #[test]+ fn the_change_order_preview_names_additions_and_removals() {+ let (add, remove) = part_diff(&["R1".into(), "R9".into(), "U1".into()], &["U1".into(), "r1".into(), "TP2".into(), "TP10".into()]);+ assert_eq!(add, vec!["R9".to_string()]);+ assert_eq!(remove, vec!["TP2".to_string(), "TP10".to_string()]);+ }++ #[test]+ fn the_pdf_outjob_prints_every_sheet_to_one_file() {+ let t = pdf_outjob("X.pdf", false);+ assert!(t.contains("OutputType1=Schematic Print"));+ assert!(t.contains("OutputDocumentPath1=[Project Physical Documents]"));+ assert!(t.contains("OutputMedium1=PDF\r\nOutputMedium1_Type=Publish"));+ assert!(t.contains("OutputFileName1=X.pdf"));+ assert!(!t.contains("Composite"));+ assert!(pdf_outjob("X.pdf", true).contains("OutputType2=Composite"));+ }+}
bridge-rs/src/verbs.rs+55−1@@ -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",+ "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", "save", "test_board", "board_shape", "pcb_report", "import_kicad", "install_extension", "extension_ping", // library authoring@@ -631,6 +631,53 @@ fn describe() -> Value { &["sheet_params", "export_manufacturing", "read_schematic"], &["Graphical, Net Tie (no BOM) and Standard (no BOM) components are left out (checks.excludedKinds).", "DNP: a variant's Not Fitted parts, or a DNP / Do Not Populate / Fitted=false parameter.", "Variants are read from the .PrjPcb text; alternate parts are listed, not substituted.", "CPL rotations are Altium's: JLCPCB's preview may need per-footprint corrections.", "A multi-channel (repeated) sheet is read once: its copies' designators land in checks.boardNotOnSheet.", "Only the parameters a BOM needs are read (some libraries carry huge blob parameters); a workspace-managed part's description comes from a Description parameter, never its vault."], json!({"projectPath": "C:/proj/board.PrjPcb", "boards": 10})),+ verb_doc("sch_place_part", "Place a component from a .SchLib onto an EXISTING sheet at a position, with its library parameters (MPN, Manufacturer, distributor numbers) and its footprint link.",+ json!({"sheet": "path to the .SchDoc (required)", "library": "path to the .SchLib (required)", "symbol": "the component's name in that library (required)", "designator": "string, default the library's (e.g. R?)", "x": "mil, default 1000 (snapped to 100)", "y": "mil, default 1000", "rotation": "0|90|180|270", "comment": "string, default the library's", "partId": "int, default 1 (a multi-part component's part)", "footprint": "a PcbLib footprint name to link instead of the library's own", "pcbLib": "path of the .PcbLib the footprint lives in (optional)", "save": "bool, default true"}),+ json!({"designator": "string", "libref": "string", "pins": "int", "footprints": "[string]", "atMil": "[x, y]", "saved": "bool"}), 120,+ "Then wire it: altium_sch_connect {sheet, connections:[{designator, pin, net}]}; number a '?' designator with altium_annotate; push it to the board with altium_update_pcb.",+ &["sch_connect", "annotate", "update_pcb", "list_library"],+ &["Places part 1 of a multi-part component unless partId says otherwise.", "Sheet coordinates are mils from the sheet's lower-left corner; altium_schematic_data shows where the other parts are."],+ json!({"sheet": "C:/proj/Main.SchDoc", "library": "C:/proj/Lib.SchLib", "symbol": "TPS54202DDCR", "designator": "U2", "x": 3000, "y": 4000})),+ verb_doc("sch_connect", "Connect pins of placed parts: a wire stub from each pin's outer end plus a net label (kind label), a power port (kind power: GND-like nets get a ground symbol) or the stub alone (kind wire). One undo step.",+ json!({"sheet": "path to the .SchDoc (required)", "connections": "[{designator, pin (number or name), net, kind: label|power|wire (default label), stubMil (default 200)}] (required)", "save": "bool, default true"}),+ json!({"connected": "int", "results": "[{designator, pin, net, kind, result, endMil}]", "failed": "[..]", "saved": "bool"}), 90,+ "Net labels with the same text connect across the project; power ports connect by net name everywhere. Check the result with altium_compile (ERC) or altium_schematic_data.",+ &["sch_place_part", "compile", "schematic_data"],+ &["A pin that already has a wire gets a second stub; read the sheet first when unsure.", "A part rotated after connecting leaves its stubs behind."],+ json!({"sheet": "C:/proj/Main.SchDoc", "connections": [{"designator": "U2", "pin": "1", "net": "GND", "kind": "power"}, {"designator": "U2", "pin": "VIN", "net": "VIN"}]})),+ verb_doc("sch_edit", "Change placed parts on a sheet: move (to x,y mil), moveBy (dx,dy), rotate (degrees), rename (designator), re-comment (comment), delete. One undo step.",+ json!({"sheet": "path to the .SchDoc (required)", "edits": "[{designator, action: move|moveBy|rotate|designator|comment|delete, x,y | dx,dy | degrees | to | text}] (required)", "save": "bool, default true"}),+ json!({"applied": "int", "results": "[{designator, action, result}]", "failed": "[..]", "saved": "bool"}), 90,+ "Wires do not follow a moved part; reconnect with altium_sch_connect. Renames and deletions reach the board with altium_update_pcb.",+ &["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("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,+ "Then altium_update_pcb carries the designators to the board.",+ &["sch_place_part", "update_pcb", "sch_edit"],+ &["Multi-part components with '?' are left alone (their parts share one designator); use Altium's Tools > Annotation for those."],+ json!({"projectPath": "C:/proj/board.PrjPcb"})),+ verb_doc("doc_params", "Read or set a sheet's document parameters: the title block (Title, DocumentNumber, Revision, DrawnBy, Author, CompanyName, ...).",+ json!({"sheet": "path to the .SchDoc (required)", "set": "{name: value} (optional; missing parameters are added)", "save": "bool, default true"}),+ json!({"params": "[{name, value}]", "set": "int", "saved": "bool"}), 60,+ "The title block shows them through =Title, =DocumentNumber, =Revision ... special strings.",+ &["sch_edit", "export_pdf"], &[],+ json!({"sheet": "C:/proj/Main.SchDoc", "set": {"Title": "12 V to 5 V buck", "Revision": "B"}})),+ verb_doc("update_pcb", "Design > Update PCB Document from the schematic (the engineering change order): parts added, removed or renamed, nets and footprints updated on the board. Previews by default; execute:true runs it (Execute Changes, Close), refusing to delete board parts unless allowRemovals:true.",+ json!({"projectPath": "path to the .PrjPcb (required)", "execute": "bool, default false - false only previews", "allowRemovals": "bool, default false - let the change order delete board parts that are on no sheet", "timeout": "seconds, default 300"}),+ json!({"executed": "bool", "preview": "{addParts, removeParts, sheetParts, boardParts}", "changeOrder": "[string] - the dialog's first page (groups carry counts)", "afterExecute": "[string]", "steps": "[..]", "dialogs": "[..] - anything Altium asked", "board": "path"}), 600,+ "Preview first (the default): removeParts are board parts on no sheet (test pads, fiducials, mounting holes) that the change order would DELETE; it refuses them unless allowRemovals:true. New parts land beside the board outline: place them (altium_move_parts) and check with altium_routing_drc.",+ &["annotate", "sch_place_part", "move_parts", "routing_drc"],+ &["Save the sheets first (the sch_* verbs save by default).", "The project must contain a .PcbDoc."],+ json!({"projectPath": "C:/proj/board.PrjPcb"})),+ verb_doc("export_pdf", "Every sheet of a project as one PDF (and the board's composite print with board:true), from an OutJob in a throwaway project; the user's project is not touched.",+ json!({"projectPath": "path to the .PrjPcb (required)", "output": "path of the .pdf, default <project>.pdf beside it", "board": "bool, default false", "timeout": "seconds, default 300"}),+ json!({"output": "path", "outputBytes": "int", "sheets": "int"}), 600,+ "Pull the file with ab's pull_file.",+ &["run_outjob", "export_manufacturing"], &[],+ json!({"projectPath": "C:/proj/board.PrjPcb"})), verb_doc("sheet_params", "Write parameters onto PLACED parts on the schematic sheets (the BOM write-back): MPN, Manufacturer, Mouser/DigiKey/LCSC numbers, anything. A missing parameter is added hidden; one undo step per sheet; each touched sheet is saved.", json!({"designators": "[string] or 'R1,R2' (required; every part of a multi-part component)", "params": "{name: value} or 'Key=Value;Key=Value' (required)", "sheet": "path to a .SchDoc - or", "projectPath": "path to the .PrjPcb: each designator is found on whichever sheet holds it", "save": "bool, default true", "timeout": "seconds, default 90"}), json!({"updated": "[designator]", "notFound": "[designator]", "sheets": "[{sheet, designators, paramsSet}]"}), 300,@@ -1292,6 +1339,13 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> "export_bom" => outputs::export_bom(args), "bom" => crate::bom::bom(args), "sheet_params" => crate::bom::sheet_params(args),+ "sch_place_part" => crate::sch::place_part(args),+ "sch_connect" => crate::sch::connect(args),+ "sch_edit" => crate::sch::edit(args),+ "annotate" => crate::sch::annotate(args),+ "doc_params" => crate::sch::doc_params(args),+ "update_pcb" => crate::sch::update_pcb(args),+ "export_pdf" => crate::sch::export_pdf(args), "export_step" => outputs::export_step(args), "test_board" => library::test_board(args), "import_kicad" => outputs::import_kicad(args),
install.sh+1@@ -21,6 +21,7 @@ for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do install_skill "$HOME_DIR/altium-wiki-import" "$HERE/skills/altium-wiki-import/SKILL.md" install_skill "$HOME_DIR/altium-a365" "$HERE/skills/altium-a365/SKILL.md" install_skill "$HOME_DIR/altium-bom" "$HERE/skills/altium-bom/SKILL.md"+ install_skill "$HOME_DIR/altium-schematic-edit" "$HERE/skills/altium-schematic-edit/SKILL.md" done # Dashboard: the pkg ships it, the dock runs `altium-dashboard serve --print-url`.
package.json+2−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.56",+ "version": "0.2.57", "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": [@@ -34,6 +34,7 @@ "package.json", "skills/altium-a365/SKILL.md", "skills/altium-bom/SKILL.md",+ "skills/altium-schematic-edit/SKILL.md", "skills/altium-boards/SKILL.md", "skills/altium-bridge-plan/SKILL.md", "skills/altium-install/SKILL.md",
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.56",+ "version": "0.2.57", "hero": { "type": "image", "path": "screenshots/hero.png"
skills/altium-schematic-edit/SKILL.mdadded+98@@ -0,0 +1,98 @@+---+name: altium-schematic-edit+description: >-+ Edit an existing Altium design's schematic and push it to the board, the way an engineer does at the+ sheet: place a library part (altium_sch_place_part), connect pins with net labels or power ports+ (altium_sch_connect), move/rotate/rename/re-comment/delete parts (altium_sch_edit), number '?'+ designators (altium_annotate), set the title block (altium_doc_params), write part parameters+ (altium_sheet_params), carry the changes to the board with a previewed engineering change order+ (altium_update_pcb), and print the schematic to PDF (altium_export_pdf). Use when asked to add a part,+ wire something up, change a value, rename or delete a component, annotate, update the PCB from the+ schematic, fill in the title block or export a schematic PDF in Altium. Trigger words: add a resistor+ in altium, place a part, connect pin, net label, power port, gnd symbol, change value, rename designator,+ delete component, annotate, update pcb, engineering change order, eco, import changes, title block,+ schematic pdf, altium_sch_connect, altium_update_pcb.+---++Parent skill: **altium-bridge**++# altium-schematic-edit++Every verb takes paths on the Windows machine, edits in place with one undo step, and saves the sheet+unless `save:false`. Read first, then edit: `altium_schematic_data {filePath, pins:true}` gives every+part's designator, position (mils from the sheet's lower-left corner), orientation and pins.++## Add a part and wire it++```bash+altium_sch_place_part '{"sheet":"C:\\proj\\Main.SchDoc","library":"C:\\proj\\Lib.SchLib","symbol":"RC0402-10K",+ "designator":"R?","x":2500,"y":2500,"comment":"10k"}'+altium_annotate '{"projectPath":"C:\\proj\\board.PrjPcb"}' # R? -> R9+altium_sch_connect '{"sheet":"C:\\proj\\Main.SchDoc","connections":[+ {"designator":"R9","pin":"1","net":"VOUT"},+ {"designator":"R9","pin":"2","net":"GND","kind":"power"}]}'+altium_update_pcb '{"projectPath":"C:\\proj\\board.PrjPcb"}' # preview+altium_update_pcb '{"projectPath":"C:\\proj\\board.PrjPcb","execute":true,"timeout":600}'+```++- `sch_place_part` copies the library part's drawing, parameters (MPN, Manufacturer, distributor numbers)+ and footprint links. `footprint` + `pcbLib` link a different footprint. Coordinates snap to 100 mil.+- `sch_connect` draws a wire stub (default 200 mil) out of each pin's end and puts a net label on it+ (`kind:"label"`), a power port (`kind:"power"`; GND, AGND, VSS... get a ground symbol, other nets a+ bar) or nothing (`kind:"wire"`). Pins are matched by number first, then by name. Each connection+ answers `ok` or why not (`pin not found on R9`).+- `annotate` gives every `R?`, `C?`, `U?` the lowest free number for its prefix across all sheets,+ top-left first. `dryRun:true` shows the plan. Multi-part components with `?` are left to Altium's own+ Tools > Annotation (`skippedMultiPart`).++## Change parts++```bash+altium_sch_edit '{"sheet":"C:\\proj\\Main.SchDoc","edits":[+ {"designator":"R5","action":"comment","text":"4k7"},+ {"designator":"C9","action":"delete"},+ {"designator":"U2","action":"move","x":4000,"y":3000},+ {"designator":"U2","action":"rotate","degrees":90},+ {"designator":"R12","action":"designator","to":"R8"}]}'+altium_sheet_params '{"projectPath":"C:\\proj\\board.PrjPcb","designators":["R5"],"params":{"MPN":"RC0402FR-074K7L"}}'+```++Wires do not follow a moved or rotated part: reconnect with `sch_connect`. Every part of a multi-part+component with that designator changes together.++## Push to the board: altium_update_pcb++Altium's Design > Update PCB Document (from the board: Import Changes), the engineering change order.++- **Preview first (the default).** `preview.addParts` are on a sheet but not the board; `preview.removeParts`+ are on the board but on no sheet. Those are often board-only test pads, fiducials and mounting holes,+ and the change order would DELETE them.+- `execute:true` runs it, and refuses while `removeParts` is not empty unless `allowRemovals:true`.+ Put board-only parts on a sheet instead when they should stay.+- The first change order on a board that was built by script asks to link parts by designator; the+ verb answers it. It presses Execute Changes, waits for every row to finish, closes, and **saves the+ board** (the change order edits it in memory only).+- New parts land beside the board outline: place them with `altium_move_parts`, check with+ `altium_routing_drc`.+- Pass `timeout` (seconds) for long verbs: ab waits for the call's own `timeout`.+- Component comment changes alone do not open a change order on AD 26.11.1 (Altium's comparator found no+ differences for them); parts, nets and parameter changes do.++## Title block and PDF++```bash+altium_doc_params '{"sheet":"C:\\proj\\Main.SchDoc","set":{"Title":"12 V to 5 V buck","Revision":"B","DrawnBy":"AI"}}'+altium_export_pdf '{"projectPath":"C:\\proj\\board.PrjPcb","board":true}' # -> C:\proj\board.pdf+```++`export_pdf` prints every sheet (and the board's composite with `board:true`) through an OutJob in a+throwaway project, so the user's project is not changed. When Altium's PCB Health Check stops the job+(shelved or modified polygons, ...), the bridge answers Generate Outputs and lists what it said in+`healthCheck`; `healthCheck:"stop"` on `altium_run_outjob` cancels instead.++## Pitfalls++- Two open projects with the same file names (a copy beside the original) confuse Altium's comparator:+ close the one you are not editing.+- Sheet edits are saved; the board is saved by `update_pcb` after a change order. `altium_close_document`+ never saves.
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.56+0.2.57
src/agent/AdomAgent.pas.tmpl+443@@ -4751,6 +4751,7 @@ Begin JStr('libReference', Comp.LibReference) + ',' + JStr('comment', Comp.Comment.Text) + ',' + JStr('description', Desc) + ',' ++ JStr('uniqueId', Comp.UniqueId) + ',' + JStr('itemGuid', Comp.ItemGUID) + ',' + JStr('vaultGuid', Comp.VaultGUID) + ',' + JStr('sourceLibrary', Comp.SourceLibraryName) + ',' +@@ -6492,6 +6493,443 @@ Begin (Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel)) And (Prim.OwnerPartDisplayMode = 0); End; +{ ------------------------------------------------------------ schematic editing (0.2.57) --- }++{ the placed parts of the current sheet with this designator (every part of a multi-part component) }+Function SheetCompsOf(Doc : ISch_Document; Des : String) : TInterfaceList;+Var+ It : ISch_Iterator;+ C : ISch_Component;+Begin+ Result := TInterfaceList.Create;+ It := Doc.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eSchComponent));+ C := It.FirstSchObject;+ While C <> Nil Do+ Begin+ If UpperCase(C.Designator.Text) = UpperCase(Des) Then Result.Add(C);+ C := It.NextSchObject;+ End;+ Doc.SchIterator_Destroy(It);+End;++{ a GND-like net gets a ground symbol, anything else a bar }+Function PowerStyleOf(Net : String) : Integer;+Var+ U : String;+Begin+ U := UpperCase(Net);+ If (U = 'GND') Or (U = 'AGND') Or (U = 'DGND') Or (U = 'PGND') Or (U = 'GNDA') Or (U = 'GNDD') Or (U = 'VSS') Or (U = '0V') Then+ Result := ePowerGndPower+ Else If (U = 'EARTH') Or (U = 'PE') Or (U = 'CHASSIS') Then+ Result := ePowerGndEarth+ Else+ Result := ePowerBar;+End;++{ sch_connect: a wire stub and a net label (kind=label), power port (kind=power) or nothing (kind=wire)+ at the outer end of pins of placed parts on the current sheet.+ payload lines: designator|pin (number or name)|net|kind|stubMil+ One undo step. Answers per line: ok, or why not. }+Function CmdSchConnect(Lines : TStringList; Payload : String) : String;+Var+ Doc : ISch_Document;+ Ops, Comps : TStringList;+ CL : TInterfaceList;+ C : ISch_Component;+ PIt : ISch_Iterator;+ Pin, Hit : ISch_Pin;+ W : ISch_Wire;+ NL : ISch_NetLabel;+ PO : ISch_PowerObject;+ Op, Des, PinKey, Net, Kind, Res, S : String;+ I, J, Dir, HX, HY, EX, EY, Stub, NOk : 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;+ Ops := TStringList.Create; Ops.Text := Payload;+ S := ''; NOk := 0;+ SchServer.ProcessControl.PreProcess(Doc, '');+ For I := 0 To Ops.Count - 1 Do+ Begin+ Op := Ops[I];+ If Trim(Op) = '' Then Continue;+ Des := FieldP(Op, 1); PinKey := FieldP(Op, 2); Net := FieldP(Op, 3); Kind := LowerCase(FieldP(Op, 4));+ Stub := StrToIntDef(FieldP(Op, 5), 200);+ If Kind = '' Then Kind := 'label';+ Res := '';+ Hit := Nil;+ CL := SheetCompsOf(Doc, Des);+ If CL.Count = 0 Then Res := 'designator not on this sheet'+ Else+ Begin+ For J := 0 To CL.Count - 1 Do+ Begin+ C := CL[J];+ PIt := C.SchIterator_Create;+ PIt.AddFilter_ObjectSet(MkSet(ePin));+ Pin := PIt.FirstSchObject;+ While Pin <> Nil Do+ Begin+ If (Pin.OwnerPartId = C.CurrentPartID) Or (Pin.OwnerPartId = 0) Then+ If (UpperCase(Pin.Designator) = UpperCase(PinKey)) Or ((Hit = Nil) And (UpperCase(Pin.Name) = UpperCase(PinKey))) Then+ If (Hit = Nil) Or (UpperCase(Pin.Designator) = UpperCase(PinKey)) Then Hit := Pin;+ Pin := PIt.NextSchObject;+ End;+ C.SchIterator_Destroy(PIt);+ End;+ If Hit = Nil Then Res := 'pin not found on ' + Des;+ End;+ CL.Free;+ If (Res = '') And (Net = '') And (Kind <> 'wire') Then Res := 'no net given';+ If Res = '' Then+ Begin+ Dir := Hit.Orientation;+ HX := Hit.Location.X; HY := Hit.Location.Y;+ If Dir = 0 Then HX := HX + Hit.PinLength+ Else If Dir = 1 Then HY := HY + Hit.PinLength+ Else If Dir = 2 Then HX := HX - Hit.PinLength+ Else HY := HY - Hit.PinLength;+ EX := HX; EY := HY;+ If Dir = 0 Then EX := HX + MilsToCoord(Stub)+ Else If Dir = 1 Then EY := HY + MilsToCoord(Stub)+ Else If Dir = 2 Then EX := HX - MilsToCoord(Stub)+ Else EY := HY - MilsToCoord(Stub);+ W := SchServer.SchObjectFactory(eWire, eCreate_Default);+ W.Location := Point(HX, HY);+ W.InsertVertex := 1;+ W.SetState_Vertex(1, Point(HX, HY));+ W.InsertVertex := 2;+ W.SetState_Vertex(2, Point(EX, EY));+ W.LineWidth := eSmall;+ Doc.RegisterSchObjectInContainer(W);+ SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, W.I_ObjectAddress);+ If Kind = 'power' Then+ Begin+ PO := SchServer.SchObjectFactory(ePowerObject, eCreate_Default);+ PO.Text := Net;+ PO.Location := Point(EX, EY);+ PO.Style := PowerStyleOf(Net);+ If Dir = 0 Then PO.Orientation := eRotate0+ Else If Dir = 1 Then PO.Orientation := eRotate90+ Else If Dir = 2 Then PO.Orientation := eRotate180+ Else PO.Orientation := eRotate270;+ PO.ShowNetName := True;+ Doc.RegisterSchObjectInContainer(PO);+ SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, PO.I_ObjectAddress);+ End+ Else If Kind = 'label' Then+ Begin+ NL := SchServer.SchObjectFactory(eNetLabel, eCreate_Default);+ NL.Text := Net;+ NL.Location := Point(EX, EY);+ If (Dir = 1) Or (Dir = 3) Then NL.Orientation := eRotate90;+ If (Dir = 2) Or (Dir = 3) Then NL.Justification := eJustify_BottomRight;+ Doc.RegisterSchObjectInContainer(NL);+ SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, NL.I_ObjectAddress);+ End;+ NOk := NOk + 1;+ Res := 'ok';+ End;+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('designator', Des) + ',' + JStr('pin', PinKey) + ',' + JStr('net', Net) + ',' + JStr('kind', Kind) + ',' + JStr('result', Res);+ If Res = 'ok' Then S := S + ',' + JRaw('endMil', '[' + IntToStr(Round(CoordToMils(EX))) + ',' + IntToStr(Round(CoordToMils(EY))) + ']');+ S := S + '}';+ End;+ SchServer.ProcessControl.PostProcess(Doc, '');+ Doc.GraphicallyInvalidate;+ Ops.Free;+ Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('connected', IntToStr(NOk)) + ',' + JRaw('results', '[' + S + ']') + '}';+End;++{ sch_edit: change placed parts on the current sheet.+ payload lines: designator|action|a|b actions: move x y (mil, the part's origin) | moveby dx dy |+ rotate 0/90/180/270 | designator new | comment text | delete+ move, moveby and rotate act on every part of a multi-part component (they are drawn apart; say which+ with a single-part designator). One undo step. }+Function CmdSchEdit(Lines : TStringList; Payload : String) : String;+Var+ Doc : ISch_Document;+ Ops : TStringList;+ CL, Dead : TInterfaceList;+ C : ISch_Component;+ Op, Des, Act, A, B, Res, S : String;+ I, J, K, Want, Have, Steps, NOk : 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;+ Ops := TStringList.Create; Ops.Text := Payload;+ Dead := TInterfaceList.Create;+ S := ''; NOk := 0;+ SchServer.ProcessControl.PreProcess(Doc, '');+ For I := 0 To Ops.Count - 1 Do+ Begin+ Op := Ops[I];+ If Trim(Op) = '' Then Continue;+ Des := FieldP(Op, 1); Act := LowerCase(FieldP(Op, 2)); A := FieldP(Op, 3); B := FieldP(Op, 4);+ Res := 'ok';+ CL := SheetCompsOf(Doc, Des);+ If CL.Count = 0 Then Res := 'designator not on this sheet'+ Else+ For J := 0 To CL.Count - 1 Do+ Begin+ C := CL[J];+ If Act = 'move' Then C.MoveToXY(MilsToCoord(StrToIntDef(A, 0)), MilsToCoord(StrToIntDef(B, 0)))+ Else If Act = 'moveby' Then C.MoveByXY(MilsToCoord(StrToIntDef(A, 0)), MilsToCoord(StrToIntDef(B, 0)))+ Else If Act = 'rotate' Then+ Begin+ Want := (StrToIntDef(A, 0) Div 90) Mod 4;+ Have := C.Orientation;+ Steps := (Want - Have + 4) Mod 4;+ For K := 1 To Steps Do C.RotateBy90(C.Location, eRotate90);+ End+ Else If Act = 'designator' Then C.Designator.Text := A+ Else If Act = 'comment' Then C.Comment.Text := A+ Else If Act = 'delete' Then Dead.Add(C)+ Else Res := 'unknown action ' + Act;+ C.GraphicallyInvalidate;+ End;+ CL.Free;+ If Res = 'ok' Then NOk := NOk + 1;+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('designator', Des) + ',' + JStr('action', Act) + ',' + JStr('result', Res) + '}';+ End;+ For I := 0 To Dead.Count - 1 Do+ Begin+ C := Dead[I];+ Doc.RemoveSchObject(C);+ SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, C.I_ObjectAddress);+ End;+ SchServer.ProcessControl.PostProcess(Doc, '');+ Doc.GraphicallyInvalidate;+ Ops.Free; Dead.Free;+ Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('applied', IntToStr(NOk)) + ',' + JRaw('results', '[' + S + ']') + '}';+End;++{ sch_place_part: place one component of the CURRENT SchLib on a sheet (opened and shown here).+ libref, sch (the sheet path), designator, x, y (mil, snapped to 100), rotation, comment, partId,+ footprint + pcblib (else the library part's own footprint models are copied by name). }+Function CmdSchPlacePart(Lines : TStringList) : String;+Var+ SLib : ISch_Lib;+ LibCmp, Inst : ISch_Component;+ Prim, Cpy : ISch_GraphicalObject;+ PIt : ISch_Iterator;+ Impl, NImpl : ISch_Implementation;+ Prm : ISch_Parameter;+ SD : IServerDocument;+ Sheet : ISch_Document;+ SchPath, Fp, Models : String;+ DX, DY, Part, K, Rot, NPins : Integer;+Begin+ SLib := SchServer.GetCurrentSchDocument;+ If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first (it must be the current document)') + '}'; Exit; End;+ LibCmp := LibCmpOf(SLib, Param(Lines, 'libref'));+ If LibCmp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','libref_not_found') + ',' + JStr('error','no component ' + Param(Lines, 'libref') + ' in ' + SLib.DocumentName) + '}'; Exit; End;+ SchPath := Param(Lines, 'sch');+ SD := Nil;+ Try+ SD := Client.OpenDocument('SCH', SchPath);+ Except+ SD := Nil;+ End;+ If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the sheet ' + SchPath) + '}'; Exit; End;+ Client.ShowDocument(SD);+ Sheet := SchServer.GetCurrentSchDocument;+ If (Sheet = Nil) Or (Sheet.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','the sheet did not become the current document') + '}'; Exit; End;+ Part := StrToIntDef(Param(Lines, 'partId'), 1);+ DX := (StrToIntDef(Param(Lines, 'x'), 1000) Div 100) * 100;+ DY := (StrToIntDef(Param(Lines, 'y'), 1000) Div 100) * 100;+ SchServer.ProcessControl.PreProcess(Sheet, '');+ Inst := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+ Inst.CurrentPartID := Part;+ Inst.DisplayMode := 0;+ Inst.LibReference := LibCmp.LibReference;+ Inst.ComponentDescription := LibCmp.ComponentDescription;+ Inst.SourceLibraryName := SLib.DocumentName;+ Inst.Location := Point(MilsToCoord(DX), MilsToCoord(DY));+ NPins := 0;+ PIt := LibCmp.SchIterator_Create;+ Prim := PIt.FirstSchObject;+ While Prim <> Nil Do+ Begin+ If ((Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or (Prim.ObjectID = eLine) Or (Prim.ObjectID = eArc) Or+ (Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or (Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel)) And+ (Prim.OwnerPartDisplayMode = 0) And ((Prim.OwnerPartId = Part) Or (Prim.OwnerPartId = 0)) Then+ Begin+ Cpy := Prim.Replicate;+ Cpy.OwnerPartId := Part;+ Cpy.OwnerPartDisplayMode := 0;+ Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));+ Inst.AddSchObject(Cpy);+ If Prim.ObjectID = ePin Then NPins := NPins + 1;+ End;+ Prim := PIt.NextSchObject;+ End;+ LibCmp.SchIterator_Destroy(PIt);+ PIt := LibCmp.SchIterator_Create;+ PIt.AddFilter_ObjectSet(MkSet(eParameter));+ Prm := PIt.FirstSchObject;+ While Prm <> Nil Do+ Begin+ If (UpperCase(Prm.Name) <> 'COMMENT') And (UpperCase(Prm.Name) <> 'DESIGNATOR') Then+ Begin+ Cpy := Prm.Replicate;+ Cpy.OwnerPartId := Part;+ Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));+ Inst.AddSchObject(Cpy);+ End;+ Prm := PIt.NextSchObject;+ End;+ LibCmp.SchIterator_Destroy(PIt);+ { the footprint: the one named, else the library part's own PCBLIB models by name }+ Fp := Param(Lines, 'footprint');+ Models := '';+ If Fp <> '' Then+ Begin+ NImpl := Inst.AddSchImplementation;+ NImpl.ClearAllDatafileLinks;+ NImpl.Description := 'Footprint';+ NImpl.ModelName := Fp;+ NImpl.ModelType := 'PCBLIB';+ NImpl.AddDataFileLink(Fp, Param(Lines, 'pcblib'), 'PCBLIB');+ NImpl.IsCurrent := True;+ Models := '"' + Fp + '"';+ End+ Else+ Begin+ PIt := LibCmp.SchIterator_Create;+ PIt.AddFilter_ObjectSet(MkSet(eImplementation));+ Impl := PIt.FirstSchObject;+ While Impl <> Nil Do+ Begin+ If UpperCase(Impl.ModelType) = 'PCBLIB' Then+ Begin+ NImpl := Inst.AddSchImplementation;+ NImpl.ClearAllDatafileLinks;+ NImpl.Description := Impl.Description;+ NImpl.ModelName := Impl.ModelName;+ NImpl.ModelType := 'PCBLIB';+ NImpl.AddDataFileLink(Impl.ModelName, Param(Lines, 'pcblib'), 'PCBLIB');+ NImpl.IsCurrent := Impl.IsCurrent;+ If Models <> '' Then Models := Models + ',';+ Models := Models + '"' + JsonEsc(Impl.ModelName) + '"';+ End;+ Impl := PIt.NextSchObject;+ End;+ LibCmp.SchIterator_Destroy(PIt);+ End;+ If Param(Lines, 'designator') <> '' Then Inst.Designator.Text := Param(Lines, 'designator')+ Else Inst.Designator.Text := LibCmp.Designator.Text;+ Inst.Designator.IsHidden := False;+ Inst.Designator.Location := Point(MilsToCoord(DX), MilsToCoord(DY + 200));+ If Param(Lines, 'comment') <> '' Then Inst.Comment.Text := Param(Lines, 'comment')+ Else Inst.Comment.Text := LibCmp.Comment.Text;+ Inst.Comment.IsHidden := False;+ Inst.Comment.Location := Point(MilsToCoord(DX), MilsToCoord(DY - 200));+ Sheet.RegisterSchObjectInContainer(Inst);+ SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);+ Rot := (StrToIntDef(Param(Lines, 'rotation'), 0) Div 90) Mod 4;+ For K := 1 To Rot Do Inst.RotateBy90(Inst.Location, eRotate90);+ SchServer.ProcessControl.PostProcess(Sheet, '');+ Sheet.GraphicallyInvalidate;+ Result := '{' + JRaw('ok','true') + ',' + JStr('document', Sheet.DocumentName) + ',' + JStr('designator', Inst.Designator.Text) + ',' ++ JStr('libref', LibCmp.LibReference) + ',' + JRaw('pins', IntToStr(NPins)) + ',' + JRaw('footprints', '[' + Models + ']') + ',' ++ JRaw('atMil', '[' + IntToStr(DX) + ',' + IntToStr(DY) + ']') + '}';+End;++{ doc_params: the current sheet's document parameters (title block: Title, DocumentNumber, Revision,+ DrawnBy, Author, CompanyName, ...). params=K=V;K=V sets them (added hidden when missing); always+ answers every document parameter. }+Function CmdDocParams(Lines : TStringList) : String;+Var+ Doc : ISch_Document;+ It : ISch_Iterator;+ Pm : ISch_Parameter;+ Items : TStringList;+ Spec, K, V, S : String;+ I, P, NSet : 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;+ NSet := 0;+ If Param(Lines, 'params') <> '' Then+ Begin+ SchServer.ProcessControl.PreProcess(Doc, '');+ Items := SplitSemi(Param(Lines, 'params'));+ For I := 0 To Items.Count - 1 Do+ Begin+ Spec := Items[I];+ P := Pos('=', Spec);+ If P > 0 Then+ Begin+ K := Trim(Copy(Spec, 1, P - 1));+ V := Trim(Copy(Spec, P + 1, Length(Spec)));+ If K <> '' Then Begin SetDocParam(Doc, K, V); NSet := NSet + 1; End;+ End;+ End;+ Items.Free;+ SchServer.ProcessControl.PostProcess(Doc, '');+ Doc.GraphicallyInvalidate;+ End;+ S := '';+ It := Doc.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eParameter));+ Pm := It.FirstSchObject;+ While Pm <> Nil Do+ Begin+ { the sheet's own parameters, not its parts' (the iterator walks into components) }+ If (Pm.Container <> Nil) And (Pm.Container.ObjectID <> eSchComponent) Then+ Begin+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('name', Pm.Name) + ',' + JStr('value', Pm.Text) + '}';+ End;+ Pm := It.NextSchObject;+ End;+ Doc.SchIterator_Destroy(It);+ Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('set', IntToStr(NSet)) + ',' + JRaw('params', '[' + S + ']') + '}';+End;++{ sch_rename_at: rename placed parts named by designator AND position (several parts on a sheet can all+ read 'R?'). payload lines: from|to|xMil|yMil (the part's Location). One undo step. }+Function CmdSchRenameAt(Lines : TStringList; Payload : String) : String;+Var+ Doc : ISch_Document;+ Ops : TStringList;+ CL : TInterfaceList;+ C, Hit : ISch_Component;+ Op, Res, S : String;+ I, J, X, Y : 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;+ Ops := TStringList.Create; Ops.Text := Payload;+ S := '';+ SchServer.ProcessControl.PreProcess(Doc, '');+ For I := 0 To Ops.Count - 1 Do+ Begin+ Op := Ops[I];+ If Trim(Op) = '' Then Continue;+ X := StrToIntDef(FieldP(Op, 3), 0); Y := StrToIntDef(FieldP(Op, 4), 0);+ Hit := Nil;+ CL := SheetCompsOf(Doc, FieldP(Op, 1));+ For J := 0 To CL.Count - 1 Do+ Begin+ C := CL[J];+ If (Abs(Round(CoordToMils(C.Location.X)) - X) <= 1) And (Abs(Round(CoordToMils(C.Location.Y)) - Y) <= 1) Then Hit := C;+ End;+ CL.Free;+ If Hit = Nil Then Res := 'no part ' + FieldP(Op, 1) + ' at that position'+ Else Begin Hit.Designator.Text := FieldP(Op, 2); Hit.GraphicallyInvalidate; Res := 'ok'; End;+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('from', FieldP(Op, 1)) + ',' + JStr('to', FieldP(Op, 2)) + ',' + JStr('result', Res) + '}';+ End;+ SchServer.ProcessControl.PostProcess(Doc, '');+ Doc.GraphicallyInvalidate;+ Ops.Free;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('results', '[' + S + ']') + '}';+End;+ Function CmdBuildSchematic(Lines : TStringList; Payload : String) : String; Var SLib : ISch_Lib;@@ -7185,6 +7623,11 @@ Begin Else If Cmd = 'net_connections' Then Result := CmdNetConnections(Lines) Else If Cmd = 'schematic_data' Then Result := CmdSchematicData(Lines) Else If Cmd = 'sheet_params' Then Result := CmdSheetParams(Lines)+ Else If Cmd = 'sch_connect' Then Result := CmdSchConnect(Lines, Payload)+ Else If Cmd = 'sch_edit' Then Result := CmdSchEdit(Lines, Payload)+ 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 = '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+15−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.56",+ "version": "0.2.57", "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",@@ -42,6 +42,7 @@ "altium_agent_status", "altium_agent_stop", "altium_animate_view",+ "altium_annotate", "altium_apply_pours", "altium_apply_silkscreen", "altium_board_shape",@@ -71,11 +72,13 @@ "altium_diagnostics", "altium_dialog_log", "altium_dismiss_dialogs",+ "altium_doc_params", "altium_ensure_licensed", "altium_eval", "altium_export_bom", "altium_export_gerber", "altium_export_manufacturing",+ "altium_export_pdf", "altium_export_step", "altium_extension_ping", "altium_extract_intlib",@@ -136,6 +139,9 @@ "altium_run_outjob", "altium_run_script", "altium_save",+ "altium_sch_connect",+ "altium_sch_edit",+ "altium_sch_place_part", "altium_schematic_data", "altium_select_net", "altium_selftest_script_error",@@ -166,6 +172,7 @@ "altium_test_board", "altium_uninstall", "altium_uninstall_library",+ "altium_update_pcb", "altium_window_info", "altium_xcopy_footprint", "altium_zoom_to"@@ -272,7 +279,13 @@ "setup_trial": 300, "a365_files_upload": 120, "bom": 600,- "sheet_params": 300+ "sheet_params": 300,+ "sch_place_part": 120,+ "sch_connect": 90,+ "sch_edit": 90,+ "annotate": 300,+ "update_pcb": 600,+ "export_pdf": 600 }, "prefixes": [ {
uninstall.sh+1−1@@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do- rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" "$HOME_DIR/altium-install" "$HOME_DIR/altium-libraries" "$HOME_DIR/altium-boards" "$HOME_DIR/altium-outputs" "$HOME_DIR/altium-instances" "$HOME_DIR/altium-licensing" "$HOME_DIR/altium-uia" "$HOME_DIR/altium-wiki-import" "$HOME_DIR/altium-a365" "$HOME_DIR/altium-bom" "$HOME_DIR/altium-bridge-dev" "$HOME_DIR/altium-bridge-publish"+ rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" "$HOME_DIR/altium-install" "$HOME_DIR/altium-libraries" "$HOME_DIR/altium-boards" "$HOME_DIR/altium-outputs" "$HOME_DIR/altium-instances" "$HOME_DIR/altium-licensing" "$HOME_DIR/altium-uia" "$HOME_DIR/altium-wiki-import" "$HOME_DIR/altium-a365" "$HOME_DIR/altium-bom" "$HOME_DIR/altium-schematic-edit" "$HOME_DIR/altium-bridge-dev" "$HOME_DIR/altium-bridge-publish" done rm -rf "$HOME/.local/share/adom/altium-bridge" rm -f "$HOME/.local/bin/altium-dashboard" "$HOME/.local/bin/altium-kicad2altium" "$HOME/.local/bin/altium-symbol-modes" "$HOME/.local/bin/altium-kicad-downgrade" "$HOME/.local/bin/altium-bom"