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.
main
Colby Knox
Restore tools/symbol_modes.py from 4d4e030 (pin electrical types): the 0.2.52 release's pkg publish (cc812e5) wrote bac…
9c53d98
1d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
#!/usr/bin/env python3
"""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).
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.
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|hide|etype LINE|mode|x1|y1|x2|y2
(etype: the KiCad pin type, input/output/bidirectional/tri_state/passive/power_in/power_out/...,
which the agent maps to Altium's pin electrical type)
"""
import sys as _sys
def _usage_guard():
if len(_sys.argv) < 5 or _sys.argv[1] in ("-h", "--help"):
print('usage: altium-symbol-modes <part.kicad_sym> <out.spec> <SymbolName> <description>\n Writes ONE symbol with its display modes as the spec altium_build_symbol_modes / altium_import_part take (symbolSpec).')
_sys.exit(0 if len(_sys.argv) > 1 and _sys.argv[1] in ("-h", "--help") else 2)
_usage_guard()
import re, sys, json
GROUPS = [
("Ethernet", lambda n: n.startswith("TR") or n.startswith("REF_")),
("USB", lambda n: n.startswith("USB")),
("Crystal", lambda n: n in ("XI", "XO")),
("Handshake", lambda n: any(k in n for k in ("PME", "SUSPEND", "RESET", "VBUS", "TEST"))),
("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()
pins = []
# the angle may be written 180.0 (a pin written that way was dropped, measured 2026-10-15)
for m in re.finditer(r'\(pin\s+(\w+)\s+(\w+)\s*\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)\s*\(length\s+([-\d.]+)\)', s):
tail = s[m.end():m.end() + 400]
name = re.search(r'\(name\s+"([^"]*)"', tail); num = re.search(r'\(number\s+"([^"]*)"', tail)
pins.append({"num": num.group(1), "name": name.group(1), "etype": m.group(1)})
pins.sort(key=lambda p: int(p["num"]) if p["num"].isdigit() else 999)
return pins
def group_of(name):
for g, f in GROUPS:
if f(name): return g
return "Other"
def wname(p): return GAP_EDGE + CW * len(p["name"])
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]
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 = [], -PITCH
for grp in cols:
for p in grp:
out.append((p, y)); y -= PITCH
y -= PITCH
return out
L, R = stack(left), stack(right)
else:
half = (len(pins) + 1) // 2
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"], 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
def four_side(pins, grouped, chip=False):
if grouped:
seq = []
for g, _ in GROUPS:
seq += [p for p in pins if group_of(p["name"]) == g]
seq += [p for p in pins if group_of(p["name"]) == "Other"]
else:
seq = pins
n = len(seq); per = (n + 3) // 4
sides = [seq[i * per:(i + 1) * per] for i in range(4)]
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 = []
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"], 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, "down"))
o = centered(len(sides[2]))
for i, p in enumerate(sides[2]): # right, bottom->top
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), 0, "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):
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 = 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
# ---- native: a small part (a resistor, a capacitor, an inductor, an LED) drawn as its KiCad symbol is,
# not as an IC box. KiCad symbols are y up in mm, as Altium's are; a KiCad pin's (at) is its wire end and
# its angle points toward the body, Altium's Location is the body end and its orientation points away.
import math
def _mil(v): return int(round(float(v) / 0.0254))
def _blocks(text, tag):
out, i = [], 0
key = "(" + tag
while True:
i = text.find(key, i)
if i < 0: return out
if text[i + len(key)] not in " \n\t\r":
i += 1; continue
depth, j = 0, i
while j < len(text):
if text[j] == "(": depth += 1
elif text[j] == ")":
depth -= 1
if depth == 0: break
j += 1
out.append(text[i:j + 1]); i = j + 1
def _xy(block, key):
m = re.search(r"\(" + key + r"\s+([-\d.]+)\s+([-\d.]+)", block)
return (float(m.group(1)), float(m.group(2))) if m else None
def native_lines(path):
"""The symbol's own drawing and pins as spec lines (mode 0), or None when it is not a small part."""
s = open(path).read()
pins = _blocks(s, "pin")
if not pins or len(pins) > 3: return None
g = []
for r in _blocks(s, "rectangle"):
a, b = _xy(r, "start"), _xy(r, "end")
if a and b: g.append("RECT|0|%d|%d|%d|%d" % (_mil(min(a[0], b[0])), _mil(min(a[1], b[1])), _mil(max(a[0], b[0])), _mil(max(a[1], b[1]))))
for pl in _blocks(s, "polyline"):
pts = [(_mil(x), _mil(y)) for x, y in re.findall(r"\(xy\s+([-\d.]+)\s+([-\d.]+)\)", pl)]
for (x1, y1), (x2, y2) in zip(pts, pts[1:]): g.append("LINE|0|%d|%d|%d|%d" % (x1, y1, x2, y2))
for ar in _blocks(s, "arc"):
a, m, b = _xy(ar, "start"), _xy(ar, "mid"), _xy(ar, "end")
if not (a and m and b): continue
# the circle through the three points; the arc runs counterclockwise from one end through mid
ax, ay, bx, by, cx_, cy_ = a[0], a[1], m[0], m[1], b[0], b[1]
d = 2 * (ax * (by - cy_) + bx * (cy_ - ay) + cx_ * (ay - by))
if abs(d) < 1e-9: g.append("LINE|0|%d|%d|%d|%d" % (_mil(ax), _mil(ay), _mil(cx_), _mil(cy_))); continue
ux = ((ax*ax + ay*ay) * (by - cy_) + (bx*bx + by*by) * (cy_ - ay) + (cx_*cx_ + cy_*cy_) * (ay - by)) / d
uy = ((ax*ax + ay*ay) * (cx_ - bx) + (bx*bx + by*by) * (ax - cx_) + (cx_*cx_ + cy_*cy_) * (bx - ax)) / d
rad = math.hypot(ax - ux, ay - uy)
ang = lambda px, py: math.degrees(math.atan2(py - uy, px - ux)) % 360
sa, ma, ea = ang(ax, ay), ang(bx, by), ang(cx_, cy_)
ccw = (ma - sa) % 360 < (ea - sa) % 360
a1, a2 = (sa, ea) if ccw else (ea, sa)
g.append("ARC|0|%d|%d|%d|%d|%d" % (_mil(ux), _mil(uy), _mil(rad), round(a1), round(a2)))
for c in _blocks(s, "circle"):
ctr = _xy(c, "center"); rm = re.search(r"\(radius\s+([-\d.]+)", c)
if ctr and rm: g.append("ARC|0|%d|%d|%d|0|360" % (_mil(ctr[0]), _mil(ctr[1]), _mil(rm.group(1))))
if not g: return None
out = []
for p in pins:
m = re.search(r"\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)", p); ln = re.search(r"\(length\s+([-\d.]+)\)", p)
num = re.search(r'\(number\s+"([^"]*)"', p); nm = re.search(r'\(name\s+"([^"]*)"', p)
x, y, a = float(m.group(1)), float(m.group(2)), int(round(float(m.group(3)))) % 360
L = float(ln.group(1)) if ln else 2.54
dx, dy = {0: (1, 0), 90: (0, 1), 180: (-1, 0), 270: (0, -1)}.get(a, (1, 0))
orient = {0: "left", 90: "down", 180: "right", 270: "up"}.get(a, "left")
name = nm.group(1) if nm and nm.group(1) != "~" else ""
et = re.match(r"\(pin\s+(\w+)", p)
out.append("PIN|0|%s|%s|%d|%d|%s|%d|hide|" % (num.group(1) if num else "?", name, _mil(x + dx * L), _mil(y + dy * L), orient, _mil(L)) + (et.group(1) if et else ""))
ref = re.search(r'\(property\s+"Reference"\s+"([A-Za-z]+)', s)
return (ref.group(1) if ref else "U"), g + out
def build(pins, libref, desc, out):
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, chip)
lines.append(f"MODE|{mode}|{name}{'-chip' if chip else ''}")
lines.append(f"RECT|{mode}|0|{-body['h']}|{body['w']}|0")
et = {p["num"]: p.get("etype", "") for p in pins}
for _, num, pname, x, y, orient in pins_out:
lines.append(f"PIN|{mode}|{num}|{pname}|{int(x)}|{int(y)}|{orient}|{PLEN}||{et.get(num, '')}")
if chip:
for (a, b, c, d) in chip_lines(body):
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)
if __name__ == "__main__":
pins = parse_pins(sys.argv[1])
nat = native_lines(sys.argv[1])
if nat:
# one mode, the part as its symbol draws it (a resistor reads as a resistor)
des, body = nat
open(sys.argv[2], "w").write("\n".join([f"META|{sys.argv[3]}|{sys.argv[4]}|{des}", "MODE|0|native"] + body) + "\n")
print(json.dumps({"pins": len(pins), "modes": 1, "native": True, "lines": len(body) + 2, "out": sys.argv[2]}))
sys.exit(0)
modes, n = build(pins, sys.argv[3], sys.argv[4], sys.argv[2])
print(json.dumps({"pins": len(pins), "modes": modes, "lines": n, "out": sys.argv[2]}))