# -*- coding: utf-8 -*- u"""R2 底板 PCB 铺铜 + 缝合过孔 + 断面校核(M5,**4 层**,D15)。 /c/Python314/python .claude/r2/pour_base.py # 只算 + 自检 + 断面校核 /c/Python314/python .claude/r2/pour_base.py --stitch # 打 GND 缝合过孔 /c/Python314/python .claude/r2/pour_base.py --push # 清旧铜面 + 建 + 重填 /c/Python314/python .claude/r2/pour_base.py --rebuild # 只重填 /c/Python314/python .claude/r2/pour_base.py --clear # 清掉全部铜面 2 层版整份在 `archive_base2L/pour_base.py`。 ⛔ **顺序**:摆位 → 布线(服务器)→ `route_base --push` → **缝合过孔** → **铺铜** → DRC + `board_check`。 重跑布线要把缝合过孔一起重跑。 ## 叠层(D15,2026-09-10 L2 由 GND 改 +3V3) | 设计 | EasyEDA | id | 铺什么 | |---|---|---|---| | L1 | Top | 1 | 12V 大铜(`pcb_base.l1_12v_rects`,硬)+ GND 底填 | | L2 | Inner1 | 15 | **整层 +3V3** | | L3 | Inner2 | 16 | **整层 GND**(L4 上全部器件与 D± 的参考)| | L4 | Bottom | 2 | 入口链 VIN_RAW / VIN_FUSED / VIN_SW(硬)+ Rs6 西侧 12V 铜块(硬)+ GND 底填 | 「硬」= 几何算出来、要过断面校核;GND 底填优先级最低,自然绕开别人的铜。 ## 入口链(L4,电流自南向北:J1 → F1 → Q1 → C2/C3/D1 → Rs6) 三段是**串联**的,中间隔着 F1 / Q1 —— 铜面搭上就把器件短路了。每段取本网在入口链带里的焊盘外框再外扩, 相邻两段在 y 方向按「两网相邻焊盘的空隙中点」切开(2 层版 `clip_chain` 的教训:按重叠区中点切会切进 Q1 里面)。 Q1 栅极网络(DZ1 / C1 / R1 与 GATE 脚)整团挖掉,给 GATE 走线。 U13.10(Kelvin 取样)与 C4(Rs6 北侧的 VIN_SW 去耦)不撑大铜面:前者走专线,后者会把 VIN_SW 拉过 Rs6 的 +12V 端。 ## 断面校核 在铜面上画断面线,数「这条线上属于本网、没被别人的焊盘 / 过孔 / 同层导线占掉」的**最宽一段**。 入口链与 L1 12V 主块的电流都沿 y ⇒ 横切、量 x 方向净宽。串联元件本身占的 y 段跳过(量到的是元件宽度不是铜)。 """ import io import json import math import os import sys import time 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 import place_base as PL # noqa: E402 PCB = B.PCB MIL = B.MIL L1, L2, L3, L4 = 1, 15, 16, 2 # EasyEDA 层 id:Top / Inner1 / Inner2 / Bottom LNAME = {L1: 'L1', L2: 'L2', L3: 'L3', L4: 'L4'} # ⚠ 打印用设计层名 —— 直接打 id 的话底层是「2」,看着像 L2 INSET = 0.5 # 铜面离板边 CLR = 0.25 # 断面校核里别人的铜要让开多少 VIA_D, VIA_HOLE = 0.6, 0.3 STITCH_PITCH = 4.0 STITCH_CLR = 0.5 # 缝合过孔到任何焊盘 / 导线 / 过孔的净距 PADS = os.path.join(HERE, '_base_pads.json') ROUTES = os.path.join(HERE, '_base_routes.json') ROUTE_SCHEMA = 8 CHAIN = [('VIN_RAW', 1), ('VIN_FUSED', 2), ('VIN_SW', 3)] # (网, 优先级),自南向北 CHAIN_GROW = 1.2 CHAIN_SKIP = {'VIN_SW': ('U13', 'C4')} # 不撑大铜面的焊盘(见文件头) GATE_PARTS = ('DZ1', 'C1', 'R1') NOTCH_CLR = 0.3 # (铜面名前缀, 标签, 网, 电流方向, 要求净宽, 是否强制, 层, 是否跳过串联元件) CUTS = [ ('PWR-VIN_RAW', u'J1→F1', 'VIN_RAW', 'y', 8.0, False, L4, True), ('PWR-VIN_FUSED', u'F1→Q1', 'VIN_FUSED', 'y', 8.0, False, L4, True), # ⚠ VIN_RAW / VIN_FUSED 的瓶颈是 DC-005 的脚与 1206 保险本身(用户 2026-09-08 接受的已知风险,设计文档 §22.6) ('PWR-VIN_SW', u'Q1→储能→Rs6', 'VIN_SW', 'y', 6.5, True, L4, True), # L1 主块上挂满了背面件的 GND / +3V3 / +12V 过孔(反焊盘),串联元件窗口对它没意义 ('PWR-12V-L1', u'L1 12V 主块', '+12V', 'y', 8.0, True, L1, False), ] def mil(v): return round(v * MIL, 4) def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600]) sys.exit(1) return r['result'] OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2500));" % PCB) _PADS = [None] def pads(): u"""板上焊盘(丢掉多边形拆条的零尺寸残留,同 route_base.load_pads)。""" if _PADS[0] is None: _PADS[0] = [p for p in json.load(io.open(PADS, encoding='utf-8')) if p[4] >= 0.05 and p[5] >= 0.05] return _PADS[0] _ROUTES = [None] def routes(): if _ROUTES[0] is None: c = json.load(io.open(ROUTES, encoding='utf-8')) if os.path.exists(ROUTES) else {} if c.get('schema') != ROUTE_SCHEMA: _ROUTES[0] = ([], [], None) else: _ROUTES[0] = (c['segs'], c['vias'], c.get('v12_patch')) return _ROUTES[0] def _layers_of(p): return (L1, L4) if p[6] == 12 else ((L1,) if p[6] == 1 else (L4,)) # ── 铜面几何 ───────────────────────────────────────────────────────────── def keepouts(): u"""所有层都不许铺铜:U20 天线净空 + 天线缺口 + 四个 M3 孔。""" d = json.load(io.open(os.path.join(HERE, '_base_place.json'), encoding='utf-8')) pos = {k: (v[0], v[1], v[2]) for k, v in d.items()} n = B.notch_rect() out = [PL.antenna_keepout(PL.sizes(), pos), (n[0] - INSET, n[1] - INSET, n[2] + INSET, n[3] + INSET)] r = B.HOLE_D / 2.0 + 0.6 for (hx, hy) in B.MOUNT_HOLES.values(): out.append((hx - r, hy - r, hx + r, hy + r)) return out def tiles(x0, y0, x1, y1, cuts): u"""把矩形按 `cuts` 切成若干块**单环**矩形(多环复杂多边形会把客户端挂死,刀片 §24)。""" xs = sorted(set([x0, x1] + [v for c in cuts for v in (c[0], c[2]) if x0 < v < x1])) out = [] for xa, xb in zip(xs, xs[1:]): if xb - xa < 0.6: continue iv = [] for c in cuts: # ⛔ y 方向也要真的相交:禁区整个在矩形上方 / 下方时,夹紧会得到一个倒过来的区间, # 照样切出一块瓦片 —— 实测 VIN_RAW 被栅极缺口「切」出一块伸到 y 22.65 的瓦片(它的上沿本来是 17.62) if c[0] < xb - 1e-9 and xa < c[2] - 1e-9 and c[1] < y1 - 1e-9 and y0 < c[3] - 1e-9: iv.append([max(y0, c[1]), min(y1, c[3])]) iv.sort() merged = [] for a, b in iv: # ⛔ 先合并再配对(嵌套禁区会让奇偶配对错位) if merged and a <= merged[-1][1] + 1e-9: merged[-1][1] = max(merged[-1][1], b) else: merged.append([a, b]) cur = y0 for a, b in merged + [[y1, y1]]: if a - cur > 0.6: out.append((round(xa, 3), round(cur, 3), round(xb, 3), round(a, 3))) cur = max(cur, b) return out def chain_pads(net): z = B.ZONES['A1'] skip = CHAIN_SKIP.get(net, ()) return [p for p in pads() if p[7] == net and p[0] not in GATE_PARTS and p[0] not in skip and z[0] <= p[2] <= z[2] and z[1] <= p[3] <= z[3]] def chain_rect(net): ps = chain_pads(net) if not ps: return None g = CHAIN_GROW z = B.ZONES['A1'] return (max(z[0], min(p[2] - p[4] / 2 for p in ps) - g), max(INSET, min(p[3] - p[5] / 2 for p in ps) - g), min(z[2], max(p[2] + p[4] / 2 for p in ps) + g), min(z[3], max(p[3] + p[5] / 2 for p in ps) + g)) def clip_chain(chain): u"""相邻两段在 y 方向切开:刀口 = 「南段最北的焊盘上沿」与「北段最南的焊盘下沿」的中点(在 x 重叠带里取)。""" out = [r for _n, r in chain] for i in range(len(out) - 1): a, b = out[i], out[i + 1] if a[3] <= b[1]: continue x0, x1 = max(a[0], b[0]), min(a[2], b[2]) pa = [p for p in chain_pads(chain[i][0]) if p[2] + p[4] / 2 > x0 and p[2] - p[4] / 2 < x1] pb = [p for p in chain_pads(chain[i + 1][0]) if p[2] + p[4] / 2 > x0 and p[2] - p[4] / 2 < x1] na = max([p[3] + p[5] / 2 for p in pa] or [None]) sb = min([p[3] - p[5] / 2 for p in pb] or [None]) m = (na + sb) / 2.0 if (na is not None and sb is not None and na < sb) else (a[3] + b[1]) / 2.0 out[i] = (a[0], a[1], a[2], round(min(a[3], m - CLR / 2), 3)) out[i + 1] = (b[0], round(max(b[1], m + CLR / 2), 3), b[2], b[3]) return out def gate_notch(): u"""Q1 栅极网络整团挖掉(GATE 焊盘 + 三颗栅极件外框,外扩 NOTCH_CLR)。""" out = [] for p in pads(): if p[7] == 'GATE' or p[0] in GATE_PARTS: out.append((round(p[2] - p[4] / 2 - NOTCH_CLR, 3), round(p[3] - p[5] / 2 - NOTCH_CLR, 3), round(p[2] + p[4] / 2 + NOTCH_CLR, 3), round(p[3] + p[5] / 2 + NOTCH_CLR, 3))) return out CHAIN_DEBUG = [] # [(网, 原始外框, 裁剪后, 挖缺口后的瓦片)],打印核对用 def plan(): u"""[(name, net, layer, rect, priority)],priority 小的先占地盘。""" outs = [] CHAIN_DEBUG[:] = [] chain = [(net, prio, chain_rect(net)) for net, prio in CHAIN] chain = [c for c in chain if c[2]] chain.sort(key=lambda c: (c[2][1] + c[2][3]) / 2.0) clipped = clip_chain([(c[0], c[2]) for c in chain]) notch = gate_notch() for (net, prio, raw), r in zip(chain, clipped): parts = tiles(r[0], r[1], r[2], r[3], notch) parts.sort(key=lambda t: -(t[2] - t[0]) * (t[3] - t[1])) CHAIN_DEBUG.append((net, raw, r, parts)) for k, t in enumerate(parts): outs.append(('PWR-%s%s' % (net, '-%d' % k if k else ''), net, L4, t, prio)) ko = keepouts() v12 = routes()[2] if v12: outs.append(('PWR-12V-PATCH', '+12V', L4, tuple(v12), 2)) # L1 12V:主块要绕开两个 M3 孔((4,4) / (4,94) 就在主块里),同样切成单环瓦片 k = 0 for r in B.l1_12v_rects(): for t in tiles(r[0], r[1], r[2], r[3], ko): outs.append(('PWR-12V-L1%s' % ('-%d' % k if k else ''), '+12V', L1, t, 2)) k += 1 d = INSET for tag, net, lay, prio in (('3V3-L2', '+3V3', L2, 5), ('GND-L3', 'GND', L3, 9), ('GND-L1', 'GND', L1, 9), ('GND-L4', 'GND', L4, 9)): for k, t in enumerate(tiles(d, d, B.BW - d, B.BH - d, ko)): outs.append(('%s-%d' % (tag, k), net, lay, t, prio)) return outs def selfcheck(ps): bad = [] ko = keepouts() for name, net, lay, r, _p in ps: if r[2] - r[0] < 0.6 or r[3] - r[1] < 0.6: bad.append(u'%s 太小: %s' % (name, r)) if r[0] < INSET - 1e-6 or r[1] < INSET - 1e-6 or r[2] > B.BW - INSET + 1e-6 or r[3] > B.BH - INSET + 1e-6: bad.append(u'%s 出了内缩线: %s' % (name, r)) for k in ko: if r[0] < k[2] - 1e-6 and k[0] < r[2] - 1e-6 and r[1] < k[3] - 1e-6 and k[1] < r[3] - 1e-6: bad.append(u'%s 压进净空区 %s' % (name, tuple(round(v, 1) for v in k))) hard = [(n, net, l, r) for (n, net, l, r, _p) in ps if n.startswith('PWR-')] for i, (na, neta, la, a) in enumerate(hard): for (nb, netb, lb, b) in hard[i + 1:]: if neta == netb or la != lb: continue if a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]: bad.append(u'%s(%s) 与 %s(%s) 同层重叠 —— 串联的两段搭上就把器件短路了' % (na, neta, nb, netb)) return bad # ── 断面校核 ───────────────────────────────────────────────────────────── def _blocks(net, layer, axis, t): u"""断面线(axis='y' ⇒ 横线 y=t;'x' ⇒ 竖线 x=t)上被别人占掉的区间:(切断, 孔洞)。 ⚠ 两类要分开:同层的**导线**把铺铜切成互不相连的几条(取最宽的一条); **焊盘和过孔**是孤立的岛 / 洞,铜从四周绕过去 —— 扣掉洞宽、不切断。 一排挨着的焊盘(缝 < 2×净距)按洞合并扣除,效果和切断一样,所以这个判据对针排也成立。 第一版把过孔、焊盘都当切断:L1 12V 主块被背面件的孔和 C2/C3 的通孔焊盘「切」成 4.95 / 6.20 mm。 """ out, holes = [], [] for p in pads(): if p[7] == net or layer not in _layers_of(p): continue if axis == 'y' and p[3] - p[5] / 2 - CLR <= t <= p[3] + p[5] / 2 + CLR: holes.append((p[2] - p[4] / 2 - CLR, p[2] + p[4] / 2 + CLR)) if axis == 'x' and p[2] - p[4] / 2 - CLR <= t <= p[2] + p[4] / 2 + CLR: holes.append((p[3] - p[5] / 2 - CLR, p[3] + p[5] / 2 + CLR)) segs, vias, _v = routes() for s in segs: if s[6] == net or s[0] != layer: continue # ⚠ 平面网(GND / +3V3 / +12V)的线都是「焊盘 → 自己的过孔」那一小截引出脚,是孤立的岛, # 铜从四周绕过去 ⇒ 按洞扣。只有信号线会从铜面一边穿到另一边、真的切断。 # (实测:VIN_SW 最窄的那一刀上只有 C6 地脚 0.8 mm 的引出脚,却被当成切断量成 6.07 mm。) dst = holes if s[6] in ('GND', '+3V3', '+12V') else out hw = s[5] / 2.0 + CLR if axis == 'y': y0, y1 = sorted((s[2], s[4])) if y0 - hw <= t <= y1 + hw: x0, x1 = sorted((s[1], s[3])) dst.append((x0 - hw, x1 + hw)) else: x0, x1 = sorted((s[1], s[3])) if x0 - hw <= t <= x1 + hw: y0, y1 = sorted((s[2], s[4])) dst.append((y0 - hw, y1 + hw)) r = VIA_D / 2.0 + CLR for v in vias: # 通孔:每一层都占,但只是洞 if v[2] == net: continue c, o = (v[1], v[0]) if axis == 'y' else (v[0], v[1]) if abs(c - t) <= r: holes.append((o - r, o + r)) return out, holes def cut_width(net, rects, axis, t, layer): u"""`rects` = 本网同层的几块瓦片(同网相邻即连成一片,先在断面线上合并)。返回最宽一条的净宽。""" iv = [] for rect in rects: if (rect[1] <= t <= rect[3]) if axis == 'y' else (rect[0] <= t <= rect[2]): iv.append(((rect[0], rect[2]) if axis == 'y' else (rect[1], rect[3]))) iv.sort() spans = [] for a, b in iv: if spans and a <= spans[-1][1] + 1e-6: spans[-1] = (spans[-1][0], max(spans[-1][1], b)) else: spans.append((a, b)) if not spans: return 0.0 cuts, holes = _blocks(net, layer, axis, t) for (b0, b1) in cuts: nxt = [] for (a0, a1) in spans: if b1 <= a0 or b0 >= a1: nxt.append((a0, a1)) continue if b0 > a0: nxt.append((a0, b0)) if b1 < a1: nxt.append((b1, a1)) spans = nxt best = 0.0 for (a0, a1) in spans: # 洞:合并重叠的反焊盘之后按实际落在这一条里的长度扣 hs = sorted((max(a0, h0), min(a1, h1)) for (h0, h1) in holes if h1 > a0 and h0 < a1) lost, cur = 0.0, a0 for h0, h1 in hs: if h1 > cur: lost += h1 - max(h0, cur) cur = h1 best = max(best, (a1 - a0) - lost) return best def series_windows(net): u"""这个网自己串联元件占的外框(整颗器件,不只本网那只脚)。""" own = set(p[0] for p in pads() if p[7] == net) by = {} for p in pads(): if p[0] not in own: continue b = by.setdefault(p[0], [1e9, 1e9, -1e9, -1e9]) b[0] = min(b[0], p[2] - p[4] / 2); b[2] = max(b[2], p[2] + p[4] / 2) b[1] = min(b[1], p[3] - p[5] / 2); b[3] = max(b[3], p[3] + p[5] / 2) return by def check_cuts(ps): print(u'') print(u'载流断面校核(逐刀切,取最窄一刀):') if not routes()[0]: print(u' ⚠ 没有 4 层布线缓存(schema %d),只按焊盘 / 过孔扣' % ROUTE_SCHEMA) bad = [] byname = {} for (n, _net, _l, r, _p) in ps: byname.setdefault(n, []).append(r) for key, label, net, axis, need, must, layer, skip_series in CUTS: rs = [r for n, v in byname.items() if n == key or n.startswith(key + '-') for r in v] if not rs: print(u' %-14s %-9s [X] 这一段没有铜面' % (label, net)) bad.append(label) continue r = (min(q[0] for q in rs), min(q[1] for q in rs), max(q[2] for q in rs), max(q[3] for q in rs)) skip = [] if skip_series: for ref, b in series_windows(net).items(): if b[0] > r[2] or b[2] < r[0] or b[1] > r[3] or b[3] < r[1]: continue skip.append((ref, (b[1] - CLR, b[3] + CLR) if axis == 'y' else (b[0] - CLR, b[2] + CLR))) lo, hi = (r[1], r[3]) if axis == 'y' else (r[0], r[2]) hs = [] t = lo + 0.25 while t <= hi - 0.25: if not any(a <= t <= b for _ref, (a, b) in skip): hs.append((cut_width(net, rs, axis, t, layer), t)) t += 0.25 worst = min(hs) if hs else (0.0, 0.0) ok = worst[0] >= need print(u' %-14s %-9s 铜面 %5.1f×%5.1f 最窄 %5.2f mm(%s=%.2f,要 %.1f) %s' % (label, net, r[2] - r[0], r[3] - r[1], worst[0], axis, worst[1], need, u'[OK]' if ok else (u'[X] 不够' if must else u'[~] 瓶颈在连接器 / 保险,不强制'))) if skip: print(u' (跳过串联元件 %s)' % ' '.join(sorted(ref for ref, _w in skip))) if not ok and must: bad.append(u'%s %s 最窄只有 %.2f mm' % (label, net, worst[0])) return bad # ── 缝合过孔 ───────────────────────────────────────────────────────────── 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 stitch_points(ps): u"""GND 缝合过孔(4 mm 网格):L1 / L4 的 GND 底填会被信号切成岛,每块岛要有孔接到 L3,否则被当孤岛删掉 (`rebuildCopperRegion` / 铺铜管理器 / DRC 三处都不报,刀片 §24.4)。 ⛔ 硬功率铜面(入口链、12V 铜块、L1 12V 大铜)里不打:GND 孔只会在大电流铜上掏洞,断面校核看不到。""" segs, vias, _v = routes() r = VIA_D / 2.0 + STITCH_CLR ko = keepouts() + [rr for (n, _net, _l, rr, _p) in ps if n.startswith('PWR-')] out = [] y = INSET + 2.0 while y <= B.BH - INSET - 2.0: x = INSET + 2.0 while x <= B.BW - INSET - 2.0: ok = not any(k[0] - r < x < k[2] + r and k[1] - r < y < k[3] + r for k in ko) if ok: ok = not any(p[2] - p[4] / 2 - r < x < p[2] + p[4] / 2 + r and p[3] - p[5] / 2 - r < y < p[3] + p[5] / 2 + r for p in pads()) if ok: ok = not any(s[6] != 'GND' and _seg_d(x, y, s[1], s[2], s[3], s[4]) < r + s[5] / 2.0 for s in segs) if ok: ok = not any(math.hypot(x - v[0], y - v[1]) < VIA_D + STITCH_CLR for v in vias) if ok: out.append((round(x, 3), round(y, 3))) x += STITCH_PITCH y += STITCH_PITCH return out def stitch(ps): pts = stitch_points(ps) print(u'GND 缝合过孔 %d 个(%g mm 网格)' % (len(pts), STITCH_PITCH)) # ⚠ 只删**旧的缝合孔**:在缝合网格上、且**不在布线缓存里**的 GND 孔。 # 布线的 GND 平面孔在 0.125 网格上,个别会恰好落在缝合网格点上 —— 只按网格删会把它们删掉,焊盘就断了。 keep = [[round(v[0], 3), round(v[1], 3)] for v in routes()[1] if v[2] == 'GND'] JS = OPEN + u""" const MIL = %s, P = %s, D = %s, H = %s, PITCH = %s, OFF = %s, KEEP = %s; const onGrid = v => { const q = (v/MIL - OFF) / PITCH; return Math.abs(q - Math.round(q)) < 1e-3; }; const kept = (x, y) => KEEP.some(k => Math.abs(k[0] - x/MIL) < 0.01 && Math.abs(k[1] - y/MIL) < 0.01); let old = 0; for (const v of (await eda.pcb_PrimitiveVia.getAll()||[])) if (String(v.net) === 'GND' && onGrid(v.x) && onGrid(v.y) && !kept(v.x, v.y)) { await eda.pcb_PrimitiveVia.delete(v.primitiveId); old++; } let n = 0; for (const p of P) { try { await eda.pcb_PrimitiveVia.create('GND', p[0]*MIL, p[1]*MIL, H*MIL, D*MIL); n++; } catch (e) {} } await eda.pcb_Document.save(); return {deleted: old, made: n, now: (await eda.pcb_PrimitiveVia.getAll()||[]).length}; """ % (MIL, json.dumps(pts), VIA_D, VIA_HOLE, STITCH_PITCH, INSET + 2.0, json.dumps(keep)) print(call(JS, timeout=900)) # ── 下发铺铜 ───────────────────────────────────────────────────────────── JS_DEL = OPEN + u""" let n = 0; for (const old of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; } await eda.pcb_Document.save(); 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; try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); } catch (e) {} } return {made: (await eda.pcb_PrimitivePour.getAll()) .map(x => String(x.pourName || '').toUpperCase())}; """ 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(2500); 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, 1500)); await eda.pcb_Document.save(); return {poured: (await eda.pcb_PrimitivePoured.getAll()).length, pour: (await eda.pcb_PrimitivePour.getAll()).length}; """ def rrect_src(x0, y0, x1, y1): pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)] # ⚠ 必须显式闭合 a = [mil(pts[0][0]), mil(pts[0][1])] for p in pts[1:]: a += ['L', mil(p[0]), mil(p[1])] return a def rebuild(): u"""重建所有铺铜(走 GUI —— `rebuildCopperRegion()` 是坏的,刀片 §24 已查清)。""" print(call(JS_REBUILD, timeout=180)) last, same = -1, 0 for _ in range(40): time.sleep(6) n = call(JS_POLL, timeout=120)['poured'] print(u' 已填 %d 块' % n) same = same + 1 if n == last else 0 last = n if same >= 2 and n > 0: break print(call(JS_DONE, timeout=180)) def push(ps): print(call(JS_DEL, timeout=300)) arr = [{'name': n, 'net': net, 'layer': l, 'prio': p, 'src': rrect_src(*r)} for (n, net, l, r, p) in ps] made = [] for k in range(0, len(arr), 12): made += call(JS_MAKE % json.dumps(arr[k:k + 12], ensure_ascii=False), timeout=600)['made'] want = set(n.upper() for (n, _net, _l, _r, _p) in ps) got = set(made) print(u'铜面建了 %d / %d 块' % (len(want & got), len(want))) if want - got: print(u' [!] 没建上: %s' % ' '.join(sorted(want - got))) rebuild() if __name__ == '__main__': if '--clear' in sys.argv: print(call(JS_DEL, timeout=300)) sys.exit(0) ps = plan() bad = selfcheck(ps) print(u'入口链(L4)逐段核对:原始外框 → 裁剪后 → 挖栅极缺口后的瓦片') for net, raw, r, parts in CHAIN_DEBUG: f = lambda q: u'x %.2f~%.2f y %.2f~%.2f' % (q[0], q[2], q[1], q[3]) # noqa: E731 print(u' %-9s 原始 %s → 裁剪 %s' % (net, f(raw), f(r))) for t in parts: e = 0.01 # 瓦片坐标四舍五入到 0.001,裁剪框没有 out = u' ⚠ 出了裁剪框' if (t[1] < r[1] - e or t[3] > r[3] + e or t[0] < r[0] - e or t[2] > r[2] + e) else u'' print(u' 瓦片 %s%s' % (f(t), out)) if out: bad.append(u'%s 的瓦片 %s 出了裁剪框' % (net, f(t))) print(u'计划建 %d 块铜面:' % len(ps)) for name, net, lay, r, prio in ps: if name.startswith('PWR-') or name.endswith('-0'): print(u' %-15s net=%-9s %s x %6.2f~%6.2f y %6.2f~%6.2f prio=%d' % (name, net, LNAME[lay], r[0], r[2], r[1], r[3], prio)) for tag in ('3V3-L2', 'GND-L3', 'GND-L1', 'GND-L4'): print(u' (%s 瓦片 %d 块,绕开天线净空 / 缺口 / 四个 M3 孔)' % (tag, sum(1 for p in ps if p[0].startswith(tag + '-')))) if bad: print(u'[X] 自检 %d 条不过:' % len(bad)) for t in bad: print(u' ' + t) else: print(u'[OK] 自检全过(不出内缩线 / 不压净空 / 入口链各段与 12V 铜块同层互不重叠)') bad += check_cuts(ps) if '--stitch' in sys.argv: stitch(ps) if '--push' in sys.argv: if bad: sys.exit(u'!! 自检 / 断面不过,不下发') push(ps) elif '--rebuild' in sys.argv: rebuild()