- 规则:铜层 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
607 行
27 KiB
Python
607 行
27 KiB
Python
# -*- 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()
|