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/prepare-dual-routing.py
"""Preserve short unchanged MCU decoupling routes, then reroute the compact PCB.
No stale PCB IDs are trusted. Old source remains in the clone baseline commit.
"""
import json,copy
from pathlib import Path
old=json.load(open('reference/index/circuit.json'));new=json.load(open('dist/dual/capture.json'))
oldplan=json.load(open('routing/route-plan.json'));inp=json.load(open('dist/dual/routing-input.json'))
def indexes(j):
sc={e['source_component_id']:e['name'] for e in j if e['type']=='source_component'}
sp={e['source_port_id']:(sc.get(e.get('source_component_id')),e.get('pin_number',e['name'])) for e in j if e['type']=='source_port'}
pp={e['pcb_port_id']:(sp.get(e.get('source_port_id')),round(e['x'],6),round(e['y'],6),tuple(e['layers'])) for e in j if e['type']=='pcb_port'}
st={e['source_trace_id']:(e.get('name'),tuple(sorted(str(sp.get(p)) for p in e['connected_source_port_ids']))) for e in j if e['type']=='source_trace'}
sn={e['source_net_id']:e['name'] for e in j if e['type']=='source_net'}
return pp,st,sn
op,ot,on=indexes(old);np,nt,nn=indexes(new)
def match(a,b):
rev={str(v):k for k,v in b.items()};return {k:rev[str(v)] for k,v in a.items() if str(v) in rev}
pm,tm,nm=match(op,np),match(ot,nt),match(on,nn)
newports={e['pcb_port_id']:e for e in new if e['type']=='pcb_port'}
fixed=[]
critical={e['source_trace_id'] for e in new if e['type']=='source_trace' and e.get('max_length',99)<=1.5}
for t in oldplan['traces']:
ports=t.get('connectsTo',[])
if not ports or any(p not in pm for p in ports):continue
cn=next((tm.get(part) or nm.get(part) for part in t['connection_name'].split('__') if tm.get(part) or nm.get(part)),None)
if not cn or cn not in critical:continue
u=copy.deepcopy(t);u['pcb_trace_id']='retained_'+str(len(fixed));u['connectsTo']=[pm[p] for p in ports];u['connection_name']=cn
if cn.startswith('source_trace_'):u['source_trace_id']=cn
else:u['source_net_id']=cn
for pt in u['route']:
for field in ['start_pcb_port_id','end_pcb_port_id']:
if field in pt:pt[field]=pm.get(pt[field],pt[field])
fixed.append(u)
# Preserve the physical through-vias embedded in saved traces as explicit routing obstacles.
retained_vias=[]
seen=set()
for t in fixed:
for q in t['route']:
if q['route_type']!='via':continue
key=(round(q['x'],6),round(q['y'],6),t['connection_name'])
if key in seen:continue
seen.add(key)
v={'type':'pcb_via','pcb_via_id':f'retained_via_{len(retained_vias)}','x':q['x'],'y':q['y'],'layers':['top','inner1','inner2','bottom'],'from_layer':'top','to_layer':'bottom','outer_diameter':q.get('via_diameter',.5),'hole_diameter':q.get('via_hole_diameter',.3)}
v.update({k:t[k] for k in ['source_trace_id','source_net_id'] if k in t})
retained_vias.append(v)
# Existing source-defined paths remain in capture; fixed reference paths add only autorouted copper.
placement=[e for e in new if not e['type'].endswith('_error') and e['type']!='pcb_copper_pour']+fixed+retained_vias
plan={'expectedInput':inp,'traces':[]}
for c in inp['connections']:
points=c['pointsToConnect']
if len(points)<2:continue
for i,p in enumerate(points[1:]):
a=points[0];ids=[a.get('pcb_port_id'),p.get('pcb_port_id')]
if not all(ids):continue
plan['traces'].append({'type':'pcb_trace','pcb_trace_id':f"dual_{c['name']}_{i}",'connection_name':c['name'],'connectsTo':ids,'route':[{'route_type':'wire','x':q['x'],'y':q['y'],'layer':q.get('layer','top'),'width':c.get('width',.15)} for q in [a,p]]})
Path('work/freerouting').mkdir(parents=True,exist_ok=True);Path('dist/revision').mkdir(exist_ok=True)
Path('work/freerouting/placement.json').write_text(json.dumps(placement));Path('dist/revision/route-plan.json').write_text(json.dumps(plan))
print('Retained',len(fixed),'reference routes; routing',len(plan['traces']),'required connections')