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 行删除
+249
查看文件
@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
u"""载板侧键触点盘 J41~J60 的**定点重摆 + GND 盘扇出**(2026-09-07,顶针改中框式焊盘之后)。
/c/Python314/python .claude/pcb2_keypads.py # 只算:目标坐标 + GND 扇出候选
/c/Python314/python .claude/pcb2_keypads.py --push # 只摆这 20 个盘(先删 GND 盘上的旧短线/过孔),读回核对
/c/Python314/python .claude/pcb2_keypads.py --gnd # 重抓障碍表 -> 给 5 个 GND 盘各打一个过孔 + 短线(净距校核过才下发)
## 为什么不用 `pcb2_place.py --push`
它会重摆全部 218 个元件;导入变更后只有这 20 个盘换了封装、坐标要按新 `POGO_XY` 挪,
其它元件一个都不该碰(碰了就要整板重布)。坐标仍从 `pcb2_place.plan()` 取 —— 事实来源不变。
## GND 盘的扇出
15 根 POGO*_* 信号由 `pcb2_route_sig.py --net ...` 重布(它整网删掉重画);
GND 是 `PRE_ONLY`,布线器不给它拉线,所以 5 个 GND 盘(J44/J48/J52/J56/J60)在这里手工扇出:
盘心 -> 0.30 mm 顶层短线 -> Ø0.61/0.305 过孔,落到 Inner2 地平面。
过孔放在盘的 **u− 侧**(触点菱形里 GND 本来就在最靠机身侧边那一角,那一侧没有别的盘),
候选位置逐个过净距校核(对焊盘 / 过孔 / 两层导线,判据同 `pcb2_route_dp.CLR`)。
"""
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, HERE)
from eda import run
import pcb2_place as P
import pcb2_route_dp as D
MIL = P.MIL
PCB = D.PCB
W_STUB = 0.30
VIA_D, VIA_H, CLR = D.VIA_D, D.VIA_H, D.CLR
HOLE2HOLE = 0.30
STATE = os.path.join(HERE, '_pcb2_keypads_state.json')
def targets():
pos = P.plan()
out = {}
for n in P.SITES:
d = P.desig(n)
for i, ref in enumerate(d['pogo']):
out[ref] = {'xy': pos[ref][:2], 'rot': pos[ref][2], 'site': n, 'tag': P.POGO_TAG[i]}
return out
def gnd_refs():
return [P.desig(n)['pogo'][3] for n in P.SITES] # POGO_XY[3] = GND
def call(js, timeout=300):
r = run("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PCB + js, timeout=timeout)
if not r.get('success'):
sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600])
return r['result']
JS_STUB_DEL = u"""
// 删掉挂在 C(盘心表)上的 GND 顶层短线(端点离盘心 <= 1.05)以及短线另一端的 GND 过孔。调用方先声明 M / C。
let dl = 0, dv = 0;
const vias = await eda.pcb_PrimitiveVia.getAll();
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
if (l.getState_Layer() !== 1 || l.getState_Net() !== 'GND') continue;
const e = [[l.getState_StartX()/M, l.getState_StartY()/M], [l.getState_EndX()/M, l.getState_EndY()/M]];
let hit = -1;
for (const [cx, cy] of C) for (let k = 0; k < 2; k++) if (Math.hypot(e[k][0]-cx, e[k][1]-cy) <= 1.05) hit = 1 - k;
if (hit < 0) continue;
for (const v of vias) if (v.getState_Net() === 'GND' && Math.hypot(v.getState_X()/M - e[hit][0], v.getState_Y()/M - e[hit][1]) < 0.05) { await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; }
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;
}
"""
def push_pos():
T = targets()
plan = [[ref, round(t['xy'][0] * MIL, 2), round(t['xy'][1] * MIL, 2), t['rot']] for ref, t in sorted(T.items())]
r = call(u"""
const M = %f, PLAN = %s, GREF = %s;
const map = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) map[c.getState_Designator()] = c;
const old = {};
for (const [d] of PLAN) { const c = map[d]; if (c) old[d] = [c.getState_X()/M, c.getState_Y()/M]; }
const C = GREF.map(d => old[d]).filter(Boolean);
""" % (MIL, json.dumps(plan), json.dumps(gnd_refs())) + JS_STUB_DEL + u"""
let n = 0;
for (const [d, x, y, rot] of PLAN) {
const c = map[d]; if (!c) continue;
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {layer: 1});
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot});
n++;
}
await eda.pcb_Document.save();
return {n, old, dl, dv};
""")
print(u'已摆 %d 个盘;删掉旧 GND 短线 %d 条 / 过孔 %d 个' % (r['n'], r['dl'], r['dv']))
json.dump(r['old'], io.open(STATE, 'w', encoding='utf-8'))
verify()
def verify():
T = targets()
r = call(u"""
const M = %f, W = %s, out = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator(); if (!W.includes(d)) continue;
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
const p = pins[0];
out[d] = [c.getState_X()/M, c.getState_Y()/M, c.getState_Layer(), p.x/M, p.y/M, p.net, +p.pad[1]/M];
}
return out;
""" % (MIL, json.dumps(sorted(T))))
bad = 0
for ref, t in sorted(T.items()):
x, y, lay, px, py, net, dia = r[ref]
dd = math.hypot(px - t['xy'][0], py - t['xy'][1])
ok = dd < 0.01 and lay == 1 and abs(dia - P.POGO_PAD_D) < 0.02
bad += not ok
print(u' %s %-4s site%d %-8s 焊盘 (%.3f, %.3f) 目标 (%.3f, %.3f) 差 %.4f L%d Ø%.2f %s'
% (u'✔' if ok else u'✘', ref, t['site'], t['tag'], px, py, t['xy'][0], t['xy'][1], dd, lay, dia, net))
if bad:
sys.exit(u'!! %d 个盘没摆到位' % bad)
print(u'20 个盘全部到位,Ø%.1f 顶层' % P.POGO_PAD_D)
# ═══════════════ GND 扇出 ═══════════════
def _clear(x, y, r, lay, pads, vias, lines, skip_ref):
u"""圆 (x,y,r) 在 lay 层(12 = 双层)离所有异网铜是否 >= CLR;同时对任何孔保 HOLE2HOLE。"""
lays = {1, 2} if lay == 12 else {lay}
for q in pads:
if q['ref'] == skip_ref:
continue
ql = {1, 2} if q['lay'] == 12 else {q['lay']}
if not (ql & lays):
continue
d = D.seg_rect((x, y), (x, y), q['x'], q['y'], q['hw'], q['hh'])
need = r + CLR if q['net'] != 'GND' else 0.0
if q['lay'] == 12: # 通孔:还要孔到孔
need = max(need, r + HOLE2HOLE) if lay == 12 else need
if d < need:
return False, u'焊盘 %s.%s(%s) %.2f' % (q['ref'], q['num'], q['net'], d)
for v in vias:
d = math.hypot(v['x'] - x, v['y'] - y)
need = r + v['r'] + CLR if v['net'] != 'GND' else 0.0
if lay == 12:
need = max(need, r + v['r'] + HOLE2HOLE)
if d < need:
return False, u'过孔(%s) %.2f' % (v['net'], d)
for l in lines:
if l['lay'] not in lays or l['net'] == 'GND':
continue
d = D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], x, y)
if d < r + l['w'] / 2 + CLR:
return False, u'导线(%s L%d) %.2f' % (l['net'], l['lay'], d)
return True, ''
def _seg_clear(a, b, w, pads, vias, lines, skip_ref):
for q in pads:
if q['ref'] == skip_ref or q['net'] == 'GND':
continue
if q['lay'] not in (1, 12):
continue
if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < w / 2 + CLR:
return False, u'焊盘 %s.%s(%s)' % (q['ref'], q['num'], q['net'])
for v in vias:
if v['net'] == 'GND':
continue
if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < w / 2 + v['r'] + CLR:
return False, u'过孔(%s)' % v['net']
for l in lines:
if l['lay'] != 1 or l['net'] == 'GND':
continue
if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < w / 2 + l['w'] / 2 + CLR:
return False, u'导线(%s)' % l['net']
return True, ''
def plan_gnd(pads, vias, lines):
T = targets()
out = {}
for ref in gnd_refs():
t = T[ref]
x, y = t['xy']
rho = P.SITES[t['site']][2]
ux = -1.0 if rho == 0 else 1.0 # 本体 u− 方向在板系里的 x 分量
cands = []
for dist in (1.85, 2.0, 2.2, 2.4):
for (dx, dy) in ((ux, 0.0), (ux * 0.7071, 0.7071), (ux * 0.7071, -0.7071), (0.0, 1.0), (0.0, -1.0)):
cands.append((x + dx * dist, y + dy * dist))
chosen = None
why = []
for vx, vy in cands:
vx, vy = round(vx, 3), round(vy, 3)
ok, msg = _clear(vx, vy, VIA_D / 2, 12, pads, vias, lines, ref)
if not ok:
why.append(u'(%.2f,%.2f) 孔撞 %s' % (vx, vy, msg)); continue
ok, msg = _seg_clear((x, y), (vx, vy), W_STUB, pads, vias, lines, ref)
if not ok:
why.append(u'(%.2f,%.2f) 线撞 %s' % (vx, vy, msg)); continue
chosen = (vx, vy)
break
if chosen is None:
print(u'!! %s 找不到扇出位置:' % ref)
for w in why[:8]:
print(u' ' + w)
sys.exit(1)
out[ref] = {'pad': (x, y), 'via': chosen}
print(u' %s site%d 盘 (%.2f, %.2f) -> 过孔 (%.2f, %.2f) 距 %.2f' % (ref, t['site'], x, y, chosen[0], chosen[1], math.hypot(chosen[0] - x, chosen[1] - y)))
return out
def push_gnd():
D.dump()
pads, vias, lines = D.geom()
plan = plan_gnd(pads, vias, lines)
centers = [list(v['pad']) for v in plan.values()]
segs = [['GND', 1, v['pad'][0], v['pad'][1], v['via'][0], v['via'][1], W_STUB] for v in plan.values()]
vs = [['GND', v['via'][0], v['via'][1]] for v in plan.values()]
r = call(u"const M = %f; const C = %s;" % (MIL, json.dumps(centers)) + JS_STUB_DEL + u"""
const SEGS = %s, VIAS = %s;
let nl = 0, nv = 0;
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) { await eda.pcb_PrimitiveLine.create(net, lay, x0 * M, y0 * M, x1 * M, y1 * M, w * M); nl++; }
for (const [net, x, y] of VIAS) { await eda.pcb_PrimitiveVia.create(net, x * M, y * M, %f * M, %f * M); nv++; }
await eda.pcb_Document.save();
return {dl, dv, nl, nv};
""" % (json.dumps(segs), json.dumps(vs), VIA_H, VIA_D))
print(u'GND 扇出下发:删旧 %d 线 / %d 孔,新画 %d 线 / %d 孔' % (r['dl'], r['dv'], r['nl'], r['nv']))
if __name__ == '__main__':
T = targets()
for ref, t in sorted(T.items()):
print(u' %-4s site%d %-8s -> (%.3f, %.3f) rot %d' % (ref, t['site'], t['tag'], t['xy'][0], t['xy'][1], t['rot']))
if '--push' in sys.argv:
push_pos()
elif '--verify' in sys.argv:
verify()
if '--gnd' in sys.argv:
push_gnd()
elif '--push' not in sys.argv and '--verify' not in sys.argv:
if os.path.exists(D.GEOM):
print(u'\nGND 扇出候选(按现有障碍表,不下发):')
plan_gnd(*D.geom())