# -*- coding: utf-8 -*- u"""对**布线器算出来的几何**自算净距 —— 不过桥接、不等 DRC。 /c/Python314/python .claude/r2/router_check.py [--list N] ## 为什么要有这个 EasyEDA 的 DRC 一轮要 ~10 分钟(下发 → 重铺 → 读面板),而且**明细行读不回来** (`drc_read.py` 能读到分类计数,读不到每一条的对象和坐标)。 靠它迭代等于每改一行代码等十分钟、还看不到是哪两个东西撞了。 ⇒ 这里按**真实几何**(线段-线段 / 线段-矩形 / 圆-圆 …)自算, 判据和设计规则一致(净距 `MIN_CLR`)。布线器的占用图是 0.25 mm 网格上的 轴对齐盒子,是个**近似**;这个检查器算的是精确距离,两者的差就是 DRC 会报的那些。 ⚠ 判据只覆盖「铜与铜」。**铺铜(pour)与过孔/钻孔的关系不在这里** —— 那一支归 EasyEDA 自己按规则裁,DRC 里显示成「到 过孔 / 到 钻孔」。 """ 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, os.path.dirname(HERE)) sys.path.insert(0, HERE) import pcb_blade as B # noqa: E402 import router as R # noqa: E402 MIN_CLR = 0.15 # 设计规则里的安全间距 def seg_seg(a, b): u"""两条线段的最小距离。""" (x1, y1, x2, y2), (x3, y3, x4, y4) = a, b 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)) d1, d2 = x2 - x1, y2 - y1 d3, d4 = 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 seg_rect(s, r): u"""线段到轴对齐矩形的最小距离(相交为 0)。""" x1, y1, x2, y2 = s rx0, ry0, rx1, ry1 = r for (px, py) in ((x1, y1), (x2, y2)): if rx0 <= px <= rx1 and ry0 <= py <= ry1: return 0.0 e = [(rx0, ry0, rx1, ry0), (rx1, ry0, rx1, ry1), (rx1, ry1, rx0, ry1), (rx0, ry1, rx0, ry0)] return min(seg_seg(s, q) for q in e) def pt_rect(px, py, r): dx = max(r[0] - px, 0.0, px - r[2]) dy = max(r[1] - py, 0.0, py - r[3]) return math.hypot(dx, dy) def main(): cache = json.load(io.open(os.path.join(HERE, '_blade_routes.json'), encoding='utf-8')) segs, vias = cache['segs'], cache['vias'] pads = R.load_pads() holes = B.phone_holes() # 缝合过孔也算进来 import pcb_route as PR stitch = [(x, y, 'GND') for (x, y) in PR.stitch_points()] allv = [(v[0], v[1], v[2]) for v in vias] + stitch bad = {} def hit(kind, msg): bad.setdefault(kind, []).append(msg) # ── 线 ↔ 线(同层异网)── bylay = {1: [], 2: []} for (l, x0, y0, x1, y1, w, net) in segs: if l in bylay: bylay[l].append((x0, y0, x1, y1, w, net)) for l, lst in bylay.items(): for i in range(len(lst)): a = lst[i] for j in range(i + 1, len(lst)): b = lst[j] if a[5] == b[5]: continue if max(a[0], a[2]) + 3 < min(b[0], b[2]) or max(b[0], b[2]) + 3 < min(a[0], a[2]): continue if max(a[1], a[3]) + 3 < min(b[1], b[3]) or max(b[1], b[3]) + 3 < min(a[1], a[3]): continue d = seg_seg(a[:4], b[:4]) - (a[4] + b[4]) / 2.0 if d < MIN_CLR: hit(u'线-线', u'%s / %s 净距 %.3f @ L%s (%.1f,%.1f)' % (a[5], b[5], d, l, a[0], a[1])) # ── 线 ↔ 焊盘(同层异网)── for (l, x0, y0, x1, y1, w, net) in segs: for p in pads: if p[6] != l or p[7] == net: continue d = seg_rect((x0, y0, x1, y1), R.pad_rect(p)) - w / 2.0 if d < MIN_CLR: hit(u'线-焊盘', u'%s 线 vs %s.%s(%s) 净距 %.3f @ L%s' % (net, p[0], p[1], p[7], d, l)) # ── 线 ↔ 过孔(异网)── for (l, x0, y0, x1, y1, w, net) in segs: for (vx, vy, vnet) in allv: if vnet == net: continue d = seg_seg((x0, y0, x1, y1), (vx, vy, vx, vy)) - w / 2.0 - R.VIA_D / 2.0 if d < MIN_CLR: hit(u'线-过孔', u'%s 线 vs %s 过孔(%.1f,%.1f) 净距 %.3f' % (net, vnet, vx, vy, d)) # ── 过孔 ↔ 焊盘 / 过孔 ↔ 过孔(异网)── for k, (vx, vy, vnet) in enumerate(allv): for p in pads: if p[7] == vnet: continue d = pt_rect(vx, vy, R.pad_rect(p)) - R.VIA_D / 2.0 if d < MIN_CLR: hit(u'过孔-焊盘', u'%s 过孔(%.1f,%.1f) vs %s.%s(%s) 净距 %.3f' % (vnet, vx, vy, p[0], p[1], p[7], d)) for (wx_, wy_, wnet) in allv[k + 1:]: if wnet == vnet: continue d = math.hypot(vx - wx_, vy - wy_) - R.VIA_D if d < MIN_CLR: hit(u'过孔-过孔', u'%s vs %s (%.1f,%.1f) 净距 %.3f' % (vnet, wnet, vx, vy, d)) # ── 定位孔(NPTH)↔ 铜 ── for hname, (hx, hy) in holes.items(): hr = B.HOLE_D / 2.0 for (l, x0, y0, x1, y1, w, net) in segs: d = seg_seg((x0, y0, x1, y1), (hx, hy, hx, hy)) - w / 2.0 - hr if d < MIN_CLR: hit(u'钻孔-线', u'%s vs %s 线 净距 %.3f' % (hname, net, d)) for (vx, vy, vnet) in allv: d = math.hypot(vx - hx, vy - hy) - hr - R.VIA_D / 2.0 if d < MIN_CLR: hit(u'钻孔-过孔', u'%s vs %s 过孔(%.1f,%.1f) 净距 %.3f' % (hname, vnet, vx, vy, d)) for p in pads: d = pt_rect(hx, hy, R.pad_rect(p)) - hr if d < MIN_CLR: hit(u'钻孔-焊盘', u'%s vs %s.%s 净距 %.3f' % (hname, p[0], p[1], d)) n = sum(len(v) for v in bad.values()) print(u'自算净距(判据 %.2f mm):%d 条' % (MIN_CLR, n)) for k in sorted(bad, key=lambda k: -len(bad[k])): print(u' %-8s %d' % (k, len(bad[k]))) lim = 8 for a in sys.argv: if a.startswith('--list'): lim = int(sys.argv[sys.argv.index(a) + 1]) for k in sorted(bad, key=lambda k: -len(bad[k])): print(u'── %s ──' % k) for m in bad[k][:lim]: print(u' ' + m) return n if __name__ == '__main__': sys.exit(1 if main() else 0)