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.16: place_on_sch sizes the sheet FROM the parts (measured bounding boxes, 1500 mil gaps, 1000 mil margins), designator above / comment below the body, description inside the frame; no SheetSizeX read-back (reports the default on ADDevelop); foreground-steal doctrine
9 files changed
+117−29
CHANGELOG.md+5@@ -1,5 +1,10 @@ # Changelog +## 0.2.16 (2026-09-19)+- `altium_place_on_sch`: the designator sits centred ABOVE the body and the Comment (part number) centred BELOW it, whatever the library part did with them (+200 mil had put U1 on top of the chip outline inside a 3700 mil body); the description text frame sits INSIDE the drawing frame (700 mil below the top edge, 700 mil short of the right edge) instead of above the sheet; the sheet size is read back from the document (`SheetSizeX/Y`, `UseCustomSheet := False` before `SheetStyle`) so parts and text land inside whatever Altium actually gave us. Agent template only.+- Doctrine, measured with John typing in Hydrogen on the same box: the capture verbs restore Altium INTO THE BACKGROUND, but the document steps (`open_document`, `close_document`, `place_on_sch`, `install_library`, `agent_stop`/`agent_start`) still raise the Altium window through `Client.ShowDocument` / script execution and steal his keyboard. Filed as an issue; until it is fixed, batch the document work and stop touching Altium the moment the proofs are in.+- The "ganged-up view" (symbol + 3D chip in the Editor panel, footprint preview in the SCH Library panel): there is no server process that activates a docked panel TAB (`Client:CustomizeResources Action=Show|ObjectKind=Panel|ID=TSchLibPanel` only ensures the panel exists); a `panel` verb is on the wish list.+ ## 0.2.15 (2026-09-19) - `altium_add_symbol_art` replaces instead of stacking (`clear`, default true) and draws the outline as eLine segments (`mode: lines`, default), the primitive that BuildSymbolModes has always rendered with; ISch_Polyline objects rendered on VL53L5CX but stayed invisible on VL53L8CX with identical geometry inside the body (measured with `altium_symbol_prims`). `symbol_prims` gates its model walk behind `models:true` (the ImplementationCount / PartCount walk halted the engine on ADDevelop).
bridge-rs/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.15"+version = "0.2.16" 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+1−1@@ -300,7 +300,7 @@ pub fn place_on_sch(args: &Value) -> Value { "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());+ hints.push("Title block filled (Title, DocumentNumber, Revision, DrawnBy) and the description placed as a text frame inside the drawing frame along the top; designators sit above each body, part numbers below.".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(", "))); }
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.15";+pub const BRIDGE_VERSION: &str = "0.2.16"; 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());
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.15",+ "version": "0.2.16", "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.15",+ "version": "0.2.16", "hero": { "type": "image", "path": "screenshots/hero.png"
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.15+0.2.16
src/agent/AdomAgent.pas.tmpl+105−22@@ -1722,7 +1722,10 @@ Var SD : IServerDocument; Sheet : ISch_Document; SchPath, LibRef, Missing, Placed, Title, DocNum, Rev, Drawn, Desc : String;- NCopied, K, W, H, XK, YK, BCX, BCY : Integer;+ NCopied, K, W, H, XK, YK, BCX, BCY, BH : Integer;+ PWs, CXs, CYs, TPs, BTs : TStringList;+ R : TCoordRect;+ TotalW, MaxH, Gap, Margin, GapA, MarginA, Cur, DX, DY, BBL, BBB, BBR, BBT, BTop, BBot : Integer; RefList : TStringList; RIt : ISch_Iterator; LRect : ISch_Rectangle;@@ -1774,22 +1777,97 @@ 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+ { Pass 1: MEASURE every part (union of its primitives' bounding rectangles, pins+ included, +400 mil a side for the pin-number text that hangs outside the stub).+ John, 2026-09-19, reviewing three 5000 mil bodies squeezed onto one sheet:+ "you are smooshing these symbols together such that their pin names overlap; no+ real EE would ever do that, they'd space them out". So the sheet is sized FROM the+ parts, never the parts from the sheet. }+ PWs := TStringList.Create; CXs := TStringList.Create; CYs := TStringList.Create;+ TPs := TStringList.Create; BTs := TStringList.Create;+ TotalW := 0; MaxH := 0;+ For K := 0 To RefList.Count - 1 Do Begin- Sheet.SheetStyle := eSheetB;- W := 17000; H := 11000;+ LibCmp := Nil;+ Iter := SLib.SchLibIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+ LibCmp := Iter.FirstSchObject;+ While LibCmp <> Nil Do+ Begin+ If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;+ LibCmp := Iter.NextSchObject;+ End;+ SLib.SchIterator_Destroy(Iter);+ BBL := 0; BBB := 0; BBR := 0; BBT := 0; BTop := 0; BBot := 0; NCopied := 0;+ PIt := LibCmp.SchIterator_Create;+ Prim := PIt.FirstSchObject;+ While Prim <> Nil Do+ Begin+ If (Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or+ (Prim.ObjectID = eLine) Or (Prim.ObjectID = eArc) Or+ (Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or+ (Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel) Then+ Begin+ R := Prim.BoundingRectangle;+ If (NCopied = 0) Or (Round(CoordToMils(R.Left)) < BBL) Then BBL := Round(CoordToMils(R.Left));+ If (NCopied = 0) Or (Round(CoordToMils(R.Bottom)) < BBB) Then BBB := Round(CoordToMils(R.Bottom));+ If (NCopied = 0) Or (Round(CoordToMils(R.Right)) > BBR) Then BBR := Round(CoordToMils(R.Right));+ If (NCopied = 0) Or (Round(CoordToMils(R.Top)) > BBT) Then BBT := Round(CoordToMils(R.Top));+ If Prim.ObjectID = eRectangle Then+ Begin+ BTop := Round(CoordToMils(R.Top)); BBot := Round(CoordToMils(R.Bottom));+ End;+ NCopied := NCopied + 1;+ End;+ Prim := PIt.NextSchObject;+ End;+ LibCmp.SchIterator_Destroy(PIt);+ If BTop = 0 Then Begin BTop := BBT; BBot := BBB; End;+ PWs.Add(IntToStr((BBR - BBL) + 800));+ CXs.Add(IntToStr((BBL + BBR) Div 2));+ CYs.Add(IntToStr((BBB + BBT) Div 2));+ TPs.Add(IntToStr(BTop)); BTs.Add(IntToStr(BBot));+ TotalW := TotalW + (BBR - BBL) + 800;+ If (BBT - BBB) > MaxH Then MaxH := BBT - BBB;+ End;+ { Sheet from the parts: 1000 mil margins, at least 1500 mil of air between parts, and+ room above (1700 mil description strip) and below (title block) the row. Sheet.SheetStyle+ does NOT take on ADDevelop (measured 2026-09-19: the sheet stayed the 9500 x 7500 mil+ default), so the sheet is a CUSTOM size, never smaller than A4 (11500 x 7600). }+ Gap := 1500; Margin := 1000;+ W := TotalW + Gap * (RefList.Count - 1) + 2 * Margin;+ If W < 11500 Then W := 11500;+ H := MaxH + 4200;+ If H < 7600 Then H := 7600;+ If H * 17 < W * 9 Then H := (W * 9) Div 17;+ Sheet.UseCustomSheet := True;+ Sheet.CustomX := MilsToCoord(W);+ Sheet.CustomY := MilsToCoord(H);+ Sheet.CustomXZones := (W + 2499) Div 2500;+ Sheet.CustomYZones := (H + 2499) Div 2500;+ Sheet.CustomMarginWidth := MilsToCoord(200);+ { Do NOT read SheetSizeX/Y back here: on ADDevelop they still report the 9500 x 7500 mil+ default after the custom size is set (measured 2026-09-19: the read-back shrank W to+ 9500 and squeezed three 5800 mil parts into it, pin numbers on top of each other,+ while the sheet itself drew at the custom size). W and H ARE the sheet. }+ { spread the slack evenly: the row is centred and every gap grows by the same amount }+ GapA := Gap; MarginA := Margin;+ If W > TotalW + Gap * (RefList.Count - 1) + 2 * Margin Then+ Begin+ GapA := Gap + (W - (TotalW + Gap * (RefList.Count - 1) + 2 * Margin)) Div (RefList.Count + 1);+ MarginA := (W - TotalW - GapA * (RefList.Count - 1)) Div 2; End; Placed := ''; NCopied := 0;+ Cur := MarginA; 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;+ XK := Cur + StrToInt(PWs[K]) Div 2;+ Cur := Cur + StrToInt(PWs[K]) + GapA;+ { the row sits a little below centre: the description strip takes the top 1700 mil }+ YK := (H Div 2) - 300;+ DX := XK - StrToInt(CXs[K]);+ DY := YK - StrToInt(CYs[K]); LibCmp := Nil; Iter := SLib.SchLibIterator_Create; Iter.AddFilter_ObjectSet(MkSet(eSchComponent));@@ -1810,17 +1888,13 @@ Begin Inst.LibReference := LibCmp.LibReference; Inst.ComponentDescription := LibCmp.ComponentDescription; 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;+ { body rectangle centre (mil, library frame) for the designator / comment columns }+ BCX := StrToInt(CXs[K]); BCY := StrToInt(CYs[K]); 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;@@ -1836,20 +1910,25 @@ Begin Cpy.OwnerPartDisplayMode := 0; { 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 }- Cpy.MoveByXY(MilsToCoord(XK - BCX), MilsToCoord(YK - BCY));+ Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY)); Inst.AddSchObject(Cpy); NCopied := NCopied + 1; End; Prim := PIt.NextSchObject; End; LibCmp.SchIterator_Destroy(PIt);+ { Designator centred ABOVE the body and the comment centred BELOW it, whatever the+ library part did with them (measured 2026-09-19: +200 mil put U1 on top of the chip+ outline inside a 3700 mil body). } Inst.Designator.Text := 'U' + IntToStr(K + 1); Inst.Designator.IsHidden := False;- Inst.Designator.Location := Point(MilsToCoord(XK), MilsToCoord(YK + 200));+ Inst.Designator.Justification := eJustify_BottomCenter;+ Inst.Designator.Location := Point(MilsToCoord(BCX + DX), MilsToCoord(StrToInt(TPs[K]) + DY + 100)); { 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));+ Inst.Comment.Justification := eJustify_TopCenter;+ Inst.Comment.Location := Point(MilsToCoord(BCX + DX), MilsToCoord(StrToInt(BTs[K]) + DY - 100)); Sheet.RegisterSchObjectInContainer(Inst); SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress); If Placed <> '' Then Placed := Placed + ';';@@ -1865,8 +1944,11 @@ Begin 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));+ { inside the drawing frame: the border + zone strip take ~400 mil, so the frame+ runs from 700 mil below the top edge down 1000 mil, and stops 700 mil short of+ the right edge }+ TF.Location := Point(MilsToCoord(1000), MilsToCoord(H - 1700));+ TF.Corner := Point(MilsToCoord(W - 700), MilsToCoord(H - 700)); TF.Text := Desc; TF.WordWrap := True; TF.ShowBorder := False;@@ -1880,6 +1962,7 @@ Begin SD.SetFileName(SchPath); SD.DoFileSave(''); End;+ 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','U1') + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.15",+ "version": "0.2.16", "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",