Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
66 lines
3.0 KiB
Python
66 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""v6 T15b — delete the 4 dangling track stubs DRC reported on 25-Sep (warnings track_dangling).
|
|
|
|
Each target is identified by net + layer + the dangling end DRC printed + the reported length;
|
|
the script refuses to delete anything that doesn't match exactly one segment.
|
|
|
|
python3 T15b.py hbox-watchdog-v6.kicad_pcb # dry run
|
|
python3 T15b.py hbox-watchdog-v6.kicad_pcb --apply # writes .T15bbak
|
|
"""
|
|
import re, sys, math, shutil
|
|
|
|
STUBS = [ # (net, layer, dangling end x, y, length mm) — from the 25-Sep DRC report
|
|
('/3V3_ETH', 'F.Cu', 91.7375, 56.0800, 0.3889),
|
|
('/VEH5V', 'F.Cu', 187.5000, 116.1100, 0.7071),
|
|
('/RS485_B', 'B.Cu', 145.3500, 131.1682, 0.2107),
|
|
('/DOUT_4G', 'In2.Cu', 158.4000, 80.1000, 1.6000),
|
|
# second DRC pass: the RS485_B stub was a 3-segment chain off a THT pad going nowhere
|
|
('/RS485_B', 'B.Cu', 146.2000, 132.0182, 1.2021),
|
|
('/RS485_B', 'B.Cu', 146.2000, 132.3075, 0.2894),
|
|
]
|
|
|
|
def span(s, i):
|
|
d = 0; instr = False; j = i
|
|
while True:
|
|
c = s[j]
|
|
if c == '"' and s[j-1] != '\\': instr = not instr
|
|
elif not instr:
|
|
if c == '(': d += 1
|
|
elif c == ')':
|
|
d -= 1
|
|
if d == 0: return j + 1
|
|
j += 1
|
|
|
|
def main():
|
|
path = sys.argv[1]; apply = '--apply' in sys.argv
|
|
s0 = open(path).read(); cuts = []; log = []
|
|
segs = []
|
|
for m in re.finditer(r'\n\t\(segment\b', s0):
|
|
a = m.start() + 1; e = span(s0, a); b = s0[a:e]
|
|
st = tuple(map(float, re.search(r'\(start (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
|
|
en = tuple(map(float, re.search(r'\(end (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
|
|
segs.append(dict(a=a, e=e, st=st, en=en, l=re.search(r'\(layer "([^"]+)"\)', b).group(1),
|
|
n=re.search(r'\(net "([^"]*)"\)', b).group(1)))
|
|
for net, lay, x, y, L in STUBS:
|
|
hit = [g for g in segs if g['n'] == net and g['l'] == lay
|
|
and any(math.hypot(px - x, py - y) < 0.005 for px, py in (g['st'], g['en']))
|
|
and abs(math.dist(g['st'], g['en']) - L) < 0.002]
|
|
if not hit:
|
|
log.append(f'{net} {lay} @({x},{y}): not found (already removed?)'); continue
|
|
if len(hit) > 1: raise SystemExit(f'{net}: {len(hit)} candidates, refusing')
|
|
cuts.append(hit[0]); log.append(f'delete {net} {lay} {hit[0]["st"]}->{hit[0]["en"]} ({L} mm)')
|
|
s = s0
|
|
for g in sorted(cuts, key=lambda g: -g['a']):
|
|
s = s[:g['a'] - 1] + s[g['e']:] # also drops the preceding newline
|
|
t = re.sub(r'"(?:[^"\\]|\\.)*"', '', s); assert t.count('(') == t.count(')'), 'unbalanced'
|
|
assert len(re.findall(r'\n\t\(footprint "', s)) == len(re.findall(r'\n\t\(footprint "', s0))
|
|
print('\n'.join(log))
|
|
if not cuts: print('nothing to do'); return
|
|
if apply:
|
|
shutil.copy2(path, path + '.T15bbak'); open(path, 'w').write(s); print('APPLIED (backup .T15bbak)')
|
|
else:
|
|
print('DRY RUN — re-run with --apply')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|