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')