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.48 source (not yet released, needs signing): list_library and the show_* surfaces refuse a document under an unread argument instead of opening the demo, demoFallback in every reply (#20); schematic_data refuses a SchLib as the current sheet and takes filePath (#21); build_board into a project that already lists the board reopens the project so the board has one entry and one view (#23); read_symbol returns model links (#24); release.py checks and pushes page.json and main.rs with the other stamps (#22). Live-tested on DESKTOP-BAR6S0F, Altium Designer 26.11.1; 56 tests, lint 146 verbs.
10 files changed
+179−26
bridge-rs/src/main.rs+1−1@@ -31,7 +31,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.47";+pub const BRIDGE_VERSION: &str = "0.2.48"; 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/routing.rs+64−1@@ -792,6 +792,53 @@ fn outline_spec(o: &Value) -> Result<String, Value> { } /// altium_build_board {pcbPath, libraryPath, outline, parts:[{ref, footprint, x, y, rotation, comment}], nets:{net:["R1.1",...]}}+/// The documents a .PrjPcb lists (`DocumentPath=` lines), resolved against the project's folder.+pub fn listed_documents(prj_text: &str, prj_dir: &std::path::Path) -> Vec<std::path::PathBuf> {+ prj_text.lines().filter_map(|l| l.trim().strip_prefix("DocumentPath=")).map(|d| {+ let d = d.trim();+ let pth = std::path::Path::new(d);+ if pth.is_absolute() || d.contains(':') { pth.to_path_buf() } else { prj_dir.join(d) }+ }).collect()+}++fn same_file_name(a: &std::path::Path, b: &std::path::Path) -> bool {+ match (a.file_name(), b.file_name()) {+ (Some(x), Some(y)) => x.to_string_lossy().eq_ignore_ascii_case(&y.to_string_lossy()),+ _ => false,+ }+}++/// A board built into a folder whose project already lists it (altium_build_schematic writes the+/// project first) is created as a new document that joins the project and is then renamed onto the+/// listed path: Altium holds the file twice, one entry greyed, and a second hidden view survives a+/// project close (#23). Save the project's sheets, boards and libraries, close them and the project,+/// reopen the project and show the board: one entry, one view. None when no project lists the board.+fn reopen_listing_project(pcb: &str) -> Option<String> {+ let pcb_p = std::path::Path::new(pcb);+ let dir = pcb_p.parent()?;+ let (prj, docs) = std::fs::read_dir(dir).ok()?.flatten().map(|e| e.path())+ .filter(|p| p.extension().map(|x| x.eq_ignore_ascii_case("prjpcb")).unwrap_or(false))+ .find_map(|prj| {+ let text = String::from_utf8_lossy(&std::fs::read(&prj).ok()?).to_string();+ let docs = listed_documents(&text, dir);+ docs.iter().any(|d| same_file_name(d, pcb_p)).then_some((prj, docs))+ })?;+ const KINDS: [&str; 4] = ["schdoc", "pcbdoc", "schlib", "pcblib"];+ let docs: Vec<String> = docs.iter()+ .filter(|d| d.extension().map(|x| KINDS.contains(&x.to_string_lossy().to_lowercase().as_str())).unwrap_or(false))+ .map(|d| d.display().to_string()).collect();+ for d in &docs {+ let _ = crate::surfaces::save(&json!({"filePath": d}));+ }+ for d in docs.iter().chain(std::iter::once(&prj.display().to_string())) {+ let _ = agent::call("close_document", &[("path", d.clone())], None, Duration::from_secs(30));+ }+ let prj_s = prj.display().to_string();+ let _ = crate::support::agent_open(&prj_s, 90);+ let _ = crate::support::agent_open(pcb, 90);+ Some(prj_s)+}+ pub fn build_board(args: &Value) -> Value { let pcb = args.get("pcbPath").and_then(|v| v.as_str()).unwrap_or(""); let lib = args.get("libraryPath").and_then(|v| v.as_str()).unwrap_or("");@@ -835,7 +882,8 @@ pub fn build_board(args: &Value) -> Value { let placed_refs: Vec<String> = st["components"].as_array().cloned().unwrap_or_default().iter().filter_map(|c| c["ref"].as_str().map(str::to_string)).collect(); let missing: Vec<&str> = parts.iter().filter_map(|p| p["ref"].as_str()).filter(|r| !placed_refs.iter().any(|x| x == r)).collect(); let clean = wrong.is_empty() && missing.is_empty();- json!({"success": clean, "errorCode": if clean { Value::Null } else { json!("readback_mismatch") }, "pcb": pcb, "placed": d["placed"], "padsWired": d["padsWired"],+ let reopened = reopen_listing_project(pcb);+ json!({"success": clean, "errorCode": if clean { Value::Null } else { json!("readback_mismatch") }, "pcb": pcb, "projectReopened": reopened, "placed": d["placed"], "padsWired": d["padsWired"], "outlineVertices": d["outlineVertices"], "missingParts": missing, "wrongPins": wrong, "revision": st["revision"], "unconnected": st["unconnected"], "counts": st["counts"], "_hint": if clean { "Saved and read back: every part is on the board and every listed pin on its net. altium_routing_state reads it; unconnected is the ratsnest to route." } else { "Some pins did not land on their net (wrongPins) or parts are missing: a footprint's pad names must match the netlist's pin numbers." }})@@ -1221,3 +1269,18 @@ pub fn apply_silkscreen(args: &Value) -> Value { json!({"success": d["failed"].as_i64() == Some(0), "applied": d["applied"], "failed": d["failed"], "results": d["results"], "after": after, "_hint": "after is silk_state once it landed: check each label's box (Altium's own measure) against pads, vias, bodies and the other labels."}) }++#[cfg(test)]+mod project_listing_tests {+ use super::*;++ #[test]+ fn listed_documents_resolve_against_the_project_folder() {+ let text = "[Design]\r\n[Document1]\r\nDocumentPath=Buck.SchDoc\r\n[Document2]\r\nDocumentPath=Buck.PcbDoc\r\n[Document3]\r\nDocumentPath=library\\Buck.SchLib\r\n[Document4]\r\nDocumentPath=C:\\other\\Shared.PcbLib\r\n";+ let docs = listed_documents(text, std::path::Path::new("/w"));+ assert_eq!(docs.len(), 4);+ assert!(same_file_name(&docs[1], std::path::Path::new("/elsewhere/BUCK.pcbdoc")));+ assert_eq!(docs[0], std::path::Path::new("/w").join("Buck.SchDoc"));+ assert_eq!(docs[3], std::path::PathBuf::from("C:\\other\\Shared.PcbLib"));+ }+}
bridge-rs/src/surfaces.rs+67−6@@ -7,6 +7,30 @@ use crate::{agent, dialogs, instance, shot}; use serde_json::{json, Value}; use std::time::Duration; +/// An Altium document path given under an argument this verb does not read (`path` for+/// `libraryPath`, say) used to fall through to the bundled demo, which then opened in the+/// user's Altium and answered for the wrong file (adom/altium-bridge#20). Refuse instead.+pub fn stray_path(args: &Value, reads: &[&str]) -> Option<Value> {+ const EXT: [&str; 8] = [".schlib", ".pcblib", ".schdoc", ".pcbdoc", ".prjpcb", ".intlib", ".outjob", ".prjscr"];+ let obj = args.as_object()?;+ for (k, v) in obj {+ let Some(t) = v.as_str() else { continue };+ let low = t.to_lowercase();+ if !reads.contains(&k.as_str()) && EXT.iter().any(|e| low.ends_with(e)) {+ return Some(json!({"success": false, "errorCode": "unknown_argument",+ "error": format!("'{k}' is not an argument of this verb, so {t} would have been ignored and the bundled demo opened instead"),+ "_hint": format!("Pass the document as {}.", reads.iter().map(|r| format!("'{r}'")).collect::<Vec<_>>().join(" or "))}));+ }+ }+ None+}++/// Tag a reply that answered for the bundled demo document, so a caller never mistakes it for its own.+fn mark_demo(mut out: Value, used_demo: bool) -> Value {+ out["demoFallback"] = json!(used_demo);+ out+}+ /// Open a document through the agent, run follow-up processes, return the frame. pub fn open_then(path: &str, commands: &[(&str, &str)], timeout: Duration, label: &str) -> Value { if let Some(g) = support::need_licensed() {@@ -47,7 +71,11 @@ pub fn lib_surface(args: &Value, kind: &str, default_path: &str, label: &str, af if let Some(g) = support::need_licensed() { return g; }+ if let Some(e) = stray_path(args, &["libraryPath", "filePath"]) {+ return e;+ } let t0 = support::now();+ let demo = s(args, "libraryPath").or_else(|| s(args, "filePath")).is_none(); let path = s(args, "libraryPath").or_else(|| s(args, "filePath")).unwrap_or_else(|| default_path.to_string()); let name = s(args, "symbolName").or_else(|| s(args, "footprintName")).or_else(|| s(args, "name")).unwrap_or_default(); let timeout = timeout_of(args, 60);@@ -80,7 +108,7 @@ pub fn lib_surface(args: &Value, kind: &str, default_path: &str, label: &str, af let mw = instance::main_window().unwrap_or(json!({})); let no_proof = frame.get("proofUnavailable").and_then(|b| b.as_bool()) == Some(true); let selected = sel.get("data").and_then(|d| d.get("selected")).cloned().unwrap_or(Value::Null);- let mut out = json!({"success": dl.is_empty() && !no_proof, "surface": label, "path": path,+ let mut out = json!({"success": dl.is_empty() && !no_proof, "surface": label, "path": path, "demoFallback": demo, "selected": selected, "steps": steps, "title": mw.get("title"), "dialogsDetected": dl.len(), "dialogs": dl, "shot": frame, "windowEtiquette": "background", "elapsedSec": support::elapsed_round(t0),@@ -105,23 +133,31 @@ fn out_str(v: &Value) -> String { // ---- the eight surfaces ------------------------------------------------------ pub fn show_project(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["filePath"]) { return e; }+ let demo = s(args, "filePath").is_none(); let p = s(args, "filePath").unwrap_or_else(support::demo_project);- open_then(&p, &[], timeout_of(args, 60), "project")+ mark_demo(open_then(&p, &[], timeout_of(args, 60), "project"), demo) } pub fn show_schematic(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["filePath"]) { return e; }+ let demo = s(args, "filePath").is_none(); let p = s(args, "filePath").unwrap_or_else(support::demo_sch);- open_then(&p, &[("Sch:Zoom", "Action=All")], timeout_of(args, 60), "schematic")+ mark_demo(open_then(&p, &[("Sch:Zoom", "Action=All")], timeout_of(args, 60), "schematic"), demo) } pub fn show_2d_board(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["filePath"]) { return e; }+ let demo = s(args, "filePath").is_none(); let p = s(args, "filePath").unwrap_or_else(support::demo_pcb);- open_then(&p, &[("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")], timeout_of(args, 120), "2d_board")+ mark_demo(open_then(&p, &[("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")], timeout_of(args, 120), "2d_board"), demo) } pub fn show_3d_board(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["filePath"]) { return e; }+ let demo = s(args, "filePath").is_none(); let p = s(args, "filePath").unwrap_or_else(support::demo_pcb);- open_then(&p, &[("PCB:SwitchTo3D", ""), ("PCB:Zoom", "Action=All")], timeout_of(args, 120), "3d_board")+ mark_demo(open_then(&p, &[("PCB:SwitchTo3D", ""), ("PCB:Zoom", "Action=All")], timeout_of(args, 120), "3d_board"), demo) } pub fn show_symbol(args: &Value) -> Value {@@ -154,6 +190,10 @@ fn lib_kind_and_path(args: &Value) -> (String, String) { } pub fn show_library(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["libraryPath", "filePath"]) {+ return e;+ }+ let demo = s(args, "libraryPath").or_else(|| s(args, "filePath")).is_none(); let (kind, path) = lib_kind_and_path(args); let mut a = args.clone(); a["libraryPath"] = json!(path);@@ -164,10 +204,15 @@ pub fn show_library(args: &Value) -> Value { r["components"] = lst.get("data").and_then(|d| d.get("names")).cloned().unwrap_or(Value::Null); r["componentCount"] = lst.get("data").and_then(|d| d.get("count")).cloned().unwrap_or(Value::Null); }+ r["demoFallback"] = json!(demo); r } pub fn list_library(args: &Value) -> Value {+ if let Some(e) = stray_path(args, &["libraryPath", "filePath"]) {+ return e;+ }+ let demo = s(args, "libraryPath").or_else(|| s(args, "filePath")).is_none(); let (kind, path) = lib_kind_and_path(args); if let Some(g) = support::need_licensed() { return g;@@ -180,7 +225,7 @@ pub fn list_library(args: &Value) -> Value { let mut lst = agent::call("lib_list", &[("kind", kind.clone()), ("limit", limit)], None, Duration::from_secs(60)); lst["path"] = json!(path); lst["kind"] = json!(kind);- lst+ mark_demo(lst, demo) } // ---- window / document verbs -------------------------------------------------@@ -632,3 +677,19 @@ pub fn demo_live(args: &Value) -> Value { "nextCall": {"verb": "altium_demo", "args": {"live": true, "stage": next_stage}}, "_hint": "SYNC MODEL (best): pre-cache the whole narration once with `adom-tts load` (one marker per stop), then per stop fire `adom-tts cue <id> <n> --confirm` in the background and IMMEDIATELY make nextCall — the prefetched cue starts instantly and this verb's in-bridge choreography (~5-17s) runs under the voice; advance when BOTH finish. Fallback without 1.9.47: say --play detached per stop. On the last call (done:true) restore the user's own document."}) }++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn stray_path_refuses_a_document_under_an_unread_argument() {+ let e = stray_path(&json!({"path": "C:/x/Lib.SchLib"}), &["libraryPath", "filePath"]).expect("refused");+ assert_eq!(e["errorCode"], "unknown_argument");+ assert!(e["_hint"].as_str().unwrap().contains("libraryPath"));+ assert!(stray_path(&json!({"libraryPath": "C:/x/Lib.SchLib", "symbolName": "R1"}), &["libraryPath", "filePath"]).is_none());+ assert!(stray_path(&json!({}), &["filePath"]).is_none());+ assert!(stray_path(&json!({"reason": "show it", "timeout": 30}), &["filePath"]).is_none());+ assert!(stray_path(&json!({"board": "C:/x/B.PcbDoc"}), &["filePath"]).is_some());+ }+}
bridge-rs/src/verbs.rs+16−7@@ -497,11 +497,11 @@ fn describe() -> Value { json!({"data": "{designator, pattern, count, otherPrimitives, primitives: [{kind: track|arc|pad|text, layerId, ...}]}", "roundtripMs": "int"}), 120, "Use it to verify an imported footprint on a altium_test_board: pad layerId 74 = multi-layer (plated through), which is what altium_plate_vias produces.", &["component_pins", "plate_vias", "test_board", "pcb_report"], &["Needs a .PcbDoc current: it reads a placed component, not a PcbLib member (altium_pcb_report reads libraries).", "3D bodies, fills and regions only show up in otherPrimitives."], json!({"designator": "U1"})),- verb_doc("schematic_data", "Components on the CURRENT schematic sheet with their pins (number, name, electrical type), position and orientation. designator filters to one part.",- json!({"designator": "string - optional filter", "limit": "int, default 500", "pins": "bool, default true", "params": "bool, default false - include each component's parameters"}),+ verb_doc("schematic_data", "Components on a schematic sheet (filePath, else the CURRENT sheet) with their pins (number, name, electrical type), position and orientation. designator filters to one part.",+ json!({"filePath": "path to the .SchDoc - optional, made current first (alias sheet)", "designator": "string - optional filter", "limit": "int, default 500", "pins": "bool, default true", "params": "bool, default false - include each component's parameters"}), json!({"data": "{document, count, components: [{designator, partId, libReference, comment, xMil, yMil, orientation, pins: [{number, name, electrical}], params: []}]}", "roundtripMs": "int"}), 120, "Pin-level view of a sheet. params[] is empty unless you pass params:true; altium_read_schematic is the fuller parameter view.",- &["read_schematic", "cross_check", "show_schematic"], &["Needs a .SchDoc current; one sheet only.", "electrical is Altium's pin-type ordinal, not a name."], json!({"designator": "U1"})),+ &["read_schematic", "cross_check", "show_schematic"], &["One sheet per call. Without filePath the current document must be a .SchDoc: a schematic library is refused (no_current_schematic), not read as an empty sheet.", "electrical is Altium's pin-type ordinal, not a name."], json!({"designator": "U1"})), // ---- surfaces ---- verb_doc("show_project", "Open a project (.PrjPcb) through the agent and return a proof frame of Altium's main window. With no filePath it opens Altium's bundled example project (Bluetooth Sentinel).", json!({"filePath": "path to a .PrjPcb - default: the bundled Bluetooth Sentinel example", "timeout": "seconds, default 60"}),@@ -718,11 +718,11 @@ fn describe() -> Value { "altium_save afterwards; altium_show_symbol to see where the text landed.", &["set_params", "read_symbol", "show_symbol"], &["'parameter not found' = the name must match a parameter altium_read_symbol lists.", "Visible parameters stack over the symbol body: show few."], json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "param": "Manufacturer Part Number", "visible": true})),- verb_doc("read_symbol", "Read a SchLib component back: description, display-mode count, the pins of mode 0 (designator, name) and every parameter with its hidden flag.",+ verb_doc("read_symbol", "Read a SchLib component back: description, display-mode count, the pins of mode 0 (designator, name), every parameter with its hidden flag, and its model links (footprints, 3D, sim).", json!({"libraryPath": "path to a .SchLib - optional; opened first", "symbolName": "string", "timeout": "seconds, default 90"}),- json!({"data": "{libref, description, displayModeCount, pinsMode0: int, pins: [{des, name}], params: [{name, value, hidden}]}", "roundtripMs": "int"}), 90,- "The verification step after altium_import_part / altium_set_params. For the drawing and the footprint models use altium_symbol_prims.",- &["symbol_prims", "set_params", "list_library", "show_symbol"], &["Footprint models are NOT reported here (not a missing link): altium_symbol_prims {models:true}.", "Only mode 0 pins are listed."],+ json!({"data": "{libref, description, displayModeCount, pinsMode0: int, pins: [{des, name}], params: [{name, value, hidden}], models: [{type, name, isCurrent}]}", "roundtripMs": "int"}), 90,+ "The verification step after altium_import_part / altium_set_params: models[] shows which footprint the symbol is bound to (type PCBLIB, isCurrent true for the one a placed part uses). For the drawing use altium_symbol_prims.",+ &["symbol_prims", "set_params", "list_library", "show_symbol"], &["The SCH Library panel's Model pane can lag behind altium_show_symbol; models[] here is the symbol's own record.", "Only mode 0 pins are listed."], json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX"})), verb_doc("clone_symbol", "Copy a symbol inside the current SchLib under a new name, all display modes included.", json!({"libraryPath": "path to a .SchLib - optional; opened first", "from": "string - existing LibReference", "to": "string - new LibReference", "timeout": "seconds, default 90"}),@@ -1210,6 +1210,15 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> Some(d) => board_read("footprint_primitives", vec![("designator", d)], 120), }, "schematic_data" => {+ // a named sheet becomes the current document first; without one the verb reads the current sheet+ // and refuses a library (#21)+ if let Some(e) = surfaces::stray_path(args, &["filePath", "sheet"]) { return e; }+ if let Some(p) = s(args, "filePath").or_else(|| s(args, "sheet")) {+ if !p.to_lowercase().ends_with(".schdoc") {+ return json!({"success": false, "errorCode": "not_a_sheet", "error": format!("{p} is not a .SchDoc"), "_hint": "schematic_data reads a schematic sheet; a .SchLib is read with altium_read_symbol / altium_list_library."});+ }+ if let Some(e) = support::must_open(&p, 60) { return e; }+ } let pins = if args.get("pins").and_then(|b| b.as_bool()) == Some(false) { "0" } else { "1" }; // The agent has always honoured params=1; the dispatch never sent it, so every reply // carried an empty params[] and the doc had to apologise for it. Off by default:
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.47",+ "version": "0.2.48", "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.47",+ "version": "0.2.48", "hero": { "type": "image", "path": "screenshots/hero.png"
scripts/release.py+7−4@@ -30,7 +30,7 @@ Rules (insiders-tier-publish, verbatim where it matters): every publish is insid on a human's words; an AI never promotes on its own judgment; one plain semver per build in both tiers; every insiders publish appends its release-log entry before it is called done. -Versions live in ONE place: src/bridge.json (BRIDGE_VERSION, main.rs, package.json are checked).+Versions live in ONE place: src/bridge.json (BRIDGE_VERSION, main.rs, package.json, page.json are checked). """ import datetime import hashlib@@ -124,10 +124,13 @@ def lint_verbs(): def lint_versions(ver):- """One version, three stamps beside bridge.json — refuse a half-bumped release."""+ """One version, four stamps beside bridge.json — refuse a half-bumped release. page.json is the+ wiki page's own version (0.2.47 shipped with it at 0.2.46); main.rs is what the exe reports+ (0.2.46 shipped reporting 0.2.45, adom/altium-bridge#22).""" stamps = { "src/BRIDGE_VERSION": (SRC / "BRIDGE_VERSION").read_text().strip(), "package.json": json.loads((ROOT / "package.json").read_text())["version"],+ "page.json": json.loads((ROOT / "page.json").read_text())["version"], } m = re.search(r'BRIDGE_VERSION: &str = "([^"]+)"', (RS / "src" / "main.rs").read_text()) stamps["bridge-rs/src/main.rs"] = m.group(1) if m else "?"@@ -446,8 +449,8 @@ def ship_insiders(log_line): print("== source stamps to the product page (public manifest NOT touched) ==") run("adom-wiki", "repo", "push", PAGE, "-m", f"v{ver} (insiders): {log_line}",- "--files", "CHANGELOG.md", "src/bridge.json", "src/BRIDGE_VERSION", "package.json",- cwd=ROOT)+ "--files", "CHANGELOG.md", "src/bridge.json", "src/BRIDGE_VERSION", "package.json", "page.json",+ "bridge-rs/src/main.rs", cwd=ROOT) print("== skills pkg in lockstep (release rows share the version namespace, adom/wiki#170) ==") run("adom-wiki", "pkg", "publish", "--org", "adom", cwd=ROOT)
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.47+0.2.48
src/agent/AdomAgent.pas.tmpl+20−3@@ -3892,7 +3892,8 @@ Var Prim : ISch_GraphicalObject; Pin : ISch_Pin; Pm : ISch_Parameter;- Pins, Params : String;+ Impl : ISch_Implementation;+ Pins, Params, Models : String; NPins : Integer; Begin SLib := SchServer.GetCurrentSchDocument;@@ -3932,11 +3933,25 @@ Begin Prim := It.NextSchObject; End; C.SchIterator_Destroy(It);+ { the symbol's model links (footprints, sim, 3D): the eImplementation child iterator, as in+ CmdSymbolPrims (ImplementationCount halts the script on some builds) (#24) }+ Models := '';+ It := C.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eImplementation));+ Impl := It.FirstSchObject;+ While Impl <> Nil Do+ Begin+ If Models <> '' Then Models := Models + ',';+ Models := Models + '{' + JStr('type', Impl.ModelType) + ',' + JStr('name', Impl.ModelName) + ',' ++ JRaw('isCurrent', JBool(Impl.IsCurrent)) + '}';+ Impl := It.NextSchObject;+ End;+ C.SchIterator_Destroy(It); Result := '{' + JRaw('ok','true') + ',' + JStr('libref', C.LibReference) + ',' + JStr('description', C.ComponentDescription) + ',' + JRaw('displayModeCount', IntToStr(C.DisplayModeCount)) + ',' + JRaw('pinsMode0', IntToStr(NPins)) + ',' +- JRaw('pins', '[' + Pins + ']') + ',' + JRaw('params', '[' + Params + ']') + '}';+ JRaw('pins', '[' + Pins + ']') + ',' + JRaw('params', '[' + Params + ']') + ',' + JRaw('models', '[' + Models + ']') + '}'; End; { Copy a footprint inside the current PcbLib under a new name (per-primitive). }@@ -4532,7 +4547,9 @@ Var WantPins, WantParams : Boolean; Begin Doc := SchServer.GetCurrentSchDocument;- If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;+ If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;+ { a schematic LIBRARY is an ISch_Document too: read as a sheet it answered ok with 0 parts (#21) }+ If Doc.ObjectID = eSchLib Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','the current document is a schematic library, not a sheet: ' + Doc.DocumentName) + '}'; Exit; End; Want := Param(Lines, 'designator'); Limit := StrToIntDef(Param(Lines, 'limit'), 500); WantPins := Param(Lines, 'pins') <> '0';
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.47",+ "version": "0.2.48", "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",