# -*- coding: utf-8 -*- u"""载板的**焊盘级净间距**实测检查。 /c/Python314/python .claude/pcb2_clear.py [--min 0.2] [--top 40] ## 为什么要单独有这一条 `pcb2_place.py` 的本地检查用的是**元件焊盘包围盒**,判据是「不重叠」。 §25.3 那个错误(立创给顶针的 Ø2.5 焊盘 vs 侧键触点最近 2.217 mm) 正好卡在这两者之间:包围盒不重叠、但两个圆焊盘的净间距是 **负的**。 所以摆位过了不等于能打样,**必须再按焊盘真实形状量一次净间距**。 这里不依赖 EasyEDA 的 DRC —— 没布线时 DRC 会刷出 1200+ 条「连接性错误」, 真正的间距错误埋在里面看不见(成因见 `drc_read.py` 的注释)。 ## 算法 按层分桶(多层焊盘 layer=12 进所有铜层),网格哈希做邻域筛, 两两算**形状到形状**的最短距离: · RECT / OVAL 都按「圆角矩形」处理,退化成两个轴对齐矩形的间距, ELLIPSE 按圆算 —— 保守方向(矩形算出来的间距 <= 真实带圆角的间距)。 · 同网络的跳过(同网络挨着是正常的)。 """ import json, math, os, sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run PCB = 'f126ffb29c92f9b2' MIL = 39.3700787401575 COPPER = (1, 15, 16, 2) def fetch(): r = run(""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1500)); const out=[]; for (const p of await eda.pcb_PrimitivePad.getAll()) out.push([p.getState_Layer(), p.getState_X(), p.getState_Y(), p.getState_Rotation(), p.getState_Pad(), p.getState_Net(), p.getState_PadNumber()]); // ⚠⚠ 焊盘归属哪个元件,两条看着能用的路都是错的: // ① 坐标匹配 getAllPinsByPrimitiveId 的 x/y —— 有浮点尾巴,且 POLYGON 焊盘的 // x/y 根本不是形心,会漏一大片; // ② `getState_ParentComponentPrimitiveId()` —— **pcb_PrimitivePad.getAll() 回来的 // 对象上没有这个方法**(TypeError: not a function)。写 try/catch 包起来的话 // 它会静默地把**所有**焊盘都当成自由焊盘,看不出错。 // 真正的判据:**焊盘的 primitiveId 以父元件的 primitiveId 为前缀**。 // 元件 C701 = "356da09e850d14fa",它 1 脚 = "356da09e850d14fae16"。 const name = {}; for (const c of await eda.pcb_PrimitiveComponent.getAll()) name[c.getState_PrimitiveId()] = c.getState_Designator(); const ids = Object.keys(name).sort((a, b) => b.length - a.length); // 长的优先,避免前缀套前缀 const own = []; for (const p of await eda.pcb_PrimitivePad.getAll()) { const pid = p.getState_PrimitiveId(); let hit = ''; for (const id of ids) if (pid.length > id.length && pid.indexOf(id) === 0) { hit = name[id]; break; } own.push(hit); } return {pads: out, own}; """ % PCB, timeout=300) if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def deg(rot): u"""⚠⚠ **焊盘的 rotation 是弧度,元件的 rotation 是角度。** `pcb_PrimitivePad.getState_Rotation()` 回来的是 `1.5707963267948963`(= π/2), 而 `pcb_PrimitiveComponent.getState_Rotation()` 回来的是 `90`。 按角度去判 `rot % 180 == 90`,弧度值永远不等于 90 —— 于是**所有旋转过的 矩形焊盘长短边都会被认反**。J23 那 12 个 0.30 × 1.30 的信号焊盘因此被 误判成两两重叠 0.30 mm(真实净间距 +0.20 mm),13 条假报。 这类错误最阴的地方是**方向对了一半**:不旋转的焊盘全对,只有旋转过的错, 所以整体看上去像是「某个封装有问题」,而不像是「换算错了」。 """ return rot * 180.0 / math.pi if abs(rot) <= 2 * math.pi + 1e-6 else rot def half(shape, rot, x, y): u"""焊盘的中心 + 轴对齐半宽/半高(mm)。圆/椭圆额外返回半径,用来算真圆间距。 ⚠️ `POLYGON` 型焊盘(J23 的 4 个电源/屏蔽脚就是)**点表是绝对坐标**, 不是相对焊盘原点的 —— 直接拿 x/y 当中心会算到十万八千里去。 这里改用点表自己的包围盒,忽略 x/y。 """ kind = shape[0] if kind == 'POLYGON': pts = [v for v in shape[1] if isinstance(v, (int, float))] xs, ys = pts[0::2], pts[1::2] cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0 return (cx / MIL, cy / MIL, (max(xs) - min(xs)) / 2.0 / MIL, (max(ys) - min(ys)) / 2.0 / MIL, None) w, h = float(shape[1]) / MIL, float(shape[2]) / MIL if round(deg(rot)) % 180 == 90: w, h = h, w r = w / 2.0 if (kind == 'ELLIPSE' and abs(w - h) < 1e-6) else None return x / MIL, y / MIL, w / 2.0, h / 2.0, r def gap(a, b): u"""两个焊盘的净间距(mm)。两个真圆用圆心距,否则用轴对齐矩形。""" dx, dy = abs(a['x'] - b['x']), abs(a['y'] - b['y']) if a['r'] is not None and b['r'] is not None: return math.hypot(dx, dy) - a['r'] - b['r'] return max(dx - a['hw'] - b['hw'], dy - a['hh'] - b['hh']) def main(): minclr = 0.2 # 跨元件判据 intra = 0.0 # 同封装内只报**负间距**(真重叠) topn = 40 if '--min' in sys.argv: minclr = float(sys.argv[sys.argv.index('--min') + 1]) if '--top' in sys.argv: topn = int(sys.argv[sys.argv.index('--top') + 1]) raw = fetch() own = raw['own'] pads = [] for lay, x, y, rot, shape, net, num in raw['pads']: cx, cy, hw, hh, rr = half(shape, rot, x, y) ref = own[len(pads)] pads.append({'lay': lay, 'x': cx, 'y': cy, 'hw': hw, 'hh': hh, 'r': rr, 'net': net or '', 'num': num, 'ref': ref}) print(u'焊盘 %d 个,铜层 %s,判据「不同网络净间距 >= %.2f mm」' % (len(pads), COPPER, minclr)) worst, inner = [], [] for lay in COPPER: cur = [p for p in pads if p['lay'] == lay or p['lay'] == 12] cell = 3.0 grid = {} for i, p in enumerate(cur): grid.setdefault((int(p['x'] // cell), int(p['y'] // cell)), []).append(i) seen = set() for (gx, gy), idxs in grid.items(): near = [] for ax in (-1, 0, 1): for ay in (-1, 0, 1): near += grid.get((gx + ax, gy + ay), []) for i in idxs: for j in near: if j <= i or (i, j) in seen: continue seen.add((i, j)) a, b = cur[i], cur[j] if a['net'] and a['net'] == b['net']: continue g = gap(a, b) same = bool(a['ref']) and a['ref'] == b['ref'] if g < (intra if same else minclr): (inner if same else worst).append((g, lay, a, b)) worst.sort(key=lambda t: t[0]) inner.sort(key=lambda t: t[0]) if inner: print(u'\n同一封装内**负间距**(焊盘真重叠)%d 对 —— 这是封装自身的几何,' u'不是摆位问题,但出现就说明封装本身要复核:' % len(inner)) for g, lay, a, b in inner[:12]: print(u' L%-2d %7.3f mm %s.%s(%s) x %s.%s(%s)' % (lay, g, a['ref'], a['num'], a['net'] or u'悬空', b['ref'], b['num'], b['net'] or u'悬空')) if len(inner) > 12: print(u' ...还有 %d 对' % (len(inner) - 12)) if not worst: print(u'\n通过:**跨元件**的不同网络焊盘对,净间距全部 >= %.2f mm' % minclr) return 1 if inner else 0 print(u'\n!! 跨元件 %d 对低于 %.2f mm(由小到大):' % (len(worst), minclr)) for g, lay, a, b in worst[:topn]: print(u' L%-2d %7.3f mm %s.%s(%s) @(%.2f,%.2f) x %s.%s(%s) @(%.2f,%.2f)' % (lay, g, a['ref'] or '?', a['num'], a['net'] or '悬空', a['x'], a['y'], b['ref'] or '?', b['num'], b['net'] or '悬空', b['x'], b['y'])) if len(worst) > topn: print(u' ...还有 %d 对' % (len(worst) - topn)) return 1 if __name__ == '__main__': sys.exit(main())