文件
phonerail/.claude/r2/archive_base2L/pour_base.py
T
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

890 行
43 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB 的缝合过孔 + 铺铜 + 断面校核(M8e)。
/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 # 只重填
⛔ **顺序**(刀片 M7 §27.10 的结论,两层板同样成立):
摆位 → 布线 → 下发 → **缝合过孔** → 斜切 → **铺铜**。
重跑布线器要把缝合过孔和斜切一起重跑。
## 叠层(方案 §6.1)
| 设计 | EasyEDA | id | 内容 |
|---|---|---|---|
| L1 | Top Layer | 1 | 全部元件 + 全部信号 + 12V 宽铜 + GND 底填 |
| L2 | Bottom Layer | 2 | **完整地平面**(D± 走廊下方一刀不许割,§6.2 第 1 条)|
## 两个只有这块板才有的约束
1. **`U20` 天线净空**:ESP32-S3-WROOM-1 的天线那一头(焊盘北沿外 7.5 mm)
**两层都不许有铜**。铜面不能带洞(`createComplexPolygon` 传多个环会把客户端挂死,
刀片 §24 的坑),⇒ 用**若干块单环矩形**绕开它,同网接壤会自然连成一片。
2. **入口链 9 A 靠铺铜**:`VIN_RAW`/`VIN_FUSED`/`VIN_SW`/`+12V` 在 input 带里各自
铺一块顶层铜(优先级比 GND 高,先占地盘),断面要 ≥ 方案 §6.4 的要求。
⚠ 这四个是**串联的四段**,不是一个网 —— 每段各铺各的,中间隔着 F1 / Q1 / Rs6。
## 断面校核(`--cuts`,办法沿用 R1 `pcb1_pour.py`)
在铺铜上画一条断面线,数「这条线上属于本网、且没有被别人的焊盘/过孔/导线占掉」的
长度。⚠ 它算的是**计划的铜面**,不是板上填出来的铜 —— 填的时候还会被反焊盘再吃掉一点,
所以判据留了 15% 余量。真正的判据仍是「铺完截图 + DRC」。
"""
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
L_TOP, L_BOT = 1, 2
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 = 7
# 入口链四段的铺铜:从各自的焊盘外框算,不手抄坐标(摆位一改就跟着走)
POWER_NETS = [('VIN_RAW', 1), ('VIN_FUSED', 2), ('VIN_SW', 3)]
# ⚠ `+12V` **不按焊盘外框算** —— 它不是入口链里的一段,是遍布全板的母线。
# 三段几何写在 `pcb_base.BUS_IN/BUS_UP/BUS_SLOT`(那里是唯一事实来源,
# `place_base` 也拿它当摆位禁区)。
# ⛔ 入口链的铜只能从 y=6.3 往上:**输入带最南那 5 mm 归 12V 横脊**(`pcb_base.SPINE`)。
# 两者同层不同网,搭上就把 `Rs6` 短路了。
# M9:南带整条都归入口链(横脊没了 —— 12V 不再需要穿过 D±,它全在东缘)
POWER_BAND = (0.0, 0.8, B.BW, B.ZONES['input'][3])
POWER_GROW = {'VIN_RAW': 1.2, 'VIN_FUSED': 1.2, 'VIN_SW': 1.2}
# Q1 栅极网络(DZ1 钳位 / C1 栅源电容 / R1 下拉)。它们的焊盘**不撑大**入口链铜面
# (和 U13.10 同理:不是 9 A 主电流焊盘),而且整团从铜面里挖出一个缺口给 GATE 走线。
# ⛔ 不挖的话 Q1.4 北、西、南三面是 VIN_FUSED、东面是 VIN_SW,中间只剩 0.25 mm 的缝 ——
# 布线器报「GATE: Q1.4 出不来」,看着像拥塞,其实是铜面把栅极脚封死了。
GATE_PARTS = ('DZ1', 'C1', 'R1')
NOTCH_CLR = 0.3 # 缺口在 GATE 焊盘外框外再让多少
NOTCH_HEAD = 1.5 # 缺口在栅极件北侧留多高:GATE 要从 C1 北边绕过去
# 落在硬功率铜面里 / GATE 缺口里的 **GND 贴片焊盘**:(位号, 脚, 过孔在哪一侧, 是否挖地岛)。
# ⛔ GND 不走线、只靠铺铜 + 过孔。这几只脚四周全是 VIN_SW 铜(或是缺口里的一小块孤立地铜),
# 没有专用过孔的话它们根本接不到地 —— D1 是 TVS,浪涌电流全靠这一只脚回地。
# 通孔地脚(C2/C3 的电解、J1)自己穿到底层地平面,不在此列。
# 地岛从铜面里挖一条缝通到铜面外(S = 向南到板边,N = 向北出 VIN_SW 顶边),过孔排在开口那一侧。
GND_PAD_VIAS = [('D1', '2', 'S', True), ('C5', '1', 'N', True), ('R1', '1', 'N', False)]
ISL_M = 0.6 # 地岛在焊盘外框外让多少
ISL_VIA_GAP = 0.25 # 过孔铜边到焊盘边(别让焊锡流进孔里)
ISL_VIA_PITCH = 0.9 # 同排过孔中心距(≥ 0.6 的同网孔距)
# 断面校核(方案 §6.4)。入口链电流方向是 +x ⇒ 断面是**竖直**的,量 y 方向的净铜高,
# 而且要取沿途的**最小值**(第一版取了最大值,等于没查)。
# (名字, 网, 是否强制)
# (铜面名, 标签, 网, 沿哪个轴流 'x'/'y', 要求净铜, 是否强制)
CUTS = [
('PWR-VIN_RAW', u'J1→F1 入口', 'VIN_RAW', 'x', 8.0, False),
('PWR-VIN_FUSED', u'F1→Q1', 'VIN_FUSED', 'x', 8.0, False),
# ⚠ `VIN_SW` 的最窄点在 `Rs6`/`U13` 那一头(Kelvin 要求它们贴在一起)。
# 6.5 mm 在 9 A 下 ΔT≈25 K 而不是 20 K,只有分流电阻前后 2~3 mm 一小段,可接受。
('PWR-VIN_SW', u'Q1→储能→Rs6', 'VIN_SW', 'x', 6.5, True),
# 西缘高速路的电流是**沿 y** 流的 ⇒ 断面是水平的,量 x 方向净铜宽。
# 写成 'x' 的话量到的是它 105 mm 的长度,永远「通过」,等于没查。
('PWR-12V-0', u'12V 西缘高速路', '+12V', 'y', 8.0, True),
]
CUT_MIN_W = 8.0 # IPC-2221 1 oz 外层 9 A ΔT20K ⇒ ≥8 mm 净铜宽
# ── 低压轨的「软铺铜」(2026-09-09 用户拍板:先按分布把电源网铺铜)────────────
# ⛔ **为什么单独一类、名字不叫 `PWR-`**:
# `PWR-` 是**硬铺铜** —— 几何算出来、要过断面校核,而且会当障碍喂给布线器
# (`route_base.pour_blocks()`),别的网一律不许穿过去。12V 母线被一根信号线
# 切断 = 9 A 没了,而铺铜管理器 / DRC / 连通性三个都只说「有铜」。
# `RAIL-` 是**软铺铜**:只是「这一带的余铜归这个网」,铺铜时自然绕开已布的信号线,
# **不喂给布线器** —— 喂了的话 ctrl 带 57 个件的信号一根都出不去。
# 判据也换一个:硬铺铜看断面,软铺铜看 **`board_check` 从板上读回来的连通性**,
# 因为它被信号切碎成孤岛是完全可能的,而那只有读铜才看得见。
#
# 依据是 `_base_pads.json` 实测的分布,不是画出来的(见 §28.15):
# `+3V3` 47 只脚里 **26 只**在 ctrl 带 x 28~86 / y 86~112(其余 15 只在槽上、4 只在风扇带)
# `+3V3_HUB` 21 只脚分成 U32/去耦组与 U31/C737 两簇
# `+5V` ctrl 带东头 2 只(`R706` / `L3`,5V Buck 那一窝);槽上那 10 只要走走廊,不在这
# ⚠ 三块互不重叠(不同网同层重叠 = 短路),`selfcheck()` 会查。
# ⚠ M9 暂时清空:`+3V3` 与 `+3V3_HUB` 的脚在西区互相穿插(排布器按「离目标最近」摆的),
# 两个网划不出互不重叠的矩形。新拓扑没有 M8 那道墙,先让布线器直接布,
# 看失败分布再决定要不要为它们改摆位 + 加软铺铜。
RAILS = [
# U31 与 U32 相距较远,分成两块局部软铜,中间由 A* 明确连接。
# 这比一块 24x60 mm 大面更不容易被控制信号切成孤岛。
# M12 采用当前手工布局后旧坐标已失效:U31 移到 y=73.6,U32 移到 y=52.9。
('RAIL-3V3HUB-LDO', '+3V3_HUB', (77.8, 71.5, 90.2, 75.7)),
('RAIL-3V3HUB-HUB', '+3V3_HUB', (84.0, 47.8, 102.0, 60.7)),
# ⛔ **`+3V3` 试过铺两块,反而更差**(45 → 58 条):软铺铜会逼每只**区外**的脚
# 各自去够铜面,而原来布线器可以把它们串成一条链。
# ⇒ 判据:软铺铜只在「大部分脚都在区内」时划算(`+3V3_HUB` 21 只脚全在西区 ⇒ 值),
# `+3V3` 47 只脚里 15 只在槽上、6 只在风扇带 ⇒ 不值,留给布线器。
]
# ⚠ `VIN_RAW` / `VIN_FUSED` 两段**不强制**:它们的瓶颈是 DC-005 的三只脚和 1206 保险
# 本身(座子额定 2~5 A、这块板要过 9 A,是用户 2026-09-08 明确接受的已知风险,
# 设计文档 §22.6)。板上再宽的铜也救不了插头内芯与弹片的接触面。
# 真正必须够的是它们之间那两段长铜:`VIN_SW`(储能到检流)和 `+12V`(母线)。
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"""底板焊盘,**按 `place_base.ROUTE_FIX` 的计划位置**(与 `route_base` 的路由模型一致)。
⛔ 以前这里读的是实测焊盘,而布线器读的是改位后的:DZ1 从 x=38.6 挪到 29.3 之后,
实测算出的 VIN_FUSED/VIN_SW 分界是 32.9,改位后算出的是 30.5(VIN_SW 盖住 Q1.3)——
布线器看到的铜面和摆位下发后真正铺出来的对不上,而且哪一边都不报错。
"""
if _PADS[0] is None:
_PADS[0], _moved = PL.route_fixed_pads(json.load(io.open(PADS, encoding='utf-8')))
return _PADS[0]
def routes():
if not os.path.exists(ROUTES):
return [], []
c = json.load(io.open(ROUTES, encoding='utf-8'))
if c.get('schema') != ROUTE_SCHEMA or c.get('slot_rot') != B.SLOT_ROT:
return [], []
return c['segs'], c['vias']
# ── 铜面几何 ─────────────────────────────────────────────────────────────
def keepouts():
u"""两层都不许铺铜的地方:`U20` 天线净空 + **天线缺口** + 四个 M3 安装孔。
⛔ 缺口那一块**板材是挖掉的**,而这份脚本的铜面几何是按 `BW × BH` 的矩形算的
(`tiles()` 只认矩形,不认板框折线)。不把缺口喂进来,GND 瓦片会照样铺到
板外的空气里 —— 板框对不上铜面,DRC 报「铜到板边」,而铺铜管理器不报。
"""
sz = PL.sizes()
n = B.notch_rect()
out = [PL.antenna_keepout(sz),
(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` 切成若干块**单环**矩形。
⛔ 不能用带洞的复杂多边形:`createComplexPolygon()` 传多个环会把客户端挂死
(刀片 §24,请求 30 s 超时、进度恒 0/0,和「按钮失灵」分不清)。
同网的矩形接壤会自然连成一片铜,效果一样。
"""
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
# ⛔ **禁区的 y 区间必须先合并再配对**。原来是「按 c[1] 排序后把
# [起, 止] 一对一对塞进 ys,再 `zip(ys[0::2], ys[1::2])`」——
# 那套只在禁区互不重叠时成立。天线缺口 (84.5~107.5) 和天线净空 (86~106)
# 是**嵌套**的,塞进去就变成 [.., 84.5, 107.5, 86.0, 106.0, ..],
# 奇偶配对当场错位 ⇒ 瓦片照样盖住缺口,而**只有 `selfcheck` 那一条
# 「压进净空区」看得出来**(铺铜管理器和 DRC 都不会响)。
iv = []
for c in cuts:
if c[0] < xb - 1e-9 and xa < c[2] - 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 power_rect(net):
u"""入口链某一段的铜面:这个网在 input 带里全部焊盘的外框 + `POWER_GROW`。"""
z = POWER_BAND
xs, ys = [], []
for p in pads():
if p[7] != net or not (z[1] <= p[3] <= z[3]):
continue
# U13.10 是 Rs6 上游的 Kelvin/监测脚,不是 9 A 主电流焊盘。
# 把它纳入包围盒会让 VIN_SW 铜面跨过 Rs6 的 +12V 端。
if net == 'VIN_SW' and p[0] == 'U13':
continue
if p[0] in GATE_PARTS:
continue
xs += [p[2] - p[4] / 2, p[2] + p[4] / 2]
ys += [p[3] - p[5] / 2, p[3] + p[5] / 2]
if not xs:
return None
g = POWER_GROW[net]
return (max(INSET, min(xs) - g), max(z[1] + INSET, min(ys) - g),
min(B.BW - INSET, max(xs) + g), min(z[3], max(ys) + g))
def clip_chain(chain):
u"""入口链四段是**串联**的(中间隔着 F1 / Q1 / Rs6),铜面搭上就把器件短路了。
`chain` = [(net, rect)],已按 x 排好。相邻两段重叠时竖切一刀,两边各退 CLR/2。
⛔ 刀口**按焊盘空隙定,不按重叠区中点**:在两块铜 y 方向重叠的那一带里,取
「西段网最东的焊盘」与「东段网最西的焊盘」之间的中点。
取重叠区中点的旧写法在 Q1 这里是错的 —— Q1 是南北两排脚(南排源极、北排漏极),
VIN_SW 的外框又被北边的 C4/C6 拉到 x=25.8,中点直接落进 Q1 里面(x=30.6,
VIN_SW 盖住 Q1.3)。以前没出事,只是因为 DZ1 旧位置把 VIN_FUSED 撑到了 x=40。
⚠ 不能靠铺铜优先级解决:优先级只保证「低的那块被让开」,让出来的形状是不可预期的
细条,断面校核就没意义了。
"""
out = [r for _n, r in chain]
ps = pads()
def band(net, y0, y1):
return [p for p in ps if p[7] == net and p[0] not in GATE_PARTS
and not (net == 'VIN_SW' and p[0] == 'U13')
and p[3] - p[5] / 2 < y1 and p[3] + p[5] / 2 > y0]
for i in range(len(out) - 1):
a, b = out[i], out[i + 1]
if a[2] <= b[0]:
continue
y0, y1 = max(a[1], b[1]), min(a[3], b[3])
pa, pb = band(chain[i][0], y0, y1), band(chain[i + 1][0], y0, y1)
ea = max([p[2] + p[4] / 2 for p in pa] or [None])
wb = min([p[2] - p[4] / 2 for p in pb] or [None])
if ea is not None and wb is not None and ea < wb:
m = (ea + wb) / 2.0
else:
m = (a[2] + b[0]) / 2.0 # 没有干净的焊盘空隙时退回旧规则
out[i] = (a[0], a[1], round(min(a[2], m - CLR / 2), 3), a[3])
out[i + 1] = (round(max(b[0], m + CLR / 2), 3), b[1], b[2], b[3])
return out
def gate_notch():
u"""GATE 缺口(矩形列表,喂给 `tiles()`)。
主块:GATE 焊盘 + 三颗栅极件的外框,向外让 NOTCH_CLR、北侧留 NOTCH_HEAD,南到板边。
竖条:比主块更高的 GATE 焊盘(Q1.4 是 2 mm 长的 SOIC 脚)整只让出来 ——
只挖主块的话 Q1.4 北半截仍在 VIN_FUSED 铜面里,模型里等于异网铜压在焊盘上。
"""
ps = pads()
g = [p for p in ps if p[7] == 'GATE' and p[3] <= POWER_BAND[3]]
sm = [p for p in ps if p[0] in GATE_PARTS]
if not g or not sm:
return []
y1 = round(max(p[3] + p[5] / 2 for p in sm) + NOTCH_HEAD, 3)
out = [(round(min(p[2] - p[4] / 2 for p in g) - NOTCH_CLR, 3), 0.0,
round(max(p[2] + p[4] / 2 for p in g + sm) + NOTCH_CLR, 3), y1)]
for p in g:
if p[3] + p[5] / 2 + NOTCH_CLR > y1:
out.append((round(p[2] - p[4] / 2 - NOTCH_CLR, 3), 0.0,
round(p[2] + p[4] / 2 + NOTCH_CLR, 3),
round(p[3] + p[5] / 2 + NOTCH_CLR, 3)))
return out
def gnd_pad_vias():
u"""`GND_PAD_VIAS` 的几何:(要从入口链铜面里挖掉的地岛矩形, 专用 GND 过孔坐标)。"""
ps = pads()
islands, vias = [], []
for ref, pin, side, cut in GND_PAD_VIAS:
hit = [p for p in ps if p[0] == ref and str(p[1]) == pin and p[6] == L_TOP]
if len(hit) != 1 or hit[0][7] != 'GND':
raise RuntimeError(u'%s.%s 应是唯一的顶层 GND 贴片焊盘' % (ref, pin))
p = hit[0]
x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2
x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2
n = int((x1 - x0) / ISL_VIA_PITCH) + 1
vy = y0 - ISL_VIA_GAP - VIA_D / 2 if side == 'S' else y1 + ISL_VIA_GAP + VIA_D / 2
for k in range(n):
vias.append((round(p[2] + (k - (n - 1) / 2.0) * ISL_VIA_PITCH, 3), round(vy, 3)))
if cut:
# 缝一直通到板边 / 板顶:`tiles()` 只拿它切入口链那几块铜,切不到别的东西
islands.append((round(x0 - ISL_M, 3), 0.0 if side == 'S' else round(y0 - ISL_M, 3),
round(x1 + ISL_M, 3), round(y1 + ISL_M, 3) if side == 'S' else B.BH))
return islands, vias
def drain_piece(outs):
u"""Q1 漏极排(北排四脚,VIN_SW)的铜面。
⛔ 入口链是按 x 串起来切的,而 Q1 是**南北两排**:南排源极归 VIN_FUSED,北排漏极
在 VIN_SW 那块的西边界之外 —— 9 A 只能靠 A* 的 2 mm 走线从四只漏极脚爬进铜面
(2 mm 1 oz 外层只带 ~4 A)。这里按漏极排外框补一块,东头与 VIN_SW 已有的块搭接,
南沿让开 VIN_FUSED。西沿只让 CLR:再往西是死头,断面校核会把它量成瓶颈。
"""
ps = [p for p in pads() if p[0] == 'Q1' and p[7] == 'VIN_SW']
if not ps:
return None
g = POWER_GROW['VIN_SW']
x0 = min(p[2] - p[4] / 2 for p in ps) - CLR
x1 = max(p[2] + p[4] / 2 for p in ps) + g
y0 = min(p[3] - p[5] / 2 for p in ps) - g
y1 = max(p[3] + p[5] / 2 for p in ps) + g
vf = [r for (_n, net, _l, r, _p) in outs if net == 'VIN_FUSED' and r[0] < x1 and x0 < r[2]]
y0 = max([y0] + [r[3] + CLR for r in vf])
sw = [r for (_n, net, _l, r, _p) in outs
if net == 'VIN_SW' and r[1] < y1 and y0 < r[3] and r[0] >= x0]
if sw:
x1 = max(x1, min(r[0] for r in sw) + 0.6) # 保证与已有 VIN_SW 块搭接
return (round(x0, 3), round(y0, 3), round(x1, 3), round(y1, 3))
def plan():
u"""[(name, net, layer, rect, priority)],priority 小的先铺、先占地盘。"""
outs = []
chain = [(net, prio, power_rect(net)) for net, prio in POWER_NETS]
chain = [c for c in chain if c[2]]
chain.sort(key=lambda c: (c[2][0] + c[2][2]) / 2.0)
clipped = clip_chain([(c[0], c[2]) for c in chain])
cuts = gate_notch() + gnd_pad_vias()[0]
for (net, prio, _r), r in zip(chain, clipped):
parts = tiles(r[0], r[1], r[2], r[3], cuts) if cuts else [r]
# 最大一块沿用原名(CUTS 按名找),其余加序号;check_cuts 会把同名前缀的块一起算
parts.sort(key=lambda t: -(t[2] - t[0]) * (t[3] - t[1]))
for k, t in enumerate(parts):
outs.append(('PWR-%s%s' % (net.strip('+'), '-%d' % k if k else ''),
net, L_TOP, t, prio))
dp = drain_piece(outs)
if dp:
outs.append(('PWR-VIN_SW-Q1', 'VIN_SW', L_TOP, dp, dict(POWER_NETS)['VIN_SW']))
for k, r in enumerate(B.bus_rects()):
outs.append(('PWR-12V-%d' % k, '+12V', L_TOP, r, 4))
# `+3V3` 五块槽墙(几何与判据见 `pcb_base.wall3v3_rects` / `wall_check`)。
# 与 12V 同为**硬铺铜**:喂给布线器当障碍,且脚从 A* 里摘掉。
for k, r in enumerate(B.wall3v3_rects()):
outs.append(('PWR-3V3-%d' % k, '+3V3', L_TOP, r, 4))
# 低压轨的软铺铜:排在硬铺铜之后、GND 之前
for name, net, r in RAILS:
outs.append((name, net, L_TOP, r, 5))
# ⚠ `PWR-12V-*` 里 0=输入带母线 1=横脊 2~7=五根齿(3 号槽那根是两块拼的 L)
ko = keepouts()
d = INSET
for tag, lay in (('TOP', L_TOP), ('BOT', L_BOT)):
for k, t in enumerate(tiles(d, d, B.BW - d, B.BH - d, ko)):
outs.append(('GND-%s-%d' % (tag, k), 'GND', lay, t, 9))
return outs
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 wall_check():
u"""`+3V3` 槽墙的判据(几何算不出来,只有拿实测焊盘表比才算数)。
⛔ 三条都要过,缺一条墙就是白做:
① **15 只槽脚全被盖住** —— 与 12V 那条「10 排焊盘全被盖住」同理:
少盖一只,铺铜管理器和 DRC 都只说「有铜」,而那只脚是断的。
② **一只异网针都不许压到**(`NC` 空网也算异网):南排 NC(73)、北排 NC(71/72)
就贴在 3V3 针旁边,方块一大就压上去,铺铜会绕开它们**把墙在针排上切碎**。
③ **桥的过孔要落在墙里**,否则过孔与铜面之间靠铺铜「碰巧」相接。
"""
bad = []
rects = B.wall3v3_rects()
ps = pads()
c = B.W3V3_CLR - 0.05 # 判据比构造留的净距松 0.05
def inside(x0, y0, x1, y1):
return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and
r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects)
own = 0
for p in ps:
px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2
px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2
if p[7] == '+3V3':
if inside(px0, py0, px1, py1):
own += 1
continue
for r in rects:
if px0 - c < r[2] and r[0] < px1 + c and py0 - c < r[3] and r[1] < py1 + c:
bad.append(u'3V3 槽墙压到异网针 %s.%s(%s) @%.2f,%.2f'
% (p[0], p[1], p[7] or 'NC', p[2], p[3]))
break
slots = set(PL.SLOT_ORDER)
slot_own = sum(1 for p in ps if p[7] == '+3V3' and p[0] in slots)
if own != slot_own:
bad.append(u'3V3 槽脚 %d 只,墙只盖住 %d 只' % (slot_own, own))
_segs, vias = B.wall3v3_bridges()
for (vx, vy) in vias:
r = VIA_D / 2.0
if not inside(vx - r, vy - r, vx + r, vy + r):
bad.append(u'3V3 桥的过孔 (%.2f, %.2f) 不在墙里' % (vx, vy))
return bad, own, slot_own
def bus_check():
u"""12V 干道/铜墙必须覆盖全部槽脚,且不能压到异网焊盘。"""
rects = B.bus_rects()
ps = pads()
slots = set(B.USB_SLOT_REF.values())
clear = CLR - 0.05
def inside(p):
x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2
x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2
return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and
r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects)
own = [p for p in ps if p[0] in slots and p[7] == '+12V']
missed = [p for p in own if not inside(p)]
bad = [u'12V 铜墙没盖住 %s.%s @%.2f,%.2f' % (p[0], p[1], p[2], p[3])
for p in missed]
for p in ps:
if p[7] == '+12V':
continue
px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2
px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2
if any(px0 - clear < r[2] and r[0] < px1 + clear and
py0 - clear < r[3] and r[1] < py1 + clear for r in rects):
bad.append(u'12V 干道/铜墙压到异网针 %s.%s(%s) @%.2f,%.2f'
% (p[0], p[1], p[7] or 'NC', p[2], p[3]))
return bad, len(own) - len(missed), len(own)
def selfcheck(ps):
bad = []
ko = [k for k in keepouts() if k not in B.bus_rects()]
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)))
# 入口链各段不许互相重叠(同层,靠 F1/Q1/Rs6 断开)。
# ⚠ **只判不同网**:`+12V` 自己那几块(母线 / 横脊)是故意相接的,重叠才对。
pw = [(n, net, r) for (n, net, _l, r, _p) in ps
if n.startswith('PWR-') or n.startswith('RAIL-')]
for i, (na, na_net, a) in enumerate(pw):
for (nb, nb_net, b) in pw[i + 1:]:
if na_net == nb_net:
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, na_net, nb, nb_net))
return bad
# ── 断面校核 ─────────────────────────────────────────────────────────────
def cut_height(net, rect, x):
u"""在 x 处竖切一刀,`rect` 这块铜面上属于 `net` 的**最宽一段**连续净铜高(mm)。
别人的焊盘 / 过孔 / 导线按 `CLR` 膨胀之后从这条线上扣掉。
⚠ 取「最宽一段」而不是总和:被挖成两条 4 mm 的铜带,不等于一条 8 mm 的。
"""
if not (rect[0] <= x <= rect[2]):
return 0.0
spans = [(rect[1], rect[3])]
blocks = []
for p in pads():
if p[7] == net or p[6] not in (1, 12):
continue
if p[2] - p[4] / 2 - CLR <= x <= p[2] + p[4] / 2 + CLR:
blocks.append((p[3] - p[5] / 2 - CLR, p[3] + p[5] / 2 + CLR))
segs, vias = routes()
for s in segs:
if s[6] == net or s[0] != 1:
continue
x0, x1 = sorted((s[1], s[3]))
hw = s[5] / 2.0 + CLR
if x0 - hw <= x <= x1 + hw:
y0, y1 = sorted((s[2], s[4]))
blocks.append((y0 - hw, y1 + hw))
for v in vias:
if len(v) > 2 and v[2] == net:
continue
r = VIA_D / 2.0 + CLR
if abs(v[0] - x) <= r:
blocks.append((v[1] - r, v[1] + r))
for (b0, b1) in blocks:
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
return max([q[1] - q[0] for q in spans] or [0.0])
def cut_width_y(net, rect, y):
u"""在 y 处横切一刀,最宽一段连续净铜宽(mm)。"""
if not (rect[1] <= y <= rect[3]):
return 0.0
spans = [(rect[0], rect[2])]
blocks = []
for p in pads():
if p[7] == net or p[6] not in (1, 12):
continue
if p[3] - p[5] / 2 - CLR <= y <= p[3] + p[5] / 2 + CLR:
blocks.append((p[2] - p[4] / 2 - CLR, p[2] + p[4] / 2 + CLR))
segs, vias = routes()
for q in segs:
if q[6] == net or q[0] != 1:
continue
y0, y1 = sorted((q[2], q[4]))
hw = q[5] / 2.0 + CLR
if y0 - hw <= y <= y1 + hw:
x0, x1 = sorted((q[1], q[3]))
blocks.append((x0 - hw, x1 + hw))
for v in vias:
if len(v) > 2 and v[2] == net:
continue
rr = VIA_D / 2.0 + CLR
if abs(v[1] - y) <= rr:
blocks.append((v[0] - rr, v[0] + rr))
for (b0, b1) in blocks:
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
return max([q[1] - q[0] for q in spans] or [0.0])
def series_windows(net):
u"""这个网自己串联元件占的坐标窗口(本体的焊盘外框)。
⛔ 断面校核**必须跳过这些窗口**:最窄的一刀永远落在串联元件身上
(2512 分流电阻自己就只有 3.2 mm 宽、DC 座三只脚挤在 5 mm 里),
量到的是元件的宽度不是铜的宽度,判据就没意义了。
元件本身的载流由选型保证(`Rs6` 是 2512 3 W、`F1` 是 12 A 保险),
这里要查的是**元件之间那段铜**。
"""
# ⛔ 窗口要按**整颗器件**的焊盘外框算,不是只按它属于本网的那只脚。
# 只按那只脚的话,`D1`(TVS:一端 VIN_SW、一端 GND)只有阴极那半边被跳过,
# 阳极的 GND 盘照样把断面掐断 —— 实测 `VIN_SW` 被判成 4.35 mm。
# 物理上电流是**穿过器件**的,整颗器件占的那一段本来就不该算铜宽。
ps = pads()
own = set(p[0] for p in ps if p[7] == net)
by = {}
for p in ps:
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'载流断面校核(逐刀切,取**最窄**一刀,跳过串联元件本身):')
bad = []
byname = {}
for (n, _net, _l, r, _p) in ps:
byname.setdefault(n, []).append(r)
for key, label, net, axis, need, must in CUTS:
# 挖了 GATE 缺口之后一段铜会被切成几块(`PWR-X`、`PWR-X-1`…):每一刀取包含它的那块
rs = [r for n, v in byname.items() if n == key or n.startswith(key + '-') for r in v]
if not rs:
print(u' %-18s %-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 = []
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[0] - CLR, b[2] + CLR) if axis == 'x' else (b[1] - CLR, b[3] + CLR)))
lo, hi = (r[0], r[2]) if axis == 'x' else (r[1], r[3])
hs = []
t = lo + 0.25
while t <= hi - 0.25:
if not any(a <= t <= b for _ref, (a, b) in skip):
f = cut_height if axis == 'x' else cut_width_y
hs.append((max(f(net, q, t) for q in rs), t))
t += 0.25
worst = min(hs) if hs else (0.0, 0.0)
ok = worst[0] >= need
print(u' %-18s %-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 board_tracks():
return call(OPEN + "return (await eda.pcb_PrimitiveLine.getAll()||[])"
".filter(l=>l.layer===1||l.layer===2)"
".map(l=>[l.startX/%s,l.startY/%s,l.endX/%s,l.endY/%s,"
"l.lineWidth/%s,String(l.net||'')]);" % (MIL, MIL, MIL, MIL, MIL),
timeout=300)
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(tracks=None):
u"""GND 缝合过孔的坐标。
⛔ **顶层的地铜被信号切成很多块岛,每块岛至少要有一个缝合过孔**,
否则「移除孤岛」把它整块删掉 —— 而 `rebuildCopperRegion()` /
铺铜管理器 / DRC **三个地方都不报错**(刀片 §24.4)。
4 mm 网格是为了让最小的岛也有机会摊上一个。
"""
d = pads()
if tracks is None:
tracks = board_tracks()
r = VIA_D / 2.0 + STITCH_CLR
ko = keepouts()
# ⛔ **硬功率铜面里不打缝合孔**:那里顶层没有地铜,GND 孔只会在 9 A 铜上掏洞
# (4 mm 网格一刀能掏掉 3~4 mm 净铜),而断面校核看不到缝合孔。
ko += [rr for (n, _net, _l, rr, _p) in plan() if n.startswith('PWR-')]
# 专用 GND 过孔(地岛 / 缺口里的地脚)永远在,网格点让开它们
own = gnd_pad_vias()[1]
out = list(own)
ko += [(vx - VIA_D, vy - VIA_D, vx + VIA_D, vy + VIA_D) for (vx, vy) in own]
y = INSET + 2.0
while y <= B.BH - INSET - 2.0:
x = INSET + 2.0
while x <= B.BW - INSET - 2.0:
ok = True
for k in ko:
if k[0] - r < x < k[2] + r and k[1] - r < y < k[3] + r:
ok = False
break
if ok:
for p in d:
# ⛔ **通孔地脚不能当「自己人」跳过**:缝合过孔落在它身上,
# DRC 报「钻孔到钻孔 0 mil」(实测 J1_3 / C2_2 / J33_1 三处)。
# 同网也要留钻孔间距,这是工艺规则不是电气规则。
pass
if 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:
ok = False
break
if ok:
for t in tracks:
if t[5] == 'GND':
continue
if _seg_d(x, y, t[0], t[1], t[2], t[3]) < r + t[4] / 2.0:
ok = False
break
if ok:
out.append((round(x, 3), round(y, 3)))
x += STITCH_PITCH
y += STITCH_PITCH
return out
def stitch():
pts = stitch_points()
print(u'GND 缝合过孔 %d 个(%g mm 网格)' % (len(pts), STITCH_PITCH))
JS = OPEN + u"""
const MIL = %s, P = %s, D = %s, H = %s;
let old = 0;
for (const v of (await eda.pcb_PrimitiveVia.getAll()||[]))
if (String(v.net) === 'GND') { 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)
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 rebuild():
print(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__':
# 铜面按 ROUTE_FIX 的计划位置算(见 pads());板端还没挪件就铺,铜和件对不上。
if ('--push' in sys.argv or '--stitch' in sys.argv) and PL.route_fix_moves():
sys.exit(u'!! ROUTE_FIX 还有 %d 件没下发到板端(%s);先 place_base --push 再 '
u'--adopt-live --dump,然后才能铺铜' % (len(PL.route_fix_moves()),
' '.join(sorted(PL.route_fix_moves()))))
if '--clear' in sys.argv:
print(call(JS_DEL, timeout=300))
sys.exit(0)
ps = plan()
bad = selfcheck(ps)
print(u'计划建 %d 块铜面:' % len(ps))
for name, net, lay, r, prio in ps:
if not name.startswith('GND-') or name.endswith('-0'):
print(u' %-12s net=%-9s L%d x %6.2f~%6.2f y %6.2f~%6.2f prio=%d'
% (name, net, lay, r[0], r[2], r[1], r[3], prio))
print(u' (GND 瓦片 %d 块,绕开天线净空与四个 M3 孔)'
% sum(1 for p in ps if p[0].startswith('GND-')))
if bad:
print(u'[X] 自检 %d 条不过:' % len(bad))
for t in bad:
print(u' ' + t)
else:
print(u'[OK] 自检全过(不出内缩线 / 不压净空 / 入口链四段互不重叠)')
wbad, wown, wtot = wall_check()
bs, bv = B.wall3v3_bridges()
print(u'3V3 槽墙:%d 块矩形,盖住 %d/%d 只槽脚;走廊底层桥 %d 段 / 过孔 %d'
% (len(B.wall3v3_rects()), wown, wtot, len(bs), len(bv)))
if wbad:
print(u'[X] 3V3 槽墙自检 %d 条不过:' % len(wbad))
for t in wbad:
print(u' ' + t)
bad += wbad
else:
print(u'[OK] 3V3 槽墙:槽脚全盖住 / 不压异网针 / 桥的过孔都在墙里')
bbad, bown, btot = bus_check()
print(u'12V 干道/铜墙:盖住 %d/%d 只插槽电源脚' % (bown, btot))
if bbad:
print(u'[X] 12V 干道/铜墙自检 %d 条不过:' % len(bbad))
for t in bbad:
print(u' ' + t)
bad += bbad
else:
print(u'[OK] 12V 干道/铜墙:槽脚全盖住 / 不压异网针')
cbad = check_cuts(ps)
bad += cbad
if '--stitch' in sys.argv:
stitch()
if '--push' in sys.argv:
if bad:
sys.exit(u'!! 自检不过,不下发')
push(ps)
elif '--rebuild' in sys.argv:
rebuild()