R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+213
查看文件
@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
u"""底板:把**封装自带、API 读不出来的钻孔**从内层铺铜的反焊盘里反解出来(照 `.claude/pcb2_npth.py`,skill §9u)。
/c/Python314/python .claude/r2/npth_base.py # 打印
/c/Python314/python .claude/r2/npth_base.py --write # 存 _base_npth.json(布线 / 摆位读它)
为什么要做:DRC 报「挖槽区域 到 导线 0mil」—— 对象 e149 / e150(五个 `PCIE-SMD_PCIE-64P11L` 插槽都有)、
e55 / e56(J23 USB-C)。这些是封装里的非金属化定位孔,`route_base` 的障碍表里没有它们,
所以 USB1 D±、+5V 等线直接从孔上走过去 —— **下单钻孔会把线切断**。
做法:L2 / L3 整层铜面在每个孔处都挖了反焊盘;拟合圆弧,用过孔的反焊盘标定净距,
圆心对不上任何过孔 / 焊盘的就是封装自带的孔。⚠ 前提是 L2/L3 已经填了铜(`pour_base --push` 之后)。
"""
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)
from eda import run # noqa: E402
import pcb_base as B # noqa: E402
OUT = os.path.join(HERE, '_base_npth.json')
U10 = 3.9370078740157477 # getState_PourFills() 坐标单位 10 mil
MINPTS, TOL = 10, 0.010
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1500));
const byId = {};
for (const p of await eda.pcb_PrimitivePour.getAll())
byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''), p.getState_Layer()];
const rings = [];
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
const info = byId[q.getState_PourPrimitiveId()]; if (!info || (info[1] !== 15 && info[1] !== 16)) continue;
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
for (const s of f) { if (s.fill === false) continue;
for (const r of ((s.path && s.path.complexPolygon) || [])) rings.push(r); }
}
const vias = ((await eda.pcb_PrimitiveVia.getAll()) || []).map(v => [v.x / %s, v.y / %s, v.diameter / %s / 2]);
const pads = ((await eda.pcb_PrimitivePad.getAll()) || []).map(p => [p.x / %s, p.y / %s]);
return {rings, vias, pads};
"""
def ring_to_poly(ring):
u"""一个环 -> 折线点列(mm)。坐标单位 10 mil;'ARC' 后第一个数是角度(§9j 两个坑)。按 ≤5° 细分。"""
pts, i, cur = [], 0, None
while i < len(ring):
v = ring[i]
if v == 'L':
i += 1
continue
if v == 'ARC':
ang, ex, ey = ring[i + 1], ring[i + 2] / U10, ring[i + 3] / U10
i += 4
x0, y0 = cur
th = math.radians(ang)
dx, dy = ex - x0, ey - y0
c = math.hypot(dx, dy)
if c < 1e-9 or abs(math.sin(th / 2.0)) < 1e-9:
pts.append((ex, ey))
cur = (ex, ey)
continue
R = c / (2.0 * math.sin(th / 2.0))
d = math.sqrt(max(R * R - (c / 2.0) ** 2, 0.0)) * (1 if R > 0 else -1)
ox, oy = (x0 + ex) / 2.0 - dy / c * d, (y0 + ey) / 2.0 + dx / c * d
a0 = math.atan2(y0 - oy, x0 - ox)
n = max(2, int(abs(ang) / 5.0) + 1)
for k in range(1, n + 1):
a = a0 + th * k / n
pts.append((ox + abs(R) * math.cos(a), oy + abs(R) * math.sin(a)))
cur = (ex, ey)
continue
cur = (ring[i] / U10, ring[i + 1] / U10)
pts.append(cur)
i += 2
return pts
def circle3(a, b, c):
ax, ay = a
bx, by = b
cx, cy = c
d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
if abs(d) < 1e-9:
return None
ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d
uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d
return (ux, uy, math.hypot(ax - ux, ay - uy))
def arcs(poly):
out, n, i = [], len(poly), 0
while i < n - 2:
c = circle3(poly[i], poly[i + 1], poly[i + 2])
if not c or c[2] > 5.0:
i += 1
continue
j = i + 3
while j < n and abs(math.hypot(poly[j][0] - c[0], poly[j][1] - c[1]) - c[2]) < TOL:
j += 1
if j - i >= MINPTS:
pts = poly[i:j]
out.append(circle3(pts[0], pts[len(pts) // 2], pts[-1]) or c)
i = j
else:
i += 1
return out
def main():
r = run(JS % (B.PCB, B.MIL, B.MIL, B.MIL, B.MIL, B.MIL), timeout=300)
if not r.get('success'):
sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400])
d = r['result']
if not d['rings']:
sys.exit(u'L2/L3 没有填充 —— 先 `pour_base --push`')
# ⚠ EasyEDA 的 y 轴:板上图元 y 与铺铜环 y 的符号要对上(先按原样比,对不上再翻)
found = []
for rg in d['rings']:
pl = ring_to_poly(rg)
if len(pl) >= 3:
found += arcs(pl)
uniq = {}
for cx, cy, rr in found:
uniq[(round(cx, 2), round(cy, 2), round(rr, 2))] = (cx, cy, rr)
circles = list(uniq.values())
vias, pads = d['vias'], d['pads']
for flip in (1, -1):
clr = sorted(rr - v[2] for cx, cy, rr in circles for v in vias
if math.hypot(cx - v[0], flip * cy - v[1]) < 0.02)
if len(clr) > 10:
break
if len(clr) <= 10:
sys.exit(u'过孔反焊盘对不上(%d 个)—— 坐标系没对齐' % len(clr))
C = clr[len(clr) // 2]
print(u'圆弧 %d 段;净距标定 %d 个过孔,中位数 %.4f mm(离散度 %.4f),y 符号 %+d'
% (len(circles), len(clr), C, clr[-1] - clr[0], flip))
unknown = []
for cx, cy, rr in circles:
y = flip * cy
if any(math.hypot(cx - v[0], y - v[1]) < 0.05 for v in vias):
continue
hr = rr - C
if not 0.1 <= hr <= 2.0:
continue
near = min(math.hypot(cx - p[0], y - p[1]) for p in pads)
if near < 0.05:
continue # 通孔焊盘自己的孔
unknown.append((round(cx, 3), round(y, 3), round(hr, 3), round(near, 2)))
# 同一个孔在 L2 / L3 各一份,按 0.05 合并
merged = []
for u in sorted(unknown, key=lambda t: (t[1], t[0])):
if not any(math.hypot(u[0] - m[0], u[1] - m[1]) < 0.05 for m in merged):
merged.append(u)
# 槽孔焊盘(J1 DC 座、J23 外壳腿)的反焊盘是跑道形:两端半圆**同半径、同一条轴线上、相距 ≤ 2.6**,成对出现。
# 真的圆孔(插槽定位柱 x 44.15 / 67.30、J23 两个 Ø0.6)都是孤立的。
def paired(u):
return any(m is not u and abs(m[2] - u[2]) < 0.02 and
((abs(m[0] - u[0]) < 0.01 and abs(m[1] - u[1]) < 2.6) or
(abs(m[1] - u[1]) < 0.01 and abs(m[0] - u[0]) < 2.6)) for m in merged)
holes = [u for u in merged if not paired(u)]
print(u'对不上任何过孔 / 焊盘中心的圆弧 %d 个;去掉成对的槽孔焊盘端头后,真的圆孔 %d 个:'
% (len(merged), len(holes)))
for cx, cy, hr, near in holes:
print(u' (%7.3f, %7.3f) Ø%.3f 离最近焊盘 %.2f' % (cx, cy, hr * 2, near))
if '--write' in sys.argv:
# [x, y, 孔半径, 反焊盘半径]:反焊盘半径 = 孔半径 + 实测铜面净距,布线禁区按它取(不必知道 NPTH 用的是哪条净距规则)
json.dump([[h[0], h[1], h[2], round(h[2] + C, 3)] for h in holes], open(OUT, 'w', encoding='utf-8'))
print(u'已写入 %s' % os.path.basename(OUT))
def _seg_d(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
L = dx * dx + dy * dy
t = 0.0 if L < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def check_routes():
u"""校验布线缓存(服务器结果):没有线 / 孔进 NPTH 的反焊盘圈。纯几何,不布线。"""
holes = json.load(open(OUT, encoding='utf-8'))
d = json.load(open(os.path.join(HERE, '_base_routes.json'), encoding='utf-8'))
vr = 0.3
hit = {}
for s in d['segs']:
for h in holes:
g = _seg_d(h[0], h[1], s[1], s[2], s[3], s[4]) - s[5] / 2.0 - h[3]
if g < 0:
hit.setdefault((s[6], u'L%d 线' % s[0]), []).append((h[0], h[1], g))
for v in d['vias']:
for h in holes:
g = math.hypot(v[0] - h[0], v[1] - h[1]) - vr - h[3]
if g < 0:
hit.setdefault((v[2], u'孔'), []).append((h[0], h[1], g))
if not hit:
print(u'[OK] 布线缓存 %d 段 / %d 孔,没有进 %d 个 NPTH 的反焊盘圈' % (len(d['segs']), len(d['vias']), len(holes)))
return True
print(u'[X] %d 处压 NPTH:' % len(hit))
for (net, kind), v in sorted(hit.items()):
print(u' %-12s %s %s 最深 %.3f' % (net, kind, sorted(set((a, b) for a, b, _g in v)), min(g for *_x, g in v)))
return False
if __name__ == '__main__':
if '--check-routes' in sys.argv:
sys.exit(0 if check_routes() else 1)
main()