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
0.2.47 source (not yet released, needs signing): AI Flow Altium lane verbs. Live board edits (routing_state, route_net,…
2f3deab
5d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
#!/usr/bin/env python3
"""kicad2altium.py - turn a .kicad_mod into a line-oriented Altium footprint spec
the resident agent's build_footprint command consumes (no JSON parsing in
DelphiScript). Units: mm in, mm out; Y is flipped (KiCad Y-down -> Altium Y-up).
Spec lines (pipe-separated):
META|name|description
PAD|designator|x|y|w|h|shape|hole|layer|crpct shape: round|rect|oval|roundrect ; layer: top|multi
LINE|layer|x1|y1|x2|y2|width
ARC|layer|cx|cy|r|width|start|stop full circles: 0..360, width = stroke
DISC|layer|cx|cy|r filled circle (drawn as arc with width=r)
POLY|layer|width|x1,y1;x2,y2;...
Layers: overlay (F.SilkS) | fab (F.Fab -> Mechanical 13) | courtyard (F.CrtYd -> Mechanical 15)
| balls (User.9 -> Mechanical 9 "Adom.SolderBalls") | comment (Cmts.User -> Mechanical 1)
"""
import sys as _sys
def _usage_guard():
if len(_sys.argv) < 3 or _sys.argv[1] in ("-h", "--help"):
print('usage: altium-kicad2altium <part.kicad_mod> <out.spec> [FootprintName] [description]\n Writes the line-oriented footprint spec altium_build_footprint / altium_import_part take (footprintSpec).')
_sys.exit(0 if len(_sys.argv) > 1 and _sys.argv[1] in ("-h", "--help") else 2)
_usage_guard()
import re, sys, json
LAYER = {"F.SilkS": "overlay", "F.Fab": "fab", "F.CrtYd": "courtyard", "User.9": "balls",
"Cmts.User": "comment", "F.Cu": "top", "*.Cu": "multi"}
def num(x): return float(x)
def parse_sexpr(s):
"""Minimal s-expression parser -> nested lists (strings kept)."""
tokens = re.findall(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()]+', s)
stack = [[]]
for t in tokens:
if t == '(':
stack.append([])
elif t == ')':
top = stack.pop(); stack[-1].append(top)
else:
stack[-1].append(t.strip('"') if t.startswith('"') else t)
return stack[0]
def find(node, key):
return [n for n in node if isinstance(n, list) and n and n[0] == key]
def first(node, key):
f = find(node, key)
return f[0] if f else None
def layer_of(node):
l = first(node, 'layer')
if l: return LAYER.get(l[1])
ls = first(node, 'layers')
if ls:
for v in ls[1:]:
if v in LAYER: return LAYER[v]
if v.endswith('.Cu'): return 'multi' if v.startswith('*') else 'top'
return None
def convert(path, name=None, description=""):
tree = parse_sexpr(open(path).read())
fp = tree[0]
assert fp[0] in ('footprint', 'module'), fp[0]
name = name or fp[1]
out = [f"META|{name}|{description}"]
stats = {"pads": 0, "th": 0, "lines": 0, "arcs": 0, "discs": 0, "polys": 0, "layers": set()}
for n in fp[2:]:
if not isinstance(n, list): continue
k = n[0]
if k == 'pad':
des, kind, shape = n[1], n[2], n[3]
at = first(n, 'at'); size = first(n, 'size')
x, y = num(at[1]), -num(at[2])
w, h = num(size[1]), num(size[2])
rot = num(at[3]) if len(at) > 3 else 0.0
if rot in (90.0, 270.0): w, h = h, w
hole = 0.0
d = first(n, 'drill')
if d: hole = num(d[1])
layer = 'multi' if kind == 'thru_hole' else 'top'
crp = 0
if shape == 'roundrect':
rr = first(n, 'roundrect_rratio')
crp = int(round(num(rr[1]) * 100)) if rr else 25
shape_a = 'roundrect'
elif shape in ('circle',):
shape_a = 'round'
elif shape == 'oval':
shape_a = 'oval'
else:
shape_a = 'rect'
out.append(f"PAD|{des}|{x:.4f}|{y:.4f}|{w:.4f}|{h:.4f}|{shape_a}|{hole:.3f}|{layer}|{crp}")
stats['pads'] += 1; stats['th'] += (kind == 'thru_hole')
elif k == 'fp_line':
lay = layer_of(n)
if not lay: continue
s, e = first(n, 'start'), first(n, 'end')
st = first(n, 'stroke'); wd = num(first(st, 'width')[1]) if st else num(first(n, 'width')[1])
out.append(f"LINE|{lay}|{num(s[1]):.4f}|{-num(s[2]):.4f}|{num(e[1]):.4f}|{-num(e[2]):.4f}|{wd:.3f}")
stats['lines'] += 1; stats['layers'].add(lay)
elif k == 'fp_circle':
lay = layer_of(n)
if not lay: continue
c, e = first(n, 'center'), first(n, 'end')
r = ((num(e[1]) - num(c[1]))**2 + (num(e[2]) - num(c[2]))**2) ** 0.5
fill = first(n, 'fill')
filled = bool(fill and fill[1] in ('solid', 'yes'))
st = first(n, 'stroke'); wd = num(first(st, 'width')[1]) if st else 0.1
if filled:
out.append(f"DISC|{lay}|{num(c[1]):.4f}|{-num(c[2]):.4f}|{r:.4f}")
stats['discs'] += 1
else:
out.append(f"ARC|{lay}|{num(c[1]):.4f}|{-num(c[2]):.4f}|{r:.4f}|{wd:.3f}|0|360")
stats['arcs'] += 1
stats['layers'].add(lay)
elif k == 'fp_poly':
lay = layer_of(n)
if not lay: continue
pts = first(n, 'pts')
xy = [(num(p[1]), -num(p[2])) for p in find(pts, 'xy')]
st = first(n, 'stroke'); wd = num(first(st, 'width')[1]) if st else 0.1
# polygons go out as closed line loops: robust on any mechanical layer
for i in range(len(xy)):
a, b = xy[i], xy[(i + 1) % len(xy)]
out.append(f"LINE|{lay}|{a[0]:.4f}|{a[1]:.4f}|{b[0]:.4f}|{b[1]:.4f}|{wd:.3f}")
stats['lines'] += 1
stats['polys'] += 1; stats['layers'].add(lay)
elif k == 'fp_rect':
lay = layer_of(n)
if not lay: continue
s, e = first(n, 'start'), first(n, 'end')
x1, y1, x2, y2 = num(s[1]), -num(s[2]), num(e[1]), -num(e[2])
st = first(n, 'stroke'); wd = num(first(st, 'width')[1]) if st else 0.1
for a, b in (((x1, y1), (x2, y1)), ((x2, y1), (x2, y2)), ((x2, y2), (x1, y2)), ((x1, y2), (x1, y1))):
out.append(f"LINE|{lay}|{a[0]:.4f}|{a[1]:.4f}|{b[0]:.4f}|{b[1]:.4f}|{wd:.3f}")
stats['lines'] += 1
stats['layers'].add(lay)
stats['layers'] = sorted(stats['layers'])
return "\n".join(out) + "\n", stats
if __name__ == '__main__':
src, dst = sys.argv[1], sys.argv[2]
name = sys.argv[3] if len(sys.argv) > 3 else None
desc = sys.argv[4] if len(sys.argv) > 4 else ""
text, stats = convert(src, name, desc)
open(dst, 'w').write(text)
print(json.dumps({"out": dst, **stats}))