文件
phonerail/.claude/r2/pour_base.py
T

723 行
35 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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 | **整层 GND**(2026-09-10 深夜由 +3V3 改:双地,L1 信号参考 L2 地;+3V3 改回布线)|
| L3 | Inner2 | 16 | **整层 GND**(L4 上全部器件与 D± 的参考)|
| L4 | Bottom | 2 | 入口 VIN_RAW / VIN_FUSED / SW12 / VIN_SW(硬,D16 手画矩形 PD_POUR)+ Rs6 西侧 12V 铜块(硬)+ GND 底填 |
| L1 | Top | 1 | (D16)U37 正上方 VIN_FUSED 小岛(pcb_base.PD_ISLAND),从 12V 大铜里扣出 |
「硬」= 几何算出来、要过断面校核;GND 底填优先级最低,自然绕开别人的铜。
## 入口(D16,2026-09-11 起:J1 USB-C → F1 → U37 LM61495 → L4 → C2/C3/D1 → Rs6)
原来的 DC-005 → F1 → Q1 那条链是「每段取本网焊盘外框外扩、段间按空隙中点切开」自动撑出来的(chain_rect / clip_chain,
函数还留着)。D16 的新件东一颗西一颗(U36 / R12 / C12 在东、F1 在中、C5 在西),外框一撑就把 U37 整颗包进 VIN_FUSED ⇒
改成 **按电流路径手画的矩形** `PD_POUR`:同层不同网互不重叠、串联元件(F1 / U37 / L4)两侧不搭,别的网的焊盘 / 线 / 孔铺铜自己绕。
VIN_SW 北段(C2 / D1 / C3 → Rs6.1)仍按焊盘外框撑(U13 / C4 / R10 不撑:Kelvin / 去耦 / FB 采样)。
## 断面校核
在铜面上画断面线,数「这条线上属于本网、没被别人的焊盘 / 过孔 / 同层导线占掉」的**最宽一段**。
入口链与 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 # 缝合过孔到任何焊盘 / 导线 / 过孔的净距
# 天线净空周边加密(2026-09-11 第 4 阶段,乐鑫 ESP32-S3 布局指南:天线附近地孔 2~3 mm):
# 离净空框 0.9~2.2 mm 的一圈里,按 ANT_PITCH 贪心挑孔。
ANT_PITCH = 2.0
ANT_BAND = (0.9, 3.0) # 上限 2.2 时南北两侧全被缺口的铺铜退让吃掉,只剩西侧 6 颗
# 所有缝合孔都落在这个子网格上(4 mm 主网格的约数);删旧缝合孔 / 布线认缝合孔都按它判。
STITCH_SUB = 1.0
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', 'R10')} # 不撑大铜面的焊盘(见文件头;R10 = D16 的 FB 采样)
# ── D16 南带功率铜(手画矩形,见文件头;摆位 = place_base.PD_HAND)──────────────────────────
PD_VIN_SW_N0 = 30.4 # VIN_SW 北段(按焊盘外框撑的那块)的下沿 = 输出电容一行的上沿
PD_POUR = [
# (名字, 网, 层, 矩形, 优先级)
('PWR-VIN_RAW', 'VIN_RAW', L4, (9.2, 8.2, 15.8, 11.9), 1), # J1 两组 VBUS → F1.1(CC 的孔在里面,铺铜自己绕)
('PWR-VIN_FUSED', 'VIN_FUSED', L4, (11.2, 12.2, 22.1, 14.2), 1), # F1.2 → 东去的母线
('PWR-VIN_FUSED-1', 'VIN_FUSED', L4, (19.8, 14.2, 20.85, 18.75), 1), # 母线 → C20 / U37.14(南侧 VIN)
('PWR-VIN_FUSED-2', 'VIN_FUSED', L4, (22.1, 12.95, 27.4, 16.0), 1), # 东段:C22 / C12 / U36
('PWR-VIN_FUSED-L1', 'VIN_FUSED', L1, B.PD_ISLAND, 1), # L1 小岛:连通 U37 两侧 VIN(用户选)
('PWR-SW12', 'SW12', L4, (16.2, 20.2, 19.6, 21.2), 1), # U37.16 ↔ L4.1(夹在 U37 两只 PGND 角脚之间)
('PWR-VIN_SW', 'VIN_SW', L4, (0.5, 17.6, 9.5, PD_VIN_SW_N0), 3), # L4.2 西端 + C5 → 北
('PWR-VIN_SW-1', 'VIN_SW', L4, (0.5, 27.2, 21.6, PD_VIN_SW_N0), 3), # 输出电容一行(C14~C17)
]
GATE_PARTS = ('DZ1', 'C1', 'R1')
NOTCH_CLR = 0.3
# (铜面名前缀, 标签, 网, 电流方向, 要求净宽, 是否强制, 层, 是否跳过串联元件)
CUTS = [
# D16:20V 侧 ≤5 A,瓶颈是 C 座两组 VBUS 焊盘(各 0.6 mm 宽)本身 ⇒ 只报不强制
('PWR-VIN_RAW', u'J1→F1', 'VIN_RAW', 'y', 3.0, False, L4, True),
('PWR-VIN_SW', u'L4→储能→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[:] = []
# D16:入口铜块手画(PD_POUR)+ VIN_SW 北段按焊盘外框撑(下沿贴输出电容一行)
for (n, net, lay, r, prio) in PD_POUR:
outs.append((n, net, lay, tuple(r), prio))
z = B.ZONES['A1']
vn = [p for p in chain_pads('VIN_SW') if p[3] >= PD_VIN_SW_N0]
if vn:
g = CHAIN_GROW
r = (max(z[0], min(p[2] - p[4] / 2 for p in vn) - g), PD_VIN_SW_N0,
min(z[2], max(p[2] + p[4] / 2 for p in vn) + g), min(z[3], max(p[3] + p[5] / 2 for p in vn) + g))
outs.append(('PWR-VIN_SW-2', 'VIN_SW', L4, tuple(round(v, 3) for v in r), 3))
CHAIN_DEBUG.append(('VIN_SW', r, r, [r]))
pd4 = [r for (_n, _net, lay, r, _p) in outs if lay == L4]
island = B.PD_ISLAND
ko = keepouts()
v12 = routes()[2]
if v12:
outs.append(('PWR-12V-PATCH', '+12V', L4, tuple(v12), 2))
# L1 12V:主块要绕开两个 M3 孔((4,4) / (4,94) 就在主块里)和 D16 的 VIN_FUSED 小岛,同样切成单环瓦片
isl_cut = (island[0] - 0.5, island[1] - 0.5, island[2] + 0.5, island[3] + 0.5)
k = 0
for r in B.l1_12v_rects():
for t in tiles(r[0], r[1], r[2], r[3], ko + [isl_cut]):
outs.append(('PWR-12V-L1%s' % ('-%d' % k if k else ''), '+12V', L1, t, 2))
k += 1
# ⛔ GND 底填**不许和同层的功率铜面重叠**(2026-09-10 实测):读回的优先级**不按创建顺序** ——
# 整板那块 GND-L1-4 / GND-L4-4 拿到 18,L1 12V 主块与 J37/J38 指头 23~33、VIN_SW 主块 23,
# 数字小的先铺,GND 把地盘占完,功率铜面一块铜都没填出来(83 块只填 61,断面校核全白做)。
# 挖掉之后两边不重叠,谁先铺都一样。功率铜块四周各多挖 0.6。
pwr_cut = {L1: list(B.l1_12v_rects()) + [isl_cut],
L4: [(r[0] - 0.6, r[1] - 0.6, r[2] + 0.6, r[3] + 0.6) for r in pd4] + ([tuple(v12)] if v12 else [])}
d = INSET
# 2026-09-10 深夜:L2 由整层 +3V3 改**整层 GND**(双地,用户拍板;+3V3 改回布线网)
for tag, net, lay, prio in (('GND-L2', 'GND', L2, 9), ('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 + pwr_cut.get(lay, []))):
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
# 封装自带的 NPTH(插槽定位柱 / J23,`npth_base.py`):按反焊盘半径的方框避开
npth = json.load(io.open(os.path.join(HERE, '_base_npth.json'), encoding='utf-8'))
ko = (keepouts() + [rr for (n, _net, _l, rr, _p) in ps if n.startswith('PWR-')] +
[(h[0] - h[3], h[1] - h[3], h[0] + h[3], h[1] + h[3]) for h in npth])
pp = pads()
def free(x, y):
if any(k[0] - r < x < k[2] + r and k[1] - r < y < k[3] + r for k in ko):
return False
if 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 pp):
return False
if 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):
return False
return not any(math.hypot(x - v[0], y - v[1]) < VIA_D + STITCH_CLR for v in vias)
out = []
y = INSET + 2.0
while y <= B.BH - INSET - 2.0:
x = INSET + 2.0
while x <= B.BW - INSET - 2.0:
if free(x, y):
out.append((round(x, 3), round(y, 3)))
x += STITCH_PITCH
y += STITCH_PITCH
# 天线净空一圈加密(子网格点,离净空框 ANT_BAND,彼此 ≥ ANT_PITCH,和主网格孔也 ≥ ANT_PITCH)
d = json.load(io.open(os.path.join(HERE, '_base_place.json'), encoding='utf-8'))
ak = PL.antenna_keepout(PL.sizes(), {k: (v[0], v[1], v[2]) for k, v in d.items()})
o0, n0 = INSET + 2.0, len(out)
cands = []
for i in range(int((ak[0] - ANT_BAND[1] - o0) // STITCH_SUB), int((ak[2] + ANT_BAND[1] - o0) // STITCH_SUB) + 2):
for j in range(int((ak[1] - ANT_BAND[1] - o0) // STITCH_SUB), int((ak[3] + ANT_BAND[1] - o0) // STITCH_SUB) + 2):
x, y = round(o0 + i * STITCH_SUB, 3), round(o0 + j * STITCH_SUB, 3)
dk = math.hypot(max(ak[0] - x, x - ak[2], 0.0), max(ak[1] - y, y - ak[3], 0.0))
if ANT_BAND[0] <= dk <= ANT_BAND[1] and INSET + 0.5 <= x <= B.BW - INSET - 0.5 \
and INSET + 0.5 <= y <= B.BH - INSET - 0.5:
cands.append((dk, y, x))
for _dk, y, x in sorted(cands):
if any(math.hypot(x - o[0], y - o[1]) < ANT_PITCH - 1e-6 for o in out):
continue
if free(x, y):
out.append((x, y))
print(u' 天线净空周边加密:%d 个(离净空框 %.1f~%.1f mm、间距 ≥ %.1f)'
% (len(out) - n0, ANT_BAND[0], ANT_BAND[1], ANT_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_SUB, 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_DEL_L2 = OPEN + u"""
let n = 0;
for (const old of await eda.pcb_PrimitivePour.getAll())
if (old.getState_Layer() === %d) { await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
await eda.pcb_Document.save();
return {deleted: n};
""" % L2
JS_LAYER_NETS = OPEN + u"""
const o = {};
for (const p of await eda.pcb_PrimitivePour.getAll()) {
const k = p.getState_Layer() + '|' + p.getState_Net(); o[k] = (o[k] || 0) + 1; }
return o;
"""
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))
JS_FILLED = OPEN + u"""
const filled = new Set();
for (const q of await eda.pcb_PrimitivePoured.getAll()) filled.add(q.getState_PourPrimitiveId());
const out = [];
for (const p of await eda.pcb_PrimitivePour.getAll())
out.push([String(p.getState_PourName()||''), filled.has(p.getState_PrimitiveId()) ? 1 : 0]);
return out;
"""
def verify_fill():
u"""哪些铜面没填出来。GND / +3V3 小瓦片没有本网孔会被当孤岛删掉,属正常;
**`PWR-*`(入口链、12V 铜块、L1 12V)一块都不许空** —— 2026-09-10 就是它们被 GND 抢了地盘,DRC / board_check 都不报。"""
rows = call(JS_FILLED, timeout=300)
empty = [n for n, f in rows if not f]
bad = [n for n in empty if n.upper().startswith('PWR-')]
print(u'铜面 %d 块,填出 %d 块;空的 %d 块%s' % (len(rows), len(rows) - len(empty), len(empty),
(u':' + ' '.join(empty)) if empty else u''))
if bad:
print(u'[X] 功率铜面没填出来:%s' % ' '.join(bad))
else:
print(u'[OK] 功率铜面全部填出')
return not bad
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 not verify_fill():
sys.exit(u'!! 功率铜面有空的 —— 断面校核作废,别往下走')
if __name__ == '__main__':
if '--clear' in sys.argv:
print(call(JS_DEL, timeout=300))
sys.exit(0)
if '--verify-fill' in sys.argv:
sys.exit(0 if verify_fill() else 1)
ps = plan()
bad = selfcheck(ps)
if '--swap-l2' in sys.argv:
# 第 5 阶段(2026-09-11):只把板上 L2 的铜面轮廓换成计划里的 GND-L2 瓦片,**不重填**
# —— 其它层不动;填充留到服务器重布之后的铺铜步骤统一 `--push`。
if bad:
sys.exit(u'!! 自检不过,不换 L2')
l2 = [p for p in ps if p[2] == L2]
assert l2 and all(p[1] == 'GND' for p in l2), u'计划里的 L2 不是整层 GND'
print(call(JS_DEL_L2, timeout=300))
arr = [{'name': n, 'net': net, 'layer': l, 'prio': p, 'src': rrect_src(*r)}
for (n, net, l, r, p) in l2]
call(JS_MAKE % json.dumps(arr, ensure_ascii=False), timeout=600)
got = call(JS_LAYER_NETS, timeout=300)
on_l2 = {k: v for k, v in got.items() if k.startswith('%d|' % L2)}
print(u'板上 L2 铜面:%s(计划 GND ×%d)' % (on_l2, len(l2)))
ok = on_l2 == {'%d|GND' % L2: len(l2)}
print(u'[OK] L2 已是整层 GND 轮廓(未填充)' if ok else u'[X] L2 读回不对')
sys.exit(0 if ok else 1)
print(u'VIN_SW 北段(按焊盘外框撑):')
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 ('GND-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()