app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
Publish 0.1.26
0b71841
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
#!/usr/bin/env python3
"""KiCad mask obstacles, including complete custom-pad primitives, in board mm.
Does not substitute a custom pad's anchor for its copper/mask polygon.
Unsupported custom primitives fail closed. Caller supplies body/via obstacles.
"""
import json,re,math,argparse
from pathlib import Path
from shapely.geometry import Polygon,Point,LineString,box,mapping
from shapely.affinity import rotate,translate
from shapely.ops import unary_union
def parse(text):
ts=iter(re.findall(r'"(?:\\.|[^"\\])*"|[^\s()]+|[()]',text))
def rec():
a=[]
for t in ts:
if t==')':return a
a.append(rec() if t=='(' else json.loads(t) if t.startswith('"') else t)
return a
return rec()[0]
def sub(n,k):return [x for x in n if isinstance(x,list) and x and x[0]==k]
def one(n,k):return next(iter(sub(n,k)),[k])[1:]
def xy(n,k):return tuple(map(float,one(n,k)[:2]))
def primitive(n):
kind=n[0];width=float((one(n,'width') or [0])[0]);fill=one(n,'fill')==['yes']
if kind=='gr_poly':
p=Polygon([tuple(map(float,v[1:3])) for v in one(n,'pts') if v[0]=='xy']);g=p if fill else p.exterior
elif kind=='gr_rect':
a,b=xy(n,'start'),xy(n,'end');p=box(min(a[0],b[0]),min(a[1],b[1]),max(a[0],b[0]),max(a[1],b[1]));g=p if fill else p.exterior
elif kind=='gr_line':g=LineString([xy(n,'start'),xy(n,'end')])
elif kind=='gr_circle':
center=xy(n,'center');p=Point(center).buffer(math.dist(center,xy(n,'end')),quad_segs=24);g=p if fill else p.exterior
else:raise ValueError('Unsupported custom-pad primitive: '+kind)
return g.buffer(width/2) if width else g
def pad_shape(p):
w,h=map(float,one(p,'size'));kind=p[3];rect=box(-w/2,-h/2,w/2,h/2)
if kind=='custom':
primitives=one(p,'primitives')
if not primitives:raise ValueError('Custom pad without primitives')
anchor=one(one(p,'options'),'anchor');base=Point(0,0).buffer(min(w,h)/2) if anchor==['circle'] else rect
return unary_union([base]+[primitive(n) for n in primitives])
if kind=='rect':return rect
if kind=='circle':return Point(0,0).buffer(w/2,quad_segs=24)
if kind=='oval':return LineString([(-(w-h)/2,0),((w-h)/2,0)]).buffer(h/2) if w>=h else LineString([(0,-(h-w)/2),(0,(h-w)/2)]).buffer(w/2)
if kind=='roundrect':
r=min(w,h)*float((one(p,'roundrect_rratio') or [0])[0]);return box(-w/2+r,-h/2+r,w/2-r,h/2-r).buffer(r) if r else rect
raise ValueError('Unsupported mask pad shape: '+kind)
def obstacles(root,clearance=0.0):
result=[];setup=one(root,'setup');globalmargin=float((one(setup,'pad_to_mask_clearance') or [0])[0])
for f in sub(root,'footprint'):
ref=next((x[2] for x in sub(f,'property') if x[1]=='Reference'),'?');at=list(map(float,one(f,'at')));fa=at[2] if len(at)>2 else 0;fm=float((one(f,'solder_mask_margin') or [globalmargin])[0])
for idx,p in enumerate(sub(f,'pad')):
layers=one(p,'layers');sides=[s for s in ['F','B'] if s+'.Mask' in layers or '*.Mask' in layers]
if not sides:continue
pa=list(map(float,one(p,'at')));angle=pa[2] if len(pa)>2 else fa;center=rotate(Point(pa[:2]),-fa,origin=(0,0));margin=float((one(p,'solder_mask_margin') or [fm])[0]);g=pad_shape(p).buffer(margin+clearance)
if g.is_empty:continue
g=translate(rotate(g,-angle,origin=(0,0)),at[0]+center.x,at[1]+center.y)
for side in sides:result.append(dict(id=f'{ref}:pad:{p[1]}:{idx}',reference=ref,pad=p[1],kind='pad-mask-or-hole',layer=side+'.SilkS',polygon=mapping(g),box=list(g.bounds),sourceShape=p[3],maskMargin=margin,silkClearance=clearance))
return result
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--board',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=obstacles(parse(Path(a.board).read_text()));Path(a.out).write_text(json.dumps(r));print(len(r),'native mask obstacles; custom pads',sum(o['sourceShape']=='custom' for o in r))