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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#!/usr/bin/env python3
"""Optional bounded leader candidate routing for the rectangle planner.
Caller supplies per-label leaderTargets and routeBounds in board millimetres.
Routes are candidate geometry, requiring native clearance and visual review.
"""
import argparse,json,math,heapq
from pathlib import Path
def prepare(spec):
gap=spec.get('clearance',.15);step=spec.get('leaderGrid',.1);fixed=[o['box'] for o in spec['obstacles']]
def hit(a,b,g=0):return not(a[2]+g<=b[0] or b[2]+g<=a[0] or a[3]+g<=b[1] or b[3]+g<=a[1])
for group in spec['labels']:
if not group.get('leaderTargets'):continue
rb=group['routeBounds'];nx=round((rb[2]-rb[0])/step)+1;ny=round((rb[3]-rb[1])/step)+1
def point(n):return rb[0]+n[0]*step,rb[1]+n[1]*step
def cell(p):return round((p[0]-rb[0])/step),round((p[1]-rb[1])/step)
blocked=set()
def occupy(b):
x0=max(0,math.ceil((b[0]-gap-.03-rb[0])/step));x1=min(nx-1,math.floor((b[2]+gap+.03-rb[0])/step));y0=max(0,math.ceil((b[1]-gap-.03-rb[1])/step));y1=min(ny-1,math.floor((b[3]+gap+.03-rb[1])/step))
return {(x,y) for x in range(x0,x1+1) for y in range(y0,y1+1)}
for b in fixed:blocked.update(occupy(b))
target_list=[cell(p) for p in group['leaderTargets']];target_list=[n for n in target_list if n not in blocked];targets=set(target_list[:1]);accepted=[]
if not targets:group['candidates']=[];continue
candidates=[c for c in group['candidates'] if not any(hit(c['box'],b,gap) for b in fixed)]
candidates.sort(key=lambda c:-(c['font']*spec.get('fontWeight',70)-math.dist(c['position'],group['anchor'])*spec.get('distanceWeight',7)))
bands={}
for c in candidates:bands.setdefault(c['font'],[]).append(c)
candidates=[c for band in bands.values() for c in band[:spec.get('leaderAttemptsPerFont',180)]]
for c in candidates:
if sum(q['font']==c['font'] for q in accepted)>=spec.get('leaderCandidatesPerFont',40):continue
if c.get('angle',0)==0 and abs(c['position'][1]-group['anchor'][1])<=.3 and math.dist(c['position'],group['anchor'])<3.5:accepted.append(c);continue
b=c['box'];occupied=blocked|occupy(b)
def free(n):return 0<=n[0]<nx and 0<=n[1]<ny and n not in occupied
starts=set();pad=gap+.08
for x in range(math.floor((b[0]-pad-rb[0])/step),math.ceil((b[2]+pad-rb[0])/step)+1):starts.update([(x,cell((0,b[1]-pad))[1]),(x,cell((0,b[3]+pad))[1])])
for y in range(math.floor((b[1]-pad-rb[1])/step),math.ceil((b[3]+pad-rb[1])/step)+1):starts.update([(cell((b[0]-pad,0))[0],y),(cell((b[2]+pad,0))[0],y)])
starts={n for n in starts if free(n)};ends={n for n in targets if free(n)}
if not ends or not starts:continue
def h(n):return min(math.dist(n,z) for z in ends)
costs={n:0 for n in starts};prev={n:None for n in starts};heap=[(h(n),0,n) for n in starts];heapq.heapify(heap);found=None
while heap:
_,g,n=heapq.heappop(heap)
if g!=costs[n]:continue
if n in ends:found=n;break
for dx,dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]:
z=(n[0]+dx,n[1]+dy)
if not free(z) or (dx and dy and (not free((n[0]+dx,n[1])) or not free((n[0],n[1]+dy)))):continue
ng=g+math.hypot(dx,dy)
if ng<costs.get(z,1e99):costs[z]=ng;prev[z]=n;heapq.heappush(heap,(ng+h(z),ng,z))
if found is None:continue
path=[]
while found is not None:path.append(point(found));found=prev[found]
path.reverse()
if len(path)<2:accepted.append(c);continue
# Keep only changes in direction. Native EDA may round corners inside this envelope.
reduced=[path[0]]
for a,q,z in zip(path,path[1:],path[2:]):
if abs((q[0]-a[0])*(z[1]-q[1])-(q[1]-a[1])*(z[0]-q[0]))>1e-8:reduced.append(q)
reduced.append(path[-1]);rects=[b]+[[min(a[0],z[0])-.025,min(a[1],z[1])-.025,max(a[0],z[0])+.025,max(a[1],z[1])+.025] for a,z in zip(reduced,reduced[1:])]
if any(hit(a,z,gap) for a in rects[1:] for z in fixed):continue
accepted.append(dict(c,boxes=rects,leaderPoints=reduced))
group['candidates']=accepted;print(group['id'],len(accepted),'routable candidates',flush=True)
return spec
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();s=prepare(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(s,indent=2))