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
v0.1.24: board outline section + two edge checks
8 files changed
+124−13
CHANGELOG.md+8@@ -1,5 +1,13 @@ # Changelog +## 0.1.24 — 2026-09-01+- **`read_board` gains an `outline` section** — the board extent, read through the SAME guarded pattern the library reader already uses, because a library's internal board has a nil `BoardOutline` and touching `BoundingRectangle` on it halts the engine (measured 2026-08-30). It reports which source it used: `boardOutline` (the real outline's bounding box) or `boardBounds` (everything on the board, including strays outside it).+- **Two review checks unlocked by it**, both named for what they actually measure:+ - `components_outside_board_outline` (error) — a part outside the board's bounding box.+ - `components_near_board_edge` (warning) — a component ORIGIN within a margin of the edge (default 20 mil). Not called `pads_near_board_edge`: per-pad geometry is not in the board read, and the catalog's name would claim a precision this does not have.+- **Both go silent when the extent came from `boardBounds`.** That fallback includes stray primitives outside the real outline, so using it to declare a part "off-board" would produce confident false errors. A weaker source yields no finding rather than a wrong one.+- `notImplemented` now names the bounding-box limitation explicitly: on an L-shaped or slotted board, a part inside the box but outside the real outline is NOT caught.+ ## 0.1.23 — 2026-09-01 - **`altium_review_board`: design review over ONE board read.** Phase 2 of the plan, from the lint catalog. Five checks, all pure functions over data the bridge already reads — so adding them cost no new Altium API surface, which is the whole point: a wrong identifier in the agent halts the script engine behind a modal on the user's screen. - `placeholder_values` (error) — a component still carrying Altium's default Comment. It reaches the BOM as "Comment".
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.23",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.23/adom-bridge-altium-v0.1.23.zip",- "sha256": "45c55098e23334a1b3b98407c2835396a0c26d21db52a0a4a9ff98b87fc28dd7",- "size": 122949,+ "version": "0.1.24",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.24/adom-bridge-altium-v0.1.24.zip",+ "sha256": "db394b52c27eb6c501c25dc2779b05133ec1d70ec587baf3be41b3a9ddf10870",+ "size": 124096, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.24.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.23",+ "version": "0.1.24", "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",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.23",+ "version": "0.1.24", "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": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.23+0.1.24
src/agent/AdomAgent.pas.tmpl+39−2@@ -3716,6 +3716,41 @@ Begin Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JStr('sections', Sects) + S + '}'; End; +{ The board's extent, for the checks that need to know where the edge is. Uses the SAME guarded+ pattern the library reader already relies on: a library's internal board has a NIL BoardOutline and+ touching BoundingRectangle on it halts the engine (measured 2026-08-30). Reports which source it+ used, because the outline's bounding box and the board's bounding box are not the same claim. }+Function CmdReadOutline(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ R : TCoordRect;+ Src : String;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then+ Begin+ Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';+ Exit;+ End;+ If Board.BoardOutline <> Nil Then+ Begin+ R := Board.BoardOutline.BoundingRectangle;+ Src := 'boardOutline';+ End+ Else+ Begin+ R := Board.BoundingRectangle;+ Src := 'boardBounds';+ End;+ Result := '{' + JRaw('ok','true') + ',' + JStr('source', Src) + ',' ++ JRaw('leftMil', IntToStr(Round(CoordToMils(R.Left)))) + ',' ++ JRaw('bottomMil', IntToStr(Round(CoordToMils(R.Bottom)))) + ',' ++ JRaw('rightMil', IntToStr(Round(CoordToMils(R.Right)))) + ',' ++ JRaw('topMil', IntToStr(Round(CoordToMils(R.Top)))) + ',' ++ JRaw('widthMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' ++ JRaw('heightMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + '}';+End;+ { ONE round trip to READ the whole board. Composed from the EXISTING readers on purpose: no new Altium API surface, so it cannot introduce an undeclared identifier (which halts the engine behind a modal). The bridge shapes these into typed views; recorded dumps are the unit-test fixtures. }@@ -3731,7 +3766,7 @@ Begin Exit; End; Sects := Param(Lines, 'sections');- If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy';+ If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy,outline'; S := ''; If Pos('components', Sects) > 0 Then S := S + ',' + JRaw('components', CmdComponentData(Lines)); If Pos('nets', Sects) > 0 Then S := S + ',' + JRaw('nets', CmdListNets(Lines));@@ -3739,6 +3774,7 @@ Begin If Pos('copper', Sects) > 0 Then S := S + ',' + JRaw('copper', CmdCopperLayers(Lines)); If Pos('rules', Sects) > 0 Then S := S + ',' + JRaw('rules', CmdReadRules(Lines)); If Pos('occupancy', Sects) > 0 Then S := S + ',' + JRaw('occupancy', CmdLayerOccupancy(Lines));+ If Pos('outline', Sects) > 0 Then S := S + ',' + JRaw('outline', CmdReadOutline(Lines)); Result := '{' + JRaw('ok','true') + ',' + JStr('board', Board.FileName) + ',' + JStr('sections', Sects) + S + '}'; End; @@ -3753,6 +3789,7 @@ Begin Else If Cmd = 'read_board' Then Result := CmdReadBoard(Lines) Else If Cmd = 'layer_occupancy' Then Result := CmdLayerOccupancy(Lines) Else If Cmd = 'read_schematic' Then Result := CmdReadSchematic(Lines)+ Else If Cmd = 'read_outline' Then Result := CmdReadOutline(Lines) Else If Cmd = 'lib_list' Then Result := CmdLibList(Lines) Else If Cmd = 'lib_select' Then Result := CmdLibSelect(Lines) Else If Cmd = 'compile' Then Result := CmdCompile(Lines)@@ -3803,7 +3840,7 @@ Begin gRunning := False; Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}'; End- Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy,read_schematic') + '}';+ Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy,read_schematic,read_outline') + '}'; End; { ------------------------------------------------------------------ loop --- }
src/server.py+70−4@@ -1567,13 +1567,79 @@ def _chk_duplicate_designators(board, sheet): return out ++def _outline_box(board):+ """The board extent, or None. `source` matters: 'boardOutline' is the real outline's bounding+ box; 'boardBounds' is everything on the board including stray primitives outside it, which is a+ much weaker claim and must not be used to say a part is off-board."""+ o = board.get("outline") or {}+ if not o.get("ok"):+ return None+ for k in ("leftMil", "rightMil", "bottomMil", "topMil"):+ if not isinstance(o.get(k), (int, float)):+ return None+ return o+++def _chk_components_outside_board(board, sheet):+ """error — a placed component sitting outside the board extent.++ NOTE this is a BOUNDING BOX test, not a polygon test: on an L-shaped or slotted board a part+ inside the bounding box but outside the real outline is NOT caught, and the message says so+ rather than implying more precision than the data carries."""+ box = _outline_box(board)+ if not box or box.get("source") != "boardOutline":+ return []+ l, r, b, tp = box["leftMil"], box["rightMil"], box["bottomMil"], box["topMil"]+ out = []+ for c in (board.get("components") or {}).get("components") or []:+ x, y = c.get("xMil"), c.get("yMil")+ if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):+ continue+ if x < l or x > r or y < b or y > tp:+ out.append({"check": "components_outside_board_outline", "severity": "error",+ "designator": c.get("designator"),+ "message": f"{c.get('designator')} sits at ({x}, {y}) mil, outside the board "+ f"bounding box ({l}..{r}, {b}..{tp} mil)."})+ return out+++def _chk_components_near_board_edge(board, sheet, margin_mil=20):+ """warning — a component origin within `margin` of the board edge.++ Named for what it measures: COMPONENT ORIGINS, not pads. A pad-to-edge check needs per-pad+ geometry the board read does not carry yet, and calling this 'pads_near_board_edge' would claim+ a precision it does not have."""+ box = _outline_box(board)+ if not box or box.get("source") != "boardOutline":+ return []+ l, r, b, tp = box["leftMil"], box["rightMil"], box["bottomMil"], box["topMil"]+ out = []+ for c in (board.get("components") or {}).get("components") or []:+ x, y = c.get("xMil"), c.get("yMil")+ if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):+ continue+ if x < l or x > r or y < b or y > tp:+ continue # already reported as outside+ d = min(x - l, r - x, y - b, tp - y)+ if d < margin_mil:+ out.append({"check": "components_near_board_edge", "severity": "warning",+ "designator": c.get("designator"),+ "message": f"{c.get('designator')} origin is {int(d)} mil from the board edge "+ f"(under the {margin_mil} mil margin)."})+ return out++ BOARD_CHECKS = [_chk_placeholder_values, _chk_single_pin_nets, _chk_unused_mechanical_layers,- _chk_off_grid_components, _chk_duplicate_designators]+ _chk_off_grid_components, _chk_duplicate_designators,+ _chk_components_outside_board, _chk_components_near_board_edge] # Named so a clean result is not mistaken for a complete one. REVIEW_NOT_IMPLEMENTED = {- "components_outside_board_outline": "needs the board outline, which read_board does not carry yet",- "pads_near_board_edge": "needs the board outline",+ "pads_near_board_edge": "needs per-PAD geometry; components_near_board_edge measures component "+ "origins instead, which is a weaker claim",+ "outline_shape_checks": "the outline read carries a BOUNDING BOX, not the polygon — a part inside "+ "the box but outside an L-shaped or slotted outline is not caught", "unrouted_connections": "needs the ratsnest", "silkscreen_over_pad": "needs per-layer primitive geometry", "via_antennas": "needs via/track topology",@@ -1590,7 +1656,7 @@ def h_review_board(args): g = _need_licensed() if g: return g- r = agent.call("read_board", {"sections": "components,nets,layers,copper,occupancy",+ r = agent.call("read_board", {"sections": "components,nets,layers,copper,occupancy,outline", "limit": args.get("limit", 2000)}, timeout=float(args.get("timeout", 90))) board = r.get("data") or {}