文件
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

467 行
24 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 PCB2-CARRIER 的铺铜 —— 事实来源,改铜面改这里再 `--push`。
/c/Python314/python .claude/pcb2_pour.py # 只打印计划
/c/Python314/python .claude/pcb2_pour.py --push # 清旧 -> 建边框 -> 重建 -> 确认 -> 存盘 -> 回读
/c/Python314/python .claude/pcb2_pour.py --verify # 只回读实测铺出来的铜(不改板)
## 叠层(§25.6 ①,**层角色和下板是镜像的**)
| 设计文档 | EasyEDA 层 | id | 载板上干什么 |
|---|---|---|---|
| L1 | Top Layer | 1 | 信号:服务带、P+/P− 焊盘、U5n、U31、J23 |
| L2 | Inner1 | 15 | **电源面(分割)** —— 见下,⛔ 还没划 |
| L3 | Inner2 | 16 | **完整地平面,不许割断** ✅ 已铺 |
| L4 | Bottom | 2 | 信号:J31/J32、整个 Hub |
地平面放 Inner2 而不是 Inner1,是因为 `U32` 在**底层**,所有长距离 D± 都从底层出发,
底层的参考面是 Inner2 —— 反过来的话底层 D± 就以分割的电源面为参考(§10.1 不许)。
## ✅ Inner1 分割电源面已划(2026-09-05 夜)
五块 `VOn_P` 岛 + GND 底填,定义在下面的 `ISLANDS`,实测见 §两个校核脚本。
排母坐标:J31 x 37.869~86.129 / J32 x 133.869~182.129,两行 y = **66.23 / 68.77**
(`pcb2-header-pads.json`)。
### 两个必须都过的校核(**都不看 EDA 的段数**)
/c/Python314/python .claude/inner1_check.py # 下发前:按 ISLANDS + 反焊盘预测
/c/Python314/python .claude/inner1_verify.py # 下发后:按 EDA 实际吐的铜实测
/c/Python314/python .claude/inner1_diffref.py # §10.1:顶层 5 段 *J 差分下方是不是一整块
实测结果(2026-09-05):五块岛各自 **1 个连通域**、5 个本网通孔焊盘全在里面,
最窄断面 **7.20 / 7.60 / 7.70 / 11.70 / 12.90 mm**(下限 7.0)。
DRC 499 = 同封装内间距 115(与基线一致)+ 连接性 384(比铺岛前的 396 少 12)+ 间距错误 0。
### ⚠️ 焊盘连接方式:五路 `VOn_P` 必须是「全连接」
默认的 `copperRegion` 规则是 **十字花**(`connectMode: 0`,辐条 0.254 mm)。
十字花下铜面多边形会把**自己的**焊盘也挖空、只留 4 条描边辐条,
于是 `inner1_verify.py` 量到的断面是 **0.3 mm** —— 那是**度量假象**,
但假象和真断路在数据上分不出来。所以给 VOn_P 挂了 `PLANE-DIRECT`
(`pcb2_rules.py` 的 `PLANE_RULE`,`connectMode: 1`),铜面才是一整块。
GND 留默认十字花(~200 个通孔焊盘要手焊)。**电气上两者都够,理由见 pcb2_rules.py。**
⚠️ 那 80 个 2.54 间距的通孔在内层会开出两条反焊盘带
(Ø1.7 焊盘 + 0.2 净距 ⇒ 相邻反焊盘之间只剩 **0.44 mm** 铜网)。
平面不会被切断,但**电源岛的颈宽别指望从排母底下横穿过去**,要绕。
落地成这样(**只做两类,其余全部 GND 底填**):
| 岛 | 网络 | 为什么 | 最小颈宽 |
|---|---|---|---|
| 5 块 | `VO1_P`~`VO5_P` | 每路 4 A | **≥ 7.0 mm** |
| 底填 | `GND` | 剩余全部,**优先级数字给最大** | — |
> ⚠️ **2026-09-05 夜简化:`+5V` 和 `+3V3_HUB` 两块岛取消了**(§25.10)。
> 排母逐针分配按几何重排之后,`+5V` 从 3 针变成 **10 针、分散在两颗排母的南北四处**,
> 每个入口只喂本站的 `U2n` + `C70n`、段电流 ≤0.6 A ⇒ **顶层 0.5~0.8 mm 走线就够**,
> 内层不用给它留地方。`+3V3_HUB`(420 mA、局部)同理。
> ⇒ Inner1 只剩「**五块 `VOn_P` + GND 底填**」,五路 4 A 各走各的,互不挤。
颈宽是按 **IPC-2221 内层(0.5 oz)** 算的,不是外层:
`A = (I/(0.024·ΔT^0.44))^(1/0.725)`,ΔT=20 °C ⇒ 4 A 需 188 mil² / 0.685 mil = **6.97 mm**,
2.8 A 需 **4.3 mm**。两排手机之间的走廊只有 35.8 mm 高(y 49.66~85.5),
所以**不能**像原计划那样在 Inner1 上开十二块岛,装不下。
`VBUS1~5` 故意不做岛:每口 0.5 A、走线只在本服务带内约 18 mm,`PWR1A`(0.6 mm) 足够。
额外好处:顶层那 5 段 `*J` 差分(U5n→J7n,5 mm)下方就是**完整的 GND 底填**,
参考面比分割的电源面干净。
走廊里 Inner1 已知的障碍(内层只怕直插焊盘,不怕表贴件)。
⚠️ **2026-09-05 五个电解整体上移了 2.5 mm**(`pcb2_place.BAND['c1']` 的 v −11.0 → −8.0,
成因见 §25.9),下面这几个 y 是**改后**的值:
| 障碍 | 位置 |
|---|---|
| `C801/C811/C821`(上排三站)Ø12.5 罐 | y **70.75~83.25** |
| `C831/C841`(下排两站)Ø12.5 罐 | y **51.95~64.45** |
| 两个中跨 M3 的 Ø6 焊盘 | (76.6, 80.1) / (148.1, 80.1) |
| **J31/J32 的 80 个通孔焊盘** | 两行 y 66.23 / 68.77,x 见上 |
## 四条铺铜的硬经验(都在 skill §9j~§9l,这里只留判据)
1. **多边形末尾必须把首点再写一遍**,不闭合的话 `create()` 直接不建(下板 `pcb_pour.py` 的坑)。
`create()` **成功了也会抛异常**,返回值和 try/catch 都不能当判据 —— 只能建完 `getAll()` 按名字核。
⚠️ 铺铜名字会被转成大写,比对要 `toUpperCase()`。
2. **优先级是「数字小的先铺、占地盘」**(对话框底部自己写着),
而 **`create()` 的 `pourPriority` 参数不生效** —— 优先级是**按层、按创建顺序**编的。
⚠️ **步长是 2 不是 1**:同层建 6 块,回读是 **1,3,5,7,9,11**(2026-09-05 实测)。
所以只能断言**相对次序**,别断言「第 k 块 == k」—— 第一版就是这么写的,误报了一次。⚠️ 别拿 `modify(id, {pourPriority})` 去补设:不生效,还会把客户端弄卡死。
⇒ **所以 `plan()` 的顺序就是优先级**:电源岛写在前面(拿 1,2,3…),
**GND 底填写在最后**(拿最大的数字)。`--push` 会回读优先级并断言它等于计划里的次序。
2026-09-05 在本板 Inner1 上做过对照实验证实(skill §9j 有数据)。
3. **填充只能点 GUI 菜单**(`rebuildCopperRegion()` 必然抛)。点了没反应 =
客户端卡在后台任务上(窗口底部一条 0% 进度条),`location.reload()` 后一次就通;
⚠️ **重载会丢掉重载前建的边框,即使 save 过** —— 所以 `--push` 一气跑完,别分两次。
4. **`preserveSilos=False` + 区域内没有本网焊盘/过孔 = 整块被当孤岛删光**
(边框还在、`nPoured` 是 0)。**扇出必须排在铺铜之前。**
"""
import json
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
MIL = 39.3700787401575
L_TOP, L_PWR, L_GND, L_BOT = 1, 15, 16, 2 # ⚠ 载板:Inner1=电源 / Inner2=地
BW, BH = 220.0, 135.0
CORNER_R = 5.0
INSET = 0.5 # 铜面离板边
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1800));" % PCB)
def call(js, timeout=120):
r = run(OPEN + js, timeout=timeout)
if not r.get('success'):
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:500])
return r['result']
def board_poly(inset=INSET):
u"""板框内缩 inset 的圆角矩形 -> TPCB_PolygonSourceArray(mil)。
格式:x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...
ARC 的起点是「当前点」,角度正值逆时针(本工程 y 轴朝上)。
⚠️ API 枚举不到板框的圆弧(`pcb_PrimitiveArc.getAll()` 返回 0),
所以这里不去读板框,按几何自己造。打样前用 Gerber 预览复核板框闭合。
"""
d, R = inset, CORNER_R
seq = [('L', BW - R, d), ('A', BW - d, R),
('L', BW - d, BH - R), ('A', BW - R, BH - d),
('L', R, BH - d), ('A', d, BH - R),
('L', d, R), ('A', R, d)] # 最后一段弧回到起点,自然闭合
src, mode = [R * MIL, d * MIL], None
for k, x, y in seq:
if k == 'L':
if mode != 'L':
src.append('L')
mode = 'L'
src += [x * MIL, y * MIL]
else:
src.append('ARC')
mode = 'ARC'
src += [90.0, x * MIL, y * MIL]
return src
def polysrc(pts):
u"""点列 -> TPCB_PolygonSourceArray(全直边)。
⚠️ 末尾把首点再写一遍 —— 不闭合的话 `create()` 什么也不建(下板踩过)。
"""
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 polysrc([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
from shapely.geometry import box as _sbox
from shapely.ops import unary_union as _union
def body_bbox(rects):
return (min(r[0] for r in rects), min(r[1] for r in rects),
max(r[2] for r in rects), max(r[3] for r in rects))
def _poly_of_rects(rects):
u"""若干轴对齐矩形的并 -> 逆时针点列(mm)。必须并成**一个**多边形。"""
u = _union([_sbox(*r) for r in rects])
if u.geom_type != 'Polygon':
raise SystemExit(u'岛的矩形并不成一块: %s' % u.geom_type)
u = u.simplify(0.001)
pts = list(u.exterior.coords)[:-1]
if not u.exterior.is_ccw:
pts.reverse()
return [(round(x, 3), round(y, 3)) for x, y in pts]
def island_poly(body, branch):
u"""整块岛 = 走廊主体(4 A 路径)∪ 分支(穿筋的颈 + 主板投影下方那块)。"""
return _poly_of_rects(list(body) + list(branch))
def island_body_poly(body):
return _poly_of_rects(list(body))
# ── v3(2026-09-06,§25.16):槽在主板远端边,岛 = 走廊主体 + 整块主板投影下方的带 ──
#
# 4 A 路径:排母针(走廊,y 66~69)→ 主板投影下方一路到 TP8n1(v=42,贴着远端槽)。
# 中间没有槽挡路(槽在投影外侧 v 45.5~48.5),整块岛就是一条 34 mm 宽的带,
# 7 mm 颈宽规则对整块岛都判(branch 留空)。
# 走廊主体的边界沿用 v1 的推导(每条都被具体的异网排母针顶住,见 obsolete 版注释):
# VO1 右 44.30 <- J31.7 +5V;VO2 左 80.00 <- J31.33 GND + MH5;VO3 颈 168.5~175.9 <- J32.27/35;
# VO4 颈 71.5~79.9 <- J31.28/36;VO5 颈 152.8~160.6 <- J32.16/24。
# 主板下方那块取本体 u 12~46(避开 u<8 的顶针过孔),v −1 ~ 14(v3b:TP/电容组都贴着接口边,v 3.5~11;C8n1 已是 SMD)。
# 电池槽 (u 18.8~28.8, v −4.2~−1.2) 落在走廊主体里,铺铜自己让开,剩余宽度仍 > 20。
# 定位孔(NPTH)落在带里没关系,铺铜自己让开,带宽 34 远大于 7。
def _site_rect(n, u0, v0, u1, v1):
import pcb2_place as P
ax, ay, _ = P.to_board(n, u0, v0)
bx, by, _ = P.to_board(n, u1, v1)
return (round(min(ax, bx), 3), round(min(ay, by), 3), round(max(ax, bx), 3), round(max(ay, by), 3))
# 2026-09-06 v5:主板整体转 180°(接口边朝板边)之后,电容组 / J8n / C8n1 都到了远离走廊的那一头(v 5~20 → 板边一侧),
# 所以主板投影下方那块岛改成**整条竖带 u 8~44 × v −1~47**:从板边一直贯通到走廊(v 44.66 是主板远端边,47 伸进走廊 2.3 让它和主体搭上)。
# u ≤ 44 避开尾插排线车道 / J7n / U5n(u 44.6~63,D± 在那里要 GND 参考),顶针在 u 2~6 也在带外。
# VO2 的走廊主体(x 80~87.3)和它的竖带(x 99.6~135.6)不相接,加一条 **Hub 北边的横带 y 75.5~83**(Hub 卫星最北 C742/C749/C751 到 y 74.97),高 7.5。
# ⚠️ 这条横带把走廊北带 y 75.5~83 变成 VO2_P 平面 —— 那里的差分只能走**底层**(参考 Inner2 GND),别走顶层。
ISLANDS = [
('VO1', 'VO1_P', [(12.5, 67.6, 44.3, 83.0), _site_rect(1, 8.0, -1.0, 44.0, 47.0)], []),
('VO2', 'VO2_P', [(80.0, 67.6, 87.3, 83.0), (87.0, 75.5, 100.5, 83.0), _site_rect(2, 8.0, -1.0, 44.0, 47.0)], []),
('VO3', 'VO3_P', [(152.0, 70.5, 195.0, 83.0), (168.5, 67.6, 175.9, 70.5), _site_rect(3, 8.0, -1.0, 44.0, 47.0)], []),
# v5 布线(2026-09-06):VO4 主体东界 89→87,给 site4 短差分 / pair4 / +3V3 干线的顶层段让出 GND 参考(x ≥ 87.3 全是 GND)
('VO4', 'VO4_P', [(61.0, 44.0, 87.0, 65.15), (71.5, 63.5, 79.9, 67.4), _site_rect(4, 8.0, -1.0, 44.0, 47.0)], []),
('VO5', 'VO5_P', [(132.5, 44.0, 172.0, 65.15), (152.8, 63.5, 160.6, 67.4), _site_rect(5, 8.0, -1.0, 44.0, 47.0)], []),
]
# ── 要建的铜面:name, net, layer, 多边形 ──────────────────────────────
# ⚠️⚠️ **这张表的顺序就是优先级**(同层内按创建顺序从 1 编,数字小的先铺、占地盘)。
# 所以:**电源岛写在前面,GND 底填写在同层的最后一条。**
def plan():
ps = []
# Inner1(L_PWR=15):**五块 VOn_P 岛写在最前面**,拿优先级 1~5、先占地盘;
# GND 底填排在同层最后一条,拿 6,只填剩下的。
# `+5V` / `+3V3_HUB` 不开岛 —— 重排逐针分配之后它们分散取电、走顶层线(§25.10)。
for name, net, body, branch in ISLANDS:
ps.append((u'%s-INNER1' % name, net, L_PWR, polysrc(island_poly(body, branch))))
ps.append((u'GND-INNER1', 'GND', L_PWR, board_poly()))
# Inner2:整板地平面,一块到底,不许割断(§10.1 / §25.6 ①)。
# 这一层只有它一块,优先级不参与裁剪。
ps.append((u'GND-INNER2', 'GND', L_GND, board_poly()))
return ps
def with_prio(ps):
u"""按「同层内创建顺序 = 优先级」给每块算出**预期的名次**(不是优先级数值)。
⚠️ 2026-09-05 实测:优先级数值**不是 1,2,3…,是 1,3,5,7,9,11**(同层内步长 2)。
一块的时候是 1,六块就是 1..11 的奇数 —— 所以只能断言**次序**,
不能断言「第 k 块的优先级 == k」(第一版就是这么写的,误报了一次)。
"""
seen, out = {}, []
for name, net, layer, src in ps:
seen[layer] = seen.get(layer, 0) + 1
out.append((name, net, layer, src, seen[layer]))
return out
JS_DEL = u"""
let n = 0;
for (const o of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(o.primitiveId); n++; }
return {deleted: n};
"""
JS_MAKE = u"""
const PLAN = %s;
// ⚠️ 顺序即优先级:PLAN 是按「同层内谁该赢谁就在前面」排好的,**不要重排这个循环**。
for (const p of PLAN) {
const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]);
if (!poly) continue;
// ⚠️ create() **成功了也会抛**,返回值和 try/catch 都不能当判据。
// 第 7 个参数 pourPriority 传什么都没用,真正决定优先级的是**这个循环的次序**。
try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); }
catch (e) {}
}
// ⚠️⚠️ 这里**不能**用 `pcb_PrimitivePour.modify(id, {pourPriority})` 去补设优先级:
// 不生效(回读不变),还会把客户端弄进「后台任务卡在零进度」的状态 ——
// 之后点「重建所有」永远填 0 块,只能重载,而重载又会丢掉已建的边框。见 skill §9j / §9k。
return (await eda.pcb_PrimitivePour.getAll())
.map(x => [String(x.getState_PourName()||'').toUpperCase(), x.getState_PourPriority()]);
"""
# 工具 → 铺铜管理器... → 重建所有(菜单项是 React 元素,按钮派发真事件)
JS_REBUILD = 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 = t => { for (const el of document.querySelectorAll('*'))
if (el.children.length===0 && (el.textContent||'').trim()===t) return el; return null; };
const btn = t => { for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()===t && b.offsetParent!==null) return b; return null; };
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
const cancel = btn('取消'); if (cancel) { nat(cancel); await sleep(1200); }
if (!fire(leaf('工具'))) return {err: 'no 工具'};
await sleep(900);
if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'};
await sleep(2500);
const b = btn('重建所有');
if (!b) return {err: 'no 重建所有'};
nat(b);
// 卡住的判据:窗口底部一条不动的 0% 进度条 -> 客户端卡在后台任务上,要 location.reload()
const pct = [];
for (const d of document.querySelectorAll('*'))
if (d.offsetParent!==null && d.children.length===0 && /^\\d+%$/.test((d.textContent||'').trim())) pct.push(d.textContent.trim());
return {fired: true, pct};
"""
JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};"
JS_DONE = u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { nat(b); break; }
await sleep(1500);
await eda.pcb_Document.save();
await sleep(2000);
const fills = [];
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
fills.push(f.length);
}
// ⚠️ 返回铺铜名,用来确认打的是**载板**而不是上一条命令留下的下板(skill §9l)
return {fills, pour: (await eda.pcb_PrimitivePour.getAll())
.map(p => [p.getState_PourName(), p.getState_Net(), p.getState_Layer()])};
"""
JS_VERIFY = u"""
const byId = {};
for (const p of await eda.pcb_PrimitivePour.getAll())
byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''),
p.getState_PourPriority(), p.getState_Layer()];
// ⚠️ 环是 `x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...` —— **'ARC' 后面第一个数是角度,不是坐标**。
// 直接把所有数字按奇偶分 x/y 会错位,实测把包围盒算成 -22.9 ~ 219.4(角度混进去了)。
const walk = (ring, hit) => {
let i = 0, mode = 'L';
while (i < ring.length) {
const v = ring[i];
if (v === 'L') { mode = 'L'; i++; continue; }
if (v === 'ARC') { mode = 'ARC'; i++; i++; continue; } // 跳过角度
hit(ring[i], ring[i + 1]); i += 2;
}
};
const out = [];
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
const info = byId[q.getState_PourPrimitiveId()] || ['?', '?', '?'];
let rings = 0, x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9, npt = 0;
for (const seg of f) {
const cp = (seg.path && seg.path.complexPolygon) || [];
rings += cp.length;
for (const ring of cp) walk(ring, (x, y) => {
npt++;
if (x < x0) x0 = x; if (y < y0) y0 = y;
if (x > x1) x1 = x; if (y > y1) y1 = y;
});
}
out.push({name: info[0], prio: info[1], layer: info[2],
segs: f.length, rings: rings, npt: npt,
bb: npt ? [x0, y0, x1, y1] : null});
}
return out;
"""
U10MIL = 3.9370078740157477 # getState_PourFills() 的坐标单位:10 mil / 单位
def verify():
u"""回读**真正铺出来的铜**,不是边框。判据是 `rings > segs` —— 多出来的环就是洞。"""
out = call(JS_VERIFY)
print(u'──── 铺铜实测(读回 getState_PourFills)────')
for e in out:
bb = e['bb']
geo = (u'%.1f,%.1f ~ %.1f,%.1f' % (bb[0] / U10MIL, bb[1] / U10MIL,
bb[2] / U10MIL, bb[3] / U10MIL)) if bb else u'空'
print(u' %-12s prio=%-3s layer=%-3s %d 段 / %d 环 / %d 点 包围盒 %s'
% (e['name'], e['prio'], e['layer'], e['segs'], e['rings'], e['npt'], geo))
if not out:
print(u' [!!] 一块铜都没铺出来 —— 看 --push 那边的排查提示')
return out
def main():
ps = with_prio(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' %-12s net=%-9s layer=%-3d prio=%-2d %d 点 / %d 段弧'
% (name, net, layer, prio, n_pt, n_arc))
if '--verify' in sys.argv:
verify()
return
if '--push' not in sys.argv:
print(u'(未加 --push 没有下发;--verify 回读实测铺铜结果)')
return
print(u'')
print(u'清掉旧铜面: %d 块' % call(JS_DEL)['deleted'])
batch = [{'name': n, 'net': net, 'layer': l, 'src': src, 'prio': pr}
for n, net, l, src, pr in ps]
made = call(JS_MAKE % json.dumps(batch, ensure_ascii=False))
print(u'已建: ' + ' '.join(u'%s(prio %s)' % (n, p) for n, p in made))
miss = [n for n, _, _, _, _ in ps if n.upper() not in [m[0] for m in made]]
if miss:
sys.exit(u'!! 这几块没建出来: ' + ' '.join(miss))
# 断言的是**同层内优先级的相对次序**和 plan() 一致(数值是 1,3,5,… 不连续,见 with_prio)
got = dict((m, p) for m, p in made)
bad = []
for layer in sorted(set(l for _, _, l, _, _ in ps)):
seq = [(n, got[n.upper()]) for n, _, l, _, _ in ps if l == layer]
for (na, pa), (nb, pb) in zip(seq, seq[1:]):
if not pa < pb:
bad.append((layer, na, pa, nb, pb))
if bad:
print(u' [!!] 同层内优先级次序和 plan() 不一致(层, 前, 前优先级, 后, 后优先级): %s' % bad)
print(u' 优先级 = 同层内的创建顺序,对不上说明 plan() 的次序被打乱了,')
print(u' 重叠的铜面会按错的顺序裁剪。**先修好再往下走。**')
sys.exit(1)
print(u' 优先级次序 OK:' + ' | '.join(
u'L%d %s' % (layer, ' < '.join(u'%s(%s)' % (n, got[n.upper()])
for n, _, l, _, _ in ps if l == layer))
for layer in sorted(set(l for _, _, l, _, _ in ps))))
r = call(JS_REBUILD)
if r.get('err'):
sys.exit(u'!! 打不开铺铜管理器: ' + r['err'])
# 2026-09-07:正常一次填充 7 块 < 30 s;卡在 0/7 超过 ~90 s 就是客户端卡住了(§9k),
# 再等到 4 分钟也不会好,早点退出去重载。
for i in range(22):
time.sleep(4)
n = call(JS_POLL, timeout=40)['poured']
print(u' 填充 %d / %d' % (n, len(ps)))
if n >= len(ps):
break
else:
print(u'!! 填充一直上不去。看看窗口最底下是不是有条卡在 0% 的进度条 ——')
print(u' 那是客户端卡在后台任务上,跑 location.reload() 重载再来(skill §9k)。')
print(u' ⚠️ 重载会丢掉已建的边框,重载后要从 --push 头上重跑。')
sys.exit(1)
d = call(JS_DONE)
print(u'完成:铜面 %d 块,各自填充 %s' % (len(d['pour']), d['fills']))
for name, net, layer in d['pour']:
print(u' %-12s net=%-9s layer=%d' % (name, net, layer))
print(u'')
verify()
if __name__ == '__main__':
main()