# -*- 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()