文件
phonerail/.claude/dump_pads.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

154 行
8.0 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""把 PCB 上全部焊盘 + 过孔 + 走线抓成 `.claude/pcb-copper.json`(布线用的障碍表)。
/c/Python314/python .claude/dump_pads.py
比 `refresh_cache.py` 轻:那个要开全部 12 页原理图,这个只读 PCB。
布局一动就重跑。
## 存了什么(单位一律 mm)
`pads` 每个焊盘:net / x / y / 形状 / 旋转 / layer / padType / 钻孔(含偏移与旋转)
`vias` 过孔:net / x / y / 内外径
`lines` 走线:net / layer / 两端 / 线宽
## 两个坑
1. **`pad` 数组里的 w/h 是「未旋转」的**,真实朝向要配合同一条记录里的 `rot`。
(`pcb_place.py` 的 `FP_PAD` 只收 `rot==0` 的实例,就是为了躲这一条。)
2. `layer` 1=Top 2=Bottom 15=Inner1-GND 16=Inner2-PWR,**12=多层(真正的通孔盘)**。
⚠️ **「通孔盘」的判据是 `layer == 12`,不是 `hole` 非空,也不是 `padType`。**
`padType` 这块板上全是 0(连 J24/J25 的排针针脚也是 0),而 `hole` 会**骗人**:
3. ⭐ **POLYGON 焊盘一律带一个假的 `hole`**(2026-09-06 第十九轮实测)。
板上 11 个 POLYGON 盘(U1~U5 的散热盘 pin15、U6/U7 的 pin17、J22 的四个壳体盘)
全部满足 **`hole = 0.600 × min(w, h)`** —— 一个派生出来的占位值,
EasyEDA 自己渲染出来那几个盘是**实心的,没有孔**。
它们的 `layer` 都是 1(只在顶层),物理上也不可能有孔。
⇒ 这里在落盘前把**单铜层焊盘(layer 1/2)的 `hole` 一律清成 None**,
并断言被清掉的那些确实符合 0.6 倍关系;不符合的会打印出来要人看一眼。
**不清的后果是静默的**:`pcb_fanout.one_net()` 拿 `not p['hole']` 当「这是贴片盘」用,
于是五路 Buck 的散热盘(同时是 PGND 和唯一的散热路径)**整批被滤掉、一个过孔都没打**,
而连接性 DRC 只会含糊地多出几条,没人会把它和「footprint 里一个用不上的字段」联系起来。
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
# 默认下板;载板用 `python .claude/dump_pads.py 942ed1560d81c1e7 pcb2-copper.json`
PCB = sys.argv[1] if len(sys.argv) > 1 else '7e9415a16a909c51'
MIL = 39.3701
OUT = os.path.join(HERE, sys.argv[2] if len(sys.argv) > 2 else 'pcb-copper.json')
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1200));
const M = 39.3701, q = v => Math.round(v / M * 1e4) / 1e4;
// ⚠️ getAll() 的焊盘坐标是干净的 (4, 79.3),getAllPinsByPrimitiveId 却给 (4.0005, 79.2988)
// —— 同一个焊盘差约 1 µm 的浮点噪声。连接键必须放宽,精确匹配只能对上 10 个。
const k2 = v => Math.round(v / M * 100) / 100;
const pads = (await eda.pcb_PrimitivePad.getAll()).map(p => ({
net: p.net || '', x: q(p.x), y: q(p.y), rot: p.rotation || 0,
layer: p.layer, num: String(p.padNumber),
shape: Array.isArray(p.pad) ? [p.pad[0]].concat(p.pad.slice(1).map(v => typeof v === 'number' ? q(v) : v)) : null,
hole: Array.isArray(p.hole) ? [p.hole[0]].concat(p.hole.slice(1).map(q)) : null,
// ⚠️ 孔有自己的偏移和旋转,和焊盘的不一定一致。
// J22/J23 的 USB-C 壳体脚:铜箔 1.2 x 1.8,槽 0.8 x 1.5 —— 槽的长轴与铜箔垂直,
// 槽会**伸出铜箔外** 0.15 mm。只建模铜箔的话,过孔会直接压到槽上(实测踩过)。
hox: q(p.holeOffsetX || 0), hoy: q(p.holeOffsetY || 0), hrot: p.holeRotation || 0,
padType: p.padType,
}));
const vias = (await eda.pcb_PrimitiveVia.getAll()).map(v => ({
net: v.net || '', x: q(v.x), y: q(v.y),
hole: q(v.holeDiameter), dia: q(v.diameter)}));
const lines = (await eda.pcb_PrimitiveLine.getAll()).map(l => ({
net: l.net || '', layer: l.layer, x1: q(l.startX), y1: q(l.startY),
x2: q(l.endX), y2: q(l.endY), w: q(l.lineWidth)}));
// 位号 -> 引脚,用来给焊盘认领主人。
// ⚠️ **不要在 JS 里做 round-to-key 的匹配**:pad 的坐标是干净的、pin 的带 1 µm 噪声,
// 两边各自 Math.round 到 0.01 时,落在 .xx5 上的值会分别进到不同的桶 ——
// 2026-09-06 实测 846 个焊盘里 88 个认不到主人(J75 pin15/16 就在其中,
// 害得「J7n 的 GND 脚整批跳过」漏掉了两个)。这里只回传原始坐标,Python 端按最近邻配。
const own = [];
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.designator || c.name;
for (const p of await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.primitiveId))
own.push({d, x: q(p.x), y: q(p.y), n: String(p.padNumber)});
}
return {pads, vias, lines, own};
"""
def main():
r = run(JS % PCB, timeout=600)
if not r.get('success'):
print(u'失败:', json.dumps(r, ensure_ascii=False)[:500]); sys.exit(1)
d = r['result']
# ── POLYGON 焊盘的假 `hole`:清掉,见文件头第 3 条 ──────────────────
# 判据是**物理的**:只落在一个铜层上的焊盘(layer 1/2)不可能有孔。
# 0.6 倍那条只用来核对「我理解的这件事没变」,不当判据用 ——
# 哪天真出现一个带孔的单层盘,它会被打印出来而不是被悄悄放过。
ghost, odd = 0, []
for q in d['pads']:
if q['hole'] and q['layer'] in (1, 2):
sh = q['shape']
w = h = None
if sh and sh[0] == 'POLYGON':
pts = [v for v in sh[1] if isinstance(v, (int, float))]
xs, ys = pts[0::2], pts[1::2]
w, h = (max(xs) - min(xs)) / MIL, (max(ys) - min(ys)) / MIL
elif sh and len(sh) >= 3 and isinstance(sh[1], (int, float)):
w, h = sh[1], sh[2]
r6 = (q['hole'][1] / min(w, h)) if (w and h and min(w, h)) else None
if r6 is None or abs(r6 - 0.6) > 0.01:
odd.append((q.get('des'), q['num'], q['hole'], r6))
q['hole'] = None
ghost += 1
if ghost:
print(u' 单铜层焊盘上的假钻孔已清 %d 个(POLYGON 盘的 0.6×min 占位值)' % ghost)
for des, num, ho, r6 in odd:
print(u' ⚠ %s.%s 的 hole=%s 不符合 0.6×min(比值 %s)—— 人看一眼是不是真孔'
% (des, num, ho, ('%.3f' % r6) if r6 else '?'))
# 引脚按「焊盘号 + 粗网格」分桶,再在 3 x 3 个桶里取最近的一个(容差 0.02 mm)。
# 分桶只是为了不做 846 x 3000 的全比较,边界值落哪个桶都无所谓 —— 邻桶也会查。
CELL = 0.5
buck = {}
for o in d['own']:
buck.setdefault((o['n'], int(o['x'] // CELL), int(o['y'] // CELL)), []).append(o)
miss = 0
for p in d['pads']:
gx, gy = int(p['x'] // CELL), int(p['y'] // CELL)
best, bd = '', 0.02
for i in (-1, 0, 1):
for j in (-1, 0, 1):
for o in buck.get((p['num'], gx + i, gy + j), ()):
r2 = ((o['x'] - p['x']) ** 2 + (o['y'] - p['y']) ** 2) ** 0.5
if r2 < bd:
bd, best = r2, o['d']
p['des'] = best
if not best:
miss += 1
d.pop('own')
io.open(OUT, 'w', encoding='utf-8').write(
json.dumps(d, ensure_ascii=False, separators=(',', ':')))
nets = {}
for p in d['pads']:
nets[p['net']] = nets.get(p['net'], 0) + 1
tht = sum(1 for p in d['pads'] if p['hole'])
print(u'焊盘 %d(其中通孔 %d / 贴片 %d),过孔 %d,走线 %d'
% (len(d['pads']), tht, len(d['pads']) - tht, len(d['vias']), len(d['lines'])))
print(u'认领到位号的焊盘 %d' % sum(1 for p in d['pads'] if p['des']))
print(u'网络 %d 个,最大的十个:' % len(nets))
for n, c in sorted(nets.items(), key=lambda kv: -kv[1])[:10]:
print(u' %-10s %d 个焊盘' % (n or u'(无网络)', c))
print(u'-> %s' % OUT)
if __name__ == '__main__':
main()