文件
phonerail/.claude/pcb_pour.py
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

505 行
25 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""PhoneRail 的铺铜/平面层模型 —— 事实来源,改铜面改这里再 --push。
/c/Python314/python .claude/pcb_pour.py [--push] [--only L2|L3]
## 叠层(§10.1,layerStatus=1 的正好这四层)
| 设计文档 | EasyEDA 层 | id | 这里干什么 |
|---|---|---|---|
| L1 | Top Layer | 1 | 功率器件 + 信号,**D± 走这层,以 L2 为参考面** |
| L2 | Inner1-GND | 15 | **完整地平面,不许割断**(§10.1)|
| L3 | Inner2-PWR | 16 | 12V 母线 + 5V + 3V3 + 五路输出,**分割灌铜** |
| L4 | Bottom Layer | 2 | 控制信号 + ESP32 |
## 板框
160 × 125 mm,四角 R5(圆心正好落在四个 M3 定位孔的中心 (5,5)/(155,5)/(5,120)/(155,120))。
⚠️ **API 枚举不到那四段圆弧**:`pcb_PrimitiveArc.getAll()` 和 `getAllPrimitiveId()`
都返回 0,layer 11 上全工程只有 4 条直线(端点恰好是 R5 的切点)。
但渲染上角是实实在在的圆弧、闭合的。所以这里**不去读板框,按几何自己造多边形**。
(打样前记得用 Gerber 预览复核一次板框闭合。)
## 多边形不闭合就建不出来,而且 `create()` 成功了也会抛 ⚠️⚠️
两个坑叠在一起,实测(2026-09-03):
1. **`TPCB_PolygonSourceArray` 必须显式闭合** —— 末尾要把首点再写一遍。
文档写着「如果首尾不重合将会自动重合」,**对铺铜边框不成立**:
不闭合时 `pcb_PrimitivePour.create()` 直接不建,板上一块铜面都不多。
(`board_poly()` 一直能用,是因为它最后那段圆弧正好回到起点。)
2. **`create()` 抛异常不等于失败。** 闭合的多边形建得出来,
但调用照样抛 `错误:无法创建覆铜边框图元,可能是传入的参数不正确。`
—— 所以**返回值和 try/catch 都不能当判据**,只能建完 `getAll()` 按名字核对。
顺带一提:**铺铜名字会被 EasyEDA 转成大写**,比对时要 `toUpperCase()`。
一次分不清这两个的排查过程:不闭合的矩形(PA-OPEN)建完 `getAll()` 数量不变,
闭合的(PB-CLOSE)数量 +1 —— 两者都抛了一模一样的异常。
## 填充只能点菜单,`rebuildCopperRegion()` 是坏的 ⚠️
`pcb_PrimitivePour.create()` 建出来的只是**铺铜边框**(`nPour` 会 +1),
里头一片铜都没有,`pcb_PrimitivePoured.getAll()` 仍是 0,网络也不算连通。
`IPCB_PrimitivePour.rebuildCopperRegion()`(文档标 BETA)**必然抛**
`TypeError: Cannot read properties of undefined (reading 'length')`,约 157 ms。
四种写法都试过、都一样:`getAll()` 拿回来的对象、`toAsync()/done()` 异步式、
`createPolygon` 建的、`createComplexPolygon` 建的。
唯一能用的路子是 GUI:**工具 → 铺铜管理器... → 重建所有 → 确认**。
本文件的 `--push` 已经把这一串用 React 的 `onClick` 打进去了
(菜单项是 React 元素,用 `__reactProps$*.onClick`;`重建所有` 是原生 `<button>`,
两种都试,见 `drc_read.py` 的注释)。
**判据是 `nPoured`,不是 `nPour`。** 2026-09-03 实测:GND-L2 一块铜面重建耗时 < 2 s,
重建之后 DRC 的连接性错误从 1467 掉到 925。
## ⚠️ 本文件里的「优先级」那一列是**错的**,只是这块板上不要紧(2026-09-05 查清)
`plan()` 给 GND 1、给电源岛 5,隐含「数字大 = 优先」。**反了。**
铺铜管理器对话框自己写着「重建多个铺铜时,系统将按照优先级数字**从小到大**来执行铺铜」——
先铺的占地盘,所以 **GND 底填要给最大的数字**。
这块板上没出事,是因为这 19 块铜面**同层从不重叠**(`audit()` 的 MIN_GAP 就是在保这一条),
优先级压根没参与裁剪。**抄到载板去之前必须先把这一位翻过来**(载板 Inner1 是
分割电源面 + GND 底填,全靠优先级互相裁)。
顺带:`create()` 的第 7 个参数 `pourPriority` **不生效** —— 传进去的值被忽略,
实测这 14 块回读的是 1,2,3,4,4,5,5,6,7,8,9,10,11,12(基本是创建顺序)。
真要控制优先级只能建完再 `setState_PourPriority()` 逐块设并回读核对。见 skill §9j。
## ESP32 天线禁铺区
U20 在 (143, 46)、rot=270,本体框(rot=0)是 (-9.5, -9.35, 9.5, 16.75) ——
焊盘只在三面,**天线那一端(+Y)没有焊盘**,本体比焊盘框多伸 7.4 mm。
rot=270 之后天线朝 **+X**,落在 x≈152~160、y≈36.5~55.5。
模组 datasheet 要求**所有层**在天线下方都不铺铜,所以每一层的多边形都要挖这个缺口。
缺口开到右板边,不会把地平面割成两块(§10.1 那条「不许割断」仍然满足)。
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '7e9415a16a909c51'
MIL = 39.3701
L_TOP, L_GND, L_PWR, L_BOT = 1, 15, 16, 2
BOARD = (0.0, 0.0, 160.0, 125.0)
R_CORNER = 5.0
INSET = 0.5 # 铜面离板边
# 天线禁铺(含 1 mm 余量),x 一直开到板边
ANT = (151.0, 35.5, 160.0, 56.5)
def board_poly(inset=INSET, notch=ANT):
u"""板框内缩 inset 的圆角矩形,右边挖掉天线缺口。
返回 TPCB_PolygonSourceArray(mil):
x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...
ARC 的起点是「当前点」,角度正值逆时针(本工程 y 轴朝上)。
"""
x0, y0, x1, y1 = BOARD
d = inset
r = R_CORNER - d # 圆心不变,半径跟着缩
cx0, cy0, cx1, cy1 = x0 + R_CORNER, y0 + R_CORNER, x1 - R_CORNER, y1 - R_CORNER
ny0, ny1 = notch[1], notch[3]
nx = notch[0]
pts = [] # (mode, ...) 顺序即绘制顺序,逆时针
def L(x, y):
pts.append(('L', x, y))
def A(x, y):
pts.append(('ARC', 90.0, x, y))
start = (cx0, y0 + d) # 底边左端切点
L(cx1, y0 + d) # 底边 →
A(x1 - d, cy0) # 右下角圆弧
L(x1 - d, ny0) # 右边 ↑ 到缺口下沿
L(nx, ny0) # 缺口 ←
L(nx, ny1) # 缺口 ↑
L(x1 - d, ny1) # 缺口 →
L(x1 - d, cy1) # 右边 ↑
A(cx1, y1 - d) # 右上角
L(cx0, y1 - d) # 顶边 ←
A(x0 + d, cy1) # 左上角
L(x0 + d, cy0) # 左边 ↓
A(cx0, y0 + d) # 左下角,回到起点
src = [start[0] * MIL, start[1] * MIL]
mode = None
for p in pts:
if p[0] == 'L':
if mode != 'L':
src.append('L'); mode = 'L'
src += [p[1] * MIL, p[2] * MIL]
else:
src.append('ARC'); mode = 'ARC'
src += [p[1], p[2] * MIL, p[3] * MIL]
return src
def poly_from_pts(pts):
u"""任意直边多边形 -> TPCB_PolygonSourceArray(mil)。pts 是 [(x, y), ...] mm。
⚠️ **末尾必须把首点再写一遍**,见文件头「多边形不闭合就建不出来」。
"""
src = [pts[0][0] * MIL, pts[0][1] * MIL, 'L']
for x, y in list(pts[1:]) + [pts[0]]:
src += [x * MIL, y * MIL]
return src
def rect(x0, y0, x1, y1):
return poly_from_pts([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
def lshape(bx, by, tx, ty, ax, ay):
u"""主矩形 (bx,by)-(tx,ty) 左边再挂一块 (ax,ay)-(bx,ty) 的臂。"""
return poly_from_pts([(bx, by), (tx, by), (tx, ty), (ax, ty), (ax, ay), (bx, ay)])
# ── 要建的铜面 ─────────────────────────────────────────────────────────
# name, net, layer, 多边形, 优先级
#
# ══ L3(Inner2-PWR)══════════════════════════════════════════════════════
# 水平分层,和 L1 上器件的分带一一对应,过孔就都是最短的:
#
# y 110.0~116.2 VOn_P 五路输出到手机(在 **L4**,见下)
# y 82.5~ 99.5 VOn 电感 + 输出电容 + eFuse 输入侧(顺带给 eFuse 铺散热铜)
# y 67.5~ 81.9 +12V 五路 Buck 的输入母线(横贯整板)
# y 33.0~ 67.5 +12V band B 的支路 + 母线源头(x 38.5~95)
# y 49.0~ 66.5 $1N236 Q1 输出 → 体电容 → MLCC → Rs6 上游(x 14.4~35)
# y 49.5~ 56.5 $1N227 F1 → Q1 输入(x 14.6~19.0)
# y 45.5~ 55.5 $1N223 J1 → F1(x 3~14)
#
# ⚠️ 分界线的位置是**焊盘边**定的,不是整数好看:
# +12V 顶边 81.9 —— 再高就撞 C102 那一排 1210 的 +12V 焊盘(y 80.2~81.7);
# VOn 底边 82.3 —— C107 pin1 是 rot90 的 1210,焊盘竟然跨到 y 83.07~84.55,
# 比元件中心低 1.6 mm。按"元件在 y85.4"算会把它漏在两块铜面之间的缝里。
#
# ⚠️ **U8 / U13 三个 Kelvin 脚一律不进铜面**(MSOP-10,0.5 mm 间距)。
# U13.10 是 VIN+($1N236)、U13.9 是 VIN−(+12V),两脚相距 0.5 mm ——
# 任何一条分界线都塞不进去。它们走顶层细线,本来 Kelvin 就不该载流。
# 所以 $1N236 右边界停在 x35.0、+12V 左边界从 x38.5 起,中间 3.5 mm 全让给它们。
#
# ══ L4(Bottom)══════════════════════════════════════════════════════════
# 五路 VOn_P(Rs 下游 → J2)借用连接器带那条空走廊。这一层是 1 oz,
# 4 A 只要 1.34 mm,给了 6.2 mm 宽还带一条下伸的臂去接分流电阻。
# y 上限卡在 116.2 是被 (5,120)/(155,120) 那两个 M3 定位孔逼的:
# Φ6.0 焊盘 + 0.2 间距 = 铜面要离孔心 3.2 mm 以上,116.2 刚好剩 3.8 mm。
CH_X0, CH_PITCH = 1.0, 32.0 # 每路铜面的左边界与节距
CH_W_POUR = 30.0 # 铜面宽度(留 2 mm 路间隙)
def plan():
out = [
(u'GND-L2', 'GND', L_GND, board_poly(), 1),
# 8 A 输入链,三段各自一块(都很短,宽度按"横向电流的截面 = y 跨度"算)
(u'VIN-RAW-L3', '$1N223', L_PWR, rect(3.0, 45.5, 14.0, 55.5), 5),
(u'VIN-FUSED-L3', '$1N227', L_PWR, rect(14.6, 49.5, 19.0, 56.5), 5),
(u'VIN-SW-L3', '$1N236', L_PWR, lshape(19.5, 49.0, 35.0, 66.5, 14.4, 58.5), 5),
# +12V 母线:band A 横贯 + band B 一块(源头 Rs6 在 x40)
# 三段:下带 x14.6~95 y33~48.3(去接 U30 的 VIN,它在 x18.6~26.7)、
# 中带 x38.5~95 y48.3~67.5(源头 Rs6.2 在 x40.07)、顶带横贯全板 y67.5~81.9。
# 下带左边界 14.6 是被 $1N223 那块(x3~14)逼的,留 0.6 mm。
(u'12V-L3', '+12V', L_PWR,
poly_from_pts([(14.6, 33.0), (95.0, 33.0), (95.0, 67.5), (158.0, 67.5),
(158.0, 81.9), (1.0, 81.9), (1.0, 67.5), (38.5, 67.5),
(38.5, 48.3), (14.6, 48.3)]), 2),
# ── 三条低压轨(2026-09-04 加)─────────────────────────────────
# 加之前它们靠细线连,`+3V3` 8 条、`+3V3_HUB` 5 条自动布线器没布通。
# 都放 L3,塞进 12V 那几条带子中间的空地,互相与 12V 都留 >= 0.5 mm。
#
# +5V:U24~U28 五个 VBUS 负载开关排在 y11.5~12 那一行,最坏 2.8 A(§9.4)。
# 只做 y5.5~14 这一条横带;U30 输出那几个焊盘在 y41~45.5,
# 被 12V 下带(y33~48.3)占着,进不来,仍走顶层粗线下来接这条带。
(u'5V-L3', '+5V', L_PWR, rect(11.0, 5.5, 151.0, 14.0), 3),
# +3V3_HUB:USB2517 + U31 LDO 那一坨,x59~111 / y15~31 正好是它们的地盘。
# ⚠️ 与 +5V 在 y 上错开(14.0 / 15.0,净空 1 mm)—— 这一带两个网络的焊盘
# 是交错的(U31 的进/出脚挨着),靠 y 分层比靠 x 分区干净。
(u'3V3HUB-L3', '+3V3_HUB', L_PWR, rect(59.0, 15.0, 111.0, 31.0), 3),
# +3V3 分两块(同一个网络两块铜面,靠已有走线连起来):
# 控制区 —— 12V 下带/中带的右边(x>95)那块空地,ESP32 / PCA9555 / 595 / DAC 都在这里。
# ⚠️ 右边界 150.5 是被 ESP32 天线禁铺区(x>=151)逼的,不能再往右。
(u'3V3-CTRL-L3', '+3V3', L_PWR, rect(95.5, 33.5, 150.5, 67.0), 3),
# 通道区 —— VOn 铜面(y82.5~99.5)之上那一片,六片 INA226 的电源脚都在 y100~105。
(u'3V3-CH-L3', '+3V3', L_PWR, rect(5.0, 100.3, 155.0, 124.0), 3),
]
for n in range(1, 6):
x0 = CH_X0 + (n - 1) * CH_PITCH
x1 = x0 + CH_W_POUR
out.append((u'VO%d-L3' % n, 'VO%d' % n, L_PWR, rect(x0, 82.5, x1, 99.5), 4))
# VOn_P 在 L4:连接器带 + 一条下伸到分流电阻上游焊盘的臂
out.append((u'VO%d_P-L4' % n, 'VO%d_P' % n, L_BOT,
poly_from_pts([(x0, 104.5), (x0 + 3.5, 104.5), (x0 + 3.5, 110.0),
(x1, 110.0), (x1, 116.2), (x0, 116.2)]), 4))
return out
# ── 本地检查(不连 EasyEDA)───────────────────────────────────────────────
def _poly_pts(src):
u"""把 TPCB_PolygonSourceArray 还原成 mm 的顶点表。只处理直边(本文件的 L3/L4 都是)。"""
pts, i = [], 0
while i < len(src):
v = src[i]
if v == 'L':
i += 1; continue
if v == 'ARC':
i += 4; continue # 板框那块有弧,这里只用来算包围盒
pts.append((src[i] / MIL, src[i + 1] / MIL)); i += 2
return pts
def _inside(px, py, pts):
u"""射线法。边界上算在里面(本文件的分界线都留了 >= 0.4 mm 净空,不会踩边界)。"""
n, inside = len(pts), False
j = n - 1
for i in range(n):
xi, yi = pts[i]; xj, yj = pts[j]
if (yi > py) != (yj > py) and px < (xj - xi) * (py - yi) / (yj - yi + 1e-30) + xi:
inside = not inside
j = i
return inside
def _bbox(pts):
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
return min(xs), min(ys), max(xs), max(ys)
MIN_GAP = 0.4 # 同层两块铜面之间的最小净空(安全间距 0.2 的两倍,留余量)
def _seg_dist(p, q, r, t):
u"""两条线段的最短距离。相交返回 0。"""
def d_pt_seg(px, py, ax, ay, bx, by):
dx, dy = bx - ax, by - ay
L2 = dx * dx + dy * dy
s = 0.0 if L2 == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L2))
return ((px - ax - s * dx) ** 2 + (py - ay - s * dy) ** 2) ** 0.5
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
d1, d2 = cross(r, t, p), cross(r, t, q)
d3, d4 = cross(p, q, r), cross(p, q, t)
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
return 0.0
return min(d_pt_seg(p[0], p[1], r[0], r[1], t[0], t[1]),
d_pt_seg(q[0], q[1], r[0], r[1], t[0], t[1]),
d_pt_seg(r[0], r[1], p[0], p[1], q[0], q[1]),
d_pt_seg(t[0], t[1], p[0], p[1], q[0], q[1]))
def _poly_gap(A, B):
u"""两个简单多边形的最近距离;一个包含另一个或相交时返回 0。"""
if any(_inside(x, y, B) for x, y in A) or any(_inside(x, y, A) for x, y in B):
return 0.0
best = 1e9
for i in range(len(A)):
for j in range(len(B)):
best = min(best, _seg_dist(A[i], A[(i + 1) % len(A)],
B[j], B[(j + 1) % len(B)]))
return best
# 故意不进铜面的焊盘(后缀匹配位号)。漏在这张表以外的都要查。
# U8/U13 的 0.5 mm 间距 Kelvin 脚 —— 分界线塞不进去,本来也不载流
# DZ1/C1 —— Q1 的栅极钳位,接的是 $1N227 但只有 µA 级
# R%01 —— VOn 的反馈分压抽头,落在 +12V 顶带里,走顶层细线
# R%19 —— 输出泄放 2512,只有放电脉冲电流
# 这两条轨的铜面**故意只盖一部分**:它们的焊盘散布全板,铜面只占 12V 带子之间的空地,
# 其余焊盘靠走线接过来。所以别把「漏掉」当错报,只报个数。
POUR_PARTIAL = ('+5V', '+3V3')
EXPECT_OUT = ('U8.', 'U9.', 'U10.', 'U11.', 'U12.', 'U13.', 'DZ1.', 'C1.1', 'R101.', 'R201.', 'R301.', 'R401.', 'R501.',
'R119.', 'R219.', 'R319.', 'R419.', 'R519.')
def audit():
u"""铜面之间不许挨太近;每个网络的焊盘要么在自己的铜面里,要么得记在案。"""
import collections
dump = os.path.join(HERE, 'pcb-copper.json')
pads = json.load(io.open(dump, encoding='utf-8'))['pads'] if os.path.exists(dump) else []
by_net = collections.defaultdict(list)
for q in pads:
by_net[q['net']].append(q)
ps = [(n, net, l, _poly_pts(src), pr) for n, net, l, src, pr in plan()]
bad = 0
# 1) 同层铜面不许挨得太近。
# ⚠️ 不能用包围盒 —— 12V-L3 是个横跨全板的 L 形,它的包围盒把 band B 全套进去,
# 四块输入链铜面全被误报成"重叠 -32 mm"。必须逐边算真距离。
for i in range(len(ps)):
for j in range(i + 1, len(ps)):
a, b = ps[i], ps[j]
if a[2] != b[2]:
continue
d = _poly_gap(a[3], b[3])
if d < MIN_GAP:
print(u' !! %s 与 %s 同在 layer %d,最近处只有 %.2f mm(要求 >= %.1f)'
% (a[0], b[0], a[2], d, MIN_GAP))
bad += 1
# 2) 每块铜面盖住了本网络的几个焊盘 / 漏了哪些
print(u'')
print(u'%-14s %-9s %s' % (u'铜面', u'网络', u'本网焊盘 在内/总数(漏掉的要走顶层线)'))
# ⚠️ 一个网络可能有**多块**铜面(+3V3 就是两块)。判「焊盘在不在铜面里」
# 必须拿同网络所有铜面做并集,否则每一块都会把另一块盖住的焊盘报成意外漏掉。
same_net = {}
for _n, _net, _l, _pts, _p in ps:
same_net.setdefault(_net, []).append(_pts)
for name, net, layer, pts, prio in ps:
if net == 'GND':
continue
mine = by_net.get(net, [])
ins = [q for q in mine
if any(_inside(q['x'], q['y'], t) for t in same_net[net])]
out = [q for q in mine if q not in ins]
tags = ['%s.%s' % (q['des'] or '?', q['num']) for q in out]
odd = ([] if net in POUR_PARTIAL else
[t for t in tags if not t.startswith(EXPECT_OUT) and not t.startswith('?.')])
note = u''
if tags:
note = (u' %d 个在铜面外,走线接(本网铜面只盖一部分)' % len(tags)
if net in POUR_PARTIAL else u' 漏 %d(预期之内)' % len(tags))
if odd:
note = u' !! 意外漏掉: ' + ' '.join(odd[:8])
bad += 1
print(u'%-14s %-9s %d / %d%s' % (name, net, len(ins), len(mine), note))
if not mine:
# ⚠️ 多半是 $1N223 这类**自动命名**的网络改了号(原理图一重新生成网表就会变)。
# pcb_fanout.py 的 TARGET 里也有同样三个名字,要一起改。
print(u' !! 板上根本没有这个网络 —— 自动命名的网络改号了?')
bad += 1
elif not ins:
print(u' !! 一个都没盖住 —— 网络名或坐标错了')
bad += 1
print(u'')
print(u'铜面 %d 块,问题 %d 条' % (len(ps), bad))
return bad
OPEN = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1200));
""" % PCB
# ⚠️ 桥接单次请求 30 s 就超时(skill §9c)。15 块铜面 + 重建全部远超这个数,
# 所以拆成四步,每一步都短:建(分批)→ 点重建 → 轮询 → 确认+存盘。
JS_DEL = OPEN + u"""
let n = 0;
for (const old of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
return {deleted: n};
"""
JS_MAKE = OPEN + u"""
const PLAN = %s;
for (const p of PLAN) {
const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]);
if (!poly) continue;
// ⚠️ create() **成功了也会抛**(见文件头),返回值和 try/catch 都不能当判据
try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); }
catch (e) {}
}
const made = (await eda.pcb_PrimitivePour.getAll()).map(x => String(x.pourName || '').toUpperCase());
return {made};
"""
# 工具 → 铺铜管理器... → 重建所有(React 元素用 __reactProps$*.onClick,按钮用原生 click)
JS_REBUILD = OPEN + u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t);
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
const leaf = (txt) => { for (const el of document.querySelectorAll('*'))
if (el.children.length===0 && (el.textContent||'').trim()===txt) return el; return null; };
const btn = (txt) => { for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()===txt && b.offsetParent!==null) return b; return null; };
if (!fire(leaf('工具'))) return {err: 'no 工具'};
await sleep(900);
if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'};
await sleep(2200);
const b = btn('重建所有');
if (!b) return {err: 'no 重建所有'};
return {fired: fire(b)};
"""
JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};"
JS_DONE = u"""
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t);
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
let ok = null;
for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { ok = b; break; }
if (ok) fire(ok);
await new Promise(r => setTimeout(r, 1200));
await eda.pcb_Document.save();
return {poured: (await eda.pcb_PrimitivePoured.getAll()).length,
pour: (await eda.pcb_PrimitivePour.getAll()).length};
"""
def main():
ps = plan()
print(u'计划建 %d 块铜面:' % len(ps))
for name, net, layer, src, prio in ps:
n_arc = sum(1 for v in src if v == 'ARC')
n_pt = sum(1 for v in src if isinstance(v, float)) // 2
print(u' %-10s net=%-6s layer=%-3d 多边形 %d 点 / %d 段弧'
% (name, net, layer, n_pt, n_arc))
audit()
if '--push' not in sys.argv and '--rebuild' not in sys.argv:
print(u'(未加 --push,没有下发)')
return
def call(js, timeout=60):
r = run(js, timeout=timeout)
if not r.get('success'):
print(u'失败:', json.dumps(r, ensure_ascii=False)[:400]); sys.exit(1)
return r['result']
print(u'')
if '--rebuild' in sys.argv:
# 铜面本身没变,只是扇出改了 -> 只重填,别重建边框
made = [n.upper() for n, _, _, _, _ in ps]
else:
print(u'清掉旧铜面:', call(JS_DEL)['deleted'], u'块')
for k in ([] if '--rebuild' in sys.argv else range(0, len(ps), 5)): # 5 块一批,稳在 30 s 以内
batch = [{'name': n, 'net': net, 'layer': l, 'src': src, 'prio': pr}
for n, net, l, src, pr in ps[k:k + 5]]
made = call(JS_MAKE % json.dumps(batch, ensure_ascii=False))['made']
print(u' 已建 %d / %d' % (len(made), len(ps)))
miss = [n for n, _, _, _, _ in ps if n.upper() not in made]
if miss:
print(u'!! 这几块没建出来:', ' '.join(miss)); sys.exit(1)
r = call(JS_REBUILD)
if r.get('err'):
print(u'!! 打不开铺铜管理器:', r['err']); sys.exit(1)
import time
for i in range(40): # 最多等 ~200 s
time.sleep(5)
n = call(JS_POLL, timeout=30)['poured']
print(u' 填充 %d / %d' % (n, len(ps)))
if n >= len(ps):
break
d = call(JS_DONE)
print(u'完成:铜面 %d 块,已填充 %d 块' % (d['pour'], d['poured']))
if __name__ == '__main__':
main()