AnasSarkiz/NEMA14_RP2040
A NEMA motor assembly concept with a controller board, four proposed M3 screws and spacers, motor lead splices, and a JST 4-pin connector harness.
- Version
- 1.13.0
- License
- ISC
- Stars
- 0
scripts/merge-feedback-placement.py
"""Merge fresh native geometry, rerouting only copper affected by moved lands.
Courtyard space beneath component bodies is not an electrical copper obstacle.
Native pads, all source pins and original net identities remain authoritative.
"""
import json,sys
from shapely.geometry import Point,LineString
import verify_supplier_connectivity as g
fresh=json.load(open('artifacts/final-source.circuit.json'));names=set(sys.argv[1:]);assert not [e for e in fresh if e['type'].endswith('_error')],'Fresh source has errors'
fs={e['source_port_id']:e for e in fresh if e['type']=='source_port'}
assert fs.keys()==g.sp.keys()
for id,p in g.sp.items():
for k in ['pin_number','name','source_component_id','subcircuit_connectivity_map_key']:assert p.get(k)==fs[id].get(k),(id,k)
def pads(data):
ss={e['source_component_id']:e['name'] for e in data if e['type']=='source_component'};cc={e['pcb_component_id']:e for e in data if e['type']=='pcb_component'}
return [e for e in data if e['type'] in ['pcb_smtpad','pcb_plated_hole'] and ss[cc[e['pcb_component_id']]['source_component_id']] in names]
old,new=pads(g.j),pads(fresh);changed_ports={e['pcb_port_id'] for e in old};kept=[];removed=[]
def layers(e):return e.get('layers',[e.get('layer','top')])
for e in g.j:
typ=e['type']
if typ not in ['pcb_trace','pcb_via']:continue
hit=False
if typ=='pcb_trace':
if set(e.get('pcb_port_ids',[]))&changed_ports:hit=True
for a,b in zip(e['route'],e['route'][1:]):
if hit:break
if a['route_type']!=b['route_type'] or a['route_type']!='wire' or a['layer']!=b['layer']:continue
geom=LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2)
if any(a['layer'] in layers(p) and g.key(e)==g.key(p) and geom.intersects(g.geometry(p)) for p in old):hit=True
if any(a['layer'] in layers(p) and g.key(e)!=g.key(p) and geom.distance(g.geometry(p))<.153 for p in new):hit=True
elif typ=='pcb_via':
geom=Point(e['x'],e['y']).buffer(e['outer_diameter']/2)
if any(geom.distance(g.geometry(p))<(.003 if g.key(e)==g.key(p) else .153) for p in new):hit=True
# Enlarged fastener copper envelopes also invalidate legacy traces/vias.
if not hit:
for keep in [q for q in fresh if q['type']=='pcb_keepout']:
center=keep['center'];region=Point(center['x'],center['y']).buffer(keep['radius']+.153,quad_segs=128)
if typ=='pcb_via':hit=Point(e['x'],e['y']).buffer(e['outer_diameter']/2).intersects(region)
else:hit=any(a['route_type']==b['route_type']=='wire' and a['layer']==b['layer'] and a['layer'] in keep['layers'] and LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2).intersects(region) for a,b in zip(e['route'],e['route'][1:]))
if hit:break
if hit:removed.append({'id':e.get('pcb_trace_id',e.get('pcb_via_id')),'net':g.nets.get(g.key(e))})
else:kept.append(e)
board=[{**e,'is_via_in_pad_allowed':True} if e['type']=='pcb_board' else e for e in fresh if e['type'] not in ['pcb_trace','pcb_via','pcb_copper_pour'] and not e['type'].endswith(('_error','_warning'))]+kept
g.path.write_text(json.dumps(board,indent=2)+'\n');out=g.ROOT/'artifacts/reference-feedback/selective-placement-merges.json';hist=json.loads(out.read_text()) if out.exists() else [];hist.append({'changed_components':sorted(names),'removed_actual_affected_copper':removed,'unaffected_copper_retained':len(kept),'source_pin_roles_preserved':True});out.write_text(json.dumps(hist,indent=2)+'\n');print('Selective native geometry merge',sorted(names),'rerouting',removed)