# -*- coding: utf-8 -*- u"""把 Inner1 上**真正铺出来的铜**读回来栅格化,量连通性与最窄断面。 /c/Python314/python .claude/inner1_verify.py [--grid 0.05] ## 为什么不能只看 `pcb2_pour.py --verify` 打印的「段数」 `getState_PourFills()` 把一块铜面吐成十几个 fill,**那多半是切块不是断开**: 相邻块共边。所以「17 段」既可能是一整块被切碎,也可能真碎成 17 座孤岛, 段数本身分辨不出来。唯一靠得住的判据是**把所有环画进同一张栅格再做连通域**。 `inner1_check.py` 是**下发前**按 ISLANDS 多边形 + 反焊盘自己算的(预测), 本脚本是**下发后**按 EDA 实际吐出来的铜算的(实测)。两个都要过。 ⚠️ 环的格式是 `x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...`, 'ARC' 后面第一个数是**角度不是坐标**;这里按起点/终点/张角反解圆心, 再按 <=5° 细分成折线 —— 直接把弧当弦会把反焊盘算大、低估颈宽。 """ import json, math, os, sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run from pcb2_pour import ISLANDS, PCB, MIL, body_bbox U10MIL = 3.9370078740157477 # getState_PourFills() 的坐标单位:10 mil NECK_MIN = 7.0 JS = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1800)); const byId = {}; for (const p of await eda.pcb_PrimitivePour.getAll()) byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''), p.getState_Net(), p.getState_Layer()]; const out = {}; for (const q of await eda.pcb_PrimitivePoured.getAll()) { const info = byId[q.getState_PourPrimitiveId()]; if (!info) continue; let f = []; try { f = q.getState_PourFills() || []; } catch (e) {} const rings = []; let spokes = 0; for (const seg of f) { const cp = (seg.path && seg.path.complexPolygon) || []; // fill=false 的 seg 是**热焊盘十字花的辐条**(描边折线,lineWidth 一般 0.254), // 不是填充区。出现它就说明这块铜面的焊盘连接方式退回了「十字花」。 if (seg.fill === false) { spokes += cp.length; continue; } for (const r of cp) rings.push(r); } out[info[0]] = {net: info[1], layer: info[2], rings: rings, spokes: spokes, nfill: f.length}; } return out; """ % PCB def ring_to_poly(ring): u"""一个环 -> 折线点列(mm)。ARC 按 <=5° 细分。""" pts, i = [], 0 cur = None while i < len(ring): v = ring[i] if v == 'L': i += 1 continue if v == 'ARC': i += 1 ang = ring[i] ex, ey = ring[i + 1], ring[i + 2] i += 3 x0, y0 = cur th = math.radians(ang) mx, my = (x0 + ex) / 2.0, (y0 + ey) / 2.0 dx, dy = ex - x0, ey - y0 c = math.hypot(dx, dy) if c < 1e-9 or abs(math.sin(th / 2.0)) < 1e-9: pts.append((ex, ey)) cur = (ex, ey) continue R = c / (2.0 * math.sin(th / 2.0)) # 带符号 d = math.sqrt(max(R * R - (c / 2.0) ** 2, 0.0)) * (1 if R > 0 else -1) ox, oy = -dy / c, dx / c # 左法向 ccx, ccy = mx + ox * d, my + oy * d a0 = math.atan2(y0 - ccy, x0 - ccx) n = max(2, int(abs(ang) / 5.0) + 1) for k in range(1, n + 1): a = a0 + th * k / n pts.append((ccx + abs(R) * math.cos(a), ccy + abs(R) * math.sin(a))) cur = (ex, ey) continue x, y = ring[i], ring[i + 1] i += 2 pts.append((x, y)) cur = (x, y) return [(x / U10MIL, y / U10MIL) for x, y in pts] def raster(polys, g): u"""所有环画进一张栅格,奇偶填充(外环填、内环挖)。""" xs = [p[0] for q in polys for p in q] ys = [p[1] for q in polys for p in q] x0, x1, y0, y1 = min(xs) - g, max(xs) + g, min(ys) - g, max(ys) + g nx = int((x1 - x0) / g) + 1 ny = int((y1 - y0) / g) + 1 edges = [] for q in polys: for k in range(len(q)): a, b = q[k], q[(k + 1) % len(q)] if a[1] != b[1]: edges.append((a, b)) grid = [bytearray(nx) for _ in range(ny)] for j in range(ny): y = y0 + (j + 0.5) * g cr = [] for (ax, ay), (bx, by) in edges: if (ay > y) != (by > y): cr.append(ax + (bx - ax) * (y - ay) / (by - ay)) cr.sort() row = grid[j] for t in range(0, len(cr) - 1, 2): # 奇偶填充 i0 = max(0, int(math.ceil((cr[t] - x0) / g - 0.5))) i1 = min(nx - 1, int(math.floor((cr[t + 1] - x0) / g - 0.5))) for i in range(i0, i1 + 1): row[i] = 1 return grid, x0, y0, nx, ny def components(grid, nx, ny): lab = [[0] * nx for _ in range(ny)] k, sizes = 0, {} for j in range(ny): for i in range(nx): if not grid[j][i] or lab[j][i]: continue k += 1 st = [(j, i)] lab[j][i] = k n = 0 while st: a, b = st.pop() n += 1 for da, db in ((1, 0), (-1, 0), (0, 1), (0, -1)): p, q = a + da, b + db if 0 <= p < ny and 0 <= q < nx and grid[p][q] and not lab[p][q]: lab[p][q] = k st.append((p, q)) sizes[k] = n return lab, sizes def main(): g = 0.05 if '--grid' in sys.argv: g = float(sys.argv[sys.argv.index('--grid') + 1]) r = run(JS, timeout=240) if not r.get('success'): sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400]) data = r['result'] pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8')) vias = json.load(open(os.path.join(HERE, '_pcb2_vias.json'), encoding='utf-8')) print(u'Inner1 实测铜面校核(栅格 %.2f mm,颈宽下限 %.1f mm)' % (g, NECK_MIN)) allok = True for name, net, _m, _n in ISLANDS: key = u'%s-INNER1' % name d = data.get(key) if not d: print(u' %-12s [!!] 没读到这块铜面' % key) allok = False continue if d.get('spokes'): print(u' %-12s [!!] 有 %d 条十字花辐条 —— 这块铜面的焊盘连接方式不是「全连接」,' u'4 A 只能从 0.254 mm 的辐条里过。去 pcb2_rules.py 看 PLANE-DIRECT 有没有指派上。' % (key, d['spokes'])) allok = False polys = [p for p in (ring_to_poly(rg) for rg in d['rings']) if len(p) >= 3] grid, x0, y0, nx, ny = raster(polys, g) lab, sizes = components(grid, nx, ny) main_k = max(sizes, key=sizes.get) if sizes else 0 area = sizes.get(main_k, 0) * g * g tot = sum(sizes.values()) * g * g bad, own = [], 0 for ref, num, pnet, layer, px, py, shape, hole in pads: if pnet != net or layer not in (12, 15): continue own += 1 px, py = px / MIL, py / MIL rad = max(shape[1], shape[2] if len(shape) > 2 and isinstance(shape[2], (int, float)) else shape[1]) rr = int((rad / MIL / 2.0 + 0.35) / g) + 1 i, j, hit = int((px - x0) / g), int((py - y0) / g), False for q in range(max(0, j - rr), min(ny, j + rr + 1)): for p in range(max(0, i - rr), min(nx, i + rr + 1)): if lab[q][p] == main_k: hit = True break if hit: break if not hit: bad.append('%s.%s' % (ref, num)) # ⚠️ 焊盘之外,**本网的过孔也必须落在主域里**:贴片电容全靠这一个孔接岛 # (`pcb2_vo_vias.py` 打的 25 个)。只查焊盘的话,孔掉进边角碎片里没人发现。 nvia = 0 for vnet, vx, vy, dia, _hole in vias: if vnet != net: continue nvia += 1 vx, vy = vx / MIL, vy / MIL rr = int((dia / MIL / 2.0 + 0.35) / g) + 1 i, j, hit = int((vx - x0) / g), int((vy - y0) / g), False for q in range(max(0, j - rr), min(ny, j + rr + 1)): for pp in range(max(0, i - rr), min(nx, i + rr + 1)): if lab[q][pp] == main_k: hit = True break if hit: break if not hit: bad.append(u'过孔(%.2f,%.2f)' % (vx, vy)) # v2:颈宽只在走廊主体里量(穿筋的颈 3 mm 只走纹波电流,§25.16) rx0, ry0, rx1, ry1 = body_bbox(_m) # 边界行只剩铺铜内缩后的一丝铜(实测 2.8 = 穿筋的颈),各缩 0.15 再量 rx0, ry0, rx1, ry1 = rx0 + 0.15, ry0 + 0.15, rx1 - 0.15, ry1 - 0.15 rowsY = [t for t in ((sum(1 for i in range(nx) if lab[j][i] == main_k and rx0 <= x0 + (i + 0.5) * g <= rx1) * g, y0 + (j + 0.5) * g) for j in range(ny) if ry0 <= y0 + (j + 0.5) * g <= ry1) if t[0] > 0] colsX = [t for t in ((sum(1 for j in range(ny) if lab[j][i] == main_k and ry0 <= y0 + (j + 0.5) * g <= ry1) * g, x0 + (i + 0.5) * g) for i in range(nx) if rx0 <= x0 + (i + 0.5) * g <= rx1) if t[0] > 0] my, mx = min(rowsY), min(colsX) ok = (not bad) and my[0] >= NECK_MIN and mx[0] >= NECK_MIN allok = allok and ok print(u' %-12s %-7s %d 环 / %d 连通域,主域 %.0f mm²(占本网铜的 %.1f%%)' % (key, net, len(polys), len(sizes), area, 100.0 * area / tot if tot else 0)) print(u' %d 焊盘 + %d 过孔 %s 最窄横断面 %5.2f @ y=%.2f 最窄纵断面 %5.2f @ x=%.2f %s' % (own, nvia, u'全在主域 ✔' if not bad else u'✘ 掉队: ' + ' '.join(bad), my[0], my[1], mx[0], mx[1], u'✅' if ok else u'❌')) for key in (u'GND-INNER1', u'GND-INNER2'): d = data.get(key) if d: print(u' %-12s %-7s %d 环' % (key, d['net'], len(d['rings']))) print(u'%s' % (u'实测全部通过。' if allok else u'!! 实测有不达标的岛。')) return 0 if allok else 1 if __name__ == '__main__': sys.exit(main())