# -*- coding: utf-8 -*- u"""§10.1 的参考面检查:顶层那 5 段 `*J` 差分(U5n→J7n)下方的 Inner1 是不是**一整块同网络的铜**。 /c/Python314/python .claude/inner1_diffref.py [--grid 0.05] [--margin 1.0] /c/Python314/python .claude/inner1_diffref.py --routes # 沿**实际走线**逐点查(长差分对用这个) 载板的层角色是镜像的(Inner1 = 分割电源面,Inner2 = 完整地), 所以**底层**的长距离 D± 参考 Inner2(完整地,没问题), 而**顶层**这 5 段 5 mm 的短差分参考的是 Inner1 —— 那一层是被五块 `VOn_P` 岛切开的。 段长只有 ~5 mm、又紧挨 ESD 器件,可以接受, 但前提是**每段下方不能横穿分割缝**(§10.1)。 判据:把两根线的包围盒外扩 `margin`,在这个窗口里 ① 只能有**一个**网络的铜面覆盖;② 覆盖必须是**一个连通域**;③ 覆盖率 100%。 """ import json, os, sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run from pcb2_pour import PCB, MIL from inner1_verify import ring_to_poly, components L_PWR = 15 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 || info[2] !== %d) continue; let f = []; try { f = q.getState_PourFills() || []; } catch (e) {} const rings = []; for (const seg of f) { if (seg.fill === false) continue; for (const r of ((seg.path && seg.path.complexPolygon) || [])) rings.push(r); } out[info[0]] = {net: info[1], rings: rings}; } return out; """ % (PCB, L_PWR) def cover(polys, x0, y0, x1, y1, g): u"""窗口内被这些环覆盖的栅格(奇偶填充,逐环按所属 fill 分组已在调用方做)。""" 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 = sorted(ax + (bx - ax) * (y - ay) / (by - ay) for (ax, ay), (bx, by) in edges if (ay > y) != (by > y)) row = grid[j] for t in range(0, len(cr) - 1, 2): i0 = max(0, int((cr[t] - x0) / g)) i1 = min(nx - 1, int((cr[t + 1] - x0) / g)) for i in range(i0, i1 + 1): row[i] = 1 return grid, nx, ny JS_TOPLINES = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1800)); const out = []; for (const o of await eda.pcb_PrimitiveLine.getAll()) { if (o.getState_Layer() !== 1) continue; const n = o.getState_Net() || ''; if (!/^(USB[0-9]_D[PM]J?|UP_D[PM])$/.test(n)) continue; out.push([n, o.getState_StartX(), o.getState_StartY(), o.getState_EndX(), o.getState_EndY()]); } return out; """ % PCB def pt_in(polys, x, y): u"""奇偶填充的点包含判定(所有环一起算,孔自然被挖掉)。""" c = False for q in polys: for k in range(len(q)): (ax, ay), (bx, by) = q[k], q[(k + 1) % len(q)] if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay): c = not c return c def by_routes(pours, step=0.1): u"""沿顶层**实际走线**逐点查参考面。 ⚠ 判据是「**每一条连通的顶层走线**下方只能有一种铜」,不是「每个网络」—— 一个网络在板上可能有好几段互不相连的顶层走线(比如 pair5:过隘口那一段 压在 `VO2_P` 岛上,进 U55 焊盘那一小段压在 GND 底填上), 各自压在各自的一整块铜上就没问题,**没有哪一段横穿了缝**。 ⚠ 「裸露」点不算违规:那是过孔 / 焊盘的反焊盘(Ø≈1.0),线穿过它天经地义。 """ r = run(JS_TOPLINES, timeout=240) if not r.get('success'): sys.exit(u'读顶层导线失败: ' + json.dumps(r, ensure_ascii=False)[:400]) segs = {} for n, x0, y0, x1, y1 in r['result']: segs.setdefault(n, []).append((x0 / MIL, y0 / MIL, x1 / MIL, y1 / MIL)) def runs(lst): u"""按端点把线段并成若干条连通走线(并查集,端点取 0.01 mm 容差)。""" par = list(range(len(lst))) def find(i): while par[i] != i: par[i] = par[par[i]]; i = par[i] return i key = {} for i, (x0, y0, x1, y1) in enumerate(lst): for k in ((round(x0, 2), round(y0, 2)), (round(x1, 2), round(y1, 2))): if k in key: a, b = find(key[k]), find(i) if a != b: par[a] = b else: key[k] = i out = {} for i, seg in enumerate(lst): out.setdefault(find(i), []).append(seg) return list(out.values()) print(u'§10.1 参考面检查(沿实际走线,步长 %.2f mm):顶层每一条连通走线下方的 Inner1' % step) allok = True for net in sorted(segs): for run_i, run_segs in enumerate(runs(segs[net])): seen, bare, tot = {}, 0, 0 xs, ys = [], [] for x0, y0, x1, y1 in run_segs: xs += [x0, x1]; ys += [y0, y1] L = ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5 m = max(2, int(L / step) + 1) for i in range(m + 1): t = i / float(m) x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t tot += 1 hit = [d['net'] for d in pours.values() if pt_in(d['polys'], x, y)] if not hit: bare += 1 for h in hit: seen[h] = seen.get(h, 0) + 1 ok = len(seen) <= 1 allok = allok and ok print(u' %-10s #%d x %6.1f~%6.1f y %5.1f~%5.1f %2d 段/%4d 点 -> %-28s 反焊盘 %3d %s' % (net, run_i, min(xs), max(xs), min(ys), max(ys), len(run_segs), tot, u' + '.join(u'%s %.0f%%' % (k, 100.0 * v / tot) for k, v in sorted(seen.items())) or u'(无铜)', bare, u'OK' if ok else u'!! 跨缝')) print(u'%s' % (u'每一条顶层走线都只压在一块铜上。' if allok else u'!! 有走线横穿了 Inner1 的分割缝 —— 违反 §10.1。')) return 0 if allok else 1 def main(): g, margin = 0.05, 1.0 if '--grid' in sys.argv: g = float(sys.argv[sys.argv.index('--grid') + 1]) if '--margin' in sys.argv: margin = float(sys.argv[sys.argv.index('--margin') + 1]) pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8')) r = run(JS, timeout=240) if not r.get('success'): sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400]) pours = dict((k, {'net': v['net'], 'polys': [p for p in (ring_to_poly(rg) for rg in v['rings']) if len(p) >= 3]}) for k, v in r['result'].items()) if '--routes' in sys.argv: return by_routes(pours) print(u'§10.1 参考面检查:顶层 5 段 `*J` 差分下方的 Inner1(窗口 = 包围盒外扩 %.1f mm)' % margin) allok = True for n in range(1, 6): pts = [(p[4] / MIL, p[5] / MIL) for p in pads if p[2] in ('USB%d_DPJ' % n, 'USB%d_DMJ' % n) and p[3] == 1] if len(pts) != 4: print(u' CH%d [!!] 只找到 %d 个焊盘,应为 4' % (n, len(pts))) allok = False continue x0 = min(p[0] for p in pts) - margin x1 = max(p[0] for p in pts) + margin y0 = min(p[1] for p in pts) - margin y1 = max(p[1] for p in pts) + margin hits = [] for name, d in pours.items(): grid, nx, ny = cover(d['polys'], x0, y0, x1, y1, g) cells = sum(sum(row) for row in grid) if not cells: continue lab, sizes = components(grid, nx, ny) hits.append((name, d['net'], cells, nx * ny, len(sizes))) tot = nx * ny if len(hits) == 1 and hits[0][4] == 1 and hits[0][2] == tot: print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> %s (%s) 一整块盖满 ✅' % (n, x0, x1, y0, y1, hits[0][0], hits[0][1])) else: allok = False print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> ❌ %s' % (n, x0, x1, y0, y1, u' / '.join(u'%s(%s) 盖 %.1f%% %d 域' % (a, b, 100.0 * c / t, k) for a, b, c, t, k in hits) or u'下方没有任何 Inner1 铜')) print(u'%s' % (u'五段全部落在同一块连续铜上。' if allok else u'!! 有段下方是分割缝或多块铜 —— 违反 §10.1,要挪岛或挪线。')) return 0 if allok else 1 if __name__ == '__main__': sys.exit(main())