# -*- coding: utf-8 -*- u"""D16 南带补线结果的本机复核(不跑任何搜索,只算几何): /c/Python314/python .claude/r2/d16_check.py [结果文件,默认 _d16_route.out.json] [--list N] ① 精确净距:新增的每段线 / 每个孔,对板上快照(_board_for_nets.json,四层全算,含内层线)+ 焊盘 (通孔算所有铜层)+ 封装 NPTH + 板边,按真实几何算最小距离,< MIN_CLR 就报(同网不算)。 ② L1 上的新线必须落在 pcb_base.PD_L1 的窗口里(西侧别处的 L1 是 12V 大铜,线会切断它)。 ③ 连通:逐网按「线-线 / 线-孔 / 线-盘 / 孔-盘」相碰合并,报新入口相关网还剩几块(与 route_nets 同口径; 不含铺铜 —— GND 这类靠铺铜成网的只看有没有孔)。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import pcb_base as B # noqa: E402 MIN_CLR = 0.15 EDGE_CLR = 0.3 VIA_HOLE = 0.3 HOLE_CLR = 0.3 # 钻孔到钻孔(DRC otherClearance 11.8 mil),不分网 WIN_TOL = 0.3 # L1 线出 PD_L1 窗口的容差(pour_base 挖 12V 时小岛外扩 0.5) LAYERS = (1, 15, 16, 2) NETS = ['PD_CC1', 'PD_CC2', 'SW12', 'BST12', 'VCC12', 'FB12', 'RT12', 'EN12', '#CFG1', 'VIN_RAW', 'VIN_FUSED', 'VIN_SW', 'SDA', 'SCL', 'NTC2', 'INA_ALERT'] # #CFG1 = U36.9 的自动网名,按脚现取 def pt_seg(px, py, ax, ay, bx, by): dx, dy = bx - ax, by - ay L = dx * dx + dy * dy t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L)) return math.hypot(px - (ax + t * dx), py - (ay + t * dy)) def seg_seg(a, b): (x1, y1, x2, y2), (x3, y3, x4, y4) = a, b d1, d2, d3, d4 = x2 - x1, y2 - y1, x4 - x3, y4 - y3 den = d1 * d4 - d2 * d3 if abs(den) > 1e-12: t = ((x3 - x1) * d4 - (y3 - y1) * d3) / den u = ((x3 - x1) * d2 - (y3 - y1) * d1) / den if 0 <= t <= 1 and 0 <= u <= 1: return 0.0 return min(pt_seg(x1, y1, x3, y3, x4, y4), pt_seg(x2, y2, x3, y3, x4, y4), pt_seg(x3, y3, x1, y1, x2, y2), pt_seg(x4, y4, x1, y1, x2, y2)) def pt_rect(px, py, r): return math.hypot(max(r[0] - px, px - r[2], 0.0), max(r[1] - py, py - r[3], 0.0)) def seg_rect(s, r): x1, y1, x2, y2 = s if (r[0] <= x1 <= r[2] and r[1] <= y1 <= r[3]) or (r[0] <= x2 <= r[2] and r[1] <= y2 <= r[3]): return 0.0 edges = [(r[0], r[1], r[2], r[1]), (r[2], r[1], r[2], r[3]), (r[2], r[3], r[0], r[3]), (r[0], r[3], r[0], r[1])] return min(seg_seg(s, e) for e in edges) def prect(p): return (p[2] - p[4] / 2, p[3] - p[5] / 2, p[2] + p[4] / 2, p[3] + p[5] / 2) def pad_layers(p): return LAYERS if p[6] == 12 else ((1,) if p[6] == 1 else (2,)) def check_clearance(new_s, new_v, old_s, old_v, pads, npth): bad = [] all_s = old_s + new_s all_v = old_v + new_v for s in new_s: l, x1, y1, x2, y2, w, n = s hw = w / 2.0 for t in all_s: if t is s or t[6] == n or t[0] != l: continue d = seg_seg((x1, y1, x2, y2), t[1:5]) - hw - t[5] / 2.0 if d < MIN_CLR - 1e-3: bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 线 %s' % (n, l, x1, y1, x2, y2, t[6]))) for v in all_v: if v[2] == n: continue d = pt_seg(v[0], v[1], x1, y1, x2, y2) - hw - v[3] / 2.0 if d < MIN_CLR - 1e-3: bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 孔 %s (%.2f,%.2f)' % (n, l, x1, y1, x2, y2, v[2], v[0], v[1]))) for p in pads: if p[7] == n or l not in pad_layers(p): continue d = seg_rect((x1, y1, x2, y2), prect(p)) - hw if d < MIN_CLR - 1e-3: bad.append((d, u'线 %s L%s (%.3f,%.3f)-(%.3f,%.3f) ↔ 焊盘 %s.%s(%s)' % (n, l, x1, y1, x2, y2, p[0], p[1], p[7] or u'空'))) for h in npth: d = pt_seg(h[0], h[1], x1, y1, x2, y2) - hw - h[3] if d < 0: bad.append((d, u'线 %s 压 NPTH (%.2f,%.2f)' % (n, h[0], h[1]))) for (x, y) in ((x1, y1), (x2, y2)): e = min(x, y, B.BW - x, B.BH - y) - hw if e < EDGE_CLR: bad.append((e, u'线 %s 离板边 %.2f' % (n, e))) if l == 1 and min(x1, x2) < 28.0: # 按同网窗口的**并集**判(CC2 一根线横跨两块窗口);窗口外扩 WIN_TOL:router 封格子是向外取整的, # 线边会出窗口零点几,而 pour_base 挖 12V 大铜时小岛外扩了 0.5,这点余量不会碰到 12V 铜 wins = [r for nn, r in B.PD_L1 if nn == n] k = max(2, int(math.hypot(x2 - x1, y2 - y1) / 0.05)) for m in range(k + 1): px, py = x1 + (x2 - x1) * m / k, y1 + (y2 - y1) * m / k if not any(r[0] - WIN_TOL <= px - hw and px + hw <= r[2] + WIN_TOL and r[1] - WIN_TOL <= py - hw and py + hw <= r[3] + WIN_TOL for r in wins): bad.append((0.0, u'L1 线 %s (%.2f,%.2f)-(%.2f,%.2f) 出了 PD_L1 窗口(会切 12V 大铜)' % (n, x1, y1, x2, y2))) break for v in new_v: for t in all_s: if t[6] == v[2]: continue d = pt_seg(v[0], v[1], t[1], t[2], t[3], t[4]) - t[5] / 2.0 - v[3] / 2.0 if d < MIN_CLR - 1e-3: bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 线 %s L%s' % (v[2], v[0], v[1], t[6], t[0]))) for u in all_v: if u is v: continue c = math.hypot(u[0] - v[0], u[1] - v[1]) # 钻孔到钻孔**不分网**(DRC otherClearance 11.8 mil = 0.3):孔径 0.3 ⇒ 圆心距 ≥ 0.6;同网孔叠在一起也算 if c - VIA_HOLE < HOLE_CLR - 1e-3 and not (u[2] == v[2] and c < 1e-3 and u is not v and False): bad.append((c - VIA_HOLE, u'孔 %s (%.2f,%.2f) ↔ 孔 %s (%.2f,%.2f) 钻孔间距 %.3f' % (v[2], v[0], v[1], u[2], u[0], u[1], c - VIA_HOLE))) continue if u[2] == v[2]: continue d = c - (u[3] + v[3]) / 2.0 if d < MIN_CLR - 1e-3: bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 孔 %s (%.2f,%.2f)' % (v[2], v[0], v[1], u[2], u[0], u[1]))) for p in pads: if p[7] == v[2]: continue d = pt_rect(v[0], v[1], prect(p)) - v[3] / 2.0 if d < MIN_CLR - 1e-3: bad.append((d, u'孔 %s (%.2f,%.2f) ↔ 焊盘 %s.%s(%s)' % (v[2], v[0], v[1], p[0], p[1], p[7] or u'空'))) for h in npth: d = math.hypot(h[0] - v[0], h[1] - v[1]) - v[3] / 2.0 - h[3] if d < 0: bad.append((d, u'孔 %s 压 NPTH (%.2f,%.2f)' % (v[2], h[0], h[1]))) bad.sort(key=lambda t: t[0]) return bad def groups_of(net, pads, all_s, all_v): ps = [p for p in pads if p[7] == net] ss = [s for s in all_s if s[6] == net] vs = [v for v in all_v if v[2] == net] items = [('p', q) for q in ps] + [('s', s) for s in ss] + [('v', v) for v in vs] par = list(range(len(items))) def f(i): while par[i] != i: par[i] = par[par[i]] i = par[i] return i def touch_sp(s, p): n = max(2, int(math.hypot(s[3] - s[1], s[4] - s[2]) / 0.05)) return s[0] in pad_layers(p) and min( pt_rect(s[1] + (s[3] - s[1]) * k / n, s[2] + (s[4] - s[2]) * k / n, prect(p)) for k in range(n + 1)) < s[5] / 2 + 0.01 for i in range(len(items)): for j in range(i + 1, len(items)): (ka, a), (kb, b) = items[i], items[j] if ka == 'p' and kb == 'p': hit = a[0] == b[0] and a[1] == b[1] # 同一只脚拆成的几块 elif ka == 'v' and kb == 'v': hit = math.hypot(a[0] - b[0], a[1] - b[1]) < 0.61 elif ka == 's' and kb == 's': hit = a[0] == b[0] and seg_seg(a[1:5], b[1:5]) < (a[5] + b[5]) / 2 + 0.01 else: (k1, x), (k2, y) = sorted([(ka, a), (kb, b)], key=lambda z: z[0]) if (k1, k2) == ('p', 's'): hit = touch_sp(y, x) elif (k1, k2) == ('p', 'v'): hit = pt_rect(y[0], y[1], prect(x)) < 0.3 else: # ('s', 'v') hit = pt_seg(y[0], y[1], x[1], x[2], x[3], x[4]) < x[5] / 2 + 0.3 if hit and f(i) != f(j): par[f(i)] = f(j) out = {} for i, (k, it) in enumerate(items): if k == 'p': out.setdefault(f(i), set()).add('%s.%s' % (it[0], it[1])) return [sorted(v) for v in out.values()] def main(): args = [a for a in sys.argv[1:] if not a.startswith('--')] lim = int(sys.argv[sys.argv.index('--list') + 1]) if '--list' in sys.argv else 40 args = [a for a in args if not a.isdigit()] res_file = args[0] if args else '_d16_route.out.json' res = json.load(io.open(os.path.join(HERE, res_file), encoding='utf-8')) brd = json.load(io.open(os.path.join(HERE, '_board_for_nets.json'), encoding='utf-8')) # ⚠ 通孔焊盘在 route_base 里拆出的 L1 障碍副本(脚号带 #t)不是真焊盘,这里读的是原始焊盘表,没有它们 pads = [p for p in json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8')) if p[4] > 0.05 and p[5] > 0.05] npth = json.load(io.open(os.path.join(HERE, '_base_npth.json'), encoding='utf-8')) old_s = [tuple(s[:7]) for s in brd['segs']] old_v = [(v[0], v[1], v[2], v[3] if len(v) > 3 else 0.6) for v in brd['vias']] # d16_rip 的结果带「拆除清单」:这些旧铜下发时会先删掉,不算障碍、也不算连通 rip_s = set(tuple(s[:7]) for s in res.get('ripped_segs', [])) rip_v = set(tuple(v[:3]) for v in res.get('ripped_vias', [])) if rip_s or rip_v: old_s = [s for s in old_s if s not in rip_s] old_v = [v for v in old_v if v[:3] not in rip_v] print(u'(按拆除清单去掉旧铜 %d 段 / %d 孔)' % (len(rip_s), len(rip_v))) new_s = [tuple(s[:7]) for s in res['segs']] new_v = [(v[0], v[1], v[2], 0.6) for v in res['vias']] bad = check_clearance(new_s, new_v, old_s, old_v, pads, npth) print(u'新增 %d 段 / %d 孔;净距 / 窗口问题 %d 条' % (len(new_s), len(new_v), len(bad))) for d, m in bad[:lim]: print(u' %6.3f %s' % (d, m)) all_s, all_v = old_s + new_s, old_v + new_v print(u'连通(按铜,不含铺铜):') left = [] cfg1 = [q[7] for q in pads if q[0] == 'U36' and str(q[1]) == '9'][0] for net in NETS: net = cfg1 if net == '#CFG1' else net g = groups_of(net, pads, all_s, all_v) if len(g) > 1: left.append(net) print(u' %-10s %d 块:%s' % (net, len(g), (u' | '.join(u','.join(x[:4]) for x in g))[:320])) else: print(u' %-10s 连通' % net) refs = set() for p in pads: if p[7] == 'GND' and p[6] != 12: refs.add(p[0]) gnd_new = [p for p in pads if p[7] == 'GND' and p[6] == 2 and p[0] in set(json.load( io.open(os.path.join(HERE, '_base_place_live.json'), encoding='utf-8'))) and p[0] in res.get('pd_refs', [])] if gnd_new: # 按「位号.脚号」合并:一只脚常拆成几块焊盘(U37.1 = 主块 + 小块),接在任一块上都算接上 byk = {} for p in gnd_new: byk.setdefault((p[0], str(p[1])), []).append(p) nov = [u'%s.%s' % k for k, ps in sorted(byk.items()) if not any(any(v[2] == 'GND' and pt_rect(v[0], v[1], prect(p)) < 0.3 for v in all_v) or any(s[6] == 'GND' and s[0] == 2 and seg_rect(s[1:5], prect(p)) < s[5] / 2 + 0.01 for s in all_s) for p in ps)] print(u'新件 GND 贴片脚没接任何线 / 孔(只能靠铺铜):%s' % (u' '.join(nov) or u'无')) print(u'[OK] 可以下发' if not bad and not left else u'[X] 净距 %d 条 / 未连通 %s' % (len(bad), left)) if __name__ == '__main__': main()