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.51 source (not yet released, needs signing): IC symbols' pins start on the body edge (symbol_modes wrote the outer end, so every pin floated one pin length off the box); regression test test_symbol_modes.py
8 files changed
+67−11
CHANGELOG.md+6@@ -1,5 +1,11 @@ # Changelog +## 0.2.51 (2026-10-19)+- **IC symbols attach to their box again.** `altium-symbol-modes` (`tools/symbol_modes.py`, the spec behind `build_symbol_modes` / `import_part`) wrote each pin's outer end where Altium's `Pin.Location` wants the end on the body, so every pin of a 2-column or 4-side symbol floated one pin length (300 mil) off the box with its name in the gap. Pins now start on the body edge in all ten display modes. Small parts drawn from their KiCad symbol (resistors, capacitors, inductors, LEDs) were never affected.+- `dev-skills/altium-bridge-test/test_symbol_modes.py`: every pin of every mode must start on its body edge (fails on 0.2.50, passes on 0.2.51).+- Symbols already built with 0.2.50 or earlier keep the gap until rebuilt: run `altium-symbol-modes` again and `altium_build_symbol_modes` (it replaces the symbol), then `altium_add_models` to put the footprint link and parameters back.++ ## 0.2.50 (2026-10-16) - **Templates are found on every Altium build.** `build_schematic` and `export_manufacturing` look for Altium's sheet and OutJob templates under the running install's own Public Documents folder (ADDevelop, AD26, ...) instead of AD26 only, so they work on Altium Designer Develop. - **`symbol_mode` no longer halts the script on Altium Designer Develop**: it referenced an undeclared identifier (`NOld`). `build_symbol_modes` reports `replaced`.
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.50";+pub const BRIDGE_VERSION: &str = "0.2.51"; 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());
dev-skills/altium-bridge-test/test_symbol_modes.pyadded+47@@ -0,0 +1,47 @@+#!/usr/bin/env python3+"""Regression test for tools/symbol_modes.py: every PIN's x,y (Altium's Pin.Location, the BODY end) sits on+an edge of its mode's RECT, so the pin attaches to the body and its name lands inside it. Until 0.2.51 the+outer end was written, and every IC-style pin floated one pin length off the box.++ python3 dev-skills/altium-bridge-test/test_symbol_modes.py+"""+import os, subprocess, sys, tempfile++ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))+TOOL = os.path.join(ROOT, "tools", "symbol_modes.py")++PINS = [("1", "GND"), ("2", "SW"), ("3", "VIN"), ("4", "FB"), ("5", "EN"), ("6", "BOOT"), ("7", "PGOOD"), ("8", "VSS")]+SYM = '(kicad_symbol_lib (symbol "T" (symbol "T_1_1"\n' + "".join(+ f' (pin bidirectional line (at {-5.08 if i < 4 else 5.08} {2.54 * (i % 4)} {0 if i < 4 else 180}) (length 2.54)\n'+ f' (name "{n}" (effects (font (size 1.27 1.27)))) (number "{d}" (effects (font (size 1.27 1.27)))))\n'+ for i, (d, n) in enumerate(PINS)) + "))\n"+++def main():+ with tempfile.TemporaryDirectory() as t:+ sym, spec = os.path.join(t, "t.kicad_sym"), os.path.join(t, "t.spec")+ open(sym, "w").write(SYM)+ subprocess.run([sys.executable, TOOL, sym, spec, "T", "test part"], check=True, capture_output=True)+ rects, pins = {}, []+ for line in open(spec):+ f = line.strip().split("|")+ if f[0] == "RECT":+ rects[f[1]] = tuple(int(v) for v in f[2:6])+ elif f[0] == "PIN":+ pins.append((f[1], f[2], int(f[4]), int(f[5]), f[6]))+ bad = []+ for mode, num, x, y, orient in pins:+ x1, y1, x2, y2 = rects[mode]+ lo_x, hi_x, lo_y, hi_y = min(x1, x2), max(x1, x2), min(y1, y2), max(y1, y2)+ on = {"left": x == lo_x and lo_y <= y <= hi_y, "right": x == hi_x and lo_y <= y <= hi_y,+ "up": y == hi_y and lo_x <= x <= hi_x, "down": y == lo_y and lo_x <= x <= hi_x}[orient]+ if not on:+ bad.append(f"mode {mode} pin {num} ({orient}) at {x},{y} is not on the body edge {rects[mode]}")+ if bad:+ print("FAIL\n " + "\n ".join(bad[:12]))+ sys.exit(1)+ print(f"PASS: {len(pins)} pins across {len(rects)} display modes all start on the body edge")+++if __name__ == "__main__":+ main()
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.50",+ "version": "0.2.51", "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.50",+ "version": "0.2.51", "hero": { "type": "image", "path": "screenshots/hero.png"
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.50+0.2.51
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.50",+ "version": "0.2.51", "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",
tools/symbol_modes.py+9−6@@ -7,6 +7,9 @@ name gap 40 from the body edge, 2-col body width = longest left name + longest right name + 200 middle gap; 4-side body side = max(pins-per-side span, longest top/bottom name + longest opposite + 200). Chip-outline modes draw the die in the measured free middle and skip it when the middle is under 300 mil.+A PIN's x,y is its BODY end, on the rectangle's edge (Altium's Pin.Location; the pin runs outward from there+in its orientation). Until 2026-10-19 the outer end was written here, so every pin floated one pin length+off the body with its name in the gap. Spec lines: META|libref|desc|designator MODE|i|name RECT|mode|x1|y1|x2|y2 PIN|mode|number|name|x|y|orient|len LINE|mode|x1|y1|x2|y2 """@@ -79,8 +82,8 @@ def two_column(pins, grouped, chip=False, wrap=False): H = max([-y for _, y in L] + [-y for _, y in R]) + PITCH lw = max([wname(p) for p, _ in L] or [0]); rw = max([wname(p) for p, _ in R] or [0]) W = grid(lw + rw + GAP_MID + (500 if chip else 0))- res = [("PIN", p["num"], p["name"], -PLEN, y, "left") for p, y in L]- res += [("PIN", p["num"], p["name"], W + PLEN, y, "right") for p, y in R]+ res = [("PIN", p["num"], p["name"], 0, y, "left") for p, y in L]+ res += [("PIN", p["num"], p["name"], W, y, "right") for p, y in R] free = (lw, -H + PITCH, W - rw, -PITCH) # middle clear area (x1,y1,x2,y2) return {"w": W, "h": H, "free": free}, res @@ -103,16 +106,16 @@ def four_side(pins, grouped, chip=False): return grid((S - (k - 1) * PITCH) / 2.0) o = centered(len(sides[0])) for i, p in enumerate(sides[0]): # left, top->bottom- res.append(("PIN", p["num"], p["name"], -PLEN, -(o + i * PITCH), "left"))+ res.append(("PIN", p["num"], p["name"], 0, -(o + i * PITCH), "left")) o = centered(len(sides[1])) for i, p in enumerate(sides[1]): # bottom, left->right- res.append(("PIN", p["num"], p["name"], o + i * PITCH, -S - PLEN, "down"))+ res.append(("PIN", p["num"], p["name"], o + i * PITCH, -S, "down")) o = centered(len(sides[2])) for i, p in enumerate(sides[2]): # right, bottom->top- res.append(("PIN", p["num"], p["name"], S + PLEN, -S + (o + i * PITCH), "right"))+ res.append(("PIN", p["num"], p["name"], S, -S + (o + i * PITCH), "right")) o = centered(len(sides[3])) for i, p in enumerate(sides[3]): # top, right->left- res.append(("PIN", p["num"], p["name"], S - (o + i * PITCH), PLEN, "up"))+ res.append(("PIN", p["num"], p["name"], S - (o + i * PITCH), 0, "up")) free = (lw, -S + bw, S - rw, -tw) # middle clear of every name run return {"w": S, "h": S, "free": free}, res