skill
EDA Skillpack
Public Made by Adomby adom
All skills for all EDA capabilities across the Adom ecosystem.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
"""Read a .kicad_pcb with the Python standard library: footprints and pads (absolute positions), tracks, arcs,
vias, zones with their filled polygons per layer, and the aux origin. No KiCad install needed.
Coordinates are returned in mm in KiCad's own frame (x right, y DOWN). `design(p)` converts a point to the
board's design frame (origin at the aux origin when one is set, y up), which is how the reports print positions.
"""
import math, re
_TOK = re.compile(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()]+')
def parse(text):
stack = [[]]
for t in _TOK.findall(text):
if t == '(':
stack.append([])
elif t == ')':
e = stack.pop(); stack[-1].append(e)
else:
stack[-1].append(t[1:-1] if t.startswith('"') else t)
return stack[0][0]
def kids(e, head):
return [c for c in e if isinstance(c, list) and c and c[0] == head]
def kid(e, head):
k = kids(e, head)
return k[0] if k else None
def num(v):
return float(v)
def rot(x, y, deg):
"""KiCad's rotation in its y-down frame: a positive angle turns counter-clockwise as seen on screen."""
a = math.radians(deg)
return x * math.cos(a) + y * math.sin(a), -x * math.sin(a) + y * math.cos(a)
class Board:
def __init__(self, path):
self.path = path
root = parse(open(path, encoding='utf-8').read())
setup = kid(root, 'setup')
ao = kid(setup, 'aux_axis_origin') if setup else None
self.origin = (num(ao[1]), num(ao[2])) if ao else (0.0, 0.0)
self.nets = {}
for n in kids(root, 'net'):
if len(n) >= 3: self.nets[n[1]] = n[2]
self.pads, self.footprints = [], {}
for f in kids(root, 'footprint'):
at = kid(f, 'at'); fx, fy = num(at[1]), num(at[2]); frot = num(at[3]) if len(at) > 3 else 0.0
ref = next((p[2] for p in kids(f, 'property') if p[1] == 'Reference'), None)
if ref is None:
fr = kid(f, 'fp_text'); ref = fr[2] if fr else '?'
self.footprints[ref] = {'at': (fx, fy), 'rot': frot, 'layer': kid(f, 'layer')[1]}
for p in kids(f, 'pad'):
pat = kid(p, 'at'); px, py = num(pat[1]), num(pat[2]); prot = num(pat[3]) if len(pat) > 3 else 0.0
dx, dy = rot(px, py, frot)
size = kid(p, 'size'); w, h = num(size[1]), num(size[2])
netk = kid(p, 'net'); net = (netk[2] if len(netk) > 2 else self.nets.get(netk[1], netk[1])) if netk else None
layers = kid(p, 'layers')[1:] if kid(p, 'layers') else []
self.pads.append({'ref': ref, 'num': p[1], 'kind': p[2], 'shape': p[3], 'x': fx + dx, 'y': fy + dy,
'w': w, 'h': h, 'rot': prot, 'net': net, 'layers': layers})
self.tracks, self.vias = [], []
for s in kids(root, 'segment'):
st, en = kid(s, 'start'), kid(s, 'end')
self.tracks.append({'a': (num(st[1]), num(st[2])), 'b': (num(en[1]), num(en[2])), 'w': num(kid(s, 'width')[1]),
'layer': kid(s, 'layer')[1], 'net': self._net(s), 'arc': False})
for s in kids(root, 'arc'):
st, md, en = kid(s, 'start'), kid(s, 'mid'), kid(s, 'end')
self.tracks.append({'a': (num(st[1]), num(st[2])), 'b': (num(en[1]), num(en[2])), 'mid': (num(md[1]), num(md[2])),
'w': num(kid(s, 'width')[1]), 'layer': kid(s, 'layer')[1], 'net': self._net(s), 'arc': True})
for v in kids(root, 'via'):
at = kid(v, 'at')
self.vias.append({'x': num(at[1]), 'y': num(at[2]), 'd': num(kid(v, 'size')[1]), 'net': self._net(v)})
self.zones = []
for z in kids(root, 'zone'):
net = self._net(z)
fills = []
for fp in kids(z, 'filled_polygon'):
lay = kid(fp, 'layer')[1]
pts = [(num(q[1]), num(q[2])) for q in kid(fp, 'pts') if isinstance(q, list) and q[0] == 'xy']
fills.append({'layer': lay, 'pts': pts})
lk = kid(z, 'layers') or kid(z, 'layer')
self.zones.append({'net': net, 'layers': list(lk[1:]) if lk else [], 'fills': fills})
def _net(self, e):
n = kid(e, 'net')
if not n: return None
return n[2] if len(n) > 2 else self.nets.get(n[1], n[1])
def pad(self, ref, number):
for p in self.pads:
if p['ref'] == ref and p['num'] == str(number): return p
raise KeyError(f'{ref}.{number}')
def pads_of(self, ref):
return [p for p in self.pads if p['ref'] == ref]
def design(self, p):
return (round(p[0] - self.origin[0], 3), round(self.origin[1] - p[1], 3))
def pad_polygon(p, grow=0.0):
"""The pad as a rotated rectangle (round/oval/roundrect pads are approximated by their bounding rectangle;
circles are kept as circles by callers that need it)."""
hw, hh = p['w'] / 2 + grow, p['h'] / 2 + grow
out = []
for cx, cy in ((-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)):
dx, dy = rot(cx, cy, p['rot'])
out.append((p['x'] + dx, p['y'] + dy))
return out
def inside(pt, poly):
x, y = pt; c = False
for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1]):
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
c = not c
return c
def area(poly):
return abs(sum(a[0] * b[1] - b[0] * a[1] for a, b in zip(poly, poly[1:] + poly[:1]))) / 2