# -*- coding: utf-8 -*- u"""底板:把**封装自带、API 读不出来的钻孔**从内层铺铜的反焊盘里反解出来(照 `.claude/pcb2_npth.py`,skill §9u)。 /c/Python314/python .claude/r2/npth_base.py # 打印 /c/Python314/python .claude/r2/npth_base.py --write # 存 _base_npth.json(布线 / 摆位读它) 为什么要做:DRC 报「挖槽区域 到 导线 0mil」—— 对象 e149 / e150(五个 `PCIE-SMD_PCIE-64P11L` 插槽都有)、 e55 / e56(J23 USB-C)。这些是封装里的非金属化定位孔,`route_base` 的障碍表里没有它们, 所以 USB1 D±、+5V 等线直接从孔上走过去 —— **下单钻孔会把线切断**。 做法:L2 / L3 整层铜面在每个孔处都挖了反焊盘;拟合圆弧,用过孔的反焊盘标定净距, 圆心对不上任何过孔 / 焊盘的就是封装自带的孔。⚠ 前提是 L2/L3 已经填了铜(`pour_base --push` 之后)。 """ 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) from eda import run # noqa: E402 import pcb_base as B # noqa: E402 OUT = os.path.join(HERE, '_base_npth.json') U10 = 3.9370078740157477 # getState_PourFills() 坐标单位 10 mil MINPTS, TOL = 10, 0.010 JS = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1500)); const byId = {}; for (const p of await eda.pcb_PrimitivePour.getAll()) byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''), p.getState_Layer()]; const rings = []; for (const q of await eda.pcb_PrimitivePoured.getAll()) { const info = byId[q.getState_PourPrimitiveId()]; if (!info || (info[1] !== 15 && info[1] !== 16)) continue; let f = []; try { f = q.getState_PourFills() || []; } catch (e) {} for (const s of f) { if (s.fill === false) continue; for (const r of ((s.path && s.path.complexPolygon) || [])) rings.push(r); } } const vias = ((await eda.pcb_PrimitiveVia.getAll()) || []).map(v => [v.x / %s, v.y / %s, v.diameter / %s / 2]); const pads = ((await eda.pcb_PrimitivePad.getAll()) || []).map(p => [p.x / %s, p.y / %s]); return {rings, vias, pads}; """ def ring_to_poly(ring): u"""一个环 -> 折线点列(mm)。坐标单位 10 mil;'ARC' 后第一个数是角度(§9j 两个坑)。按 ≤5° 细分。""" pts, i, cur = [], 0, None while i < len(ring): v = ring[i] if v == 'L': i += 1 continue if v == 'ARC': ang, ex, ey = ring[i + 1], ring[i + 2] / U10, ring[i + 3] / U10 i += 4 x0, y0 = cur th = math.radians(ang) dx, dy = ex - x0, ey - y0 c = math.hypot(dx, dy) if c < 1e-9 or abs(math.sin(th / 2.0)) < 1e-9: pts.append((ex, ey)) cur = (ex, ey) continue R = c / (2.0 * math.sin(th / 2.0)) d = math.sqrt(max(R * R - (c / 2.0) ** 2, 0.0)) * (1 if R > 0 else -1) ox, oy = (x0 + ex) / 2.0 - dy / c * d, (y0 + ey) / 2.0 + dx / c * d a0 = math.atan2(y0 - oy, x0 - ox) n = max(2, int(abs(ang) / 5.0) + 1) for k in range(1, n + 1): a = a0 + th * k / n pts.append((ox + abs(R) * math.cos(a), oy + abs(R) * math.sin(a))) cur = (ex, ey) continue cur = (ring[i] / U10, ring[i + 1] / U10) pts.append(cur) i += 2 return pts def circle3(a, b, c): ax, ay = a bx, by = b cx, cy = c d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) if abs(d) < 1e-9: return None ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d return (ux, uy, math.hypot(ax - ux, ay - uy)) def arcs(poly): out, n, i = [], len(poly), 0 while i < n - 2: c = circle3(poly[i], poly[i + 1], poly[i + 2]) if not c or c[2] > 5.0: i += 1 continue j = i + 3 while j < n and abs(math.hypot(poly[j][0] - c[0], poly[j][1] - c[1]) - c[2]) < TOL: j += 1 if j - i >= MINPTS: pts = poly[i:j] out.append(circle3(pts[0], pts[len(pts) // 2], pts[-1]) or c) i = j else: i += 1 return out def main(): r = run(JS % (B.PCB, B.MIL, B.MIL, B.MIL, B.MIL, B.MIL), timeout=300) if not r.get('success'): sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400]) d = r['result'] if not d['rings']: sys.exit(u'L2/L3 没有填充 —— 先 `pour_base --push`') # ⚠ EasyEDA 的 y 轴:板上图元 y 与铺铜环 y 的符号要对上(先按原样比,对不上再翻) found = [] for rg in d['rings']: pl = ring_to_poly(rg) if len(pl) >= 3: found += arcs(pl) uniq = {} for cx, cy, rr in found: uniq[(round(cx, 2), round(cy, 2), round(rr, 2))] = (cx, cy, rr) circles = list(uniq.values()) vias, pads = d['vias'], d['pads'] for flip in (1, -1): clr = sorted(rr - v[2] for cx, cy, rr in circles for v in vias if math.hypot(cx - v[0], flip * cy - v[1]) < 0.02) if len(clr) > 10: break if len(clr) <= 10: sys.exit(u'过孔反焊盘对不上(%d 个)—— 坐标系没对齐' % len(clr)) C = clr[len(clr) // 2] print(u'圆弧 %d 段;净距标定 %d 个过孔,中位数 %.4f mm(离散度 %.4f),y 符号 %+d' % (len(circles), len(clr), C, clr[-1] - clr[0], flip)) unknown = [] for cx, cy, rr in circles: y = flip * cy if any(math.hypot(cx - v[0], y - v[1]) < 0.05 for v in vias): continue hr = rr - C if not 0.1 <= hr <= 2.0: continue near = min(math.hypot(cx - p[0], y - p[1]) for p in pads) if near < 0.05: continue # 通孔焊盘自己的孔 unknown.append((round(cx, 3), round(y, 3), round(hr, 3), round(near, 2))) # 同一个孔在 L2 / L3 各一份,按 0.05 合并 merged = [] for u in sorted(unknown, key=lambda t: (t[1], t[0])): if not any(math.hypot(u[0] - m[0], u[1] - m[1]) < 0.05 for m in merged): merged.append(u) # 槽孔焊盘(J1 DC 座、J23 外壳腿)的反焊盘是跑道形:两端半圆**同半径、同一条轴线上、相距 ≤ 2.6**,成对出现。 # 真的圆孔(插槽定位柱 x 44.15 / 67.30、J23 两个 Ø0.6)都是孤立的。 def paired(u): return any(m is not u and abs(m[2] - u[2]) < 0.02 and ((abs(m[0] - u[0]) < 0.01 and abs(m[1] - u[1]) < 2.6) or (abs(m[1] - u[1]) < 0.01 and abs(m[0] - u[0]) < 2.6)) for m in merged) holes = [u for u in merged if not paired(u)] print(u'对不上任何过孔 / 焊盘中心的圆弧 %d 个;去掉成对的槽孔焊盘端头后,真的圆孔 %d 个:' % (len(merged), len(holes))) for cx, cy, hr, near in holes: print(u' (%7.3f, %7.3f) Ø%.3f 离最近焊盘 %.2f' % (cx, cy, hr * 2, near)) if '--write' in sys.argv: # [x, y, 孔半径, 反焊盘半径]:反焊盘半径 = 孔半径 + 实测铜面净距,布线禁区按它取(不必知道 NPTH 用的是哪条净距规则) json.dump([[h[0], h[1], h[2], round(h[2] + C, 3)] for h in holes], open(OUT, 'w', encoding='utf-8')) print(u'已写入 %s' % os.path.basename(OUT)) def _seg_d(px, py, x0, y0, x1, y1): dx, dy = x1 - x0, y1 - y0 L = dx * dx + dy * dy t = 0.0 if L < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L)) return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy)) def check_routes(): u"""校验布线缓存(服务器结果):没有线 / 孔进 NPTH 的反焊盘圈。纯几何,不布线。""" holes = json.load(open(OUT, encoding='utf-8')) d = json.load(open(os.path.join(HERE, '_base_routes.json'), encoding='utf-8')) vr = 0.3 hit = {} for s in d['segs']: for h in holes: g = _seg_d(h[0], h[1], s[1], s[2], s[3], s[4]) - s[5] / 2.0 - h[3] if g < 0: hit.setdefault((s[6], u'L%d 线' % s[0]), []).append((h[0], h[1], g)) for v in d['vias']: for h in holes: g = math.hypot(v[0] - h[0], v[1] - h[1]) - vr - h[3] if g < 0: hit.setdefault((v[2], u'孔'), []).append((h[0], h[1], g)) if not hit: print(u'[OK] 布线缓存 %d 段 / %d 孔,没有进 %d 个 NPTH 的反焊盘圈' % (len(d['segs']), len(d['vias']), len(holes))) return True print(u'[X] %d 处压 NPTH:' % len(hit)) for (net, kind), v in sorted(hit.items()): print(u' %-12s %s %s 最深 %.3f' % (net, kind, sorted(set((a, b) for a, b, _g in v)), min(g for *_x, g in v))) return False if __name__ == '__main__': if '--check-routes' in sys.argv: sys.exit(0 if check_routes() else 1) main()