R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发

- 规则:铜层 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
这个提交包含在:
zhou yang
2026-09-10 21:02:10 +08:00
共同撰写人 Claude Opus 5
父节点 18d3d69856
当前提交 efcba30a0c
共修改 280 个文件,包含 92007 行新增和 15053 行删除
+178
查看文件
@@ -0,0 +1,178 @@
# -*- 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)