R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+489
-471
@@ -1,471 +1,489 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R2 刀片 PCB 的铺铜(M4c-3)。
|
||||
|
||||
/c/Python314/python .claude/r2/pcb_pour.py # 只算 + 自检
|
||||
/c/Python314/python .claude/r2/pcb_pour.py --push # 清旧铜面 + 重建 + 重填
|
||||
/c/Python314/python .claude/r2/pcb_pour.py --rebuild # 只重填(铜面没改,只是扇出/布线改了)
|
||||
|
||||
## 叠层(方案 §7.1)
|
||||
|
||||
| 设计 | EasyEDA | id | 内容 |
|
||||
|---|---|---|---|
|
||||
| L1 | Top Layer | 1 | 功率 + 信号;**D± 走这层、以 L2 为参考** |
|
||||
| L2 | Inner1 | 15 | **完整地平面,不许割断** |
|
||||
| L3 | Inner2 | 16 | `VO_P` 岛 + `+12V` 岛,其余 GND 底填 |
|
||||
| L4 | Bottom | 2 | 控制信号 + GND 底填 |
|
||||
|
||||
## 三个只有这块板才有的约束
|
||||
|
||||
1. **金手指区不许铺外层铜**:`J1` 的 64 个指是 L1/L2 上的焊盘,间距只有 0.3 mm。
|
||||
外层铜面一旦下探到 y < 指顶(5.6)+间距,就会把相邻的指连起来 —— 而且**是不是短路
|
||||
要看它连到哪两根**,DRC 只报间距不报「这两根本来就该分开」。
|
||||
⇒ 外层铜面一律 `y >= TOP_POUR_Y`(11.5,比缺口顶 7.45 还高一截)。
|
||||
内层(L2/L3)没有指,可以一直铺到板边内缩,只要避开缺口。
|
||||
|
||||
2. **缺口要挖掉**:防呆缺口是真的铣穿的,四层都要让开(含 0.5 mm 内缩)。
|
||||
|
||||
3. **优先级是「数字小的先铺、先占地盘」**(R1 `pcb_pour.py` 头上那条已经查清的翻案)。
|
||||
⇒ `VO_P` / `+12V` 两个岛给 1 / 2,GND 底填给 9。
|
||||
|
||||
## 三个 API 坑(全部照抄 R1 `pcb_pour.py` 的结论,别再踩一遍)
|
||||
|
||||
· 多边形**必须显式闭合**(末尾把首点再写一遍),否则 `create()` 一块都不建;
|
||||
· `create()` **抛异常不等于失败** —— 判据只能是建完 `getAll()` 按名字(会被转成大写)核对;
|
||||
· `rebuildCopperRegion()` 是坏的,**只能走 GUI**:工具 → 铺铜管理器... → 重建所有 → 确认。
|
||||
判据是 `pcb_PrimitivePoured` 的数量,不是 `pcb_PrimitivePour`。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place),
|
||||
# 先插 .claude 再插 HERE 的话,`import pcb_pour` 会拿到 R1 那一份,
|
||||
# 而且**不报错**,只在调到 R2 专有的函数时才炸。
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
sys.path.insert(0, HERE)
|
||||
from eda import run # noqa: E402
|
||||
import pcb_blade as B # noqa: E402
|
||||
import fp_edge # noqa: E402
|
||||
|
||||
PCB = B.PCB
|
||||
MIL = B.MIL
|
||||
L_TOP, L_GND, L_PWR, L_BOT = 1, 15, 16, 2
|
||||
|
||||
INSET = 0.5 # 铜面离板边
|
||||
TOP_POUR_Y = 11.5 # 外层铜面的下沿(金手指区一点外层铜都不许有)
|
||||
# ⛔ 2026-09-12:板框开了台阶(`pcb_blade.STEP_H` / `TONGUE_X0/X1`)之后,**舌头两侧根本没有板材**
|
||||
# ⇒ 那两段的铜下沿必须抬到 `STEP_H + INSET`,否则铜会悬在被切掉的地方。
|
||||
# 舌头那一段仍按 TOP_POUR_Y / INSET 走(外层不进金手指区、内层绕缺口)。
|
||||
STEP_POUR_Y = round(B.STEP_H + INSET, 3)
|
||||
NOTCH_KEEP = 0.5 # 缺口周围再让 0.5
|
||||
|
||||
# L3 两个岛(x0, y0, x1, y1)
|
||||
# ⛔⛔ 2026-09-11 晚(方案 §35):主板转 180°、功率链转 180° ⇒ 两个岛上下对调。
|
||||
# · `VO_P` 岛在**上半**:功率链顶上的 Rs1 / U3 → 西去 J5 / 阻尼组 / DIS → 两槽之间的 C60。
|
||||
# 电池槽是铣穿的,岛做成**绕开电池槽的 U 形单环**(三块矩形拼,`VO_RECTS`),不用带洞多边形。
|
||||
# · `+12V` 岛在**下半**:金手指 12V 三指的 launch 孔(x 28.5~30.5, y 11.75)→ 东去 F1 的 +12V 脚(功率链最底)。
|
||||
# 12V 从此**不在 L1 / L4 上走线**(原来那根 L4 x≈42.5 的竖线把整块板东西切成两半)。
|
||||
# 下沿 9.0:金手指地母线的孔在 y 7.0 / 8.2,孔 + 净距到 8.8;上沿 20:信号 launch 孔在 y 11.75 一字排开
|
||||
# (1 mm 一个,反焊盘把 11.25~12.25 那一条几乎切断),电流要从它上面 8 mm 宽的那条过。
|
||||
VO_Y0 = 38.0 # 岛主体下沿(DIS / 阻尼组最南到 ~39.8)
|
||||
VO_TOP_Y = round(B.SLOT_Y[0] - 0.56, 3) # 主体上沿:离电池槽底 0.56(槽边到铜 ≥ 0.5)
|
||||
_bat = B.flex_slots()['BAT']
|
||||
VO_RECTS = [
|
||||
(26.0, VO_Y0, B.BW - 0.5, VO_TOP_Y), # 主体:J5 / 阻尼组 / DIS / U2 以北
|
||||
(25.6, VO_TOP_Y, round(_bat[0] - 0.56, 3), B.BH - 0.5), # 两槽之间往上:C60
|
||||
(round(_bat[2] + 0.56, 3), VO_TOP_Y, B.BW - 0.5, B.BH - 0.5), # 电池槽以东往上:Rs1 / U3 / C13
|
||||
]
|
||||
VO_ISLAND, VO_LOBE = VO_RECTS[0], VO_RECTS[1] # 老名字留给别的脚本(board_check 等)
|
||||
# ⛔ 2026-09-12 台阶之后改成**阶梯形**:舌头那一段(x TONGUE_X0~TONGUE_X1)仍下探到 9.0 去接
|
||||
# 12V 三指的 launch 孔(x 28.5~30.5 / y 11.75,全在舌头内);舌头以东那一段抬到 STEP_POUR_Y
|
||||
# —— 那边的板材已经被台阶铣掉了。断面:东段由 11.0 收到 7.5 mm 宽(1 oz 内层,1.5 A 峰值远够)。
|
||||
V12_ISLAND = (27.0, 9.0, 84.0, 20.0) # 老名字留着(board_check 等按它取包围盒)
|
||||
V12_LOW_Y, V12_TOP_Y = 9.0, 20.0
|
||||
|
||||
|
||||
def mil(v):
|
||||
return round(v * MIL, 4)
|
||||
|
||||
|
||||
def rrect(x0, y0, x1, y1, r=0.0, seg=8):
|
||||
u"""圆角矩形点列(逆时针,闭合)。r=0 就是普通矩形。"""
|
||||
if r <= 0:
|
||||
pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||||
else:
|
||||
pts = []
|
||||
for cx, cy, a0 in ((x1 - r, y0 + r, 270), (x1 - r, y1 - r, 0),
|
||||
(x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180)):
|
||||
for k in range(seg + 1):
|
||||
a = math.radians(a0 + 90.0 * k / seg)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0]) # ⚠ 必须显式闭合
|
||||
return out
|
||||
|
||||
|
||||
def notch_cut():
|
||||
u"""缺口在铜面里要挖掉的矩形(含让位)。"""
|
||||
ml, mr = B.notch_break()
|
||||
return (ml - NOTCH_KEEP, -1.0, mr + NOTCH_KEEP, fp_edge.NOTCH_D + NOTCH_KEEP)
|
||||
|
||||
|
||||
def inner_poly():
|
||||
u"""内层铜面:板框内缩 INSET,底边在缺口处绕过去(一条闭合折线,不用挖孔)。"""
|
||||
d = INSET
|
||||
r = B.CORNER_R - d
|
||||
x0, y0, x1, y1 = d, d, B.BW - d, B.BH - d
|
||||
nx0, _ny0, nx1, ny1 = notch_cut()
|
||||
pts = [(nx1, y0)] # 缺口右侧起步,沿底边往右
|
||||
pts.append((x1 - r, y0))
|
||||
for cx, cy, a0 in ((x1 - r, y0 + r, 270), (x1 - r, y1 - r, 0),
|
||||
(x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180)):
|
||||
for k in range(9):
|
||||
a = math.radians(a0 + 90.0 * k / 8)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
pts.append((nx0, y0)) # 底边左段走到缺口左沿
|
||||
pts.append((nx0, ny1)) # 绕缺口
|
||||
pts.append((nx1, ny1))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def outer_poly():
|
||||
u"""外层铜面:只在 y >= TOP_POUR_Y 的部分,金手指区一点铜都没有。"""
|
||||
d = INSET
|
||||
r = B.CORNER_R - d
|
||||
x0, x1, y1 = d, B.BW - d, B.BH - d
|
||||
pts = [(x0, TOP_POUR_Y), (x1, TOP_POUR_Y)]
|
||||
for cx, cy, a0 in ((x1 - r, y1 - r, 0), (x0 + r, y1 - r, 90)):
|
||||
for k in range(9):
|
||||
a = math.radians(a0 + 90.0 * k / 8)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def src(pts):
|
||||
u"""点列 -> TPCB_PolygonSourceArray(mil): x1 y1 'L' x2 y2 'L' ..."""
|
||||
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 gnd_tiles(y_lo):
|
||||
u"""地铜的**若干块单环矩形**,绕开铣穿的排线槽。
|
||||
|
||||
⛔ **不能用带洞的复杂多边形**:`pcb_MathPolygon.createComplexPolygon()` 传多个环时
|
||||
会把客户端挂死 —— 请求 30 s 超时、铺铜边框停在半路、进度恒为 0/0,
|
||||
和「GUI 按钮失灵」完全无法区分(2026-09-08 在这上面绕了很久)。
|
||||
⇒ 一律单环。要挖洞就拆成几块矩形拼,铜面之间同网互相接壤会自然连成一片。
|
||||
|
||||
做法:按所有槽的 x 边把板切成竖条,每条里按 y 找槽之间的空隙。
|
||||
(2026-09-09 改:原来的「上下横带」写法假设各窗 y 区间不重叠,两条并排的排线槽
|
||||
会把槽本身当成一条带铺进去。)
|
||||
`y_lo` = 这一层铜面的下沿(外层 11.5 / 内层 0.5)。
|
||||
"""
|
||||
d = INSET
|
||||
x0, x1 = d, B.BW - d
|
||||
y1 = B.BH - d
|
||||
cuts = [(a - INSET, b - INSET, c + INSET, e + INSET) for (a, b, c, e) in B.mill_cuts()]
|
||||
# ⛔ 台阶两端也是切分点:舌头外的竖条下沿要抬到 STEP_POUR_Y(那边没有板材)
|
||||
xs = sorted(set([x0, x1, B.TONGUE_X0, B.TONGUE_X1] + [v for c in cuts for v in (c[0], c[2])]))
|
||||
tiles = []
|
||||
for xa, xb in zip(xs, xs[1:]):
|
||||
if xb - xa < 0.6:
|
||||
continue
|
||||
in_tongue = (xa >= B.TONGUE_X0 - 1e-9 and xb <= B.TONGUE_X1 + 1e-9)
|
||||
ys = [y_lo if in_tongue else max(y_lo, STEP_POUR_Y)]
|
||||
for c in sorted(cuts, key=lambda c: c[1]):
|
||||
if c[0] < xb - 1e-9 and xa < c[2] - 1e-9:
|
||||
ys += [c[1], c[3]]
|
||||
ys.append(y1)
|
||||
for k in range(0, len(ys) - 1, 2):
|
||||
ya, yb = ys[k], ys[k + 1]
|
||||
if yb - ya > 0.6:
|
||||
tiles.append((xa, ya, xb, yb))
|
||||
# ⛔ 内层的下沿会盖住金手指的**防呆缺口**(铣穿的)。把缺口那一段单独让开:
|
||||
# y 抬到缺口顶之上,再补两块把缺口左右两侧的底部填回来。
|
||||
nx0, _a, nx1, ny1 = notch_cut()
|
||||
if y_lo < ny1:
|
||||
cut = ny1
|
||||
out = []
|
||||
for (a, b, c, e) in tiles:
|
||||
if e <= cut:
|
||||
continue
|
||||
if b < cut and (a < nx1 and c > nx0):
|
||||
out.append((a, cut, c, e))
|
||||
if a < nx0:
|
||||
out.append((a, b, nx0, cut))
|
||||
if c > nx1:
|
||||
out.append((nx1, b, c, cut))
|
||||
else:
|
||||
out.append((a, b, c, e))
|
||||
tiles = [t for t in out if t[2] - t[0] > 0.6 and t[3] - t[1] > 0.6]
|
||||
return tiles
|
||||
|
||||
|
||||
def v12_poly():
|
||||
u"""`+12V` 岛(L3):**阶梯形单环**。舌头段下探到 V12_LOW_Y 接金手指的 launch 孔,
|
||||
舌头以东抬到 STEP_POUR_Y(板框台阶把那块板材铣掉了)。
|
||||
|
||||
⛔ 一律单环(带洞的复杂多边形会把客户端挂死,见 `gnd_tiles` 的说明)。"""
|
||||
x0, x1 = V12_ISLAND[0], V12_ISLAND[2]
|
||||
xt = B.TONGUE_X1
|
||||
assert x0 >= B.TONGUE_X0 and xt < x1, u'+12V 岛的阶梯点跑到舌头外了'
|
||||
pts = [(x0, V12_LOW_Y), (xt, V12_LOW_Y), (xt, STEP_POUR_Y), (x1, STEP_POUR_Y),
|
||||
(x1, V12_TOP_Y), (x0, V12_TOP_Y)]
|
||||
out = [pts[0]]
|
||||
for q in pts[1:]:
|
||||
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
|
||||
out.append(q)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def vo_poly():
|
||||
u"""VO_P 岛:主体 + 两个上叶(夹着电池槽),拼成一个 U 形**单环**(逆时针,显式闭合)。
|
||||
|
||||
2026-09-11 晚起(方案 §35)。上叶 w 骑在主体上沿、西沿可以比主体再往西一点(C60 的 VO_P 盘)。"""
|
||||
a, w, e = VO_RECTS
|
||||
assert abs(a[3] - w[1]) < 1e-9 and abs(a[3] - e[1]) < 1e-9 and e[2] == a[2] and w[2] < e[0], u'上叶必须骑在主体上沿'
|
||||
pts = [(a[0], a[1]), (a[2], a[1]), (e[2], e[3]), (e[0], e[3]), (e[0], a[3]),
|
||||
(w[2], a[3]), (w[2], w[3]), (w[0], w[3]), (w[0], a[3]), (a[0], a[3])]
|
||||
out = [pts[0]]
|
||||
for q in pts[1:]:
|
||||
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
|
||||
out.append(q)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def plan():
|
||||
u"""[(name, net, layer, [环...], priority)],priority 小的先铺、先占地盘。"""
|
||||
outs = []
|
||||
outs.append(('VO-L3', 'VO_P', L_PWR, [src(vo_poly())], 1))
|
||||
outs.append(('V12-L3', '+12V', L_PWR, [src(v12_poly())], 2))
|
||||
for tag, layer, y_lo in (('L2', L_GND, INSET), ('TOP', L_TOP, TOP_POUR_Y),
|
||||
('BOT', L_BOT, TOP_POUR_Y)):
|
||||
for k, (a, b, c, d2) in enumerate(gnd_tiles(y_lo)):
|
||||
outs.append(('GND-%s-%d' % (tag, k), 'GND', layer,
|
||||
[src(rrect(a, b, c, d2))], 9))
|
||||
return outs
|
||||
|
||||
|
||||
def _ring_pts(ring):
|
||||
u"""`src()` 生成的 [x, y, 'L', x, y, 'L', ...](mil)还原成 [(x, y)] 点列(mm)。"""
|
||||
pts = [(float(ring[0]) / MIL, float(ring[1]) / MIL)]
|
||||
for k in range(3, len(ring), 3):
|
||||
pts.append((float(ring[k]) / MIL, float(ring[k + 1]) / MIL))
|
||||
return pts
|
||||
|
||||
|
||||
def selfcheck(ps):
|
||||
bad = []
|
||||
for name, _net, _lay, rings, _p in ps:
|
||||
for k, s in enumerate(rings):
|
||||
if s[0] != s[-2] or s[1] != s[-1]:
|
||||
bad.append(u'%s 第 %d 个环没闭合' % (name, k))
|
||||
# 外层铜面不许下探进金手指区
|
||||
top = fp_edge.EDGE_CLR + fp_edge.LONG_L
|
||||
if TOP_POUR_Y <= top:
|
||||
bad.append(u'外层铜下沿 %.2f 压到指顶 %.2f' % (TOP_POUR_Y, top))
|
||||
if TOP_POUR_Y <= fp_edge.NOTCH_D:
|
||||
bad.append(u'外层铜下沿 %.2f 低于缺口顶 %.2f' % (TOP_POUR_Y, fp_edge.NOTCH_D))
|
||||
# ⛔ 台阶:舌头两侧不许有任何铜低于 STEP_POUR_Y(那块板材已经铣掉了)。
|
||||
# ⚠ 判据必须**逐边**走,不能按整个环的包围盒判:`+12V` 岛是阶梯形,
|
||||
# 它的 y=9.0 那一段只在舌头内,按包围盒(min y 9.0 且 max x 84)会误报。
|
||||
for name, _net, _lay, rings, _p in ps:
|
||||
for ring in rings:
|
||||
pts = _ring_pts(ring)
|
||||
worst = None
|
||||
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
|
||||
n = max(2, int(math.hypot(bx - ax, by - ay) / 0.25) + 1)
|
||||
for i in range(n + 1):
|
||||
t = i / float(n)
|
||||
x, y = ax + (bx - ax) * t, ay + (by - ay) * t
|
||||
if y < STEP_POUR_Y - 1e-6 and (x < B.TONGUE_X0 - 1e-6 or x > B.TONGUE_X1 + 1e-6):
|
||||
if worst is None or y < worst[1]:
|
||||
worst = (x, y)
|
||||
if worst:
|
||||
bad.append(u'%s 伸进台阶切掉的区域(最深处 x %.2f / y %.2f < %.2f)'
|
||||
% (name, worst[0], worst[1], STEP_POUR_Y))
|
||||
# 内层铜面必须绕开缺口
|
||||
nx0, _a, nx1, ny1 = notch_cut()
|
||||
if nx1 - nx0 < fp_edge.NOTCH_W + 2 * fp_edge.LEAD_IN:
|
||||
bad.append(u'缺口挖得比口部还窄')
|
||||
# 两个岛不许互相重叠(同层,优先级只有 1/2 之差,重叠就说不清谁裁谁)
|
||||
b = V12_ISLAND
|
||||
for a in VO_RECTS:
|
||||
if a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]:
|
||||
bad.append(u'VO 岛和 +12V 岛在 L3 上重叠')
|
||||
# 岛不许压到铣穿的槽(含 0.5 净距)—— 客户端会自己让,但让出来的形状谁也没核过
|
||||
for (x0, y0, x1, y1) in B.mill_cuts():
|
||||
for a in VO_RECTS + [b]:
|
||||
if a[0] < x1 + 0.5 and x0 - 0.5 < a[2] and a[1] < y1 + 0.5 and y0 - 0.5 < a[3]:
|
||||
bad.append(u'L3 岛 %s 压到槽 (%.1f~%.1f, %.1f~%.1f)' % (a, x0, x1, y0, y1))
|
||||
return bad
|
||||
|
||||
|
||||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2000));" % PCB)
|
||||
|
||||
JS_DEL = OPEN + u"""
|
||||
let n = 0;
|
||||
for (const old of await eda.pcb_PrimitivePour.getAll()) {
|
||||
await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
|
||||
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_PRIO = OPEN + u"""
|
||||
const P = %s;
|
||||
let k = 0;
|
||||
for (const o of await eda.pcb_PrimitivePour.getAll()) {
|
||||
const want = P[String(o.pourName || '').toUpperCase()];
|
||||
if (want === undefined) continue;
|
||||
try { await eda.pcb_PrimitivePour.modify(o.primitiveId, {pourPriority: want}); k++; }
|
||||
catch (e) {}
|
||||
}
|
||||
return {set: k, now: (await eda.pcb_PrimitivePour.getAll())
|
||||
.map(o => [String(o.pourName||''), o.pourPriority])};
|
||||
"""
|
||||
|
||||
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 call(js, timeout=120):
|
||||
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']
|
||||
|
||||
|
||||
def main():
|
||||
ps = plan()
|
||||
print(u'计划建 %d 块铜面:' % len(ps))
|
||||
for name, net, layer, rings, prio in ps:
|
||||
npt = sum((len(r) + 1) // 3 for r in rings)
|
||||
print(u' %-8s net=%-6s layer=%-3d %d 环 / %3d 点 prio=%d'
|
||||
% (name, net, layer, len(rings), npt, prio))
|
||||
bad = selfcheck(ps)
|
||||
if bad:
|
||||
print(u'[X] 自检不过:')
|
||||
for b in bad:
|
||||
print(u' ' + b)
|
||||
sys.exit(1)
|
||||
print(u'[OK] 自检通过:多边形全闭合 / 外层铜不进金手指区 / 内层绕开缺口 / 两岛不重叠')
|
||||
if '--push' not in sys.argv and '--rebuild' not in sys.argv:
|
||||
print(u'(未加 --push,没有下发)')
|
||||
return
|
||||
|
||||
if '--rebuild' not in sys.argv:
|
||||
print(u'清掉旧铜面: %d 块' % call(JS_DEL)['deleted'])
|
||||
for k in range(0, len(ps), 3): # 3 块一批,稳在超时以内
|
||||
batch = [{'name': n, 'net': net, 'layer': l, 'src': s, 'prio': pr}
|
||||
for n, net, l, s, pr in ps[k:k + 3]]
|
||||
made = call(JS_MAKE % json.dumps(batch), timeout=180)['made']
|
||||
print(u' 已建 %d 块' % len(made))
|
||||
want = {n.upper() for n, _, _, _, _ in ps}
|
||||
got = set(call("return (await eda.pcb_PrimitivePour.getAll())"
|
||||
".map(x=>String(x.pourName||'').toUpperCase());"))
|
||||
if not want <= got:
|
||||
print(u'[X] 没建上: %s' % (want - got))
|
||||
sys.exit(1)
|
||||
# ⚠ create() 的 pourPriority 参数不生效(R1 skill §9j),建完再逐块设
|
||||
r = call(JS_PRIO % json.dumps({n.upper(): pr for n, _, _, _, pr in ps}), timeout=180)
|
||||
print(u' 优先级已设 %d 块: %s' % (r['set'], r['now']))
|
||||
|
||||
before = len(call("return (await eda.pcb_PrimitivePour.getAll()).map(o=>o.pourName);"))
|
||||
r = call(JS_REBUILD, timeout=120)
|
||||
if r.get('err'):
|
||||
print(u'[X] 打不开铺铜管理器: %s' % r['err'])
|
||||
sys.exit(1)
|
||||
# ⚠ 退出条件是「**数量稳定**」,不是「全部填上」。
|
||||
# 有的铜面本来就填不出来(`V12-L3` 在 `+12V` 还没有过孔扎进 L3 之前是孤岛,
|
||||
# 会被「移除孤岛」删掉),写成 `n >= len(ps)` 的话每次都要空转满 200 秒,
|
||||
# 而且看上去像是卡死。
|
||||
prev, same = -1, 0
|
||||
for _i in range(40):
|
||||
time.sleep(5)
|
||||
n = call(JS_POLL, timeout=60)['poured']
|
||||
print(u' 填充 %d / %d' % (n, len(ps)))
|
||||
if n >= len(ps):
|
||||
break
|
||||
same = same + 1 if n == prev else 0
|
||||
prev = n
|
||||
if same >= 2 and n > 0:
|
||||
print(u' (%d 块已稳定,另 %d 块没有可接的铜,属正常)' % (n, len(ps) - n))
|
||||
break
|
||||
fin = call(JS_DONE, timeout=120)
|
||||
print(u' 收尾: %s' % fin)
|
||||
# ⛔ 铺铜**边框**会莫名其妙变少(实测 6→3、5→2)。哪一步删的还没查清,
|
||||
# 但「静默变少」是最坏的形态:铜面没了,DRC 只报连接性,看上去像布线没做完。
|
||||
# 这里把它变成硬报错。
|
||||
if fin['pour'] < before:
|
||||
print(u' [X] 铺铜边框从 %d 变成 %d —— 有东西把它们删了,**不要在这个状态下导出**'
|
||||
% (before, fin['pour']))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R2 刀片 PCB 的铺铜(M4c-3)。
|
||||
|
||||
/c/Python314/python .claude/r2/pcb_pour.py # 只算 + 自检
|
||||
/c/Python314/python .claude/r2/pcb_pour.py --push # 清旧铜面 + 重建 + 重填
|
||||
/c/Python314/python .claude/r2/pcb_pour.py --rebuild # 只重填(铜面没改,只是扇出/布线改了)
|
||||
|
||||
## 叠层(方案 §7.1)
|
||||
|
||||
| 设计 | EasyEDA | id | 内容 |
|
||||
|---|---|---|---|
|
||||
| L1 | Top Layer | 1 | 功率 + 信号;**D± 走这层、以 L2 为参考** |
|
||||
| L2 | Inner1 | 15 | **完整地平面,不许割断** |
|
||||
| L3 | Inner2 | 16 | `VO_P` 岛 + `+12V` 岛,其余 GND 底填 |
|
||||
| L4 | Bottom | 2 | 控制信号 + GND 底填 |
|
||||
|
||||
## 三个只有这块板才有的约束
|
||||
|
||||
1. **金手指区不许铺外层铜**:`J1` 的 64 个指是 L1/L2 上的焊盘,间距只有 0.3 mm。
|
||||
外层铜面一旦下探到 y < 指顶(5.6)+间距,就会把相邻的指连起来 —— 而且**是不是短路
|
||||
要看它连到哪两根**,DRC 只报间距不报「这两根本来就该分开」。
|
||||
⇒ 外层铜面一律 `y >= TOP_POUR_Y`(11.5,比缺口顶 7.45 还高一截)。
|
||||
内层(L2/L3)没有指,可以一直铺到板边内缩,只要避开缺口。
|
||||
|
||||
2. **缺口要挖掉**:防呆缺口是真的铣穿的,四层都要让开(含 0.5 mm 内缩)。
|
||||
|
||||
3. **优先级是「数字小的先铺、先占地盘」**(R1 `pcb_pour.py` 头上那条已经查清的翻案)。
|
||||
⇒ `VO_P` / `+12V` 两个岛给 1 / 2,GND 底填给 9。
|
||||
|
||||
## 三个 API 坑(全部照抄 R1 `pcb_pour.py` 的结论,别再踩一遍)
|
||||
|
||||
· 多边形**必须显式闭合**(末尾把首点再写一遍),否则 `create()` 一块都不建;
|
||||
· `create()` **抛异常不等于失败** —— 判据只能是建完 `getAll()` 按名字(会被转成大写)核对;
|
||||
· `rebuildCopperRegion()` 是坏的,**只能走 GUI**:工具 → 铺铜管理器... → 重建所有 → 确认。
|
||||
判据是 `pcb_PrimitivePoured` 的数量,不是 `pcb_PrimitivePour`。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place),
|
||||
# 先插 .claude 再插 HERE 的话,`import pcb_pour` 会拿到 R1 那一份,
|
||||
# 而且**不报错**,只在调到 R2 专有的函数时才炸。
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
sys.path.insert(0, HERE)
|
||||
from eda import run # noqa: E402
|
||||
import pcb_blade as B # noqa: E402
|
||||
import fp_edge # noqa: E402
|
||||
|
||||
PCB = B.PCB
|
||||
MIL = B.MIL
|
||||
L_TOP, L_GND, L_PWR, L_BOT = 1, 15, 16, 2
|
||||
|
||||
INSET = 0.5 # 铜面离板边
|
||||
TOP_POUR_Y = 11.5 # 外层铜面的下沿(金手指区一点外层铜都不许有)
|
||||
# ⛔ 2026-09-12:板框开了台阶(`pcb_blade.STEP_H` / `TONGUE_X0/X1`)之后,**舌头两侧根本没有板材**
|
||||
# ⇒ 那两段的铜下沿必须抬到 `STEP_H + INSET`,否则铜会悬在被切掉的地方。
|
||||
# 舌头那一段仍按 TOP_POUR_Y / INSET 走(外层不进金手指区、内层绕缺口)。
|
||||
STEP_POUR_Y = round(B.STEP_H + INSET, 3)
|
||||
NOTCH_KEEP = 0.5 # 缺口周围再让 0.5
|
||||
|
||||
# L3 两个岛(x0, y0, x1, y1)
|
||||
# ⛔⛔ 2026-09-11 晚(方案 §35):主板转 180°、功率链转 180° ⇒ 两个岛上下对调。
|
||||
# · `VO_P` 岛在**上半**:功率链顶上的 Rs1 / U3 → 西去 J5 / 阻尼组 / DIS → 两槽之间的 C60。
|
||||
# 电池槽是铣穿的,岛做成**绕开电池槽的 U 形单环**(三块矩形拼,`VO_RECTS`),不用带洞多边形。
|
||||
# · `+12V` 岛在**下半**:金手指 12V 三指的 launch 孔(x 28.5~30.5, y 11.75)→ 东去 F1 的 +12V 脚(功率链最底)。
|
||||
# 12V 从此**不在 L1 / L4 上走线**(原来那根 L4 x≈42.5 的竖线把整块板东西切成两半)。
|
||||
# 下沿 9.0:金手指地母线的孔在 y 7.0 / 8.2,孔 + 净距到 8.8;上沿 20:信号 launch 孔在 y 11.75 一字排开
|
||||
# (1 mm 一个,反焊盘把 11.25~12.25 那一条几乎切断),电流要从它上面 8 mm 宽的那条过。
|
||||
# ⛔⛔ 2026-09-14 BLADE-PCB_1(71×62、金手指居中、用户新摆位):两个岛按新焊盘重画,R 版的定义见
|
||||
# `archive_blade_R_0914/pcb_pour.py`。都用**点列直接给出的单环**,路由器用的 `*_RECTS` 是它们的矩形分解。
|
||||
# · `VO_P`(4 A):主体 x 22.2~55.5 / y 34.8~电池槽底 −0.56,罩住 Rs1.1 (36.3,36.6)、R19.1、C60.1 (32.5,40.5)、
|
||||
# J5.1 (47.9,43.0)、C61~C64.2 (51.6,46.7~53.4);再往南伸一条 x 22.2~27.5 的腿到 y 20.2 接 U3.8 / C65.2
|
||||
# (INA226 的 VBUS 采样 + 去耦,电流 µA 级,腿被别的网的反焊盘啃几口也无妨)。
|
||||
# · `+12V`(1.45 A):舌头段 y 9.0~19.0 横贯(接 J1 A2/A3/B1~B3 五指在 x 19~21 的 launch 孔),
|
||||
# 东头拐上去一条 x 49.5~57.5 / y STEP_POUR_Y~30.0 的腿接 F1.1 (53.7,28.4)。
|
||||
# 两岛最近处:12V 腿顶 30.0 vs VO_P 主体底 34.8,12V 横带顶 19.0 vs VO_P 腿底 20.2 ⇒ 净距 ≥ 1.2。
|
||||
VO_TOP_Y = round(B.SLOT_Y[0] - 0.56, 3) # 主体上沿:离电池槽底 0.56(槽边到铜 ≥ 0.5)
|
||||
# ⛔⛔ 2026-09-15 BLADE-PCB_1 **用户新摆位**(件全挪过、铜清空重布):三个 L3 岛按新焊盘重画。
|
||||
# 上一版(09-14 晚)的定义见 `archive_blade1_0914_layout/pcb_pour.py`。L3 除这三个岛外不放别的铜。
|
||||
# · `VO_P`(4 A):x 35.5~59.0 / y 36.3~电池槽底 −0.56,罩住 Rs1.1 (52.6,37.4)、R19.1 (56.6,37.4)、
|
||||
# J5.1 (47.9,40.5)、C60.1 (38.5,48.9)、C61~C64.2 (42.4~49.0,48.9)。
|
||||
# 南沿 36.3 = Rs1.1 焊盘南沿以北 ⇒ Kelvin 取样线 U3.9→Rs1.1 一路上的换层孔都落在岛外(岛只在焊盘处接取样线)。
|
||||
# C65.2(U3.8 旁的去耦)不进岛,由 router.HAND_TIES 一段线接到 U3.8。
|
||||
# · `+12V`(1.45 A):x 18.75~23.9 / y 9.0~27.2 竖条,接 J1 A2/A3/B1~B3 五指的 launch 孔 (x 19~21) 和 F1.1 (21.9,25.6)。
|
||||
# · `+5V`(≤0.5 A):金手指 A19 (x 39) / A32 (x 52) 的 launch 孔 → 大块 x 24.5~54.0 / y 9.0~34.8 →
|
||||
# 西臂 x 1.0~24.5 / y 27.9~34.8 罩 U5.1 (6.2,31.5)、C66.2 (6.5,29.0)。
|
||||
# 离 +12V 竖条 0.6 / 0.7,离 VO_P 南沿 1.5。舌头外(y < STEP_POUR_Y)不越过 TONGUE_X1 − INSET。
|
||||
VO_RECTS = [(35.5, 36.3, 59.0, VO_TOP_Y)]
|
||||
VO_PTS = [(35.5, 36.3), (59.0, 36.3), (59.0, VO_TOP_Y), (35.5, VO_TOP_Y)]
|
||||
V12_LOW_Y, V12_TOP_Y = 9.0, 27.2
|
||||
V12_RECTS = [(18.75, V12_LOW_Y, 23.9, V12_TOP_Y)]
|
||||
V12_PTS = [(18.75, V12_LOW_Y), (23.9, V12_LOW_Y), (23.9, V12_TOP_Y), (18.75, V12_TOP_Y)]
|
||||
V12_ISLAND = V12_RECTS[0] # 老名字留着
|
||||
# ⛔ 2026-09-15 第二版:`VO`(Buck 输出 → eFuse,4 A、L1.2 到 U2 足有 15 mm)也上 L3 ——
|
||||
# 服务器第一轮 VO 以 1.5 mm 走 L4,从 U2 西侧(R11~R17 / C14 那一簇)穿过去,把 U2 的信号脚全堵死。
|
||||
# · `VO`:x 27.0~55.0 / y 16.1~33.5,罩 L1.2 (37.8,27.1)、C6.1/C7.1 (31.4,29.0/31.4)、R1.1 (27.7,27.1)、
|
||||
# U2.9~13、C8.1 (51.7,19.9)、R9.1 (52.7,18.7)。离 VO_P 南沿 36.3 有 2.8。
|
||||
# · `+5V` 相应收成:舌头横带 y 9.0~15.5(接 A19/A32 两个 launch 孔)→ 竖腿 x 24.5~26.4 北上 → 西臂 y 27.9~34.8 到 U5。
|
||||
# 离 +12V 竖条 0.6、离 VO 0.6。
|
||||
VO_N_RECTS = [(27.0, 16.1, 55.0, 33.5)]
|
||||
VO_N_PTS = [(27.0, 16.1), (55.0, 16.1), (55.0, 33.5), (27.0, 33.5)]
|
||||
_T1 = round(B.TONGUE_X1 - INSET, 3)
|
||||
# 东头台阶以上(y ≥ STEP_POUR_Y)再伸到 x 53.5,把 A32 的 launch 孔 (52.0, 11.75) 整个包进来(原来岛边 52.25 只压到孔边)
|
||||
V5_PTS = [(24.5, 9.0), (_T1, 9.0), (_T1, STEP_POUR_Y), (53.5, STEP_POUR_Y), (53.5, 15.5), (26.4, 15.5), (26.4, 34.8), (1.0, 34.8), (1.0, 27.9), (24.5, 27.9)]
|
||||
# ⛔ 2026-09-15 第三版:`+3V3` 东半块也上 L3(x 55.6~70.0 / y 10.5~35.7)。U3 / U4 / C13 / C69 的 +3V3 脚就地打孔,
|
||||
# 西边的 +3V3 脚(R13/R15/R18/R40/R41/C40/U6/金手指)照常走线接到岛上某个孔(router.HYBRID_PLANE)。
|
||||
# 离 VO 岛东沿 0.6、离 VO_P 南沿 0.6、离 +5V 东沿 ≥1.6。
|
||||
# ⛔ 同日补:服务器没把西边那组 +3V3 接到岛上(锚点孔在 (62.5,25.75),西组停在 x 45)⇒ 岛往西伸一条
|
||||
# y 34.1~35.8 的细带到 x 27.6(离 VO 顶 33.5 让 0.6、离 VO_P 底 36.3 让 0.5),把西组已有的 (30.25,35.5)、(40.75,35.5) 两个孔包进来。
|
||||
V33_RECTS = [(55.6, 10.5, 70.0, 35.8), (27.6, 34.1, 55.6, 35.8)]
|
||||
V33_PTS = [(55.6, 10.5), (70.0, 10.5), (70.0, 35.8), (27.6, 35.8), (27.6, 34.1), (55.6, 34.1)]
|
||||
V5_RECTS = [(24.5, 9.0, _T1, 15.5), (24.5, STEP_POUR_Y, 53.5, 15.5), (24.5, 9.0, 26.4, 34.8), (1.0, 27.9, 26.4, 34.8)]
|
||||
|
||||
|
||||
def mil(v):
|
||||
return round(v * MIL, 4)
|
||||
|
||||
|
||||
def rrect(x0, y0, x1, y1, r=0.0, seg=8):
|
||||
u"""圆角矩形点列(逆时针,闭合)。r=0 就是普通矩形。"""
|
||||
if r <= 0:
|
||||
pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||||
else:
|
||||
pts = []
|
||||
for cx, cy, a0 in ((x1 - r, y0 + r, 270), (x1 - r, y1 - r, 0),
|
||||
(x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180)):
|
||||
for k in range(seg + 1):
|
||||
a = math.radians(a0 + 90.0 * k / seg)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0]) # ⚠ 必须显式闭合
|
||||
return out
|
||||
|
||||
|
||||
def notch_cut():
|
||||
u"""缺口在铜面里要挖掉的矩形(含让位)。"""
|
||||
ml, mr = B.notch_break()
|
||||
return (ml - NOTCH_KEEP, -1.0, mr + NOTCH_KEEP, fp_edge.NOTCH_D + NOTCH_KEEP)
|
||||
|
||||
|
||||
def inner_poly():
|
||||
u"""内层铜面:板框内缩 INSET,底边在缺口处绕过去(一条闭合折线,不用挖孔)。"""
|
||||
d = INSET
|
||||
r = B.CORNER_R - d
|
||||
x0, y0, x1, y1 = B.BX0 + d, d, B.BX1 - d, B.BH - d
|
||||
nx0, _ny0, nx1, ny1 = notch_cut()
|
||||
pts = [(nx1, y0)] # 缺口右侧起步,沿底边往右
|
||||
pts.append((x1 - r, y0))
|
||||
for cx, cy, a0 in ((x1 - r, y0 + r, 270), (x1 - r, y1 - r, 0),
|
||||
(x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180)):
|
||||
for k in range(9):
|
||||
a = math.radians(a0 + 90.0 * k / 8)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
pts.append((nx0, y0)) # 底边左段走到缺口左沿
|
||||
pts.append((nx0, ny1)) # 绕缺口
|
||||
pts.append((nx1, ny1))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def outer_poly():
|
||||
u"""外层铜面:只在 y >= TOP_POUR_Y 的部分,金手指区一点铜都没有。"""
|
||||
d = INSET
|
||||
r = B.CORNER_R - d
|
||||
x0, x1, y1 = B.BX0 + d, B.BX1 - d, B.BH - d
|
||||
pts = [(x0, TOP_POUR_Y), (x1, TOP_POUR_Y)]
|
||||
for cx, cy, a0 in ((x1 - r, y1 - r, 0), (x0 + r, y1 - r, 90)):
|
||||
for k in range(9):
|
||||
a = math.radians(a0 + 90.0 * k / 8)
|
||||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0] - out[-1][0]) > 1e-9 or abs(p[1] - out[-1][1]) > 1e-9:
|
||||
out.append(p)
|
||||
out.append(out[0])
|
||||
return out
|
||||
|
||||
|
||||
def src(pts):
|
||||
u"""点列 -> TPCB_PolygonSourceArray(mil): x1 y1 'L' x2 y2 'L' ..."""
|
||||
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 gnd_tiles(y_lo):
|
||||
u"""地铜的**若干块单环矩形**,绕开铣穿的排线槽。
|
||||
|
||||
⛔ **不能用带洞的复杂多边形**:`pcb_MathPolygon.createComplexPolygon()` 传多个环时
|
||||
会把客户端挂死 —— 请求 30 s 超时、铺铜边框停在半路、进度恒为 0/0,
|
||||
和「GUI 按钮失灵」完全无法区分(2026-09-08 在这上面绕了很久)。
|
||||
⇒ 一律单环。要挖洞就拆成几块矩形拼,铜面之间同网互相接壤会自然连成一片。
|
||||
|
||||
做法:按所有槽的 x 边把板切成竖条,每条里按 y 找槽之间的空隙。
|
||||
(2026-09-09 改:原来的「上下横带」写法假设各窗 y 区间不重叠,两条并排的排线槽
|
||||
会把槽本身当成一条带铺进去。)
|
||||
`y_lo` = 这一层铜面的下沿(外层 11.5 / 内层 0.5)。
|
||||
"""
|
||||
d = INSET
|
||||
x0, x1 = B.BX0 + d, B.BX1 - d # 2026-09-14 晚:板框左沿不再是 0(pcb_blade.BX0)
|
||||
y1 = B.BH - d
|
||||
cuts = [(a - INSET, b - INSET, c + INSET, e + INSET) for (a, b, c, e) in B.mill_cuts()]
|
||||
# ⛔ 台阶两端也是切分点:舌头外的竖条下沿要抬到 STEP_POUR_Y(那边没有板材)
|
||||
xs = sorted(set([x0, x1, B.TONGUE_X0, B.TONGUE_X1] + [v for c in cuts for v in (c[0], c[2])]))
|
||||
tiles = []
|
||||
for xa, xb in zip(xs, xs[1:]):
|
||||
if xb - xa < 0.6:
|
||||
continue
|
||||
in_tongue = (xa >= B.TONGUE_X0 - 1e-9 and xb <= B.TONGUE_X1 + 1e-9)
|
||||
ys = [y_lo if in_tongue else max(y_lo, STEP_POUR_Y)]
|
||||
for c in sorted(cuts, key=lambda c: c[1]):
|
||||
if c[0] < xb - 1e-9 and xa < c[2] - 1e-9:
|
||||
ys += [c[1], c[3]]
|
||||
ys.append(y1)
|
||||
for k in range(0, len(ys) - 1, 2):
|
||||
ya, yb = ys[k], ys[k + 1]
|
||||
if yb - ya > 0.6:
|
||||
tiles.append((xa, ya, xb, yb))
|
||||
# ⛔ 内层的下沿会盖住金手指的**防呆缺口**(铣穿的)。把缺口那一段单独让开:
|
||||
# y 抬到缺口顶之上,再补两块把缺口左右两侧的底部填回来。
|
||||
nx0, _a, nx1, ny1 = notch_cut()
|
||||
if y_lo < ny1:
|
||||
cut = ny1
|
||||
out = []
|
||||
for (a, b, c, e) in tiles:
|
||||
if e <= cut:
|
||||
continue
|
||||
if b < cut and (a < nx1 and c > nx0):
|
||||
out.append((a, cut, c, e))
|
||||
if a < nx0:
|
||||
out.append((a, b, nx0, cut))
|
||||
if c > nx1:
|
||||
out.append((nx1, b, c, cut))
|
||||
else:
|
||||
out.append((a, b, c, e))
|
||||
tiles = [t for t in out if t[2] - t[0] > 0.6 and t[3] - t[1] > 0.6]
|
||||
return tiles
|
||||
|
||||
|
||||
def _ring(pts):
|
||||
out = [pts[0]]
|
||||
for q in pts[1:]:
|
||||
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
|
||||
out.append(q)
|
||||
out.append(out[0]) # ⚠ 必须显式闭合
|
||||
return out
|
||||
|
||||
|
||||
def v12_poly():
|
||||
u"""`+12V` 岛(L3),单环(带洞的复杂多边形会把客户端挂死,见 `gnd_tiles`)。2026-09-15 新摆位:竖条。"""
|
||||
return _ring(V12_PTS)
|
||||
|
||||
|
||||
def vo_poly():
|
||||
u"""VO_P 岛(L3),单环。"""
|
||||
return _ring(VO_PTS)
|
||||
|
||||
|
||||
def plan():
|
||||
u"""[(name, net, layer, [环...], priority)],priority 小的先铺、先占地盘。"""
|
||||
outs = []
|
||||
outs.append(('VO-L3', 'VO_P', L_PWR, [src(vo_poly())], 1))
|
||||
outs.append(('V12-L3', '+12V', L_PWR, [src(v12_poly())], 2))
|
||||
outs.append(('V5-L3', '+5V', L_PWR, [src(_ring(V5_PTS))], 3))
|
||||
outs.append(('VOB-L3', 'VO', L_PWR, [src(_ring(VO_N_PTS))], 4))
|
||||
outs.append(('V33-L3', '+3V3', L_PWR, [src(_ring(V33_PTS))], 5))
|
||||
for tag, layer, y_lo in (('L2', L_GND, INSET), ('TOP', L_TOP, TOP_POUR_Y),
|
||||
('BOT', L_BOT, TOP_POUR_Y)):
|
||||
for k, (a, b, c, d2) in enumerate(gnd_tiles(y_lo)):
|
||||
outs.append(('GND-%s-%d' % (tag, k), 'GND', layer,
|
||||
[src(rrect(a, b, c, d2))], 9))
|
||||
return outs
|
||||
|
||||
|
||||
def _ring_pts(ring):
|
||||
u"""`src()` 生成的 [x, y, 'L', x, y, 'L', ...](mil)还原成 [(x, y)] 点列(mm)。"""
|
||||
pts = [(float(ring[0]) / MIL, float(ring[1]) / MIL)]
|
||||
for k in range(3, len(ring), 3):
|
||||
pts.append((float(ring[k]) / MIL, float(ring[k + 1]) / MIL))
|
||||
return pts
|
||||
|
||||
|
||||
def selfcheck(ps):
|
||||
bad = []
|
||||
for name, _net, _lay, rings, _p in ps:
|
||||
for k, s in enumerate(rings):
|
||||
if s[0] != s[-2] or s[1] != s[-1]:
|
||||
bad.append(u'%s 第 %d 个环没闭合' % (name, k))
|
||||
# 外层铜面不许下探进金手指区
|
||||
top = fp_edge.EDGE_CLR + fp_edge.LONG_L
|
||||
if TOP_POUR_Y <= top:
|
||||
bad.append(u'外层铜下沿 %.2f 压到指顶 %.2f' % (TOP_POUR_Y, top))
|
||||
if TOP_POUR_Y <= fp_edge.NOTCH_D:
|
||||
bad.append(u'外层铜下沿 %.2f 低于缺口顶 %.2f' % (TOP_POUR_Y, fp_edge.NOTCH_D))
|
||||
# ⛔ 台阶:舌头两侧不许有任何铜低于 STEP_POUR_Y(那块板材已经铣掉了)。
|
||||
# ⚠ 判据必须**逐边**走,不能按整个环的包围盒判:`+12V` 岛是阶梯形,
|
||||
# 它的 y=9.0 那一段只在舌头内,按包围盒(min y 9.0 且 max x 84)会误报。
|
||||
for name, _net, _lay, rings, _p in ps:
|
||||
for ring in rings:
|
||||
pts = _ring_pts(ring)
|
||||
worst = None
|
||||
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
|
||||
n = max(2, int(math.hypot(bx - ax, by - ay) / 0.25) + 1)
|
||||
for i in range(n + 1):
|
||||
t = i / float(n)
|
||||
x, y = ax + (bx - ax) * t, ay + (by - ay) * t
|
||||
if y < STEP_POUR_Y - 1e-3 and (x < B.TONGUE_X0 - 1e-3 or x > B.TONGUE_X1 + 1e-3): # 1e-3:mil 往返有 ~1e-5 的舍入(舌头 52.75 时实测误报)
|
||||
if worst is None or y < worst[1]:
|
||||
worst = (x, y)
|
||||
if worst:
|
||||
bad.append(u'%s 伸进台阶切掉的区域(最深处 x %.2f / y %.2f < %.2f)'
|
||||
% (name, worst[0], worst[1], STEP_POUR_Y))
|
||||
# 内层铜面必须绕开缺口
|
||||
nx0, _a, nx1, ny1 = notch_cut()
|
||||
if nx1 - nx0 < fp_edge.NOTCH_W + 2 * fp_edge.LEAD_IN:
|
||||
bad.append(u'缺口挖得比口部还窄')
|
||||
# 两个岛不许互相重叠(同层,优先级只有 1/2 之差,重叠就说不清谁裁谁)
|
||||
b = V12_ISLAND
|
||||
for a in VO_RECTS:
|
||||
if a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]:
|
||||
bad.append(u'VO 岛和 +12V 岛在 L3 上重叠')
|
||||
# 岛不许压到铣穿的槽(含 0.5 净距)—— 客户端会自己让,但让出来的形状谁也没核过
|
||||
for (x0, y0, x1, y1) in B.mill_cuts():
|
||||
for a in VO_RECTS + [b]:
|
||||
if a[0] < x1 + 0.5 and x0 - 0.5 < a[2] and a[1] < y1 + 0.5 and y0 - 0.5 < a[3]:
|
||||
bad.append(u'L3 岛 %s 压到槽 (%.1f~%.1f, %.1f~%.1f)' % (a, x0, x1, y0, y1))
|
||||
return bad
|
||||
|
||||
|
||||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2000));" % PCB)
|
||||
|
||||
JS_DEL = OPEN + u"""
|
||||
let n = 0;
|
||||
for (const old of await eda.pcb_PrimitivePour.getAll()) {
|
||||
await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
|
||||
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_PRIO = OPEN + u"""
|
||||
const P = %s;
|
||||
let k = 0;
|
||||
for (const o of await eda.pcb_PrimitivePour.getAll()) {
|
||||
const want = P[String(o.pourName || '').toUpperCase()];
|
||||
if (want === undefined) continue;
|
||||
try { await eda.pcb_PrimitivePour.modify(o.primitiveId, {pourPriority: want}); k++; }
|
||||
catch (e) {}
|
||||
}
|
||||
return {set: k, now: (await eda.pcb_PrimitivePour.getAll())
|
||||
.map(o => [String(o.pourName||''), o.pourPriority])};
|
||||
"""
|
||||
|
||||
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 call(js, timeout=120):
|
||||
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']
|
||||
|
||||
|
||||
def main():
|
||||
ps = plan()
|
||||
print(u'计划建 %d 块铜面:' % len(ps))
|
||||
for name, net, layer, rings, prio in ps:
|
||||
npt = sum((len(r) + 1) // 3 for r in rings)
|
||||
print(u' %-8s net=%-6s layer=%-3d %d 环 / %3d 点 prio=%d'
|
||||
% (name, net, layer, len(rings), npt, prio))
|
||||
bad = selfcheck(ps)
|
||||
if bad:
|
||||
print(u'[X] 自检不过:')
|
||||
for b in bad:
|
||||
print(u' ' + b)
|
||||
sys.exit(1)
|
||||
print(u'[OK] 自检通过:多边形全闭合 / 外层铜不进金手指区 / 内层绕开缺口 / 两岛不重叠')
|
||||
if '--push' not in sys.argv and '--rebuild' not in sys.argv:
|
||||
print(u'(未加 --push,没有下发)')
|
||||
return
|
||||
|
||||
if '--rebuild' not in sys.argv:
|
||||
print(u'清掉旧铜面: %d 块' % call(JS_DEL)['deleted'])
|
||||
for k in range(0, len(ps), 3): # 3 块一批,稳在超时以内
|
||||
batch = [{'name': n, 'net': net, 'layer': l, 'src': s, 'prio': pr}
|
||||
for n, net, l, s, pr in ps[k:k + 3]]
|
||||
made = call(JS_MAKE % json.dumps(batch), timeout=180)['made']
|
||||
print(u' 已建 %d 块' % len(made))
|
||||
want = {n.upper() for n, _, _, _, _ in ps}
|
||||
got = set(call("return (await eda.pcb_PrimitivePour.getAll())"
|
||||
".map(x=>String(x.pourName||'').toUpperCase());"))
|
||||
if not want <= got:
|
||||
print(u'[X] 没建上: %s' % (want - got))
|
||||
sys.exit(1)
|
||||
# ⚠ create() 的 pourPriority 参数不生效(R1 skill §9j),建完再逐块设
|
||||
r = call(JS_PRIO % json.dumps({n.upper(): pr for n, _, _, _, pr in ps}), timeout=180)
|
||||
print(u' 优先级已设 %d 块: %s' % (r['set'], r['now']))
|
||||
|
||||
before = len(call("return (await eda.pcb_PrimitivePour.getAll()).map(o=>o.pourName);"))
|
||||
r = call(JS_REBUILD, timeout=120)
|
||||
if r.get('err'):
|
||||
print(u'[X] 打不开铺铜管理器: %s' % r['err'])
|
||||
sys.exit(1)
|
||||
# ⚠ 退出条件是「**数量稳定**」,不是「全部填上」。
|
||||
# 有的铜面本来就填不出来(`V12-L3` 在 `+12V` 还没有过孔扎进 L3 之前是孤岛,
|
||||
# 会被「移除孤岛」删掉),写成 `n >= len(ps)` 的话每次都要空转满 200 秒,
|
||||
# 而且看上去像是卡死。
|
||||
prev, same = -1, 0
|
||||
for _i in range(40):
|
||||
time.sleep(5)
|
||||
n = call(JS_POLL, timeout=60)['poured']
|
||||
print(u' 填充 %d / %d' % (n, len(ps)))
|
||||
if n >= len(ps):
|
||||
break
|
||||
same = same + 1 if n == prev else 0
|
||||
prev = n
|
||||
if same >= 2 and n > 0:
|
||||
print(u' (%d 块已稳定,另 %d 块没有可接的铜,属正常)' % (n, len(ps) - n))
|
||||
break
|
||||
fin = call(JS_DONE, timeout=120)
|
||||
print(u' 收尾: %s' % fin)
|
||||
# ⛔ 铺铜**边框**会莫名其妙变少(实测 6→3、5→2)。哪一步删的还没查清,
|
||||
# 但「静默变少」是最坏的形态:铜面没了,DRC 只报连接性,看上去像布线没做完。
|
||||
# 这里把它变成硬报错。
|
||||
if fin['pour'] < before:
|
||||
print(u' [X] 铺铜边框从 %d 变成 %d —— 有东西把它们删了,**不要在这个状态下导出**'
|
||||
% (before, fin['pour']))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
在新工单中引用
屏蔽一个用户