R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -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()
|
||||
在新工单中引用
屏蔽一个用户