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/export-freerouting.py

from freerouting_geometry import *
via_stacks={'VIA':{'outer_diameter':plan['expectedInput'].get('minViaPadDiameter',.45),'hole_diameter':plan['expectedInput'].get('minViaHoleDiameter',.2)}}
structure=[];images=[];stacks=[];places=[];wires=[];pins={};portmap={};keepouts=[];netmap={}
board=next(e for e in j if e['type']=='pcb_board');border=Polygon([(p['x'],p['y']) for p in board['outline']]).buffer(-.1)
structure.append('(boundary (path pcb 0 '+' '.join(xy(x,y) for x,y in border.exterior.coords)+'))')
# Preserve fixed Kelvin leads as copper obstacles while routing only the load terminals.
sense=[e for e in j if e['type']=='pcb_trace' and re.fullmatch(r'CURRENT_[AB]_SENSE_(POS|NEG)',st.get(e.get('source_trace_id'),{}).get('name',''))]
sense_ids={e['pcb_trace_id'] for e in sense};sense_input_ports={p['pcb_port_id'] for p in pp.values() if sc.get(sp[p['source_port_id']]['source_component_id'],{}).get('name','').startswith('U_CURRENT_') and sp[p['source_port_id']]['pin_number'] in [1,8]}
shunts=[e for e in j 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_')]
from shapely.ops import unary_union
shuntgeo=unary_union([geom(e).buffer(.03) for e in shunts])
for e in sense:
 for a,b,l,w in trace_segments(e['route']):
  g=LineString([a,b]).buffer(w/2+.005,quad_segs=8).difference(shuntgeo)
  for poly in ([g] if g.geom_type=='Polygon' else g.geoms):
   if not poly.is_empty:keepouts.append(f'(keepout KELVIN_{len(keepouts)} {polygon(poly,l)})')
for e in j:
 typ=e['type']
 if typ in ['pcb_smtpad','pcb_plated_hole']:
  n=enet(e);pid=e[typ+'_id'];g=geom(e,'outer_' if typ=='pcb_plated_hole' else '');g=box(*g.bounds) # Conservative rectangular routing envelope avoids sliver cells around sampled curves
  ls=layers if typ=='pcb_plated_hole' else e.get('layers',[e.get('layer')]);comp='P'+str(len(images));padstack='S'+str(len(stacks));pport=e.get('pcb_port_id')
  if pport in sense_input_ports:
   keepouts.append(f'(keepout SENSEPIN_{pid} {polygon(g,ls[0])})');continue
  stacks.append(f'(padstack {padstack} '+''.join('(shape '+polygon(translate(g,-e['x'],-e['y']),l)+')' for l in ls)+' (attach off))')
  images.append(f'(image {comp} (pin {padstack} 1 0 0))');places.append(f'(component {comp} (place {comp} {xy(e['x'],e['y'])} front 0))');pins.setdefault(n,[]).append(comp+'-1')
  portmap[comp]={'element':pid,'pcb_port_id':pport,'net':n};netmap.setdefault(n,[]).append(pid)
  if typ=='pcb_plated_hole':
   drill=geom(e,'hole_').buffer(.55) # .45 drill spacing + via radius .15 - copper radius .25
   for l in layers:keepouts.append(f'(via_keepout PH_{pid}_{l} {polygon(drill,l)})')
 elif typ=='pcb_hole':
  g=geom(e,'hole_' if 'hole_diameter' in e else '')
  for l in layers:keepouts.append(f'(keepout H_{e[typ+"_id"]}_{l} {polygon(g.buffer(.2),l)})')
 elif typ=='pcb_keepout':
  g=Point(e['center']['x'],e['center']['y']).buffer(e['radius']) if e['shape']=='circle' else box(e['center']['x']-e['width']/2,e['center']['y']-e['height']/2,e['center']['x']+e['width']/2,e['center']['y']+e['height']/2)
  g=box(*g.bounds) # Match tscircuit's conservative keepout checker.
  for l in e['layers']:keepouts.append(f'(keepout MOUNT_{len(keepouts)} {polygon(g,l)})')
 elif typ=='pcb_trace' and e['pcb_trace_id'] not in sense_ids:
  n=enet(e)
  for a,b,l,w in trace_segments(e['route']):
   if math.dist(a,b)>1e-7:
    protected=e['pcb_trace_id'].startswith(('pcb_trace_','retained_','ground_stitch_','bridge_ground_')) or e['pcb_trace_id']=='dual_source_trace_294_0'
    endpoints=[pp[pid] for pid in e.get('connectsTo',[]) if pid in pp]
    protected |= any(math.dist(a,(p['x'],p['y']))<1.5 and math.dist(b,(p['x'],p['y']))<1.5 for p in endpoints)
    kind='protect' if '--editable' not in sys.argv or protected else 'normal'
    wires.append(f'(wire (path {l} {u(w)} {xy(*a)} {xy(*b)}) (net {n}) (type {kind}))')
 elif typ=='pcb_via':
  n=enet(e);definition={k:e[k] for k in ['outer_diameter','hole_diameter']}
  vname=next((name for name,v in via_stacks.items() if v==definition),None)
  if vname is None:vname=f'FIX{len(via_stacks)}';via_stacks[vname]=definition
  kind='normal' if '--editable' in sys.argv and e['pcb_via_id'].startswith('edit_via_') else 'protect'
  wires.append(f'(via {vname} {xy(e["x"],e["y"])} (net {n}) (type {kind}))')
for name,v in via_stacks.items():stacks.append(f'(padstack {name} '+''.join(f'(shape (circle {l} {u(v["outer_diameter"])}))' for l in layers)+' (attach off))')

motor=set();wide={}
for c in plan['expectedInput']['connections']:
 ids=[p.get('pcb_port_id') for p in c['pointsToConnect'] if p.get('pcb_port_id') in pp]
 if ids:wide[enet(pp[ids[0]])]=max(wide.get(enet(pp[ids[0]]),.15),c.get('width',c.get('nominalTraceWidth',.15)))
for t in st.values():
 if t.get('name','').startswith('M2_OUTPUT_') or t.get('name') in ['MOTOR_A_PLUS','MOTOR_A_MINUS','MOTOR_B_PLUS','MOTOR_B_MINUS','CURRENT_A_WINDING','CURRENT_B_WINDING']:motor.add(root(t['connected_source_port_ids'][0]))
classes=[]
for n in pins:
 width=.4 if n in motor else (.4 if wide.get(n,0)>=.8 and n!=root(next(e['source_net_id'] for e in j if e['type']=='source_net' and e['name']=='GND')) else .15)
 gnd=root(next(e['source_net_id'] for e in j if e['type']=='source_net' and e['name']=='GND'))
 use_layers=' (use_layer top bottom)' if n!=gnd and (n in motor or wide.get(n,0)>=.8) else ''
 classes.append(f'(class C{len(classes)} {n} (circuit (use_via VIA){use_layers}) (rule (width {u(width)})))')
wires=list(dict.fromkeys(wires))
text='(pcb ina240\n(parser (string_quote ") (space_in_quoted_tokens on) (host_cad tscircuit))\n(resolution um 10) (unit um)\n(structure\n'+''.join(f'(layer {l} (type signal) (property (index {i})))' for i,l in enumerate(layers))+'\n'+'\n'.join(structure+keepouts)+'\n(via VIA) (rule (width 150) (clearance 110) (clearance 140 (type via_smd)) (clearance 125 (type via_wire)) (clearance 135 (type via_via)) (clearance 100 (type smd_smd)))\n)\n(placement '+'\n'.join(places)+')\n(library '+'\n'.join(images+stacks)+')\n(network '+'\n'.join(f'(net {n} (pins '+ ' '.join(p)+'))' for n,p in pins.items())+'\n'+'\n'.join(classes)+')\n(wiring '+'\n'.join(wires)+')\n)\n'
Path('work/freerouting/board.dsn').write_text(text);Path('work/freerouting/mapping.json').write_text(json.dumps({'portmap':portmap,'netmap':netmap,'motor':list(motor),'viaStacks':via_stacks},indent=2))
print('Exported',len(images),'pins,',len(pins),'nets,',len(wires),'fixed wire/via objects,',len(keepouts),'keepouts; classes',len(classes))