← Commit history

0.2.14: place_on_sch sizes the sheet, places inside the frame, fills the title block and comments; add_symbol_art draws the KiCad chip outline into SchLib symbols; symbol_prims / set_models; hints

John Lauer ·77758c4889 ·1mo ago ·parent bcfbb9f
10 files changed +554−20
CHANGELOG.md+6
@@ -1,5 +1,11 @@ # Changelog +## 0.2.14 (2026-09-19)+- **A sheet a human would sign** (John, reviewing the demo capture: symbols on top of the title block and outside the frame, empty title block, '*' comments). `altium_place_on_sch` now sizes the sheet for the part count (A4 up to two parts, B from three), places every part INSIDE the drawing frame centred on its body rectangle, sets each Comment to the part number, fills the title block from `title` / `number` / `revision` / `drawnBy` (document parameters behind the standard title block's special strings) and drops `description` as a text frame across the top. The reply carries `sheetMil`, `titleBlock{}` and `_hints` that name whatever the AI left empty and say how to write it.+- **The chip outline reaches Altium.** New `altium_add_symbol_art {libraryPath, symbolName, kicadSym}` reads the polylines adom-symbol baked into the KiCad symbol (chip-thumbnailer's outline of the side-etched model), maps the KiCad body rectangle onto the SchLib component's rectangle and draws them as polylines, so the Altium symbol carries the same chip as the KiCad and Fusion ones. Appends; run once per part.+- `altium_test_board` replies with `_hints` (list form, pcbPath, outline sizing, body check).++ ## 0.2.13 (2026-09-19) - **Several parts on ONE document** (John's ToF demo: three VL53Lx sensors on one schematic and one board). `altium_test_board` and `altium_place_on_sch` now accept a `;` list in `footprintName` / `symbolName`: every part lands on the same new board (spread evenly along X inside the wMm x hMm outline) or the same new sheet (U1..Un, 5000 mil apart), and both save to `pcbPath` / `schPath` when given. Single-part calls are unchanged; `assigns`/`stubs` apply to the last part placed. Both verbs finally have `describe` entries. Agent template only: deploy the tmpl beside the exe and `altium_agent_stop` + `_start`; the 0.2.9 exe keeps working with it. - Measured on ConfRoomROG (ADDevelop): `altium_eval` scripts that call `PCBServer.PreProcess` / `PostProcess` or even `TStringList.SaveToFile` halt before their first statement completes and leave an X2 error modal whose text is not readable (`dialogs[].body` empty); clear with `altium_dismiss_dialogs {Cancel}` + `altium_agent_recover`, twice. adom/altium-bridge#11 asks for readable eval errors and for append-to-existing placement.
bridge-rs/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.13"+version = "0.2.14" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/library.rs+129−3
@@ -287,9 +287,132 @@ pub fn place_on_sch(args: &Value) -> Value {         _ => return json!({"success": false, "error": "libraryPath (.SchLib) required"}),     };     support::agent_open(&lib, 60);-    with_shot(agent::call("place_on_sch",-        &[("sch", s_or(args, "schPath", "")), ("libref", s_or(args, "symbolName", "CH340G"))],-        None, timeout_of(args, 90)))+    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 mut r = with_shot(agent::call("place_on_sch",+        &[("sch", s_or(args, "schPath", "")), ("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)));+    let missing: Vec<&str> = [("title", &title), ("number", &number), ("revision", &revision), ("drawnBy", &drawn), ("description", &desc)]+        .iter().filter(|(_, v)| v.is_empty()).map(|(k, _)| *k).collect();+    let mut hints = vec![+        "The sheet is sized for the part count (A4 up to two parts, B from three) and every part sits inside the drawing frame as U1..Un with its part number as the Comment.".to_string(),+        "Prove it with altium_show_schematic {filePath: schPath}; after a re-run close the stale sheet first (altium_close_document {path}) or the capture shows the OLD document.".to_string()];+    if missing.is_empty() {+        hints.push("Title block filled (Title, DocumentNumber, Revision, DrawnBy) and the description placed as a text frame across the top of the sheet.".into());+    } else {+        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);+    r+}++/// Draw the chip-outline artwork (the chip-thumbnailer polylines adom-symbol baked into the KiCad+/// symbol) into a SchLib component, so the Altium symbol shows the same chip as KiCad and Fusion.+/// Reads `kicadSym` (a Windows path to the .kicad_sym on this box), takes the symbol's body rectangle+/// and every polyline, converts mm -> mil and hands them to the agent, which maps the KiCad body+/// rectangle onto the component's own rectangle.+pub fn add_symbol_art(args: &Value) -> Value {+    if let Some(g) = support::need_licensed() {+        return g;+    }+    let lib = match s(args, "libraryPath") {+        Some(l) if !l.is_empty() => l,+        _ => return json!({"success": false, "error": "libraryPath (.SchLib) required"}),+    };+    let name = s_or(args, "symbolName", "");+    let ks = s_or(args, "kicadSym", "");+    if name.is_empty() || ks.is_empty() {+        return json!({"success": false, "error": "symbolName and kicadSym (Windows path to the .kicad_sym carrying the baked outline) are required"});+    }+    let text = match std::fs::read_to_string(&ks) {+        Ok(t) => t,+        Err(e) => return json!({"success": false, "error": format!("cannot read kicadSym {ks}: {e}"), "_hint": "send_files the .kicad_sym to this box first and pass its Windows path"}),+    };+    let (rect, polys) = kicad_symbol_art(&text, &name);+    let rect = match rect {+        Some(r) => r,+        None => return json!({"success": false, "error": format!("no (rectangle ...) body found for symbol {name} in {ks}")}),+    };+    if polys.is_empty() {+        return json!({"success": false, "error": format!("no polylines in symbol {name}: bake the outline first (adom-symbol create --outline-svg <mpn>-3d-outline-heavy.svg --outline-name none)")});+    }+    const MIL: f64 = 39.3700787;+    let kr = format!("{:.0},{:.0},{:.0},{:.0}", rect.0 * MIL, rect.1 * MIL, rect.2 * MIL, rect.3 * MIL);+    let payload: String = polys.iter().map(|pl| pl.iter().map(|(x, y)| format!("{:.0},{:.0}", x * MIL, y * MIL)).collect::<Vec<_>>().join(" ")).collect::<Vec<_>>().join("\n");+    support::agent_open(&lib, 60);+    let mut r = agent::call("add_symbol_art", &[("libref", name.clone()), ("kicadRect", kr)], Some(&payload), timeout_of(args, 180));+    r["_hints"] = json!([+        format!("{} polylines drawn into {name} from the KiCad symbol's baked outline; altium_save {{filePath}} the SchLib, then altium_show_symbol to check the chip sits between the pin-name columns.", polys.len()),+        "The outline is the reviewed chip artwork (side-etched MPN for optical tops); do not re-etch or re-generate it here, reuse the wiki page's artwork cache.",+        "This verb APPENDS: running it twice draws the outline twice. remove_symbol + rebuild, or clone the part first, to redo it."]);+    r+}++/// Read-only: a SchLib component's drawing (primitive counts, body rectangle, polyline bbox, models).+pub fn symbol_prims(args: &Value) -> Value {+    let lib = match s(args, "libraryPath") {+        Some(l) if !l.is_empty() => l,+        _ => return json!({"success": false, "error": "libraryPath (.SchLib) required"}),+    };+    support::agent_open(&lib, 60);+    agent::call("symbol_prims", &[("libref", s_or(args, "symbolName", ""))], None, timeout_of(args, 60))+}++/// Replace every footprint model on a SchLib component with one PCBLIB model.+pub fn set_models(args: &Value) -> Value {+    if let Some(g) = support::need_licensed() {+        return g;+    }+    let lib = match s(args, "libraryPath") {+        Some(l) if !l.is_empty() => l,+        _ => return json!({"success": false, "error": "libraryPath (.SchLib) required"}),+    };+    support::agent_open(&lib, 60);+    let mut r = agent::call("set_models", &[("libref", s_or(args, "symbolName", "")), ("footprint", s_or(args, "footprint", ""))], None, timeout_of(args, 60));+    r["_hints"] = json!(["Replaces ALL models (set_params footprint and add_models append). altium_save the SchLib afterwards; altium_symbol_prims shows the models list."]);+    r+}++/// (body rectangle in mm as (x0,y0,x1,y1), polylines in mm) for one symbol of a .kicad_sym.+fn kicad_symbol_art(text: &str, name: &str) -> (Option<(f64, f64, f64, f64)>, Vec<Vec<(f64, f64)>>) {+    let head = format!("(symbol \"{name}\"");+    let start = match text.find(&head) { Some(i) => i, None => return (None, vec![]) };+    // the symbol block ends where the paren depth returns to zero+    let bytes = text.as_bytes(); let mut depth = 0i32; let mut end = start;+    for (i, &b) in bytes.iter().enumerate().skip(start) {+        if b == b'(' { depth += 1; } else if b == b')' { depth -= 1; if depth == 0 { end = i + 1; break; } }+    }+    let blk = &text[start..end];+    let num = |s: &str| s.trim().parse::<f64>().ok();+    let mut rect = None;+    if let Some(i) = blk.find("(rectangle (start ") {+        let rest = &blk[i + "(rectangle (start ".len()..];+        let mut it = rest.split(|c: char| c == ' ' || c == ')' || c == '(').filter(|w| !w.is_empty());+        let x0 = it.next().and_then(num); let y0 = it.next().and_then(num);+        let _end = it.next(); let x1 = it.next().and_then(num); let y1 = it.next().and_then(num);+        if let (Some(a), Some(b), Some(c), Some(d)) = (x0, y0, x1, y1) { rect = Some((a, b, c, d)); }+    }+    let mut polys = vec![];+    let mut pos = 0;+    while let Some(i) = blk[pos..].find("(polyline") {+        let s0 = pos + i; let mut d = 0i32; let mut e = s0;+        for (k, &b) in blk.as_bytes().iter().enumerate().skip(s0) {+            if b == b'(' { d += 1; } else if b == b')' { d -= 1; if d == 0 { e = k + 1; break; } }+        }+        let pb = &blk[s0..e];+        let mut pts = vec![]; let mut q = 0;+        while let Some(j) = pb[q..].find("(xy ") {+            let a = q + j + 4; let z = pb[a..].find(')').map(|k| a + k).unwrap_or(pb.len());+            let mut w = pb[a..z].split_whitespace();+            if let (Some(x), Some(y)) = (w.next().and_then(num), w.next().and_then(num)) { pts.push((x, y)); }+            q = z;+        }+        if pts.len() >= 2 { polys.push(pts); }+        pos = e;+    }+    (rect, polys) }  pub fn place_on_pcb(args: &Value) -> Value {@@ -319,6 +442,9 @@ pub fn test_board(args: &Value) -> Value {           ("assigns", s_or(args, "assigns", "")), ("stubs", s_or(args, "stubs", ""))],         None, timeout_of(args, 240));     r["windowEtiquette"] = json!("background");+    r["_hints"] = json!([+        "footprintName takes a ';' list: every part lands on THIS one new 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.",+        "Give the board an outline that fits the parts (roughly 12 mm of width per 6 mm part plus margins) and check the 3D frame: bodies come from the PcbLib (altium_add_body), a missing body means the footprint has none."]);     r } 
bridge-rs/src/main.rs+1−1
@@ -25,7 +25,7 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.13";+pub const BRIDGE_VERSION: &str = "0.2.14";  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/verbs.rs+18−3
@@ -35,7 +35,7 @@ pub const KNOWN: &[&str] = &[     "clone_symbol", "rename_symbol", "rename_footprint", "remove_symbol", "remove_footprint",     "copy_footprint", "xcopy_footprint", "swap_footprint", "symbol_mode", "cleanup_libs",     "install_library", "install_library_bundle", "install_receipt", "compile_intlib", "extract_intlib",-    "place_on_sch", "place_on_pcb", "import_part",+    "place_on_sch", "place_on_pcb", "import_part", "add_symbol_art", "symbol_prims", "set_models",     // licence / account     "license", "license_status", "ensure_licensed", "setup_trial", "signin", "signin_status",     "connect_workspace",@@ -107,8 +107,20 @@ fn describe() -> Value {             "Three parts on one board: footprintName 'VL53L5CX;VL53L7CX;VL53L8CX' + pcbPath, then altium_show_3d_board {filePath: pcbPath}. Every call creates a NEW PcbDoc.",             &["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."],             json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX;VL53L7CX;VL53L8CX", "wMm": 39, "hMm": 14, "pcbPath": "C:/proj/tof.PcbDoc"})),-        verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet as U1..Un (5000 mil apart); saves to schPath when given.",-            json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)"}),+        verb_doc("symbol_prims", "Read a SchLib component's drawing: primitive counts by kind, body rectangle, polyline bounding box, display mode and the footprint models. Read-only, use it to verify add_symbol_art / set_models.",+            json!({"libraryPath": "path (.SchLib)", "symbolName": "string"}), json!({"pins": "int", "rectangles": "int", "polylines": "int", "bodyRectMil": "[..]", "polyBboxMil": "[..]", "models": "[\"PCBLIB:NAME\"]"}), 60,+            "polylines should equal what add_symbol_art reported and polyBboxMil should sit inside bodyRectMil.", &["add_symbol_art", "set_models", "read_symbol"], &[], json!({})),+        verb_doc("set_models", "Replace every footprint model on a SchLib component with ONE PCBLIB model (set_params footprint and add_models APPEND, which is how a symbol ends up with two).",+            json!({"libraryPath": "path (.SchLib)", "symbolName": "string", "footprint": "string (PcbLib footprint name)"}), json!({"removed": "int", "footprint": "string"}), 60,+            "altium_save the SchLib afterwards.", &["symbol_prims", "set_params", "add_models"], &["Removes every existing model, including 3D-only ones."], json!({})),+        verb_doc("add_symbol_art", "Draw the chip-outline artwork (the chip-thumbnailer polylines adom-symbol baked into the KiCad symbol) into a SchLib component, fitted to its body rectangle, so the Altium symbol shows the same chip as KiCad and Fusion.",+            json!({"libraryPath": "path (.SchLib)", "symbolName": "string (LibReference)", "kicadSym": "Windows path to the .kicad_sym that carries the baked outline (stage it with send_files)"}),+            json!({"polylines": "int", "points": "int", "bodyRectMil": "[x0,y0,x1,y1]"}), 180,+            "Run once per part right after assemble-altium / build_symbol_modes, then altium_save the SchLib and altium_show_symbol to check.",+            &["show_symbol", "read_symbol", "save"], &["APPENDS on every call: run it once per symbol.", "The KiCad symbol must already carry the outline (adom-symbol create bakes it; --outline-name none for side-etched chips)."],+            json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "kicadSym": "C:/lib/VL53L5CX.kicad_sym"})),+        verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sized A4 or B for the part count, parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given.",+            json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)", "title": "string (Title)", "number": "string (DocumentNumber)", "revision": "string", "drawnBy": "string", "description": "string (what the sheet shows, 2-3 sentences)"}),             json!({"placed": "string", "count": "int", "designator": "string", "primitives": "int", "sch": "path"}), 90,             "Three parts on one sheet: symbolName 'VL53L5CX;VL53L7CX;VL53L8CX' + schPath, then altium_show_schematic {filePath: schPath}.",             &["test_board", "show_schematic", "read_schematic"], &["Every call creates a NEW SchDoc; there is no append-to-sheet yet.", "The SchLib must be the current document (the verb opens it)."],@@ -475,6 +487,9 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) ->         "compile_intlib" => library::compile_intlib(args),         "extract_intlib" => library::extract_intlib(args),         "place_on_sch" => library::place_on_sch(args),+        "add_symbol_art" => library::add_symbol_art(args),+        "symbol_prims" => library::symbol_prims(args),+        "set_models" => library::set_models(args),         "place_on_pcb" => library::place_on_pcb(args),         "import_part" => library::import_part(args),         // ---- licence / account ----
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.13",+  "version": "0.2.14",   "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+2−2
@@ -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.13",+  "version": "0.2.14",   "hero": {     "type": "image",     "path": "screenshots/hero.png"@@ -139,4 +139,4 @@     "skills/altium-wiki-import/SKILL.md",     "uninstall.sh"   ]-}\ No newline at end of file+}
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.13+0.2.14
src/agent/AdomAgent.pas.tmpl+390−6
@@ -1324,6 +1324,329 @@ End;  { Create a new schematic sheet and place the CH340G symbol (Replicated from the   currently-open SchLib) at the centre. sch=<save path> libref=<component> }+{ true/false for JSON }+Function BoolStr(B : Boolean) : String;+Begin+    If B Then Result := 'true' Else Result := 'false';+End;++{ Set (or add) a DOCUMENT parameter on a sheet: Title / DocumentNumber / Revision / DrawnBy feed the+  standard title block through its special strings. }+Procedure SetDocParam(Sheet : ISch_Document; PName : String; PVal : String);+Var+    It : ISch_Iterator;+    Pm, Existing : ISch_Parameter;+Begin+    Existing := Nil;+    It := Sheet.SchIterator_Create;+    It.AddFilter_ObjectSet(MkSet(eParameter));+    Pm := It.FirstSchObject;+    While Pm <> Nil Do+    Begin+        If Pm.Name = PName Then+        Begin+            Existing := Pm;+            Break;+        End;+        Pm := It.NextSchObject;+    End;+    Sheet.SchIterator_Destroy(It);+    If Existing <> Nil Then+        Existing.Text := PVal+    Else+    Begin+        Pm := SchServer.SchObjectFactory(eParameter, eCreate_Default);+        Pm.Name := PName;+        Pm.Text := PVal;+        Pm.IsHidden := True;+        Sheet.AddSchObject(Pm);+        SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Pm.I_ObjectAddress);+    End;+End;++{ Split on single spaces }+Function SplitSpace(S : String) : TStringList;+Var+    K : Integer;+    Part : String;+Begin+    Result := TStringList.Create;+    S := Trim(S);+    While S <> '' Do+    Begin+        K := Pos(' ', S);+        If K > 0 Then+        Begin+            Part := Copy(S, 1, K - 1);+            S := Trim(Copy(S, K + 1, Length(S)));+        End+        Else+        Begin+            Part := S;+            S := '';+        End;+        If Part <> '' Then Result.Add(Part);+    End;+End;++{ Draw the chip-outline artwork (chip-thumbnailer polylines, as baked into the KiCad symbol) into a+  SchLib component. Args: libref, kicadRect=x0,y0,x1,y1 (mils, the KiCad body rectangle). Payload:+  one polyline per line, 'x,y x,y ...' in mils of the KiCad frame. The KiCad body rectangle is mapped+  onto the component's own rectangle, so the outline lands between the pin-name columns exactly as+  it does in KiCad and Fusion (John, 2026-09-19). }+Function CmdAddSymbolArt(Lines : TStringList; Payload : String) : String;+Var+    SLib   : ISch_Lib;+    Iter   : ISch_Iterator;+    Comp   : ISch_Component;+    Rect   : ISch_Rectangle;+    Poly   : ISch_Polyline;+    PL, Pts : TStringList;+    I, J, N, NPoly, NPts : Integer;+    Name, KR, Ln, Tok : String;+    KX0, KY0, KX1, KY1, AX0, AY0, AX1, AY1, SX, SY, MX, MY, Tmp : Double;+Begin+    Name := Param(Lines, 'libref');+    KR := Param(Lines, 'kicadRect');+    SLib := SchServer.GetCurrentSchDocument;+    If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';+        Exit;+    End;+    Comp := Nil;+    Iter := SLib.SchLibIterator_Create;+    Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+    Comp := Iter.FirstSchObject;+    While Comp <> Nil Do+    Begin+        If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;+        Comp := Iter.NextSchObject;+    End;+    SLib.SchIterator_Destroy(Iter);+    If Comp = Nil Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';+        Exit;+    End;+    Rect := Nil;+    Iter := Comp.SchIterator_Create;+    Iter.AddFilter_ObjectSet(MkSet(eRectangle));+    Rect := Iter.FirstSchObject;+    If Rect <> Nil Then+    Begin+        AX0 := CoordToMils(Rect.Location.X); AY0 := CoordToMils(Rect.Location.Y);+        AX1 := CoordToMils(Rect.Corner.X); AY1 := CoordToMils(Rect.Corner.Y);+    End;+    Comp.SchIterator_Destroy(Iter);+    If Rect = Nil Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component has no body rectangle to fit the artwork into') + '}';+        Exit;+    End;+    KX0 := StrToFloat(Field(KR, 1)); KY0 := StrToFloat(Field(KR, 2));+    KX1 := StrToFloat(Field(KR, 3)); KY1 := StrToFloat(Field(KR, 4));+    If KX1 < KX0 Then Begin Tmp := KX0; KX0 := KX1; KX1 := Tmp; End;+    If KY1 < KY0 Then Begin Tmp := KY0; KY0 := KY1; KY1 := Tmp; End;+    If AX1 < AX0 Then Begin Tmp := AX0; AX0 := AX1; AX1 := Tmp; End;+    If AY1 < AY0 Then Begin Tmp := AY0; AY0 := AY1; AY1 := Tmp; End;+    If (KX1 - KX0 < 1) Or (KY1 - KY0 < 1) Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'kicadRect is degenerate: ' + KR) + '}';+        Exit;+    End;+    SX := (AX1 - AX0) / (KX1 - KX0);+    SY := (AY1 - AY0) / (KY1 - KY0);+    PL := TStringList.Create;+    PL.Text := Payload;+    NPoly := 0; NPts := 0;+    SchServer.ProcessControl.PreProcess(SLib, '');+    For I := 0 To PL.Count - 1 Do+    Begin+        Ln := Trim(PL[I]);+        If Ln = '' Then Continue;+        Pts := SplitSpace(Ln);+        N := Pts.Count;+        If N >= 2 Then+        Begin+            Poly := SchServer.SchObjectFactory(ePolyline, eCreate_Default);+            Poly.OwnerPartId := 1;+            Poly.OwnerPartDisplayMode := 0;+            Poly.LineWidth := eSmall;+            Poly.Color := $8A8A8A;+            Poly.VerticesCount := N;+            For J := 1 To N Do+            Begin+                Tok := Pts[J - 1];+                MX := AX0 + (StrToFloat(Field(Tok, 1)) - KX0) * SX;+                MY := AY0 + (StrToFloat(Field(Tok, 2)) - KY0) * SY;+                Poly.Vertex[J] := Point(MilsToCoord(MX), MilsToCoord(MY));+            End;+            Comp.AddSchObject(Poly);+            SchServer.RobotManager.SendMessage(Comp.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Poly.I_ObjectAddress);+            NPoly := NPoly + 1;+            NPts := NPts + N;+        End;+        Pts.Free;+    End;+    SchServer.ProcessControl.PostProcess(SLib, '');+    PL.Free;+    SLib.GraphicallyInvalidate;+    Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' + JRaw('polylines', IntToStr(NPoly)) + ',' ++        JRaw('points', IntToStr(NPts)) + ',' + JRaw('bodyRectMil', '[' + IntToStr(Round(AX0)) + ',' + IntToStr(Round(AY0)) + ',' + IntToStr(Round(AX1)) + ',' + IntToStr(Round(AY1)) + ']') + '}';+End;++{ Read a SchLib component drawing: primitive counts by kind, the body rectangle, the polyline+  bounding box and the footprint models. Args: libref }+Function CmdSymbolPrims(Lines : TStringList) : String;+Var+    SLib  : ISch_Lib;+    Iter  : ISch_Iterator;+    Comp  : ISch_Component;+    Prim  : ISch_GraphicalObject;+    Impl  : ISch_Implementation;+    Name, Models : String;+    NPin, NRect, NLine, NPoly, NOther, I : Integer;+    R : TCoordRect;+    PX0, PY0, PX1, PY1, RX0, RY0, RX1, RY1 : Integer;+    AnyP, AnyR : Boolean;+Begin+    Name := Param(Lines, 'libref');+    SLib := SchServer.GetCurrentSchDocument;+    If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';+        Exit;+    End;+    Comp := Nil;+    Iter := SLib.SchLibIterator_Create;+    Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+    Comp := Iter.FirstSchObject;+    While Comp <> Nil Do+    Begin+        If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;+        Comp := Iter.NextSchObject;+    End;+    SLib.SchIterator_Destroy(Iter);+    If Comp = Nil Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';+        Exit;+    End;+    NPin := 0; NRect := 0; NLine := 0; NPoly := 0; NOther := 0;+    AnyP := False; AnyR := False;+    PX0 := 0; PY0 := 0; PX1 := 0; PY1 := 0; RX0 := 0; RY0 := 0; RX1 := 0; RY1 := 0;+    Iter := Comp.SchIterator_Create;+    Prim := Iter.FirstSchObject;+    While Prim <> Nil Do+    Begin+        If Prim.ObjectID = ePin Then NPin := NPin + 1+        Else If Prim.ObjectID = eRectangle Then+        Begin+            NRect := NRect + 1;+            If Not AnyR Then+            Begin+                R := Prim.BoundingRectangle;+                RX0 := Round(CoordToMils(R.Left)); RY0 := Round(CoordToMils(R.Bottom));+                RX1 := Round(CoordToMils(R.Right)); RY1 := Round(CoordToMils(R.Top));+                AnyR := True;+            End;+        End+        Else If Prim.ObjectID = eLine Then NLine := NLine + 1+        Else If Prim.ObjectID = ePolyline Then+        Begin+            NPoly := NPoly + 1;+            R := Prim.BoundingRectangle;+            If Not AnyP Then+            Begin+                PX0 := Round(CoordToMils(R.Left)); PY0 := Round(CoordToMils(R.Bottom));+                PX1 := Round(CoordToMils(R.Right)); PY1 := Round(CoordToMils(R.Top));+                AnyP := True;+            End+            Else+            Begin+                If Round(CoordToMils(R.Left)) < PX0 Then PX0 := Round(CoordToMils(R.Left));+                If Round(CoordToMils(R.Bottom)) < PY0 Then PY0 := Round(CoordToMils(R.Bottom));+                If Round(CoordToMils(R.Right)) > PX1 Then PX1 := Round(CoordToMils(R.Right));+                If Round(CoordToMils(R.Top)) > PY1 Then PY1 := Round(CoordToMils(R.Top));+            End;+        End+        Else NOther := NOther + 1;+        Prim := Iter.NextSchObject;+    End;+    Comp.SchIterator_Destroy(Iter);+    Models := '';+    For I := 0 To Comp.ImplementationCount - 1 Do+    Begin+        Impl := Comp.Implementation[I];+        If Models <> '' Then Models := Models + ',';+        Models := Models + '"' + Impl.ModelType + ':' + Impl.ModelName + '"';+    End;+    Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' ++        JRaw('pins', IntToStr(NPin)) + ',' + JRaw('rectangles', IntToStr(NRect)) + ',' + JRaw('lines', IntToStr(NLine)) + ',' ++        JRaw('polylines', IntToStr(NPoly)) + ',' + JRaw('other', IntToStr(NOther)) + ',' ++        JRaw('displayMode', IntToStr(Comp.DisplayMode)) + ',' + JRaw('displayModeCount', IntToStr(Comp.DisplayModeCount)) + ',' ++        JRaw('currentPartId', IntToStr(Comp.CurrentPartID)) + ',' + JRaw('partCount', IntToStr(Comp.PartCount)) + ',' ++        JRaw('bodyRectMil', '[' + IntToStr(RX0) + ',' + IntToStr(RY0) + ',' + IntToStr(RX1) + ',' + IntToStr(RY1) + ']') + ',' ++        JRaw('polyBboxMil', '[' + IntToStr(PX0) + ',' + IntToStr(PY0) + ',' + IntToStr(PX1) + ',' + IntToStr(PY1) + ']') + ',' ++        JRaw('models', '[' + Models + ']') + '}';+End;++{ Replace every footprint model on a SchLib component with ONE PCBLIB model (set_params+  footprint APPENDS an implementation per call, which is how a symbol ends up with two).+  Args: libref, footprint }+Function CmdSetModels(Lines : TStringList) : String;+Var+    SLib  : ISch_Lib;+    Iter  : ISch_Iterator;+    Comp  : ISch_Component;+    Impl  : ISch_Implementation;+    Name, Fp : String;+    Removed : Integer;+Begin+    Name := Param(Lines, 'libref');+    Fp := Param(Lines, 'footprint');+    SLib := SchServer.GetCurrentSchDocument;+    If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';+        Exit;+    End;+    Comp := Nil;+    Iter := SLib.SchLibIterator_Create;+    Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+    Comp := Iter.FirstSchObject;+    While Comp <> Nil Do+    Begin+        If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;+        Comp := Iter.NextSchObject;+    End;+    SLib.SchIterator_Destroy(Iter);+    If Comp = Nil Then+    Begin+        Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';+        Exit;+    End;+    Removed := 0;+    While Comp.ImplementationCount > 0 Do+    Begin+        Impl := Comp.Implementation[0];+        Comp.RemoveSchImplementation(Impl);+        Removed := Removed + 1;+    End;+    If Fp <> '' Then+    Begin+        Impl := Comp.AddSchImplementation;+        Impl.ClearAllDatafileLinks;+        Impl.ModelName := Fp;+        Impl.ModelType := 'PCBLIB';+        Impl.AddDataFileLink(Fp, '', 'PCBLIB');+        Impl.IsCurrent := True;+    End;+    SLib.GraphicallyInvalidate;+    Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' + JRaw('removed', IntToStr(Removed)) + ',' + JStr('footprint', Fp) + '}';+End;+ Function CmdPlaceOnSch(Lines : TStringList) : String; Var     SLib   : ISch_Lib;@@ -1335,12 +1658,21 @@ Var     Cpy    : ISch_GraphicalObject;     SD     : IServerDocument;     Sheet  : ISch_Document;-    SchPath, LibRef, Missing, Placed : String;-    NCopied, K : Integer;+    SchPath, LibRef, Missing, Placed, Title, DocNum, Rev, Drawn, Desc : String;+    NCopied, K, W, H, XK, YK, BCX, BCY : Integer;     RefList : TStringList;+    RIt    : ISch_Iterator;+    LRect  : ISch_Rectangle;+    Pm     : ISch_Parameter;+    TF     : ISch_TextFrame; Begin     SchPath := Param(Lines, 'sch');     LibRef := Param(Lines, 'libref');+    Title := Param(Lines, 'title');+    DocNum := Param(Lines, 'number');+    Rev := Param(Lines, 'revision');+    Drawn := Param(Lines, 'drawnBy');+    Desc := Param(Lines, 'description');     { libref may be a ';' list: every symbol lands on ONE new sheet as U1..Un, 5000 mil apart       (John, 2026-09-19: three ToF sensors on one schematic for the demo). }     RefList := SplitSemi(LibRef);@@ -1379,10 +1711,22 @@ Begin     End;     Client.ShowDocument(SD);     Sheet := SchServer.GetCurrentSchDocument;+    { Size the sheet for the part count and place INSIDE the drawing frame: A4 (11500 x 7600 mil)+      for one or two parts, B (17000 x 11000 mil) for three or more. Parts are centred vertically+      and spread across the width with a 1500 mil margin (John, 2026-09-19: the first cut put the+      symbols on top of the title block and outside the frame). }+    W := 11500; H := 7600;+    If RefList.Count >= 3 Then+    Begin+        Sheet.SheetStyle := eSheetB;+        W := 17000; H := 11000;+    End;     Placed := '';     NCopied := 0;     For K := 0 To RefList.Count - 1 Do     Begin+    XK := 1500 + ((W - 3000) * (2 * K + 1)) Div (2 * RefList.Count);+    YK := (H Div 2) + 400;     LibCmp := Nil;     Iter := SLib.SchLibIterator_Create;     Iter.AddFilter_ObjectSet(MkSet(eSchComponent));@@ -1402,7 +1746,19 @@ Begin     Inst.DisplayMode := 0;     Inst.LibReference := LibCmp.LibReference;     Inst.ComponentDescription := LibCmp.ComponentDescription;-    Inst.Location := Point(MilsToCoord(2000 + K * 5000), MilsToCoord(4000));+    Inst.Location := Point(MilsToCoord(XK), MilsToCoord(YK));+    { the library part primitives sit around ITS origin, find the body rectangle centre so the+      body (not the origin) lands on the target point }+    BCX := 0; BCY := 0;+    RIt := LibCmp.SchIterator_Create;+    RIt.AddFilter_ObjectSet(MkSet(eRectangle));+    LRect := RIt.FirstSchObject;+    If LRect <> Nil Then+    Begin+        BCX := Round(CoordToMils(LRect.Location.X + LRect.Corner.X) / 2);+        BCY := Round(CoordToMils(LRect.Location.Y + LRect.Corner.Y) / 2);+    End;+    LibCmp.SchIterator_Destroy(RIt);     PIt := LibCmp.SchIterator_Create;     Prim := PIt.FirstSchObject;     While Prim <> Nil Do@@ -1415,9 +1771,9 @@ Begin             Cpy := Prim.Replicate;             Cpy.OwnerPartId := 1;             Cpy.OwnerPartDisplayMode := 0;-            { copied primitives keep the library coordinates; Inst.Location does not carry them+            { copied primitives keep the library coordinates, Inst.Location does not carry them               (measured 2026-09-19: three parts drew on top of each other), so shift each child }-            If K > 0 Then Cpy.MoveByXY(MilsToCoord(K * 5000), 0);+            Cpy.MoveByXY(MilsToCoord(XK - BCX), MilsToCoord(YK - BCY));             Inst.AddSchObject(Cpy);             NCopied := NCopied + 1;         End;@@ -1426,11 +1782,34 @@ Begin     LibCmp.SchIterator_Destroy(PIt);     Inst.Designator.Text := 'U' + IntToStr(K + 1);     Inst.Designator.IsHidden := False;+    Inst.Designator.Location := Point(MilsToCoord(XK), MilsToCoord(YK + 200));+    { never leave the star placeholder: the comment is the part number }+    Inst.Comment.Text := LibCmp.LibReference;+    Inst.Comment.IsHidden := False;+    Inst.Comment.Location := Point(MilsToCoord(XK), MilsToCoord(YK - 200));     Sheet.RegisterSchObjectInContainer(Inst);     SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);     If Placed <> '' Then Placed := Placed + ';';     Placed := Placed + RefList[K];     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 Title <> '' Then SetDocParam(Sheet, 'Title', Title);+    If DocNum <> '' Then SetDocParam(Sheet, 'DocumentNumber', DocNum);+    If Rev <> '' Then SetDocParam(Sheet, 'Revision', Rev);+    If Drawn <> '' Then SetDocParam(Sheet, 'DrawnBy', Drawn);+    If Desc <> '' Then+    Begin+        SetDocParam(Sheet, 'Description', Desc);+        TF := SchServer.SchObjectFactory(eTextFrame, eCreate_Default);+        TF.Location := Point(MilsToCoord(1000), MilsToCoord(H - 1500));+        TF.Corner := Point(MilsToCoord(W - 1000), MilsToCoord(H - 500));+        TF.Text := Desc;+        TF.WordWrap := True;+        TF.ShowBorder := False;+        Sheet.RegisterSchObjectInContainer(TF);+        SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, TF.I_ObjectAddress);+    End;     Sheet.GraphicallyInvalidate;     ResetParameters; AddStringParameter('Action','All'); RunProcess('Sch:Zoom');     If SchPath <> '' Then@@ -1438,7 +1817,9 @@ Begin         SD.SetFileName(SchPath);         SD.DoFileSave('');     End;-    Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JStr('placed', Placed) + ',' + JStr('designator','U1') + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';+    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','U1') + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';     RefList.Free; End; @@ -3906,6 +4287,9 @@ Begin     Else If Cmd = 'set_params' Then Result := CmdSetParams(Lines)     Else If Cmd = 'add_body' Then Result := CmdAddBody(Lines)     Else If Cmd = 'place_on_sch' Then Result := CmdPlaceOnSch(Lines)+    Else If Cmd = 'add_symbol_art' Then Result := CmdAddSymbolArt(Lines, Payload)+    Else If Cmd = 'symbol_prims' Then Result := CmdSymbolPrims(Lines)+    Else If Cmd = 'set_models' Then Result := CmdSetModels(Lines)     Else If Cmd = 'place_on_pcb' Then Result := CmdPlaceOnPcb(Lines)     Else If Cmd = 'pcb_report' Then Result := CmdPcbReport(Lines)     Else If Cmd = 'list_designators' Then Result := CmdListDesignators(Lines)
src/bridge.json+5−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.13",+  "version": "0.2.14",   "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",@@ -35,6 +35,7 @@     "altium_activate_document",     "altium_add_body",     "altium_add_models",+    "altium_add_symbol_art",     "altium_agent_recover",     "altium_agent_start",     "altium_agent_status",@@ -48,6 +49,7 @@     "altium_cleanup_libs",     "altium_clone_symbol",     "altium_close",+    "altium_close_document",     "altium_compile",     "altium_compile_intlib",     "altium_component_data",@@ -91,7 +93,6 @@     "altium_net_connections",     "altium_normalize_bodies",     "altium_open_document",-    "altium_close_document",     "altium_open_project",     "altium_pcb_report",     "altium_place_on_pcb",@@ -116,6 +117,7 @@     "altium_save",     "altium_schematic_data",     "altium_set_layer_visibility",+    "altium_set_models",     "altium_set_params",     "altium_set_visibility",     "altium_setup_trial",@@ -133,6 +135,7 @@     "altium_status",     "altium_swap_footprint",     "altium_symbol_mode",+    "altium_symbol_prims",     "altium_test_board",     "altium_uninstall",     "altium_window_info",