# -*- coding: utf-8 -*- u"""载板:把**穿过定位孔(P*H*,NPTH Φ1.8)的旧走线**局部改道(2026-09-07)。 /c/Python314/python .claude/pcb2_hole_dodge.py # 只算:列出压孔的线段 + 每段的改道结果 + 净距校核 /c/Python314/python .claude/pcb2_hole_dodge.py --push # 删旧段、画新段(先 pcb2_route_dp.py --dump 刷新障碍表) ## 为什么不整网重布 第二十八轮把 7 个定位孔按实测挪了最多 4.8 mm,7 个孔被 8 个网络的旧线穿过(DRC「钻孔到导线 0 mil」)。 其中 `VBUS2`/`VBUS3` 连着 J7n —— `pcb2_route_sig.py` 整网重画时会按手写常量重画 BM24 出线段, 而站 1~3 的元件在板上比 `plan()` 偏 (0.09, 0.66)(见 PROGRESS 第二十九轮),画出来是错的; `+3V3_HUB` 是 33 个焊盘的大网、有一支本来就布不通。⇒ 只动压孔那一段: 删掉它,用布线器的两点连线原语 `_connect()` 在小窗口里把两个端点重新接上, **全板现有铜(含孔)都是障碍**,走线方向规则(顶层水平 / 底层纵向)自动生效, 最后过 `pcb2_route_dp.check()` 同一份净距判据。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run import pcb2_route_dp as D import pcb2_route_sig as S MIL = D.MIL PCB = D.PCB HOLE_R = 0.9 # Φ1.8 NPTH MARGIN = D.CLR + 0.05 # 孔边到线边最少 0.20,再留 0.05 给栅格量化 def is_hole(p): return str(p['num']).startswith('P') and 'H' in str(p['num']) and p['lay'] == 12 and not p['net'] def offenders(pads, lines): out = [] for h in [p for p in pads if is_hole(p)]: for l in lines: if l['lay'] not in (S.L_TOP, S.L_BOT): continue d = D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], h['x'], h['y']) if d - l['w'] / 2.0 < HOLE_R + MARGIN: out.append((h, l, d)) return out def _seg_shape(l): return S.sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']) REGION_R = 2.2 # 孔心为圆心、这个半径内的本网铜整块删掉重接(> 0.9 + 0.2 + 0.3 半线宽 + 栅格余量) def _near(a, b, tol=0.02): return math.hypot(a[0] - b[0], a[1] - b[1]) < tol def _ports(net, h, lines, vias, pads, dl, dv): u"""删掉圆盘里的铜之后,圆盘外还连着的断口:(x, y, 层)。""" ends = [(l['x0'], l['y0']) for l in dl] + [(l['x1'], l['y1']) for l in dl] + [(v['x'], v['y']) for v in dv] ports = [] def add(pt, lay): for q in ports: if _near(q[:2], pt) and q[2] == lay: return ports.append((pt[0], pt[1], lay)) for l in lines: if l['net'] != net or l in dl: continue for e in ((l['x0'], l['y0']), (l['x1'], l['y1'])): if any(_near(e, d) for d in ends): add(e, l['lay']) for v in vias: if v['net'] != net or v in dv: continue if any(_near((v['x'], v['y']), d) for d in ends): add((v['x'], v['y']), S.L_TOP) add((v['x'], v['y']), S.L_BOT) for q in pads: if q['net'] != net: continue for e in ends: if abs(e[0] - q['x']) <= q['hw'] + 1e-6 and abs(e[1] - q['y']) <= q['hh'] + 1e-6: lay = S.L_TOP if q['lay'] == 12 else q['lay'] add((q['x'], q['y']), lay) break return ports def dodge(pads, vias, lines): u"""按孔逐个处理。返回 (要删的线, 要删的孔, 新路径 {key: path}, 更新后的 lines, vias)。""" lines, vias = list(lines), list(vias) todo = offenders(pads, lines) print(u'压孔的线段 %d 段(%d 个孔):' % (len(todo), len(set(h['num'] for h, _, _ in todo)))) for h, l, d in todo: print(u' %-5s (%.2f,%.2f) %-9s L%d w%.2f 心距 %.2f (%.2f,%.2f)-(%.2f,%.2f)' % (h['num'], h['x'], h['y'], l['net'], l['lay'], l['w'], d, l['x0'], l['y0'], l['x1'], l['y1'])) jobs = [] for h, l, _ in todo: if (h['num'], l['net']) not in [(j[0]['num'], j[1]) for j in jobs]: jobs.append((h, l['net'])) dels, dvias, routes = [], [], {} k = 0 for h, net in jobs: dl = [l for l in lines if l['net'] == net and l['lay'] in (S.L_TOP, S.L_BOT) and D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], h['x'], h['y']) < REGION_R] dv = [v for v in vias if v['net'] == net and math.hypot(v['x'] - h['x'], v['y'] - h['y']) < REGION_R] if not dl: continue w = max(l['w'] for l in dl) ports = _ports(net, h, lines, vias, pads, dl, dv) rest_l = [q for q in lines if q not in dl] rest_v = [q for q in vias if q not in dv] if len(ports) < 2: print(u' !! %s @ %s:删 %d 线 / %d 孔后只剩 %d 个断口,不会处理' % (net, h['num'], len(dl), len(dv), len(ports))) continue obstacles = S.shapes_of(pads, rest_v, rest_l) acc = None ok = True new_paths = [] for i, (px, py, lay) in enumerate(ports): c = S.Comp([S.sh_seg(px, py, px, py, w, lay)], (px, py), center=(px, py), layers=(lay,)) if acc is None: acc = c continue r = S._connect(net, acc, c, obstacles, w) if r is None: ok = False print(u' !! %s @ %s:断口 %d (%.2f,%.2f,L%d) 接不上' % (net, h['num'], i, px, py, lay)) break path, shp = r new_paths.append(path) obstacles.extend((sp, net) for sp in shp) acc.merge(c, shp) if not ok: continue for path in new_paths: segs, vs = S.expand(path) for x0, y0, x1, y1, lay, ww in segs: rest_l.append({'id': -1, 'net': net, 'lay': lay, 'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1, 'w': ww}) for x, y in vs: rest_v.append({'net': net, 'x': x, 'y': y, 'r': D.VIA_D / 2.0}) k += 1 routes[net + S.KSEP + 'hole%d' % k] = path print(u' ✔ %-9s 绕 %s:删 %d 线 / %d 孔,%d 个断口 → %d 段 / %d 过孔 %s' % (net, h['num'], len(dl), len(dv), len(ports), len(segs), len(vs), u' → '.join(u'(%.2f,%.2f,L%d)' % (p[0], p[1], p[2]) for p in path))) lines, vias = rest_l, rest_v dels += dl dvias += dv return dels, dvias, routes, lines, vias JS_PUSH = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1500)); const M = %f, DEL = %s, DELV = %s, SEGS = %s, VIAS = %s; let dl = 0, dvn = 0; for (const v of await eda.pcb_PrimitiveVia.getAll()) { for (const [net, x, y] of DELV) if (v.getState_Net() === net && Math.hypot(v.getState_X()/M - x, v.getState_Y()/M - y) < 0.02) { await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dvn++; break; } } for (const l of await eda.pcb_PrimitiveLine.getAll()) { const L = l.getState_Layer(); if (L !== 1 && L !== 2) continue; const x0=l.getState_StartX()/M,y0=l.getState_StartY()/M,x1=l.getState_EndX()/M,y1=l.getState_EndY()/M; for (const [net, lay, a, b, c, d] of DEL) { if (l.getState_Net() !== net || L !== lay) continue; const same = (Math.hypot(x0-a,y0-b) < 0.02 && Math.hypot(x1-c,y1-d) < 0.02) || (Math.hypot(x0-c,y0-d) < 0.02 && Math.hypot(x1-a,y1-b) < 0.02); if (same) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++; break; } } } let nl = 0, nv = 0; for (const [net, lay, x0, y0, x1, y1, w] of SEGS) { await eda.pcb_PrimitiveLine.create(net, lay, x0*M, y0*M, x1*M, y1*M, w*M); nl++; } for (const [net, x, y] of VIAS) { await eda.pcb_PrimitiveVia.create(net, x*M, y*M, %f*M, %f*M); nv++; } await eda.pcb_Document.save(); return {dl, dvn, nl, nv}; """ def main(): pads, vias, lines = D.geom() S.ALL_NETS.update(p['net'] for p in pads if p['net']) S.PADS_ALL[:] = pads dels, dvias, routes, lines2, vias2 = dodge(pads, vias, lines) if not routes: print(u'没有要改的。') return base_l = [q for q in lines if q not in dels] base_v = [q for q in vias if q not in dvias] bad = D.check(routes, pads, base_v, base_l) if bad: sys.exit(u'!! 净距校核不过,不下发。') print(u'净距校核全过 ✔') still = offenders(pads, lines2) if still: print(u'!! 改完仍有 %d 段压孔:%s' % (len(still), [(h['num'], l['net']) for h, l, _ in still])) sys.exit(1) print(u'改完后 30 个孔都干净 ✔(删 %d 线 / %d 过孔,新画 %d 条路径)' % (len(dels), len(dvias), len(routes))) if '--push' not in sys.argv: return # ⚠️ 要画的是**最终状态**里的新铜(id == -1),不是每条 route 的原样: # 后一个孔的作业可能删掉前一个作业刚算出的段(同网、落在它的圆盘里), # 那些段板上还没有、删不到,照 route 画就会多出一截同网冗余环路(2026-09-07 实测 3 线 1 孔)。 # 同理,要删的只有板上真有的(id != -1)。 segs = [[l['net'], l['lay'], l['x0'], l['y0'], l['x1'], l['y1'], l['w']] for l in lines2 if l.get('id') == -1] orig_v = set(id(v) for v in vias) vs = [[v['net'], v['x'], v['y']] for v in vias2 if id(v) not in orig_v] dl = [[l['net'], l['lay'], l['x0'], l['y0'], l['x1'], l['y1']] for l in dels if l.get('id') != -1] dv = [[v['net'], v['x'], v['y']] for v in dvias if id(v) in orig_v] print(u'下发计划:删 %d 线 / %d 孔(另有 %d 线 / %d 孔是本轮算出又被后续作业删掉的,不进板),画 %d 段 / %d 孔' % (len(dl), len(dv), len(dels) - len(dl), len(dvias) - len(dv), len(segs), len(vs))) r = run(JS_PUSH % (PCB, MIL, json.dumps(dl), json.dumps(dv), json.dumps(segs), json.dumps(vs), D.VIA_H, D.VIA_D), timeout=300) if not r.get('success'): sys.exit(u'下发失败: ' + json.dumps(r, ensure_ascii=False)[:500]) d = r['result'] print(u'下发:删 %d 线(计划 %d)/ %d 孔(计划 %d),画 %d 段 / %d 孔' % (d['dl'], len(dl), d['dvn'], len(dv), d['nl'], d['nv'])) if d['dl'] != len(dl) or d['dvn'] != len(dv): sys.exit(u'!! 删掉的数量和计划不符,去板上看看') if __name__ == '__main__': main()