seveibar/pd-dual-stepper-controller
Bottom-side PCB silkscreen artwork consisting of embedded mirrored TS Circuit SVG logos, with via-clearance-aware lower-board placement.
- Version
- 0.1.0
- License
- unset
- Stars
- 0
scripts/widen-power-routes.py
"""Widen power nets sequentially, preserving at least 0.3 mm short outer necks.
Each completed net becomes an obstacle before widening the next one.
"""
from freerouting_geometry import *
from shapely.ops import unary_union
folder=Path('work/repair');plan=json.loads((folder/'route-plan.json').read_text());base=json.load(open('dist/dual/capture.json'))
def netof(t):return enet(pp[t['connectsTo'][0]]) if t.get('connectsTo') else enet(t)
targets={}
for e in base:
if e['type']=='source_trace' and e.get('min_trace_thickness',0)>=.8:
n=root(e['connected_source_port_ids'][0]);targets[n]=max(targets.get(n,0),e['min_trace_thickness'])
gnd=root(next(e['source_net_id'] for e in base if e['type']=='source_net' and e['name']=='GND'));targets.pop(gnd,None)
pads=[];fixed=[]
for e in base:
if e['type'] in ['pcb_smtpad','pcb_plated_hole']:
for l in e.get('layers',[e.get('layer')]):pads.append((enet(e),l,geom(e,'outer_' if e['type']=='pcb_plated_hole' else '')))
elif e['type']=='pcb_via':
for l in layers:fixed.append((enet(e),l,Point(e['x'],e['y']).buffer(e['outer_diameter']/2)))
for t in [e for e in base if e['type']=='pcb_trace']:
for a,b,l,w in trace_segments(t['route']):fixed.append((enet(t),l,LineString([a,b]).buffer(w/2)))
kelvin=[]
shunt=unary_union([geom(e) for e in base if e['type']=='pcb_smtpad' and e.get('pcb_port_id') in pp and sc.get(sp[pp[e['pcb_port_id']]['source_port_id']]['source_component_id'],{}).get('name','').startswith('R_PHASE_')])
for t in base:
if t['type']=='pcb_trace' and re.fullmatch(r'CURRENT_[AB]_SENSE_(POS|NEG)',st.get(t.get('source_trace_id'),{}).get('name','')):
for a,b,l,w in trace_segments(t['route']):kelvin.append((enet(t),l,LineString([a,b]).buffer(w/2).difference(shunt.buffer(.02))))
for e in base:
if e['type']=='pcb_smtpad' and e.get('pcb_port_id') in pp:
p=sp[pp[e['pcb_port_id']]['source_port_id']]
if sc.get(p['source_component_id'],{}).get('name','').startswith('U_CURRENT_') and p.get('pin_number') in [1,8]:kelvin.append((enet(e),e['layer'],geom(e)))
board=next(e for e in base if e['type']=='pcb_board');outline=Polygon([(p['x'],p['y']) for p in board['outline']])
mounts=unary_union([box(e['center']['x']-e['radius'],e['center']['y']-e['radius'],e['center']['x']+e['radius'],e['center']['y']+e['radius']) for e in base if e['type']=='pcb_keepout' and e.get('shape')=='circle'])
changed=0;necks=0;impossible=[]
for n,target in targets.items():
metal=list(fixed)
for t in plan['traces']:
other=netof(t)
if other==n:continue
for a,b,l,w in trace_segments(t['route']):metal.append((other,l,LineString([a,b]).buffer(w/2)))
for v in t['route']:
if v['route_type']=='via':
for l in layers:metal.append((other,l,Point(v['x'],v['y']).buffer(v.get('via_diameter',.45)/2)))
obs={l:(unary_union([g for net,ll,g in pads if net!=n and ll==l]),unary_union([g for net,ll,g in metal if net!=n and ll==l]+[g for net,ll,g in kelvin if net==n and ll==l])) for l in layers}
for t in plan['traces']:
if netof(t)!=n:continue
out=[];r=t['route']
for a,b in zip(r,r[1:]):
if a['route_type']!='wire' or b['route_type'] not in ['wire','via'] or (b['route_type']=='wire' and a['layer']!=b['layer']):out.append(a);continue
d=math.hypot(a['x']-b['x'],a['y']-b['y'])
if d<1e-8:out.append(a);continue
count=max(1,math.ceil(d/.25));pg,tg=obs[a['layer']]
for k in range(count):
pts=[(a['x']+(b['x']-a['x'])*i/count,a['y']+(b['y']-a['y'])*i/count) for i in [k,k+1]];g=LineString(pts)
available=2*min(g.distance(pg)-.155,g.distance(tg)-.11,g.distance(outline.boundary)-.21,g.distance(mounts)-.11)
minimum=min(a['width'],.3)
if available<minimum-1e-6:impossible.append({'trace':t['pcb_trace_id'],'segment':pts,'availableWidth':available,'minimum':minimum})
w=max(minimum,min(target,math.floor(available*100)/100))
if w>a['width']+1e-8:changed+=1
if w<a['width']-1e-8:necks+=1
out.extend({'route_type':'wire','x':x,'y':y,'layer':a['layer'],'width':w} for x,y in pts)
out.append(r[-1]);t['route']=out
Path('reports/power-width-adjustment.json').write_text(json.dumps({'expandedSegments':changed,'neckedSegments':necks,'unresolved':impossible},indent=2))
if impossible:raise RuntimeError('Power width clearance requires rerouting; see reports/power-width-adjustment.json')
(folder/'route-plan.json').write_text(json.dumps(plan));print('Expanded',changed,'segments;',necks,'short neck segments')