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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
"""Place semantic documentation blocks; never scatter or reorder their rows.
Native measured bounds are supplied by the EDA adapter. World coordinates/mm.
"""
import numpy as np
import shapely
from shapely.geometry import box
from shapely.ops import unary_union
def place_sections(sections, labels, bounds, outline, obstacle, occupied, side):
x0,y0,x1,y1=outline.bounds
gx,gy=np.meshgrid(np.arange(x0,x1,.35),np.arange(y0,y1,.35))
gx,gy=gx.ravel(),gy.ravel()
accepted=[]; unresolved=[]; reports=[]
for section in sections:
chosen=None
for font in section.get('fonts',[.6,.5,.4,.35,.3]):
rows=section['rows']; ncols=max(map(len,rows)); colwidth=[]
for col in range(ncols):
colwidth.append(max([bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[2]-bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[0] for row in rows for j,i in enumerate(row) if j==col]+[0])+.8)
raw=[]; y=0
for row in rows:
h=max(bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[3]-bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[1] for i in row)
for col,i in enumerate(row):
size=font*section.get('sizeFactors',{}).get(str(i),1); b=bounds(i,size,0)
# Bottom is viewed from below: align right in board coordinates,
# which becomes a shared left edge in the readable bottom view.
dx=sum(colwidth[:col])-(b[2] if side=='B.SilkS' else b[0]);dy=y-b[1]
raw.append((i,size,dx,dy,[b[0]+dx,b[1]+dy,b[2]+dx,b[3]+dy]))
y+=h+max(.28,font*.65)
envelope=unary_union([box(*r[4]) for r in raw]).bounds
valid=shapely.contains(outline,shapely.box(gx+envelope[0],gy+envelope[1],gx+envelope[2],gy+envelope[3]))
# Reserve the whole section against other labels, but permit holes
# between rows when the actual strokes have clearance.
valid &= ~shapely.intersects(occupied,shapely.box(gx+envelope[0]-.15,gy+envelope[1]-.15,gx+envelope[2]+.15,gy+envelope[3]+.15))
for _,_,_,_,b in raw:
indices=np.flatnonzero(valid)
if not len(indices):break
geoms=shapely.box(gx[indices]+b[0]-.045,gy[indices]+b[1]-.045,gx[indices]+b[2]+.045,gy[indices]+b[3]+.045)
valid[indices] &= ~shapely.intersects(obstacle,geoms)
indices=np.flatnonzero(valid)
if len(indices):
ax,ay=section['anchor']; n=indices[np.argmin((gx[indices]+(envelope[0]+envelope[2])/2-ax)**2+(gy[indices]+(envelope[1]+envelope[3])/2-ay)**2)]
chosen=[]
for i,size,dx,dy,b in raw:
l=labels[i];chosen.append(dict(index=i,text=l['text'],reference=None,layer=side,role=l['role'],section=section['id'],textAlign='left',x=float(gx[n]+dx),y=float(gy[n]+dy),size=size,angle=0,box=[float(b[0]+gx[n]),float(b[1]+gy[n]),float(b[2]+gx[n]),float(b[3]+gy[n])]))
occupied=unary_union([occupied,box(envelope[0]+gx[n]-.15,envelope[1]+gy[n]-.15,envelope[2]+gx[n]+.15,envelope[3]+gy[n]+.15)])
reports.append(dict(id=section['id'],font=font,indices=[p['index'] for p in chosen],status='placed'));break
if chosen:accepted+=chosen
else:
ids=[i for row in section['rows'] for i in row];unresolved+=ids;reports.append(dict(id=section['id'],status='unresolved',indices=ids))
return accepted,unresolved,occupied,reports
if __name__=='__main__':
import argparse,json
from pathlib import Path
from shapely.geometry import shape,GeometryCollection
p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args()
d=json.loads(Path(a.input).read_text());metrics=d['metrics']
def bounds(i,size,angle):
if angle:raise ValueError('Documentation sections require readable horizontal rows')
return [v*size for v in metrics[str(i)]]
out=[];missing=[];reports=[]
for side in ['F.SilkS','B.SilkS']:
obstacle=unary_union([shape(o['polygon']) for o in d['obstacles'] if o['layer']==side])
occupied=unary_union([box(*p['box']).buffer(.035) for p in d.get('placements',[]) if p['layer']==side])
ps,ms,_,rs=place_sections([s for s in d['sections'] if s['layer']==side],d['labels'],bounds,shape(d['outline']),obstacle,occupied,side)
out+=ps;missing+=ms;reports+=rs
Path(a.out).write_text(json.dumps(dict(placements=out,unresolved=missing,sections=reports),indent=2))
print(('ERROR' if missing else 'OK')+f': {len(out)} documentation labels in ordered sections; {len(missing)} unresolved')
raise SystemExit(2 if missing else 0)