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
"""Prefer board-edge side; optionally reserve a printed contact/name/value border.
Default contactBorders=true. The four border stroke rectangles participate in the
same collision solve as text. Full enclosure must contain no unrelated obstacles.
"""
import json,argparse,math
from pathlib import Path
from shapely.geometry import shape,Point,box,LineString
from shapely.ops import nearest_points,unary_union
from shapely.prepared import prep
def apply_policy(spec,geometry,enabled=True):
outline=shape(geometry['outline']);stats={};bands={}
# Shared rails and row heights are design constraints, not independent box fits.
rows={}
for g in spec['labels']:
if g.get('orderGroup') and g['id'].startswith('MC') and g.get('candidates'):
side=g['candidates'][0]['children'][0]['layer'];rows.setdefault((side,g['orderGroup']),[]).append(g)
for (side,key),members in rows.items():
if len(members)<3:continue
xs=[g['anchor'][0] for g in members]
if max(xs)-min(xs)>.1:continue
ys=sorted(g['anchor'][1] for g in members);pitch=min(b-a for a,b in zip(ys,ys[1:]) if b-a>.1)
targets=[shape(geometry['bodies'][side][g['id']]).bounds for g in members]
if sum(xs)/len(xs)<outline.centroid.x:
left=outline.bounds[0]+.12;right=max(b[2] for b in targets)+.10
else:
left=min(b[0] for b in targets)-.10;right=outline.bounds[2]-.12
for g in members:bands[g['id']]=(left,right,pitch-.20,key)
for group in spec['labels']:
ref=group['id'];iscontact=ref.startswith(('MC','MP'))
if not iscontact:continue
anchor=Point(group['anchor']);edge=nearest_points(anchor,outline.exterior)[1];vx,vy=edge.x-anchor.x,edge.y-anchor.y
side=group['candidates'][0]['children'][0]['layer'] if group['candidates'] else geometry.get('layer','F.SilkS')
obs=[o for o in geometry['obstacles'] if o['layer']==side];target=shape(geometry['bodies'][side][ref]);fixed=prep(unary_union([shape(o['polygon']) for o in obs]));unrelated=prep(unary_union([shape(o['polygon']) for o in obs if o['reference']!=ref and o['kind'] not in ['native-silk-graphic','via-hole-and-mask']]))
mask=unary_union([shape(o['polygon']).buffer(.04) for o in obs])
kept=[];counts={'outside':0,'inside':0,'borderRejected':0}
for c in group['candidates']:
# Large primary penalty makes outside preferred ahead of font, within coverage.
direction=(c['position'][0]-anchor.x)*vx+(c['position'][1]-anchor.y)*vy
placementSide='outside' if direction>0 else 'inside';c['contactSide']=placementSide;c['qualityPenalty']=c.get('qualityPenalty',0)+(0 if placementSide=='outside' else 200)
if enabled:
bb=unary_union([target]+[box(*ch['box']) for ch in c['children']]).bounds
region=box(bb[0]-.10,bb[1]-.10,bb[2]+.10,bb[3]+.10)
if ref in bands and placementSide=='outside':
left,right,height,key=bands[ref];shared=box(left,anchor.y-height/2,right,anchor.y+height/2)
if not shared.buffer(-.06).covers(unary_union([target]+[box(*ch['box']) for ch in c['children']])):counts['borderRejected']+=1;continue
region=shared;c['alignmentGroup']=key;c['sharedBorderBounds']=list(region.bounds)
ring=region.exterior;stroke=ring.buffer(.03,join_style=2)
if not outline.contains(stroke) or unrelated.intersects(region):counts['borderRejected']+=1;continue
# Keep a visible grouping frame; make explicit safety gaps at nearby holes/artwork.
safe=ring.difference(mask);segments=[safe] if safe.geom_type=='LineString' else list(safe.geoms) if safe.geom_type=='MultiLineString' else []
segments=[line for line in segments if line.length>=.15]
if not segments or sum(line.length for line in segments)<ring.length*.70:counts['borderRejected']+=1;continue
points=list(ring.coords);paths=[list(line.coords) for line in segments]
c['children'][0]['associationSegments']=paths;c['children'][0]['contactBorder']=True;c['contactBorder']=paths;c['borderStyle']='closed' if len(segments)==1 and segments[0].is_ring else 'interrupted-for-clearance'
c['boxes'] += [list(LineString([a,b]).buffer(.03,cap_style=2).bounds) for line in paths for a,b in zip(line,line[1:])]
c['box']=list(stroke.bounds)
c['contactBorders']=enabled;kept.append(c);counts[placementSide]+=1
group['candidates']=kept;stats[ref]=counts
spec['contactBorders']=enabled;spec['contactPolicy']='nearest board edge first, coherent ordered rows, shared column border rails and row height, inside fallback';spec['contactPolicyStats']=stats
return spec
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--geometry',required=True);p.add_argument('--out',required=True);p.add_argument('--no-contact-borders',action='store_true');a=p.parse_args();r=apply_policy(json.loads(Path(a.input).read_text()),json.loads(Path(a.geometry).read_text()),not a.no_contact_borders);Path(a.out).write_text(json.dumps(r));print(json.dumps(r['contactPolicyStats']))