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/trim-same-side-stubs.py
"""Trim obsolete endpoint branches only when live physical-pad partitions remain identical."""
import json,math
from shapely.ops import unary_union
import verify_supplier_connectivity as g
errors=json.loads((g.ROOT/'artifacts/drc-report.json').read_text())['errors']
def signature(net):
elems,roots=g.groups(net);out={}
for e,r in zip(elems,roots):
if e.get('pcb_port_id'):out.setdefault(r,set()).add(e['pcb_port_id'])
return sorted(sorted(v) for v in out.values())
records=[]
for err in errors:
if err.get('pcb_trace_id','').startswith(('usb_','oscillator_','reviewed_local_critical_')):continue
if 'dangling endpoint' not in err.get('message',''):continue
e=next((e for e in g.j if e.get('pcb_trace_id')==err['pcb_trace_id']),None)
if e is None:continue
isstart=err['pcb_trace_error_id'].endswith('_start');route=e['route']
ordered=list(reversed(route)) if not isstart else list(route)
prefix=[]
for point in ordered:
if point['route_type']!='wire' or point['layer']!=ordered[0].get('layer'):break
prefix.append(point)
if len(prefix)<2:continue
tail=ordered[len(prefix):];ordered=prefix
net=g.key(e);layer=ordered[0]['layer'];geoms=[]
for other in g.j:
if other is e or g.key(other)!=net:continue
if other['type'] in ['pcb_smtpad','pcb_plated_hole'] and layer in other.get('layers',[other.get('layer','top')]):geoms.append(g.geometry(other))
elif other['type']=='pcb_via' and layer in other['layers']:
vgeom=g.Point(other['x'],other['y']).buffer(other['outer_diameter']/2)
live=any(o is not e and o['type']=='pcb_trace' and g.key(o)==net and any(a['route_type']==b['route_type']=='wire' and a['layer']==b['layer'] and g.LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(min(a['width'],b['width'])/2).intersects(vgeom) for a,b in zip(o['route'],o['route'][1:])) for o in g.j)
if live:geoms.append(vgeom)
# Only a via with another live conductor may anchor an obsolete branch.
elif other['type']=='pcb_trace':
for a,b in zip(other['route'],other['route'][1:]):
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']==layer:geoms.append(g.LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(min(a['width'],b['width'])/2))
path=g.LineString([(p['x'],p['y']) for p in ordered]);hit=path.intersection(unary_union(geoms).buffer(-.015));
if hit.is_empty:continue
pieces=list(hit.geoms) if hasattr(hit,'geoms') else [hit]
offsets=[path.project(g.Point(*p.coords[0])) for p in pieces if hasattr(p,'coords') and len(p.coords)]
offsets += [path.project(g.Point(*p.coords[-1])) for p in pieces if hasattr(p,'coords') and len(p.coords)]
if not offsets:continue
offset=min(offsets)+.015
if offset>=path.length-.01:continue
pt=path.interpolate(offset);new=[dict(ordered[0],x=pt.x,y=pt.y)];new[0].pop('start_pcb_port_id',None);new[0].pop('end_pcb_port_id',None)
dist=0
for a,b in zip(ordered,ordered[1:]):
dist+=math.hypot(b['x']-a['x'],b['y']-a['y'])
if dist>offset+1e-8:new.append(b)
new+=tail
candidate=new if isstart else list(reversed(new));before=signature(net);old=e['route'];e['route']=candidate
if signature(net)!=before:e['route']=old;continue
e['pcb_port_ids']=[p[k] for p in candidate for k in ['start_pcb_port_id','end_pcb_port_id'] if k in p]
records.append({'trace_id':e['pcb_trace_id'],'endpoint':'start' if isstart else 'end','removed_length_mm':offset,'new_endpoint_mm':[pt.x,pt.y],'live_pad_connectivity_unchanged':True})
g.path.write_text(json.dumps(g.j,indent=2)+'\n');p=g.ROOT/'artifacts/same-side-trimmed-stubs.json';history=json.loads(p.read_text()) if p.exists() else [];history.extend(records);p.write_text(json.dumps(history,indent=2)+'\n');print(json.dumps(records))