文件

127 行
6.0 KiB
Python

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