文件
phonerail/.claude/pcb2_hole_dodge.py
T

228 行
10 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()