# -*- 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())