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.45 source (not yet released): save-into-missing-folder fix + saved check, sheet grows on append, board_shape/pcb_report/read_board read the outline vertices + mechOutline (#16), test_board Comment labels + Pattern (#17), uninstall_library + list_installed_libraries (#14), X2 script box classified, panel hint; user docs for append/etiquette/panel/uninstall
Co-Authored-By: Claude Opus 5.5 <[email protected]>
15 files changed
+409−78
CHANGELOG.md+13README.md+7−6SKILL.md+4−3bridge-rs/src/dialogs.rs+8bridge-rs/src/library.rs+114−2bridge-rs/src/main.rs+1−1bridge-rs/src/misc.rs+1−1bridge-rs/src/surfaces.rs+2−1bridge-rs/src/verbs.rs+19−7package.json+1−1page.json+1−1skills/altium-boards/SKILL.md+35−14src/BRIDGE_VERSION+1−1src/agent/AdomAgent.pas.tmpl+199−39src/bridge.json+3−1CHANGELOG.md+13@@ -1,5 +1,18 @@ # Changelog +## 0.2.45 (2026-10-15)+First live run since 0.2.39 on a licensed Altium (Altium Designer Develop, Colby-ROG2). It verified the held 0.2.40-0.2.44 features and fixed what the run and two new issues found.++- **A save into a missing folder no longer fails silently.** `altium_test_board {pcbPath}` and `altium_place_on_sch {schPath}` answered with the path while Altium's save wrote nothing (the folder did not exist), so the next "append" found no board and started a new one. The bridge now creates the folder first and checks the file on disk: `saved:true`, or `errorCode: not_saved`. A document that is open but not yet saved now counts as existing for append.+- **Appending to a schematic grows the sheet** (`sheetGrown`), so the added parts stay inside the drawing frame instead of landing in the gray past its right edge.+- **`altium_board_shape` proves its own result** (adom/altium-bridge#16). It, `pcb_report {want:'board'}` and `read_board`'s outline now read the outline's vertices: `BoardOutline.BoundingRectangle` is cached and kept the 6000 x 4000 mil default after a reshape, on AD 26.9.1 and Altium Develop alike. New `mechOutline:true` draws the rectangle on Mechanical 1 so the fab Gerber has an outline (the board shape alone is plotted into none).+- **`altium_test_board` parts carry their own name and footprint** (adom/altium-bridge#17). The part name is the component's Comment, so it moves with the part (loose overlay text stayed behind and piled up in the silkscreen Gerber), and `Pattern` is the footprint name (it was empty, so BOM, STEP naming and footprint comparison saw nothing).+- **New `altium_uninstall_library` and `altium_list_installed_libraries`** (adom/altium-bridge#14). Uninstall matches `libraryName` (file stem: the SchLib and the PcbLib together) or an exact `libraryPath`, closes any open document for the library first, never touches the files, and answers `notInstalled:true` when nothing matches. Verified across a clean restart. It also cleared two dead "UI" entries left behind when 0.2.9 flattened the bundle folder.+- **The "X2 / Continue execution?" script box is classified** (`script_runtime_error`, safe action Cancel) instead of scanning as unknown: its text is painted, so it is only in the photo.+- `altium_panel`'s timeout now says why: on Altium Develop a UI Automation search of the main window takes over 20 s, and no Altium process switches a docked tab; opening a library already brings its panel to the front.+- Verified live, no code change: append on board and sheet (#11), focus kept on the user's window through document opens and agent restarts (#12), `altium_panel` answers inside its budget and the next verb is not delayed (#13).++ ## 0.2.44 (2026-09-29) - **The Altium extension DLL is signed too.** `extension/AdomBridge/AdomBridge.dll` now carries the same Adom Industries, Inc. signature as `altium-bridge.exe`: Windows Smart App Control checks the DLLs a program loads, not only the exe it starts, so an unsigned plugin could be refused on a PC where it is on. The release script signs and verifies every exe and DLL it ships and refuses to package any that is unsigned. Code otherwise identical to 0.2.43.
README.md+7−6@@ -60,7 +60,8 @@ All 16 named pins (GND/TXD/RXD/V3/UD+/UD-/XI/XO · VCC/R232/RTS#/DTR#/DCD#/RI#/D The symbol placed as U1 on a fresh sheet (per-primitive copy — the engine's group `Replicate` drops children; the bridge handles that for you). `symbolName` takes a `;` list-(from 0.2.13): every part lands on the same new sheet as U1..Un.+(from 0.2.13): every part lands on the same sheet as U1..Un. Pass an existing `schPath` to ADD+symbols to that sheet; it grows to keep them inside the frame (from 0.2.45). Sheet sizing (from 0.2.16 / 0.2.17): the sheet is sized from the parts, not the parts from the sheet. The agent first measures every symbol's bounding box (pins included, plus 250 mil a side@@ -87,11 +88,11 @@ rounded pads, sixteen through-hole heatsink vias in the EP (all designator 49 � silk pin-1 dot, and the PIN1 / GND_EP stub tracks that survived both swaps untouched. `altium_test_board` takes a `;` list in `footprintName` (from 0.2.13): the parts are spread-evenly along X on one new board. From 0.2.18 / 0.2.19 the parts are designated U1..Un (so-`component_pins` and `footprint_primitives` can address each one) and each part's name is-written on the Top Overlay under its footprint: 35 mil stroke text, centred on the part from-the text's measured width, its top 2.5 mm below the part centre so it clears the courtyard of-a 3 mm part.+evenly along X on one board. The parts are designated U1..Un (so `component_pins` and+`footprint_primitives` can address each one). Each part's name is its own Comment on the Top+Overlay under its footprint, so it moves with the part, and its `Pattern` is the footprint name+(from 0.2.45). Pass an existing `pcbPath` to ADD parts to that board (from 0.2.40), and+`altium_board_shape {mechOutline:true}` to give the fab Gerber an outline (from 0.2.45). 
SKILL.md+4−3@@ -32,7 +32,7 @@ Altium is a licensed host app. The bridge never installs it silently; it detects ## The 8 conventions 1. **Poll readiness, don't launch blindly.** `altium_readiness` → act on the row above → drive.-2. **Background by default.** Every `show_*` opens in the background and hands focus back; `windowEtiquette` in the response says where the window went. Only Altium's own cold launch foregrounds. If you must foreground, toast the user first (`notify_user`) and say why.+2. **Background by default.** Every `show_*` opens in the background and hands focus back; `windowEtiquette` in the response says where the window went. From 0.2.40 every driving verb does the same: if Altium took the foreground during the verb, the user's window is handed back and the reply carries `foregroundReturnedTo`. Only Altium's own cold launch foregrounds. If you must foreground, toast the user first (`notify_user`) and say why. 3. **`ok:true` is not enough — verify.** `show_*` and `state` return frames; look at them. `run_outjob` returns `files[]` actually written. 4. **One script at a time.** While the agent is resident, never run `altium_run_script`; it raises Altium's "Another script executing now" modal. Send work through `altium_eval` / `altium_run_command`. 5. **Never leave a modal.** Prefer commands that carry their parameters (`altium_run_command` with `params`) so no picker opens. After any mutating step, `altium_check_dialogs`.@@ -62,13 +62,14 @@ Altium is a licensed host app. The bridge never installs it silently; it detects | `altium_set_params` | parameters (LCSC Part #, MPN, …), comment, footprint link on a symbol | `libraryPath`, `symbolName`, `params`, `footprint` | | `altium_add_body` | 3D body on a footprint — STEP model or extruded box | `libraryPath`, `footprintName`, `step` \| `bwMil,bhMil,hMil` | | `altium_install_library` / `altium_compile_intlib` / `altium_extract_intlib` | Installed Libraries, .LibPkg → .IntLib, .IntLib → sources | `libraryPath` / `libpkgPath` / `intlibPath` |+| `altium_list_installed_libraries` / `altium_uninstall_library` | read Installed Libraries / remove entries (from 0.2.45; files never touched, any open document for the library is closed first, nothing matching answers `notInstalled:true`). Close Altium with a plain `altium_close` afterwards: a discard close can bring the entry back | (none) / `libraryName` (file stem: SchLib AND PcbLib) or `libraryPath`, `dryRun` | | `altium_build_footprint` / `altium_build_symbol_modes` | author a footprint from a `tools/kicad2altium.py` spec (via pads, roundrect, mech layers) / ONE symbol with N display modes | `libraryPath`, `spec` | | `altium_add_models` / `altium_symbol_mode` | N footprint models + params on a symbol / switch display mode + frame | `symbolName`, `models[]`, `params` / `mode` | | `altium_add_symbol_art` / `altium_symbol_prims` (from 0.2.15) | draw the chip outline adom-symbol baked into the KiCad symbol onto a SchLib component (replaces earlier art unless `clear:false`) / read-only primitive counts, body rectangle, polyline bbox, models | `libraryPath`, `symbolName`, `kicadSym`, `clear`, `mode:"lines"\|"polyline"` / `models:true` | | `altium_set_models` (from 0.2.15) | replace every footprint model on a symbol with ONE PCBLIB model (`set_params` footprint and `add_models` append) | `libraryPath`, `symbolName`, `footprint` | | `altium_plate_vias` (from 0.2.20) | every drilled pad in a PcbLib that loaded on the Top Layer becomes a plated Multi-Layer pad (heatsink vias); `altium_save` afterwards | `libraryPath`, `footprintName` (optional filter) |-| `altium_place_on_sch` | one symbol or a `;` list on ONE new sheet as U1..Un; the sheet is sized from the measured parts (500 mil gaps, 600 mil margins, A4 minimum), designator above and part number below each body, title block filled | `libraryPath`, `symbolName`, `schPath`, `title`, `number`, `revision`, `drawnBy`, `description` |-| `altium_test_board` / `altium_board_shape` / `altium_swap_footprint` | wired test board (one footprint or a `;` list, parts U1..Un with each part's name on the Top Overlay under its footprint), rectangular board shape, footprint swap keeping nets | `footprintName`, `wMm`,`hMm`, `pcbPath`, `assigns`, `stubs` |+| `altium_place_on_sch` | one symbol or a `;` list on ONE sheet as U1..Un (an existing `schPath` is APPENDED to, the sheet grows to fit); the sheet is sized from the measured parts (500 mil gaps, 600 mil margins, A4 minimum), designator above and part number below each body, title block filled | `libraryPath`, `symbolName`, `schPath`, `title`, `number`, `revision`, `drawnBy`, `description` |+| `altium_test_board` / `altium_board_shape` / `altium_swap_footprint` | wired test board (one footprint or a `;` list, parts U1..Un, each named by its own Comment with `Pattern` set; an existing `pcbPath` is APPENDED to), rectangular board shape (outline read back from its vertices; `mechOutline:true` adds a Mechanical 1 fab outline), footprint swap keeping nets | `footprintName`, `wMm`,`hMm`, `pcbPath`, `assigns`, `stubs` / `mechOutline` | | `altium_clone_symbol` / `altium_rename_symbol` / `altium_remove_symbol` / `altium_read_symbol` | library manipulation on a SchLib (clone carries every display mode) | `from`,`to` / `symbolName` | | `altium_copy_footprint` / `altium_rename_footprint` / `altium_remove_footprint` | the same for a PcbLib (copy carries pads, bodies, drawing) | `from`,`to` / `footprintName` | | `altium_pcb_report` / `altium_normalize_bodies` | ground truth (pads/bodies/nets/tracks with bboxes) / centre bodies on pads | `want:"board"`, `footprintName` |
bridge-rs/src/dialogs.rs+8@@ -195,6 +195,14 @@ fn scan_impl(main_pid_only: bool, sel_pid: Option<u64>) -> Vec<Value> { safe_action = Some("Choose a license".into()); meaning = "Altium's 'You are not currently using an Altium Designer license' prompt (Choose a license | Continue). Choose a license opens the License Management page; the reaper then presses Use License via UIA. 'Continue' would leave Altium in Viewer mode, where scripting is refused — never the right answer for a bridge user.".into(); }+ if category == "unknown" && title.trim() == "X2" && btns.len() == 2+ && btns.iter().any(|b| b.eq_ignore_ascii_case("ok")) && btns.iter().any(|b| b.eq_ignore_ascii_case("cancel")) {+ // the script debugger's "<error> at Line:N ... Continue execution?" box: its text is+ // painted, so it scanned as unknown (seen 2026-10-15 after a bad altium_eval)+ category = "script_runtime_error".into();+ safe_action = Some("Cancel".into());+ meaning = "A script hit a runtime error and Altium's debugger asks 'Continue execution?'. Cancel stops the script cleanly; the resident agent survives. Photograph it first: the error text is only in the picture.".into();+ } if category == "unknown" && title.to_lowercase().contains("renewal failed") { category = "licence_seat_stranded".into(); safe_action = Some("OK".into());
bridge-rs/src/library.rs+114−2@@ -293,8 +293,12 @@ pub fn place_on_sch(args: &Value) -> Value { if let Some(e) = support::must_open(&lib, 60) { return e; } let title = s_or(args, "title", ""); let number = s_or(args, "number", ""); let revision = s_or(args, "revision", ""); let drawn = s_or(args, "drawnBy", ""); let desc = s_or(args, "description", "");+ let sch = s_or(args, "schPath", "");+ if !sch.is_empty() {+ ensure_parent_dir(&sch);+ } let mut r = with_shot(agent::call("place_on_sch",- &[("sch", s_or(args, "schPath", "")), ("libref", s_or(args, "symbolName", "CH340G")),+ &[("sch", sch.clone()), ("libref", s_or(args, "symbolName", "CH340G")), ("title", title.clone()), ("number", number.clone()), ("revision", revision.clone()), ("drawnBy", drawn.clone()), ("description", desc.clone())], None, timeout_of(args, 120)));@@ -309,6 +313,7 @@ pub fn place_on_sch(args: &Value) -> Value { hints.push(format!("TITLE BLOCK INCOMPLETE: pass {} on this verb. The AI writes them: title = what the sheet is, number = the project or page id, revision, drawnBy = the AI thread on behalf of the user, description = two or three sentences from the datasheets or the wiki component page saying what the parts are and what the sheet demonstrates. An empty title block and '*' comments read as an unfinished design.", missing.join(", "))); } r["_hints"] = json!(hints);+ mark_saved(&mut r, &sch); r } @@ -436,6 +441,32 @@ pub fn place_on_pcb(args: &Value) -> Value { None, timeout_of(args, 90))) } +/// Make sure a document's target folder exists before the agent saves into it. Altium's+/// DoFileSave into a missing folder fails silently: test_board answered with the path while no+/// file was written, and the next call could not find the board to append to.+fn ensure_parent_dir(path: &str) {+ if let Some(parent) = std::path::Path::new(path).parent() {+ if !parent.as_os_str().is_empty() {+ let _ = std::fs::create_dir_all(parent);+ }+ }+}++/// After a verb that saves to `path`: did the file land? The bridge runs on the same machine as+/// Altium, so it checks the disk itself instead of trusting the agent's answer.+fn mark_saved(r: &mut Value, path: &str) {+ if path.is_empty() {+ return;+ }+ let saved = std::path::Path::new(path).is_file();+ r["saved"] = json!(saved);+ if !saved && r.get("success").and_then(|b| b.as_bool()) == Some(true) {+ r["success"] = json!(false);+ r["errorCode"] = json!("not_saved");+ r["error"] = json!(format!("the document was built but {path} was not written; check the folder is writable and the name is a valid file name"));+ }+}+ pub fn test_board(args: &Value) -> Value { if let Some(g) = support::need_licensed() { return g;@@ -443,11 +474,16 @@ pub fn test_board(args: &Value) -> Value { if let Some(p) = s(args, "libraryPath") { if let Some(e) = support::must_open(&p, 60) { return e; } }+ let pcb = s_or(args, "pcbPath", "");+ if !pcb.is_empty() {+ ensure_parent_dir(&pcb);+ } let mut r = agent::call("test_board", &[("footprint", s_or(args, "footprintName", "")), ("wMm", s_or(args, "wMm", "15")),- ("hMm", s_or(args, "hMm", "15")), ("pcb", s_or(args, "pcbPath", "")),+ ("hMm", s_or(args, "hMm", "15")), ("pcb", pcb.clone()), ("assigns", s_or(args, "assigns", "")), ("stubs", s_or(args, "stubs", ""))], None, timeout_of(args, 240));+ mark_saved(&mut r, &pcb); r["windowEtiquette"] = json!("background"); r["_hints"] = json!([ "footprintName takes a ';' list: every part lands on ONE board, spread evenly along X inside the wMm x hMm outline; pass pcbPath so the board is saved and altium_show_3d_board {filePath} can prove it.",@@ -870,6 +906,75 @@ fn install_library_bundle_inner(args: &Value, ph: &mut Phases) -> Value { out } +/// Every library in Altium's Installed Libraries list.+pub fn list_installed_libraries(args: &Value) -> Value {+ if let Some(g) = support::need_licensed() {+ return g;+ }+ let r = agent::call("list_installed_libraries", &[], None, timeout_of(args, 30));+ let d = r.get("data").cloned().unwrap_or(Value::Null);+ if d.get("ok").and_then(|b| b.as_bool()) != Some(true) {+ return r;+ }+ json!({"success": true, "count": d.get("count"), "libraries": d.get("libraries"),+ "_hint": "altium_uninstall_library {libraryName | libraryPath} removes entries; the files are never touched."})+}++fn file_stem_lower(p: &str) -> String {+ let base = p.rsplit(['\\', '/']).next().unwrap_or(p);+ base.rsplit_once('.').map(|(s, _)| s).unwrap_or(base).to_lowercase()+}++/// Unregister installed libraries: the inverse of install_library / install_library_bundle.+/// libraryPath matches one registered path exactly (case-insensitive); libraryName matches every+/// registered library whose file stem equals it (X.SchLib and X.PcbLib together). Library files+/// are never touched. Nothing matching is an answer (notInstalled:true), not a failure, so a+/// cleanup can run twice.+pub fn uninstall_library(args: &Value) -> Value {+ if let Some(g) = support::need_licensed() {+ return g;+ }+ let by_path = s(args, "libraryPath").filter(|v| !v.is_empty());+ let by_name = s(args, "libraryName").filter(|v| !v.is_empty());+ if by_path.is_none() && by_name.is_none() {+ return json!({"success": false, "errorCode": "library_required",+ "error": "pass libraryPath (an exact registered path) or libraryName (a file stem, matches both the SchLib and the PcbLib)",+ "_hint": "altium_list_installed_libraries shows what is registered"});+ }+ let listed = agent::call("list_installed_libraries", &[], None, Duration::from_secs(30));+ let d = listed.get("data").cloned().unwrap_or(Value::Null);+ if d.get("ok").and_then(|b| b.as_bool()) != Some(true) {+ return listed;+ }+ let installed: Vec<String> = d.get("libraries").and_then(|a| a.as_array())+ .map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default();+ let norm = |p: &str| p.replace('/', "\\").to_lowercase();+ let matches: Vec<String> = installed.iter().filter(|p| {+ by_path.as_deref().map(|q| norm(p) == norm(q)).unwrap_or(false)+ || by_name.as_deref().map(|n| file_stem_lower(p) == n.to_lowercase()).unwrap_or(false)+ }).cloned().collect();+ if matches.is_empty() {+ return json!({"success": true, "removed": [], "notInstalled": true, "remaining": installed.len(),+ "_hint": "nothing registered matched; altium_list_installed_libraries shows what is"});+ }+ if args.get("dryRun").and_then(|b| b.as_bool()) == Some(true) {+ return json!({"success": true, "dryRun": true, "wouldRemove": matches, "remaining": installed.len()});+ }+ for p in &matches {+ if p.contains(';') {+ return json!({"success": false, "errorCode": "unsupported_path", "error": format!("a ';' in {p} cannot be passed to the agent")});+ }+ }+ let r = agent::call("uninstall_library", &[("paths", matches.join(";"))], None, timeout_of(args, 60));+ let d = r.get("data").cloned().unwrap_or(Value::Null);+ if d.get("ok").and_then(|b| b.as_bool()) != Some(true) {+ return r;+ }+ json!({"success": true, "removed": d.get("removed"), "closedDocuments": d.get("closedDocuments"),+ "remaining": d.get("remaining"), "windowEtiquette": "background",+ "_hint": "Removed from Installed Libraries now. Close Altium with a plain altium_close afterwards: a close that DISCARDS changes (discardChanges:true) can drop library-registration changes, so the removal would come back on the next launch."})+}+ #[cfg(test)] mod tests { use super::*;@@ -903,6 +1008,13 @@ mod tests { assert_eq!(params_spec(&json!({"MPN": "X;1", "Val": "10k"})), "MPN=X,1;Val=10k"); } + #[test]+ fn uninstall_matches_by_file_stem() {+ assert_eq!(file_stem_lower(r"C:\Libs\Adom_ToF.SchLib"), "adom_tof");+ assert_eq!(file_stem_lower("C:/Libs/Adom_ToF.PcbLib"), "adom_tof");+ assert_eq!(file_stem_lower(r"C:\Libs\noext"), "noext");+ }+ #[test] fn library_name_validation_matches_python() { assert!(valid_lib_name("UTA Longhorn Racing"));
bridge-rs/src/main.rs+1−1@@ -29,7 +29,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.44";+pub const BRIDGE_VERSION: &str = "0.2.45"; 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/misc.rs+1−1@@ -164,7 +164,7 @@ pub fn panel(args: &Value) -> Value { } let timed_out = |stage: &str, t0: std::time::Instant| json!({"success": false, "errorCode": "panel_search_timeout", "panel": name, "stage": stage, "elapsedSec": t0.elapsed().as_secs_f64(), "ensured": ensured,- "_hint": "Altium's UIA tree did not answer inside the panel budget. The tab may be undocked, hidden, or the caption spelled differently (case matters). Pass ensureId (e.g. TSchLibPanel) if the panel may not exist yet; the search is abandoned, nothing was clicked."});+ "_hint": "A UI Automation search of Altium's main window did not finish inside the panel budget, so the tab could not be reached; nothing was clicked. On Altium Designer Develop this is the normal case (one search measured over 20 s on 2026-10-15), and no Altium process switches a docked tab either (Client:CustomizeResources Show/Toggle only ensures the panel exists). What does work: opening a library document brings its own panel to the front, so altium_show_library / show_symbol lands on SCH Library and show_footprint on PCB Library."}); // 1. discovery: existence + selection in one search let tab = match find_tab(hwnd, &name, left().min(10)) { Err(_) => return timed_out("find", t0),
bridge-rs/src/surfaces.rs+2−1@@ -275,9 +275,10 @@ pub fn board_shape(args: &Value) -> Value { if let Some(p) = s(args, "pcbPath") { if let Some(e) = support::must_open(&p, 60) { return e; } }+ let mech = if args.get("mechOutline").and_then(|b| b.as_bool()) == Some(true) { "1" } else { "0" }; agent::call("board_shape", &[("x0Mm", s_or(args, "x0Mm", "0")), ("y0Mm", s_or(args, "y0Mm", "0")),- ("wMm", s_or(args, "wMm", "15")), ("hMm", s_or(args, "hMm", "15"))],+ ("wMm", s_or(args, "wMm", "15")), ("hMm", s_or(args, "hMm", "15")), ("mechOutline", mech.into())], None, timeout_of(args, 120)) }
bridge-rs/src/verbs.rs+19−7@@ -11,7 +11,7 @@ use std::time::Duration; pub const KNOWN: &[&str] = &[ // health / lifecycle "readiness", "status", "diagnostics", "window_info", "state", "check_dialogs",- "dismiss_dialogs", "launch", "close", "instances", "dialog_log", "uninstall", "describe", "panel",+ "dismiss_dialogs", "launch", "close", "instances", "dialog_log", "uninstall", "describe", "panel", "list_installed_libraries", "uninstall_library", // agent / REPL "agent_start", "agent_stop", "agent_status", "agent_recover", "eval", "run_command", "run_script", "list_commands", "open_document", "open_project", "close_document",@@ -87,12 +87,22 @@ fn describe() -> Value { &["run_command"], &["An undeclared identifier HALTS the engine with a modal that Try/Except cannot catch.", "ShowMessage blocks the loop behind a modal - never use it."], json!({"text": "Begin PCBServer.GetCurrentPCBBoard.ViewManager_FullUpdate; End;"})), verb_doc("run_command", "Run an Altium process (Server:Command) through the agent.", json!({"command": "string - e.g. PCB:Zoom", "params": "string or object"}), json!({"success": "bool"}), 60, "Params are pipe-joined key=value pairs. altium_list_commands searches the catalog.", &["eval", "list_commands"], &["Some processes open a modal dialog and block the loop until it is answered."], json!({"command": "PCB:Zoom", "params": {"Action": "All"}})),+ verb_doc("list_installed_libraries", "Every library in Altium's Installed Libraries list (read-only).", json!({}),+ json!({"count": "int", "libraries": "[path]"}), 30,+ "Check before and after altium_install_library / altium_uninstall_library without a screenshot.",+ &["uninstall_library", "install_library", "install_library_bundle"], &["Needs the agent (it reads IntegratedLibraryManager inside Altium)."], json!({})),+ verb_doc("uninstall_library", "Remove libraries from Altium's Installed Libraries: the inverse of install_library / install_library_bundle. Library files are never touched.",+ json!({"libraryPath": "exact registered path", "libraryName": "file stem: matches X.SchLib AND X.PcbLib", "dryRun": "bool"}),+ json!({"removed": "[path]", "closedDocuments": "[path]", "remaining": "int", "notInstalled": "bool"}), 60,+ "Any open document for a removed library is closed first. Close Altium with a plain altium_close afterwards: a close that discards changes can drop the unregistration.",+ &["list_installed_libraries", "install_library", "close"], &["Nothing matching answers notInstalled:true (success), so a cleanup can run twice.", "discardChanges:true on altium_close can bring the removed library back on the next launch."],+ json!({"libraryName": "Adom_ToF"})), verb_doc("panel", "Activate a docked panel TAB (SCH Library, PCB Library, Components, Projects, Properties) in the BACKGROUND - the 'ganged-up view' needs the right panels in front, and no Altium server process switches a tab.", json!({"name": "string - the tab caption exactly as painted, e.g. 'SCH Library'", "ensureId": "string optional - panel ID for Client:CustomizeResources (e.g. TSchLibPanel) when the panel may not exist yet"}), json!({"selected": "bool", "lane": "already | ui_select | ui_select_raised", "verified": "bool (false when the tab exposes no selection state)", "elapsedSec": "float"}), 30, "One UIA search reads the tab's existence AND selection, then at most one SelectionItemPattern select and one verify, all inside a 24 s budget: every exit is a named answer (panel_not_found, panel_search_timeout, ui_select_gated, panel_not_selected), never ab's generic timeout.", &["show_symbol", "show_footprint", "run_command"],- &["ab gates desktop_ui_select behind the shell approval grant; without it the verb reports ui_select_gated instead of pretending.", "A plain click does not switch panel tabs - that is why this verb exists.", "lane ui_select_raised means ab fell back to a synthetic click that foregrounded Altium; the dispatch wrapper hands the user's window back."],+ &["ab gates desktop_ui_select behind the shell approval grant; without it the verb reports ui_select_gated instead of pretending.", "A plain click does not switch panel tabs - that is why this verb exists.", "On Altium Designer Develop the UI Automation search does not finish in time (panel_search_timeout, measured 2026-10-15); opening a library document already brings its library panel to the front.", "lane ui_select_raised means ab fell back to a synthetic click that foregrounded Altium; the dispatch wrapper hands the user's window back."], json!({"name": "SCH Library"})), verb_doc("read_board", "ONE round trip: components, nets, layers, copper, rules, occupancy, outline.", json!({"sections": "string csv - optional", "limit": "int"}), board.clone(), 120, "The primitive every board view derives from; board_summary / rules_by_kind / layers_in_use are shaped from the same read.",@@ -107,9 +117,9 @@ fn describe() -> Value { &["read_symbol", "show_symbol"], &["add_models APPENDS on every call (not idempotent) - repair is remove_symbol + re-import.", "The first part into a NEW library can silently skip models+params if the bridge restarted mid-flight - verify with read_symbol."], json!({})), verb_doc("test_board", "Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the existing board when pcbPath already exists (issue 11).", json!({"libraryPath": "path (.PcbLib)", "footprintName": "string | 'A;B;C'", "wMm": "number (new board only)", "hMm": "number (new board only)", "pcbPath": "path (.PcbDoc) - existing = append, new = create+save", "assigns": "des=NET;...", "stubs": "des=x,y;..."}),- json!({"placed": "string", "count": "int", "pcb": "path", "appended": "bool", "existingParts": "int", "padsOnComp": "int", "bodiesOnComp": "int"}), 240,+ json!({"placed": "string", "count": "int", "pcb": "path", "saved": "bool (checked on disk)", "appended": "bool", "existingParts": "int", "padsOnComp": "int", "bodiesOnComp": "int"}), 240, "Three parts on one board: footprintName 'VL53L5CX;VL53L7CX;VL53L8CX' + pcbPath, then altium_show_3d_board {filePath: pcbPath}. Run it again with the same pcbPath to ADD parts: they land right of the current ones, designators continue (U4...).",- &["place_on_sch", "show_3d_board", "add_body"], &["assigns/stubs apply to the LAST part placed.", "pcb is empty when pcbPath is omitted: the board exists only unsaved as PCB<n>.PcbDoc.", "Appending ignores wMm/hMm and does not grow the outline: parts can land outside it - resize with altium_board_shape if needed."],+ &["place_on_sch", "show_3d_board", "add_body"], &["assigns/stubs apply to the LAST part placed.", "pcb is empty when pcbPath is omitted: the board exists only unsaved as PCB<n>.PcbDoc.", "Appending ignores wMm/hMm and does not grow the outline: parts can land outside it - resize with altium_board_shape if needed.", "Each part's name is its own Comment (moves with the part) and its Pattern is the footprint name.", "errorCode not_saved: the board was built but the file did not land; the folder is created automatically."], json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX;VL53L7CX;VL53L8CX", "wMm": 39, "hMm": 14, "pcbPath": "C:/proj/tof.PcbDoc"})), verb_doc("plate_vias", "Make every drilled pad in a PcbLib a plated MULTI-LAYER pad (all footprints, or footprintName). adom-lbr writes heatsink vias as pads with a hole; Altium loaded them on the Top Layer, so a thermal pad's vias were not through-holes until this ran.", json!({"libraryPath": "path (.PcbLib)", "footprintName": "string (optional filter)"}), json!({"holes": "int", "plated": "int", "fixed": "Fp:Pad;..."}), 90,@@ -468,11 +478,11 @@ fn describe() -> Value { &["close_document", "check_dialogs", "close"], &["Without filePath it saves whatever has focus, which may be the user's document rather than yours.", "success only means the save process was dispatched; a Save As dialog blocks the engine until answered."], json!({"filePath": "C:/lib/Adom_ToF.SchLib"})), verb_doc("board_shape", "Redefine the CURRENT board's outline as a wMm x hMm rectangle at (x0Mm, y0Mm) by rewriting the board outline's segments.",- json!({"pcbPath": "path to a .PcbDoc - optional; opened first", "x0Mm": "number, default 0", "y0Mm": "number, default 0", "wMm": "number, default 15", "hMm": "number, default 15", "timeout": "seconds, default 120"}),- json!({"data": "{outlineMil: [left, bottom, right, top]}", "roundtripMs": "int"}), 120,+ json!({"pcbPath": "path to a .PcbDoc - optional; opened first", "x0Mm": "number, default 0", "y0Mm": "number, default 0", "wMm": "number, default 15", "hMm": "number, default 15", "mechOutline": "bool - also draw the rectangle as 0.1 mm tracks on Mechanical 1 so the fab Gerber (.GM1) has an outline", "timeout": "seconds, default 120"}),+ json!({"data": "{outlineMil: [left, bottom, right, top] read from the outline vertices, outlineSource, mechOutlineTracks}", "roundtripMs": "int"}), 120, "Then altium_save {filePath: pcbPath} and altium_show_2d_board {filePath} to see it. altium_pcb_report {want:'board'} reads outlineMil back.", &["test_board", "pcb_report", "show_2d_board", "save"],- &["errorCode no_board_outline: the current document is a PcbLib (its internal board has no outline) - pass pcbPath.", "bad_size = wMm/hMm not greater than 0; bad_number = a value was not numeric.", "It REPLACES the outline; the previous shape is not kept."],+ &["errorCode no_board_outline: the current document is a PcbLib (its internal board has no outline) - pass pcbPath.", "bad_size = wMm/hMm not greater than 0; bad_number = a value was not numeric.", "It REPLACES the outline; the previous shape is not kept.", "The board shape alone is NOT plotted into any Gerber: pass mechOutline:true (once, at the final size: each call adds a new set of tracks)."], json!({"pcbPath": "C:/proj/tof.PcbDoc", "wMm": 39, "hMm": 14})), verb_doc("pcb_report", "Numbers instead of pixels: for a PcbLib, every footprint's pads, pad bounding box, primitive census and 3D bodies; for a board, outline, components with nets and bodies, census and tracks.", json!({"path": "path to a .PcbLib or .PcbDoc - optional; opened first", "name": "string - footprint name filter (library form)", "want": "'board' - report the focused BOARD even though a PcbLib is also answering", "timeout": "seconds, default 60"}),@@ -931,6 +941,8 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> "instances" => misc::instances_verb(args), "dialog_log" => misc::dialog_log(args), "panel" => misc::panel(args),+ "list_installed_libraries" => library::list_installed_libraries(args),+ "uninstall_library" => library::uninstall_library(args), "launch" => misc::launch(args), "close" => misc::close(args), "uninstall" => misc::uninstall_verb(args),
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.44",+ "version": "0.2.45", "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.44",+ "version": "0.2.45", "hero": { "type": "image", "path": "screenshots/hero.png"
skills/altium-boards/SKILL.md+35−14@@ -25,12 +25,21 @@ courtyard + 3 mm margin so the pin-1 dot is visible on the 3D board. `footprintName` takes a `;` list (from 0.2.13): the parts are spread evenly along X inside the wMm x hMm outline on one new board, and `assigns` / `stubs` apply to the LAST part-placed. From 0.2.18 / 0.2.19 the parts are designated U1..Un (the designator text stays-hidden) with the footprint name as Comment, and each part's name is written on the Top-Overlay under its footprint: 35 mil stroke text, centred on the part using the text's-measured bounding rectangle, its top 2.5 mm below the part centre, clear of a 3 mm part's-courtyard. Size `hMm` so that label fits on the board. Every call creates a NEW PcbDoc;-without `pcbPath` it stays unsaved.+placed. The parts are designated U1..Un (the designator text stays hidden). Each part's name+is its own Comment, shown on the Top Overlay under its footprint (35 mil stroke, its top+2.5 mm below the part centre, clear of a 3 mm part's courtyard), so it moves with the part;+from 0.2.45 it is no longer loose overlay text, which stayed behind when parts were moved and+piled up in the silkscreen Gerber. From 0.2.45 each part's `Pattern` is the footprint name+too (BOM, STEP naming and footprint comparison read it). Size `hMm` so the label fits.++**Append to an existing board (from 0.2.40, verified live 0.2.45).** When `pcbPath` already+exists, or is already open in Altium, the parts are ADDED to that board: placed to the right+of the rightmost existing part at the same height, designators continuing (U4, U5, ...).+The outline and layers are left alone (`wMm`/`hMm` are ignored), so appended parts can land+outside the outline: resize with `altium_board_shape`. The reply carries `appended` and+`existingParts`. Without `pcbPath` the board stays unsaved. From 0.2.45 the bridge creates the+target folder and checks the file on disk: `saved:true`, or `errorCode: not_saved` (before+0.2.45 a save into a missing folder failed silently, so the next append started a new board). ## Schematic sheets: `altium_place_on_sch` @@ -48,16 +57,28 @@ number as Comment. From 0.2.16 / 0.2.17 the sheet is sized FROM the parts: text frame inside the drawing frame along the top, and `_hints` names any of them left empty. The sheet size is not read back from `SheetSizeX/Y`: on ADDevelop those report the 9500 x-7500 default after a custom size is set. Every call creates a NEW SchDoc (no append yet);-after a re-run `altium_close_document {path}` the stale sheet before capturing with-`altium_show_schematic {filePath}`.+7500 default after a custom size is set.++**Append to an existing sheet (from 0.2.40, verified live 0.2.45).** When `schPath` already+exists or is open, the symbols are ADDED: 4000 mil right of the rightmost symbol, in line with+the existing row, designators continuing. The title block is left alone. From 0.2.45 the sheet+GROWS to keep the new parts inside the drawing frame (`sheetGrown:true`; it never shrinks).+`saved` / `not_saved` as for boards.++## Board shape++`altium_board_shape {x0Mm,y0Mm,wMm,hMm}` sets the outline's four vertices (PointCount ++Segments[] + Rebuild, the pattern that renders on every build; `.Clear` HALTS the engine). -## Board shape: only the editor process works+Read the outline from its VERTICES, never `BoardOutline.BoundingRectangle`: that rectangle is+cached and does not follow a reshape in the same session (it kept the 6000 x 4000 mil default+while the view showed the new shape, measured on AD 26.9.1 and Altium Develop). From 0.2.45+`board_shape`, `pcb_report {want:'board'}` and `read_board`'s outline all read the vertices+and agree with the 2D view. -`IPCB_BoardOutline.PointCount/Segments/Rebuild` is a silent no-op and `.Clear` HALTS the-engine (measured). `altium_board_shape {x0Mm,y0Mm,wMm,hMm}` does it the working way: four-temporary selected tracks + process `PCB:PlaceBoardOutline Mode=BOARDOUTLINE_FROM_SEL_PRIMS`,-then deletes the tracks.+The board shape is NOT plotted into any Gerber (.GM1 / .GKO stay empty). Pass+`mechOutline:true` to also draw the rectangle as 0.1 mm tracks on Mechanical 1. Each call adds+a new set, so pass it once, at the final size. ## Placement: the four ordering laws (all measured)
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.44+0.2.45
src/agent/AdomAgent.pas.tmpl+199−39@@ -988,6 +988,63 @@ End; { --------------------------------------------- library management (Barrett port) --- } { Register a library in Altium's installed-libraries list (IntLib, SchLib, PcbLib, DbLib). }+{ Every library in Altium's Installed Libraries list, as a JSON array of paths. }+Function CmdListInstalledLibs(Lines : TStringList) : String;+Var+ I, N : Integer;+ S : String;+Begin+ N := IntegratedLibraryManager.InstalledLibraryCount;+ S := '';+ For I := 0 To N - 1 Do+ Begin+ If S <> '' Then S := S + ',';+ S := S + '"' + JsonEsc(IntegratedLibraryManager.InstalledLibraryPath(I)) + '"';+ End;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('libraries', '[' + S + ']') + '}';+End;++{ Unregister installed libraries by exact path (paths=a;b). The files are never touched. A+ library that is open as a document is closed first: an installed SchLib loads as a document+ that comes up modified, and a close that discards it drops the unregistration (measured by+ John on Altium Develop, 2026-09-23). Removal takes effect live. }+Function CmdUninstallLibrary(Lines : TStringList) : String;+Var+ PL : TStringList;+ K : Integer;+ P, Removed, Closed : String;+ Doc : IServerDocument;+Begin+ PL := SplitSemi(Param(Lines, 'paths'));+ Removed := ''; Closed := '';+ For K := 0 To PL.Count - 1 Do+ Begin+ P := PL[K];+ Doc := Nil;+ Try+ Doc := Client.GetDocumentByPath(P);+ Except+ Doc := Nil;+ End;+ If Doc <> Nil Then+ Begin+ Try+ Client.CloseDocument(Doc);+ If Closed <> '' Then Closed := Closed + ',';+ Closed := Closed + '"' + JsonEsc(P) + '"';+ Except+ End;+ End;+ IntegratedLibraryManager.UninstallLibrary(P);+ If Removed <> '' Then Removed := Removed + ',';+ Removed := Removed + '"' + JsonEsc(P) + '"';+ End;+ PL.Free;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('removed', '[' + Removed + ']') + ',' ++ JRaw('closedDocuments', '[' + Closed + ']') + ',' ++ JRaw('remaining', IntToStr(IntegratedLibraryManager.InstalledLibraryCount)) + '}';+End;+ Function CmdInstallLibrary(Lines : TStringList) : String; Var Path : String;@@ -1844,7 +1901,8 @@ Var Append : Boolean; EIt : ISch_Iterator; ECmp : ISch_Component;- NExist, ExtX, SumY : Integer;+ NExist, ExtX, SumY, GW : Integer;+ Grown : Boolean; RefList : TStringList; RIt : ISch_Iterator; LRect : ISch_Rectangle;@@ -1890,10 +1948,21 @@ Begin End; { Append mode (issue 11): sch names an EXISTING .SchDoc -> open it and add the parts to the right of what is there; the sheet keeps its size and title block. }+ SD := Nil; Append := (SchPath <> '') And FileExists(SchPath);+ If (SchPath <> '') And (Not Append) Then+ Begin+ SD := Nil;+ Try+ SD := Client.GetDocumentByPath(SchPath);+ Except+ SD := Nil;+ End;+ If SD <> Nil Then Append := True;+ End; If Append Then Begin- SD := Client.OpenDocument('SCH', SchPath);+ If SD = Nil Then SD := Client.OpenDocument('SCH', SchPath); If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the existing schematic: ' + SchPath) + '}';@@ -1921,7 +1990,7 @@ Begin Exit; End; { existing symbols: count (designators continue) and the rightmost anchor + height band }- NExist := 0; ExtX := 0; SumY := 0;+ NExist := 0; ExtX := 0; SumY := 0; Grown := False; If Append Then Begin EIt := Sheet.SchIterator_Create;@@ -2105,6 +2174,33 @@ Begin If Placed <> '' Then Placed := Placed + ';'; Placed := Placed + RefList[K]; End;+ { Appended parts extend the row to the right; grow the sheet so they stay inside the+ drawing frame (a fixed sheet left the appended part out in the gray, measured+ 2026-10-15). Never shrinks a sheet. }+ If Append Then+ Begin+ GW := Cur + 600;+ If Sheet.UseCustomSheet Then+ Begin+ If GW > Round(CoordToMils(Sheet.CustomX)) Then+ Begin+ Sheet.CustomX := MilsToCoord(GW);+ Sheet.CustomXZones := (GW + 2499) Div 2500;+ Grown := True;+ End;+ End+ Else+ Begin+ Sheet.UseCustomSheet := True;+ Sheet.CustomX := MilsToCoord(GW);+ Sheet.CustomY := MilsToCoord(H);+ Sheet.CustomXZones := (GW + 2499) Div 2500;+ Sheet.CustomYZones := (H + 2499) Div 2500;+ Sheet.CustomMarginWidth := MilsToCoord(200);+ Grown := True;+ End;+ If Grown Then W := GW;+ End; { Title block: the standard sheet renders the document parameters Title, DocumentNumber, Revision and DrawnBy through its special strings; an empty block is a demo defect. } If Append Then@@ -2140,7 +2236,7 @@ Begin PWs.Free; CXs.Free; CYs.Free; TPs.Free; BTs.Free; Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JRaw('sheetMil', '[' + IntToStr(W) + ',' + IntToStr(H) + ']') + ',' + JRaw('titleBlock', '{' + JRaw('title', BoolStr(Title <> '')) + ',' + JRaw('number', BoolStr(DocNum <> '')) + ',' + JRaw('revision', BoolStr(Rev <> '')) + ',' + JRaw('drawnBy', BoolStr(Drawn <> '')) + ',' + JRaw('description', BoolStr(Desc <> '')) + '}') + ',' +- JStr('placed', Placed) + ',' + JStr('designator','U' + IntToStr(NExist + 1)) + ',' + JRaw('appended', BoolStr(Append)) + ',' + JRaw('existingParts', IntToStr(NExist)) + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';+ JStr('placed', Placed) + ',' + JStr('designator','U' + IntToStr(NExist + 1)) + ',' + JRaw('appended', BoolStr(Append)) + ',' + JRaw('sheetGrown', BoolStr(Grown)) + ',' + JRaw('existingParts', IntToStr(NExist)) + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}'; RefList.Free; End; @@ -2427,6 +2523,28 @@ Begin Result := S; End; +{ The board outline's extent in mil as '[left,bottom,right,top]', read from the outline's own+ vertices. BoardOutline.BoundingRectangle is a cached rectangle that does not follow a reshape+ in the same session on AD 26.9.1 or Altium Develop (it kept the 6000 x 4000 mil default while+ the view showed the new shape, measured 2026-10-15); the vertices are what the view draws. }+Function OutlineMilFromVertices(BO : IPCB_BoardOutline) : String;+Var+ Seg : TPolySegment;+ K, L, B, R, T : Integer;+Begin+ L := 0; B := 0; R := 0; T := 0;+ For K := 0 To BO.PointCount - 1 Do+ Begin+ Seg := BO.Segments[K];+ If (K = 0) Or (Seg.vx < L) Then L := Seg.vx;+ If (K = 0) Or (Seg.vy < B) Then B := Seg.vy;+ If (K = 0) Or (Seg.vx > R) Then R := Seg.vx;+ If (K = 0) Or (Seg.vy > T) Then T := Seg.vy;+ End;+ Result := '[' + IntToStr(Round(CoordToMils(L))) + ',' + IntToStr(Round(CoordToMils(B))) + ',' ++ IntToStr(Round(CoordToMils(R))) + ',' + IntToStr(Round(CoordToMils(T))) + ']';+End;+ Function CmdPcbReport(Lines : TStringList) : String; Var Board : IPCB_Board;@@ -2435,7 +2553,7 @@ Var Comp : IPCB_Component; BIt : IPCB_BoardIterator; R : TCoordRect;- Name, S : String;+ Name, S, OM : String; I : Integer; Begin PLib := PCBServer.GetCurrentPCBLibrary;@@ -2494,12 +2612,15 @@ Begin { A library's internal board has no outline; touching BoundingRectangle on the nil BoardOutline halts the engine (measured 2026-08-30). } If Board.BoardOutline <> Nil Then- R := Board.BoardOutline.BoundingRectangle+ OM := OutlineMilFromVertices(Board.BoardOutline) Else+ Begin R := Board.BoundingRectangle;+ OM := '[' + IntToStr(Round(CoordToMils(R.Left))) + ',' + IntToStr(Round(CoordToMils(R.Bottom))) + ',' ++ IntToStr(Round(CoordToMils(R.Right))) + ',' + IntToStr(Round(CoordToMils(R.Top))) + ']';+ End; Result := '{' + JRaw('ok','true') + ',' + JStr('kind','board') + ',' +- JRaw('outlineMil', '[' + IntToStr(Round(CoordToMils(R.Left))) + ',' + IntToStr(Round(CoordToMils(R.Bottom))) + ',' +- IntToStr(Round(CoordToMils(R.Right))) + ',' + IntToStr(Round(CoordToMils(R.Top))) + ']') + ',' ++ JRaw('outlineMil', OM) + ',' + JRaw('components', '[' + S + ']') + ',' + JStr('census', BoardCensus(Board)) + ',' + JStr('tracks', BoardTracks(Board)) + ',' +@@ -3219,10 +3340,21 @@ Begin End; { Append mode (issue 11): pcb names an EXISTING .PcbDoc -> open it and add the parts, instead of a new board per call. The board keeps its own outline and layers. }+ SD := Nil; Append := (PcbPath <> '') And FileExists(PcbPath);+ If (PcbPath <> '') And (Not Append) Then+ Begin+ SD := Nil;+ Try+ SD := Client.GetDocumentByPath(PcbPath);+ Except+ SD := Nil;+ End;+ If SD <> Nil Then Append := True;+ End; If Append Then Begin- SD := Client.OpenDocument('PCB', PcbPath);+ If SD = Nil Then SD := Client.OpenDocument('PCB', PcbPath); If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the existing board: ' + PcbPath) + '}';@@ -3331,24 +3463,20 @@ Begin { U1..Un so the read verbs (component_pins, footprint_primitives) can address each part; the factory left every part named Designator1 (measured 2026-09-20). Name stays hidden. } Inst.Name.Text := 'U' + IntToStr(NExist + K + 1);+ { the footprint name: the primitives were copied one by one, so the component carries+ no Pattern unless it is set here, and BOM / STEP naming / footprint compare key on it }+ Inst.Pattern := LibFp.Name;+ { The part name is the component's own COMMENT, shown on the top overlay under the+ footprint, so it moves with the part (free overlay text stayed behind when parts were+ moved and piled up in the silkscreen Gerber). 35 mil stroke, its top 2.5 mm below the+ part centre, clear of a 3 mm part's courtyard; centred on its real extent. } Inst.Comment.Text := LibFp.Name;- { Part name on the top overlay under each footprint so a human reading the board- knows which chip is which (John, 2026-09-20). 35 mil stroke text, ~21 mil per- character, centred on the part; the text TOP sits 3.4 - 0.9 = 2.5 mm below the part- centre, clear of a 3 mm tall part's courtyard (1.75 mm) and its silk (1.65 mm). }- Txt := PCBServer.PCBObjectFactory(eTextObject, eNoDimension, eCreate_Default);- Txt.Layer := eTopOverlay;- Txt.Text := LibFp.Name;- Txt.Size := MilsToCoord(35);- Txt.Width := MilsToCoord(5);- Txt.XLocation := CX;- Txt.YLocation := CY - MMsToCoord(3.4);- Board.AddPCBObject(Txt);- PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Txt.I_ObjectAddress);- { centre on the text's REAL extent (a per-character guess put the labels off to the- right of every part; John, 2026-09-20): measure the bounding rectangle and shift }- R := Txt.BoundingRectangle;- Txt.MoveByXY(CX - ((R.Left + R.Right) Div 2), 0);+ Inst.CommentOn := True;+ Inst.Comment.Size := MilsToCoord(35);+ Inst.Comment.Width := MilsToCoord(5);+ Inst.Comment.MoveToXY(CX, CY - MMsToCoord(3.4));+ R := Inst.Comment.BoundingRectangle;+ Inst.Comment.MoveByXY(CX - ((R.Left + R.Right) Div 2), 0); End; AssignPadNets(Board, Inst, Param(Lines, 'assigns')); { stub tracks: des=x,y (mm from centre) -> a track from that pad to the point, on its net }@@ -3475,8 +3603,8 @@ Var Board : IPCB_Board; BO : IPCB_BoardOutline; Seg : TPolySegment;- X0, Y0, X1, Y1 : Integer;- R : TCoordRect;+ X0, Y0, X1, Y1, K, NTr : Integer;+ Trk : IPCB_Track; Begin Board := PCBServer.GetCurrentPCBBoard; If Board = Nil Then@@ -3509,12 +3637,30 @@ Begin Seg := BO.Segments[3]; Seg.vx := X0; Seg.vy := Y1; Seg.Kind := ePolySegmentLine; BO.Segments[3] := Seg; BO.Rebuild; BO.Validate;+ { The board shape is not plotted into any Gerber: a fab outline needs real tracks. On+ request, draw the rectangle on Mechanical 1 (0.1 mm) so .GM1 carries it. Additive: a+ second call draws a second set, so callers reshaping repeatedly pass it once at the end. }+ NTr := 0;+ If Param(Lines, 'mechOutline') = '1' Then+ Begin+ For K := 0 To 3 Do+ Begin+ Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);+ Trk.Layer := eMechanical1;+ Trk.Width := MMsToCoord(0.1);+ If K = 0 Then Begin Trk.X1 := X0; Trk.Y1 := Y0; Trk.X2 := X1; Trk.Y2 := Y0; End;+ If K = 1 Then Begin Trk.X1 := X1; Trk.Y1 := Y0; Trk.X2 := X1; Trk.Y2 := Y1; End;+ If K = 2 Then Begin Trk.X1 := X1; Trk.Y1 := Y1; Trk.X2 := X0; Trk.Y2 := Y1; End;+ If K = 3 Then Begin Trk.X1 := X0; Trk.Y1 := Y1; Trk.X2 := X0; Trk.Y2 := Y0; End;+ Board.AddPCBObject(Trk);+ PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Trk.I_ObjectAddress);+ NTr := NTr + 1;+ End;+ End; PCBServer.PostProcess; Board.ViewManager_FullUpdate;- R := BO.BoundingRectangle;- Result := '{' + JRaw('ok','true') + ',' +- JRaw('outlineMil', '[' + IntToStr(Round(CoordToMils(R.Left))) + ',' + IntToStr(Round(CoordToMils(R.Bottom))) + ',' +- IntToStr(Round(CoordToMils(R.Right))) + ',' + IntToStr(Round(CoordToMils(R.Top))) + ']') + '}';+ Result := '{' + JRaw('ok','true') + ',' + JRaw('outlineMil', OutlineMilFromVertices(BO)) + ',' ++ JStr('outlineSource', 'vertices') + ',' + JRaw('mechOutlineTracks', IntToStr(NTr)) + '}'; End; @@ -4740,6 +4886,8 @@ Var Board : IPCB_Board; R : TCoordRect; Src : String;+ Seg : TPolySegment;+ K, L, B, RR, T : Integer; Begin Board := PCBServer.GetCurrentPCBBoard; If Board = Nil Then@@ -4749,21 +4897,31 @@ Begin End; If Board.BoardOutline <> Nil Then Begin- R := Board.BoardOutline.BoundingRectangle;+ { vertices, not the cached BoundingRectangle (see OutlineMilFromVertices) }+ L := 0; B := 0; RR := 0; T := 0;+ For K := 0 To Board.BoardOutline.PointCount - 1 Do+ Begin+ Seg := Board.BoardOutline.Segments[K];+ If (K = 0) Or (Seg.vx < L) Then L := Seg.vx;+ If (K = 0) Or (Seg.vy < B) Then B := Seg.vy;+ If (K = 0) Or (Seg.vx > RR) Then RR := Seg.vx;+ If (K = 0) Or (Seg.vy > T) Then T := Seg.vy;+ End; Src := 'boardOutline'; End Else Begin R := Board.BoundingRectangle;+ L := R.Left; B := R.Bottom; RR := R.Right; T := R.Top; Src := 'boardBounds'; End; Result := '{' + JRaw('ok','true') + ',' + JStr('source', Src) + ',' +- JRaw('leftMil', IntToStr(Round(CoordToMils(R.Left)))) + ',' +- JRaw('bottomMil', IntToStr(Round(CoordToMils(R.Bottom)))) + ',' +- JRaw('rightMil', IntToStr(Round(CoordToMils(R.Right)))) + ',' +- JRaw('topMil', IntToStr(Round(CoordToMils(R.Top)))) + ',' +- JRaw('widthMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' +- JRaw('heightMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + '}';+ JRaw('leftMil', IntToStr(Round(CoordToMils(L)))) + ',' ++ JRaw('bottomMil', IntToStr(Round(CoordToMils(B)))) + ',' ++ JRaw('rightMil', IntToStr(Round(CoordToMils(RR)))) + ',' ++ JRaw('topMil', IntToStr(Round(CoordToMils(T)))) + ',' ++ JRaw('widthMil', IntToStr(Round(CoordToMils(RR - L)))) + ',' ++ JRaw('heightMil', IntToStr(Round(CoordToMils(T - B)))) + '}'; End; { ONE round trip to READ the whole board. Composed from the EXISTING readers on purpose: no new Altium@@ -4814,6 +4972,8 @@ Begin Else If Cmd = 'create_footprint' Then Result := CmdCreateFootprint(Lines) Else If Cmd = 'create_symbol' Then Result := CmdCreateSymbol(Lines) Else If Cmd = 'install_library' Then Result := CmdInstallLibrary(Lines)+ Else If Cmd = 'list_installed_libraries' Then Result := CmdListInstalledLibs(Lines)+ Else If Cmd = 'uninstall_library' Then Result := CmdUninstallLibrary(Lines) Else If Cmd = 'compile_intlib' Then Result := CmdCompileIntLib(Lines) Else If Cmd = 'extract_intlib' Then Result := CmdExtractIntLib(Lines) Else If Cmd = 'set_params' Then Result := CmdSetParams(Lines)
src/bridge.json+3−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.44",+ "version": "0.2.45", "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",@@ -80,6 +80,8 @@ "altium_install", "altium_install_extension", "altium_install_library",+ "altium_list_installed_libraries",+ "altium_uninstall_library", "altium_install_library_bundle", "altium_install_receipt", "altium_instances",