R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""给风扇那一窝重排后的 **GND 脚补过孔**(扎进 L2/L3 整层地)。
|
||||
|
||||
/c/Python314/python .claude/r2/fan_gnd_vias.py # 只算 + 报告
|
||||
/c/Python314/python .claude/r2/fan_gnd_vias.py --push # 下发
|
||||
|
||||
## 为什么要单独一个脚本
|
||||
|
||||
`route_base.plane_vias()` 是**全板口径**(给所有 GND 贴片焊盘打孔),直接调会在整块板上
|
||||
重打一遍孔;而 `repair_base.py` 只补信号网、根本不做平面网扇出。
|
||||
于是 2026-09-12 重排之后实测:22 件里有 **9 只 GND 脚新位置底下没有孔**
|
||||
(原来的孔还留在旧脚位上,成了孤立的缝合孔)。这种漏法 DRC 不报、
|
||||
`board_check` 也只会说「GND 靠铜面成网」,要靠这一支单独查。
|
||||
|
||||
落孔点:焊盘中心优先;中心放不下就在焊盘外圈按 0.25 网格找一个既不压异网铜、
|
||||
也不压别的孔的点,离本焊盘边不超过 `MAX_OUT`(太远就不是这只脚的回流孔了)。
|
||||
"""
|
||||
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 fp_edge # noqa: E402
|
||||
import place_base as PL # noqa: E402
|
||||
|
||||
MIL = fp_edge.MIL
|
||||
PCB = '7b9524b6aaf6f235'
|
||||
VIA_D, VIA_HOLE = 0.6, 0.3
|
||||
CLR = 0.20 # 孔到异网铜
|
||||
DRILL = 0.25 # 钻孔到钻孔
|
||||
MAX_OUT = 1.2 # 落孔点离焊盘边最远多少
|
||||
STEP = 0.125
|
||||
|
||||
|
||||
def seg_d(px, py, ax, ay, bx, by):
|
||||
dx, dy = bx - ax, by - ay
|
||||
L2 = dx * dx + dy * dy
|
||||
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L2))
|
||||
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
|
||||
|
||||
|
||||
def main():
|
||||
cu = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
|
||||
pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8'))
|
||||
segs = cu['segs']
|
||||
vias = [(v[0], v[1], v[2]) for v in cu['vias']]
|
||||
gnd = [p for p in pads if p[0] in PL.FAN_REFS and p[7] == 'GND']
|
||||
print(u'这一窝的 GND 脚:%d 只' % len(gnd))
|
||||
|
||||
def free(vx, vy, net='GND'):
|
||||
for s in segs:
|
||||
if s[6] == net:
|
||||
continue
|
||||
if seg_d(vx, vy, s[1], s[2], s[3], s[4]) < VIA_D / 2 + s[5] / 2 + CLR:
|
||||
return False
|
||||
for p in pads:
|
||||
if p[7] == net or p[6] == 12:
|
||||
continue
|
||||
dx = abs(vx - p[2]) - p[4] / 2 - VIA_D / 2
|
||||
dy = abs(vy - p[3]) - p[5] / 2 - VIA_D / 2
|
||||
d = math.hypot(max(dx, 0), max(dy, 0)) if (dx > 0 and dy > 0) else max(dx, dy)
|
||||
if d < CLR:
|
||||
return False
|
||||
for v in vias:
|
||||
if math.hypot(vx - v[0], vy - v[1]) < VIA_HOLE + DRILL:
|
||||
return False
|
||||
return True
|
||||
|
||||
made, miss = [], []
|
||||
for p in gnd:
|
||||
px, py, w, h = p[2], p[3], p[4], p[5]
|
||||
if any(math.hypot(px - v[0], py - v[1]) < max(w, h) / 2 + 0.3 for v in vias if v[2] == 'GND'):
|
||||
continue # 脚上已经有孔
|
||||
cand = [(0.0, px, py)]
|
||||
k = 1
|
||||
while k * STEP <= MAX_OUT + max(w, h) / 2:
|
||||
r = k * STEP
|
||||
for ang in range(0, 360, 15):
|
||||
vx = round(px + r * math.cos(math.radians(ang)), 4)
|
||||
vy = round(py + r * math.sin(math.radians(ang)), 4)
|
||||
dx, dy = abs(vx - px) - w / 2, abs(vy - py) - h / 2
|
||||
out = math.hypot(max(dx, 0), max(dy, 0))
|
||||
if out <= MAX_OUT:
|
||||
cand.append((r, vx, vy))
|
||||
k += 1
|
||||
got = None
|
||||
for _r, vx, vy in cand:
|
||||
if free(vx, vy):
|
||||
got = (vx, vy)
|
||||
break
|
||||
if got:
|
||||
made.append([got[0], got[1], 'GND'])
|
||||
vias.append((got[0], got[1], 'GND'))
|
||||
print(u' %s.%s (%.2f,%.2f) → 孔 (%.2f,%.2f)' % (p[0], p[1], px, py, got[0], got[1]))
|
||||
else:
|
||||
miss.append(u'%s.%s (%.2f,%.2f)' % (p[0], p[1], px, py))
|
||||
print(u'\n要补 %d 个孔;找不到落点 %d 只' % (len(made), len(miss)))
|
||||
for t in miss:
|
||||
print(u' ⛔ ' + t)
|
||||
if not made or '--push' not in sys.argv:
|
||||
return
|
||||
from eda import run
|
||||
r = run("""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,2500));
|
||||
const V = %s, MIL = %s;
|
||||
let n = 0;
|
||||
for (const [x, y, net] of V) {
|
||||
try { await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, %s*MIL, %s*MIL); n++; } catch (e) {}
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
return {made: n, now: (await eda.pcb_PrimitiveVia.getAll()||[]).length};
|
||||
""" % (PCB, json.dumps(made), MIL, VIA_HOLE, VIA_D), timeout=600)
|
||||
print(json.dumps(r.get('result'), ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户