127 行
6.0 KiB
Python
127 行
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1:找出(并删掉)**摆位改动之后悬空的铜** —— 走线 / 过孔。
|
||
|
||
/c/Python314/python .claude/pcb1_orphan_cu.py [--refresh] # 只列,不动板
|
||
/c/Python314/python .claude/pcb1_orphan_cu.py --push # 删 + 存盘
|
||
|
||
## 判据(唯一一条,故意定得很保守)
|
||
|
||
复用 `pcb1_netlist.analyze()` 把每个网络的焊盘 / 过孔 / 走线 / 铺铜连通域连成图,
|
||
然后挑出**既不含任何焊盘、也不含任何铺铜连通域**的连通碎片 ——
|
||
这种碎片的铜哪一头都没落地,只可能是「元件挪走之后留在原位的残肢」。
|
||
|
||
⚠️ 反过来的错误更贵:一段还连着焊盘的铜被误删,布线器会以为那条连接还在,
|
||
于是**再也不去补它**,而连通性统计仍然是绿的(焊盘之间确实"曾经"有路)。
|
||
所以这里绝不按「离某个挪动过的元件多近」来删,只认「哪一头都没落地」。
|
||
|
||
⚠️ `pcb1_route_sig.py --ripup` 拆的是**它自己画过的**铜(按 own_of 覆盖判据),
|
||
和这里是两件事:那个管"挡路",这个管"悬空"。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
import pcb1_netlist as NL # noqa: E402
|
||
from eda import run # noqa: E402
|
||
|
||
PCB = '7e9415a16a909c51'
|
||
|
||
|
||
def call(js, timeout=600):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:900])
|
||
return r['result']
|
||
|
||
|
||
def find(refresh=False):
|
||
pads, vias, lines = NL.geom(refresh=refresh)
|
||
pgrids = NL.pour_grids(refresh=refresh)
|
||
nets = sorted({p['net'] for p in pads if p['net']} |
|
||
{l['net'] for l in lines if l['net']} |
|
||
{v['net'] for v in vias if v['net']})
|
||
out = []
|
||
for net in nets:
|
||
items, groups = NL.analyze(net, pads, vias, lines, pgrids)
|
||
for g in groups:
|
||
kinds = {items[i][0] for i in g}
|
||
if 'pad' in kinds or 'pour' in kinds:
|
||
continue
|
||
segs = [items[i][2] for i in g if items[i][0] == 'line']
|
||
vs = [items[i][2] for i in g if items[i][0] == 'via']
|
||
if not segs and not vs:
|
||
continue
|
||
out.append(dict(net=net, lines=segs, vias=vs))
|
||
return out, (pads, vias, lines)
|
||
|
||
|
||
def main():
|
||
refresh = '--refresh' in sys.argv or '--push' in sys.argv
|
||
orph, (pads, vias, lines) = find(refresh=refresh)
|
||
nl = sum(len(o['lines']) for o in orph)
|
||
nv = sum(len(o['vias']) for o in orph)
|
||
print(u'板上 %d 焊盘 / %d 过孔 / %d 走线' % (len(pads), len(vias), len(lines)))
|
||
print(u'---- 悬空碎片 %d 个:走线 %d 段 / 过孔 %d 个 ----' % (len(orph), nl, nv))
|
||
for o in sorted(orph, key=lambda o: -(len(o['lines']) + len(o['vias']))):
|
||
xs = [c for s in o['lines'] for c in (s['x0'], s['x1'])] + [v['x'] for v in o['vias']]
|
||
ys = [c for s in o['lines'] for c in (s['y0'], s['y1'])] + [v['y'] for v in o['vias']]
|
||
print(u' %-16s %3d 线 / %2d 孔 x %7.2f~%7.2f y %7.2f~%7.2f'
|
||
% (o['net'], len(o['lines']), len(o['vias']),
|
||
min(xs), max(xs), min(ys), max(ys)))
|
||
if '--push' not in sys.argv:
|
||
print(u'\n加 --push 删除并存盘')
|
||
return
|
||
if not orph:
|
||
print(u'没有要删的'); return
|
||
|
||
# `pcb1_netlist.geom()` 的快照里没有 primitiveId,所以按**坐标 + 层 + 网络**在客户端侧匹配。
|
||
# ⚠️ 线和孔**不是同一个 API**:过孔要 `pcb_PrimitiveVia.delete()`。
|
||
# 早先版本把孔也丢给 `pcb_PrimitiveLine.delete()`,它在 try 里静默失败 ——
|
||
# 孔留在板上,而回查只数「碎片个数」,剩下的孤儿孔自己单独成一个碎片,
|
||
# 看着像"还没删干净",而不是"删错了 API"。
|
||
lines = [[o['net'], s['lay'], s['x0'], s['y0'], s['x1'], s['y1']]
|
||
for o in orph for s in o['lines']]
|
||
vias = [[o['net'], v['x'], v['y']] for o in orph for v in o['vias']]
|
||
print(u'\n删除 %d 段线 + %d 个孔(按坐标匹配)…' % (len(lines), len(vias)))
|
||
n = call(u"""
|
||
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2000));
|
||
const MIL = %f, L = %s, V = %s;
|
||
// ⚠️ **不能用字符串精确键**:两侧的 mil 换算常数不同(这里 39.3700787、
|
||
// pcb1_netlist 里是别的位数),150 mm 处相差 8e-5 mm —— 绝大多数时候
|
||
// `toFixed(3)` 舍到同一个值,偶尔跨进位就差一格,于是"删除返回 23/25"。
|
||
// 改成带容差的最近匹配,容差取 0.005 mm(远小于任何真实间距)。
|
||
const TOL = 0.005;
|
||
const near = (a, b) => Math.abs(a - b) < TOL;
|
||
const sameSeg = (r, a, b, c, d) =>
|
||
(near(r[2], a) && near(r[3], b) && near(r[4], c) && near(r[5], d)) ||
|
||
(near(r[2], c) && near(r[3], d) && near(r[4], a) && near(r[5], b));
|
||
let nl = 0, nv = 0;
|
||
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
|
||
const net = l.net || '', lay = l.layer;
|
||
const a = l.startX/MIL, b = l.startY/MIL, c = l.endX/MIL, d = l.endY/MIL;
|
||
if (L.some(r => r[0] === net && r[1] === lay && sameSeg(r, a, b, c, d))) {
|
||
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); nl++;
|
||
}
|
||
}
|
||
for (const v of await eda.pcb_PrimitiveVia.getAll()) {
|
||
const net = v.net || '', x = v.x/MIL, y = v.y/MIL;
|
||
if (V.some(r => r[0] === net && near(r[1], x) && near(r[2], y))) {
|
||
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); nv++;
|
||
}
|
||
}
|
||
await eda.pcb_Document.save();
|
||
return [nl, nv];""" % (PCB, 39.3700787, json.dumps(lines), json.dumps(vias)))
|
||
print(u' 删除返回 线 %d / 孔 %d(计划 %d / %d)' % (n[0], n[1], len(lines), len(vias)))
|
||
if n[0] != len(lines) or n[1] != len(vias):
|
||
print(u' ⚠️ 数量对不上 —— 坐标匹配漏了,下面的回查会说明还剩多少')
|
||
left, _ = find(refresh=True)
|
||
print(u'回查:悬空碎片 %d 个' % len(left))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|