228 行
10 KiB
Python
228 行
10 KiB
Python
# -*- 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()
|