- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
179 行
6.9 KiB
Python
179 行
6.9 KiB
Python
# -*- 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)
|