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.3: symbol quality pass (name-length-aware modes, hidden params, remove_symbol)
6 files changed
+82−59
CHANGELOG.md+5@@ -1,5 +1,10 @@ # Changelog +## 0.1.3 — 2026-08-30+- Symbol quality pass (John caught colliding 4-side names): tools/symbol_modes.py sizes bodies from measured pin-name lengths, std = CCW datasheet wrap (distinct from 2-column), die outline is a centred square skipped when it cannot fit; all ten modes frame-checked.+- set_params/add_models parameters are hidden metadata (visible ones piled over the symbol).+- New verb: altium_remove_symbol.+ ## 0.1.2 — 2026-08-30 - LAN7800 creative-import lane: build_footprint (spec from tools/kicad2altium.py: via pads by duplicate designator, roundrect %, named mech layers incl. Adom.SolderBalls), build_symbol_modes (10 display modes on one component) + symbol_mode (editor-driven), add_models (N footprint models + params), normalize_bodies, test_board, board_shape (PCB:PlaceBoardOutline), swap_footprint (nets kept by designator), pcb_report with pads/bodies/nets/tracks. - Dialog pre-emption: ReadAll probes with FileOpen (the debugger breaks on any exception), TxClear on agent start / before save / after recover (dangling PreProcess = "a command is currently active").
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.2",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.2/adom-bridge-altium-v0.1.2.zip",- "sha256": "5ce93c63c370684119225d35461d5764cfec30232a8603859896428c6836a49d",- "size": 89364,+ "version": "0.1.3",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.3/adom-bridge-altium-v0.1.3.zip",+ "sha256": "18d44c9c7c518464d0ba9a3e3dd2feeb9458fb02228caaf4df3a4837b1382cbc",+ "size": 89586, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.3.zipadded⋯ 1 unchanged line ⋯
bridge.json+5−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.2",+ "version": "0.1.3", "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",@@ -124,6 +124,10 @@ { "name": "altium_place_on_pcb", "description": "Place a library footprint on a fresh board"+ },+ {+ "name": "altium_remove_symbol",+ "description": "Remove a component from a SchLib by LibReference" } ], "statusVerb": "altium_status",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.2",+ "version": "0.1.3", "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": [
tools/symbol_modes.py+67−53@@ -1,19 +1,18 @@ #!/usr/bin/env python3-"""symbol_modes.py - emit ONE Altium symbol with N display modes (Altium's-alternate-symbol mechanism) from a KiCad symbol's pin table.+"""symbol_modes.py v2 - ONE Altium symbol, N display modes, with geometry sized+from the actual pin-name lengths so names never collide (ralph-tested per mode). -Spec lines (pipe-separated, units mils, Y up; the agent's build_symbol_modes reads it):- META|libref|description|designator- MODE|index|name- RECT|mode|x1|y1|x2|y2- PIN|mode|number|name|x|y|orient|length orient: left|right|up|down = side the pin sits on- LINE|mode|x1|y1|x2|y2-Looks (10): std, 2col, 4side, 2col-grouped, 4side-grouped, each x {plain, chip} (chip = a-thumbnail outline: rounded body + pin-1 dot drawn as lines inside the rectangle).+Rules (mils): pitch 100, pin length 300, char width 55 (10pt default font),+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.+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 """ import re, sys, json -GROUPS = [ # order matters: grouped looks lay pins out in this order+GROUPS = [ ("Ethernet", lambda n: n.startswith("TR") or n.startswith("REF_")), ("USB", lambda n: n.startswith("USB")), ("Crystal", lambda n: n in ("XI", "XO")),@@ -21,6 +20,7 @@ GROUPS = [ # order matters: grouped looks lay pins out in this order ("Config", lambda n: n.startswith("EE") or "GPIO" in n or n.startswith("LED")), ("Power", lambda n: n.startswith("VDD") or n == "VSS"), ]+PITCH, PLEN, CW, GAP_EDGE, GAP_MID = 100, 300, 55, 40, 200 def parse_pins(path): s = open(path).read()@@ -37,47 +37,45 @@ def group_of(name): if f(name): return g return "Other" -PITCH = 100 # mils-PLEN = 300+def wname(p): return GAP_EDGE + CW * len(p["name"]) -def two_column(pins, grouped):- """Left: first half, right: second half (std/2col). Grouped: groups stacked, a gap between."""+def grid(v): return int(round(v / 100.0) * 100)++def two_column(pins, grouped, chip=False, wrap=False): if grouped: order = [] for g, _ in GROUPS: order.append([p for p in pins if group_of(p["name"]) == g]) order.append([p for p in pins if group_of(p["name"]) == "Other"]) order = [o for o in order if o]- # split groups across the two sides by balancing counts left, right = [], [] for grp in order: (left if sum(len(x) for x in left) <= sum(len(x) for x in right) else right).append(grp) def stack(cols):- out, y = [], 0+ out, y = [], -PITCH for grp in cols: for p in grp: out.append((p, y)); y -= PITCH- y -= PITCH # gap between groups+ y -= PITCH return out L, R = stack(left), stack(right) else: half = (len(pins) + 1) // 2- L = [(p, -i * PITCH) for i, p in enumerate(pins[:half])]- R = [(p, -i * PITCH) for i, p in enumerate(pins[half:])]- h = max([-y for _, y in L] + [-y for _, y in R]) + PITCH- W = 1400- out = [("RECT", 0, 0, -h, W, 0)] # placeholder; we normalize later- body = {"w": W, "h": h}- res = []- for p, y in L:- res.append(("PIN", p["num"], p["name"], -PLEN, y - PITCH // 2, "left"))- for p, y in R:- res.append(("PIN", p["num"], p["name"], W + PLEN, y - PITCH // 2, "right"))- return body, res+ L = [(p, -(i + 1) * PITCH) for i, p in enumerate(pins[:half])]+ rp = pins[half:]+ if wrap: # CCW datasheet wrap: right side bottom->top+ R = [(p, -(len(rp) - i) * PITCH) for i, p in enumerate(rp)]+ else:+ R = [(p, -(i + 1) * PITCH) for i, p in enumerate(rp)]+ 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]+ free = (lw, -H + PITCH, W - rw, -PITCH) # middle clear area (x1,y1,x2,y2)+ return {"w": W, "h": H, "free": free}, res -def four_side(pins, grouped):- """Square: pins distributed on 4 sides, counter-clockwise from top-left like the package- (left top->bottom, bottom left->right, right bottom->top, top right->left)."""+def four_side(pins, grouped, chip=False): if grouped: seq = [] for g, _ in GROUPS:@@ -87,43 +85,59 @@ def four_side(pins, grouped): seq = pins n = len(seq); per = (n + 3) // 4 sides = [seq[i * per:(i + 1) * per] for i in range(4)]- S = (per + 1) * PITCH+ span = (per + 1) * PITCH+ lw = max([wname(p) for p in sides[0]] or [0]); rw = max([wname(p) for p in sides[2]] or [0])+ bw = max([wname(p) for p in sides[1]] or [0]); tw = max([wname(p) for p in sides[3]] or [0])+ S = grid(max(span, lw + rw + GAP_MID, bw + tw + GAP_MID) + (600 if chip else 0)) res = []- for i, p in enumerate(sides[0]): # left, top->bottom- res.append(("PIN", p["num"], p["name"], -PLEN, -(i + 1) * PITCH, "left"))- for i, p in enumerate(sides[1]): # bottom, left->right- res.append(("PIN", p["num"], p["name"], (i + 1) * PITCH, -S - PLEN, "down"))- for i, p in enumerate(sides[2]): # right, bottom->top- res.append(("PIN", p["num"], p["name"], S + PLEN, -S + (i + 1) * PITCH, "right"))- for i, p in enumerate(sides[3]): # top, right->left- res.append(("PIN", p["num"], p["name"], S - (i + 1) * PITCH, PLEN, "up"))- return {"w": S, "h": S}, res+ def centered(k): # start offset to centre k pins on a side+ 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"))+ 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"))+ 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"))+ 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"))+ free = (lw, -S + bw, S - rw, -tw) # middle clear of every name run+ return {"w": S, "h": S, "free": free}, res def chip_lines(body):- """Thumbnail outline inside the rectangle: a rounded die with a pin-1 dot (as a small box)."""- w, h = body["w"], body["h"]- m = 120- x1, y1, x2, y2 = m, -h + m, w - m, -m+ x1, y1, x2, y2 = body["free"]+ x1 += 60; y1 += 60; x2 -= 60; y2 -= 60+ if x2 - x1 < 300 or y2 - y1 < 300:+ return [] # no room: skip rather than collide+ side = min(x2 - x1, y2 - y1, 1200) # a die is square, not a sliver+ cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0+ x1, x2 = cx - side / 2, cx + side / 2+ y1, y2 = cy - side / 2, cy + side / 2 L = [(x1, y1, x2, y1), (x2, y1, x2, y2), (x2, y2, x1, y2), (x1, y2, x1, y1)]- d = 60; px, py = x1 + 100, y2 - 100 # pin-1 dot top-left- L += [(px - d, py - d, px + d, py - d), (px + d, py - d, px + d, py + d), (px + d, py + d, px - d, py + d), (px - d, py + d, px - d, py - d)]+ d = 50; px, py = x1 + 120, y2 - 120+ L += [(px - d, py - d, px + d, py - d), (px + d, py - d, px + d, py + d),+ (px + d, py + d, px - d, py + d), (px - d, py + d, px - d, py - d)] return L def build(pins, libref, desc, out):- looks = [("std", two_column, False), ("2col", two_column, False), ("4side", four_side, False),+ looks = [("std", lambda p, g, c: two_column(p, g, c, wrap=True), False),+ ("2col", two_column, False), ("4side", four_side, False), ("2col-grouped", two_column, True), ("4side-grouped", four_side, True)] lines = [f"META|{libref}|{desc}|U"] mode = 0 for name, fn, grouped in looks: for chip in (False, True):- body, pins_out = fn(pins, grouped)+ body, pins_out = fn(pins, grouped, chip) lines.append(f"MODE|{mode}|{name}{'-chip' if chip else ''}") lines.append(f"RECT|{mode}|0|{-body['h']}|{body['w']}|0") for _, num, pname, x, y, orient in pins_out:- lines.append(f"PIN|{mode}|{num}|{pname}|{x}|{y}|{orient}|{PLEN}")+ lines.append(f"PIN|{mode}|{num}|{pname}|{int(x)}|{int(y)}|{orient}|{PLEN}") if chip: for (a, b, c, d) in chip_lines(body):- lines.append(f"LINE|{mode}|{a}|{b}|{c}|{d}")+ lines.append(f"LINE|{mode}|{int(a)}|{int(b)}|{int(c)}|{int(d)}") mode += 1 open(out, "w").write("\n".join(lines) + "\n") return mode, len(lines)