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