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