main
John Lauer Publish 0.2.15 bb17f84
#!/usr/bin/env python3
"""Collision-checked visual associations for a selected silkscreen layout (mm).
Try an enclosure when requested, otherwise a curved pointer to the body's rim.
Never draw across text, pads, holes or another fitted body. Report failures.
"""
import json,argparse,math
from pathlib import Path
from shapely.geometry import shape,box,LineString,Point
from shapely.ops import unary_union,nearest_points
from shapely.prepared import prep

def associate(data):
 children=data['placements'];graphics=[];unresolved=[]
 for p in children:
  ref=p.get('reference');side=p['layer']
  if p.get('contactBorder'):
   for points in p['associationSegments']:graphics.append(dict(index=p['index'],reference=ref,layer=side,kind='outline',samples=points,contactBorder=True))
   continue
  if not ref or p.get('role')=='value':continue
  target=shape(data.get('targets',{})[str(p['index'])] if str(p['index']) in data.get('targets',{}) else data['bodies'].get(side,{}).get(ref,{'type':'GeometryCollection','geometries':[]}))
  if target.is_empty:continue
  target=target.buffer(data.get('targetMargin',.02))
  own=unary_union([box(*q['box']) for q in children if q['layer']==side and q.get('reference')==ref and (not p.get('pin') or q['index']==p['index'])]);distance=own.distance(target)
  blockers=[shape(o['polygon']) for o in data['obstacles'] if o['layer']==side]
  blockers += [box(*q['box']).buffer(.025) for q in children if q['layer']==side]
  blockers += [LineString(g['samples']).buffer(.06) for g in graphics if g['layer']==side]
  blocked=prep(unary_union(blockers))
  if data.get('style')=='enclosure' and not p.get('pin'):
   members=[box(*q['box']) for q in children if q['layer']==side and q.get('reference')==ref]
   enclosure=box(*unary_union([target]+members).bounds).buffer(.15,join_style=2).exterior
   if not blocked.intersects(enclosure.buffer(.025)):
    samples=list(enclosure.coords);p['associationOutline']=samples;graphics.append(dict(index=p['index'],reference=ref,layer=side,kind='outline',samples=samples));continue
  if distance<.20:continue
  a0,b0=nearest_points(own,target);a0=list(a0.coords)[0];b0=list(b0.coords)[0];found=None
  # Route from the label edge to just outside the target, never across its mask.
  a=tuple(a0[j]+(b0[j]-a0[j])*.10/distance for j in [0,1]);b=tuple(b0[j]-(b0[j]-a0[j])*.10/distance for j in [0,1]);dx,dy=b[0]-a[0],b[1]-a[1];length=math.hypot(dx,dy)
  if length<=.08:continue
  for bend in [.12,-.12,.3,-.3,.6,-.6,1,-1,0]:
   c1=(a[0]+dx/3-dy/length*bend,a[1]+dy/3+dx/length*bend);c2=(a[0]+dx*2/3-dy/length*bend,a[1]+dy*2/3+dx/length*bend)
   points=[a,c1,c2,b];samples=[tuple((1-t)**3*a[j]+3*(1-t)**2*t*c1[j]+3*(1-t)*t*t*c2[j]+t**3*b[j] for j in [0,1]) for t in [i/40 for i in range(41)]]
   if blocked.intersects(LineString(samples).buffer(.035)):continue
   found=dict(index=p['index'],reference=ref,layer=side,kind='curve',points=points,samples=samples);p['leaderCurve']=points;graphics.append(found);break
  if found is None:unresolved.append(dict(index=p['index'],reference=ref,layer=side,distance=distance,reason='No collision-free direct curved pointer; review enclosure or reroute'))
 return dict(placements=children,graphics=graphics,unresolvedAssociations=unresolved)
if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=associate(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(r,indent=2));print(len(r['graphics']),'pointers;',len(r['unresolvedAssociations']),'association reviews')