seveibar/am3352-dev-board

A 324-ball TI AM3352 ARM processor with DDR3, LCD, USB, Ethernet, storage, serial, analog, clock, reset, power, and JTAG interfaces, paired with configurable JST-GH board connectors and constrained PCB routing.

Version
1.0.2
License
unset
Stars
0

scripts/clean-route-bends.py

"""Simplify saved centerlines with clearance-checked straight/45-degree shortcuts.
Keeps route junctions and via contacts; synchronous DDR path lengths are preserved.
"""
import json,runpy,math,os,collections
from pathlib import Path
from shapely.geometry import Point,LineString,box
from shapely.ops import unary_union
x=runpy.run_path('scripts/audit-copper.py');d=json.load(open('design/routed-copper.json'));names={e['source_net_id']:e['name'] for e in x['c'] if e['type']=='source_net'}
fixed={l:[] for l in x['layers']};planes={}
for g,m in zip(x['shapes'],x['meta']):
 if m[2].startswith('pcb_copper_pour'):planes.setdefault((m[1],m[0]),[]).append(g)
 elif not m[2].startswith(('fr_','pcb_via')):fixed[m[0]].append((g,m[1]))
planes={k:unary_union(v).buffer(.00001) for k,v in planes.items()}
def pos(p):return (p['x'],p['y'])
def geometry(rs):
 out={l:[] for l in x['layers']}
 for r in rs:
  for p in r:
   if p['route_type']=='via':
    for l in out:out[l].append(Point(*pos(p)).buffer(p.get('via_diameter',.3)/2))
  for a,b in zip(r,r[1:]):
   if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer'] and pos(a)!=pos(b):out[a['layer']].append(LineString([pos(a),pos(b)]).buffer(a.get('width',.1)/2))
 return out
geoms={s:geometry(rs) for s,rs in d['nets'].items()};changes=[]
power={'GND','V1V8','V3V3','VDD_CORE','VDD_MPU','DDR_1V5','A3V3','RTC_1V8'}
polish=os.environ.get('POLISH_POWER_ANGLES')=='1'
for sid,rs in d['nets'].items():
 if polish and names.get(sid) not in power:continue
 name=names.get(sid,sid);net=x['find'](sid);timed=name.startswith(('DDR_','USB0_')) and name not in power;removed=0;shortened=0
 obs={l:unary_union([g for s,gs in geoms.items() if s!=sid for g in gs[l]]+[g for g,n in fixed[l] if n!=net]) for l in fixed}
 protect={pos(p) for r in rs for p in r if p['route_type']=='via'}|{pos(r[0]) for r in rs}|{pos(r[-1]) for r in rs}|{(p['x'],p['y']) for p in x['c'] if p['type']=='pcb_port'}
 for ri,r in enumerate(rs):
  if len(r)<(2 if polish else 3) or any(p['route_type']=='via' for p in r):continue
  layer=r[0]['layer'];coords=[pos(p) for p in r];width=r[0].get('width',.1);oldline=LineString(coords)
  # Preserve contacts at the interior of a segment, not only at existing vertices.
  interior=[Point(p) for p in protect if p!=coords[0] and p!=coords[-1] and oldline.distance(Point(p))<.000002]
  if any(all(p.distance(Point(v))>.000002 for v in coords) for p in interior):continue
  ownPlane=planes.get((net,layer));inPlane=ownPlane is not None and ownPlane.covers(oldline.buffer(width/2))
  others=unary_union([LineString([pos(p) for p in rr]) for j,rr in enumerate(rs) if j!=ri and len(rr)>1 and all(p['route_type']=='wire' and p['layer']==layer for p in rr)])
  i=0
  while i<len(coords)-(1 if polish else 2):
   last=next((j for j in range(i+1,len(coords)) if coords[j] in protect),len(coords)-1);chosen=None
   for j in range(last,i if polish else i+1,-1):
    a,b=coords[i],coords[j];dx=b[0]-a[0];dy=b[1]-a[1];sx=1 if dx>=0 else -1;sy=1 if dy>=0 else -1;diag=min(abs(dx),abs(dy));old=LineString(coords[i:j+1]).length
    candidates=[[a,b],[a,(a[0]+sx*diag,a[1]+sy*diag),b],[a,(b[0]-sx*diag,b[1]-sy*diag),b],[a,(a[0],b[1]),b],[a,(b[0],a[1]),b]]
    for cc in candidates:
     cc=[p for k,p in enumerate(cc) if k==0 or p!=cc[k-1]]
     angleFix=polish and j==i+1 and old>.5 and min(abs(dx),abs(dy),abs(abs(dx)-abs(dy)))>.02
     if len(cc)>=j-i+1 and not angleFix:continue
     if len(cc)<2:continue
     line=LineString(cc);delta=old-line.length
     if timed and abs(delta)>1e-6:continue
     if not timed and delta<(-old*.09 if angleFix else -.000001):continue
     if len(cc)==2 and min(abs(dx),abs(dy),abs(abs(dx)-abs(dy)))>1e-5:continue
     if line.distance(obs[layer])<width/2+.09999:continue
     if inPlane and not ownPlane.covers(line.buffer(width/2)):continue
     allowed=Point(*a).buffer(.16).union(Point(*b).buffer(.16))
     if line.difference(allowed).distance(others)<width-.00001:continue
     full=coords[:i]+cc+coords[j+1:]
     if not LineString(full).is_simple:continue
     chosen=(j,cc,delta);break
    if chosen:break
   if chosen:
    j,cc,delta=chosen;removed+=j-i+1-len(cc);shortened+=delta;coords=coords[:i]+cc+coords[j+1:]
   i+=1
  rs[ri]=[dict(route_type='wire',x=a,y=b,width=width,layer=layer) for a,b in coords]
 geoms[sid]=geometry(rs)
 Path('design/routed-copper.json').write_text(json.dumps(d,separators=(',',':'))+'\n')
 if removed or abs(shortened)>.00001:changes.append(dict(net=name,removedVertices=removed,shortenedMm=round(shortened,6),lengthPreserved=timed));print(name,removed,'bends removed',round(shortened,6),'mm shorter',flush=True)
d['planeBaseRouteCounts']={}
Path('design/routed-copper.json').write_text(json.dumps(d,separators=(',',':'))+'\n')
report={'changes':changes,'removedVertices':sum(v['removedVertices'] for v in changes),'shortenedMm':sum(v['shortenedMm'] for v in changes)}
if polish:report={'description':'Replace eligible arbitrary-angle power segments with 45-degree runs. This can add corners and length.','addedVertices':-report['removedVertices'],'addedLengthMm':-report['shortenedMm'],'changes':[{'net':v['net'],'addedVertices':-v['removedVertices'],'addedLengthMm':-v['shortenedMm']} for v in changes]}
Path('design/routing-angle-cleanup.json' if polish else 'design/routing-cleanup.json').write_text(json.dumps(report,indent=2)+'\n')