文件
phonerail/.claude/r2/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

607 行
27 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 | **整层 +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()