← 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

John Lauer ·2205540a2a ·1mo ago ·parent 3f75c3b
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",