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