124 行
4.8 KiB
Python
124 行
4.8 KiB
Python
# -*- 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()
|