R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
{"C1": [[-0.999, 0.001], [1.0, 0.001]], "C10": [[-0.545, 0.001], [0.545, 0.001]], "C11": [[0.421, -0.001], [-0.42, -0.001]], "C12": [[0.421, -0.0], [-0.419, -0.0]], "C13": [[0.421, 0.001], [-0.42, 0.001]], "C14": [[-0.545, -0.001], [0.545, -0.001]], "C2": [[-0.999, 0.0], [1.0, 0.0]], "C3": [[-0.999, 0.001], [1.0, 0.001]], "C4": [[-0.42, 0.0], [0.421, 0.0]], "C40": [[0.001, 0.42], [0.001, -0.42]], "C5": [[0.42, 0.0], [-0.421, 0.0]], "C6": [[1.593, 0.001], [-1.592, 0.001]], "C60": [[4.3, -0.001], [-4.3, -0.001]], "C61": [[1.593, -0.001], [-1.592, -0.001]], "C62": [[1.593, -0.0], [-1.592, -0.0]], "C63": [[1.593, 0.0], [-1.592, 0.0]], "C64": [[1.593, 0.001], [-1.592, 0.001]], "C65": [[0.421, 0.0], [-0.42, 0.0]], "C66": [[1.0, -0.001], [-0.999, -0.001]], "C67": [[0.42, 0.001], [-0.421, 0.001]], "C68": [[0.42, 0.001], [-0.421, 0.001]], "C69": [[0.42, 0.0], [-0.421, 0.0]], "C7": [[1.593, 0.001], [-1.592, 0.001]], "C8": [[0.419, 0.0], [-0.419, 0.0]], "C9": [[0.42, -0.001], [-0.421, -0.001]], "D1": [[-0.0, 2.361], [-0.0, -2.361]], "F1": [[-0.001, -1.446], [-0.001, 1.444]], "J2": [[1.449, -4.375], [1.449, 4.375], [-1.45, -1.874], [-1.45, -0.624], [-1.45, 0.626], [-1.45, 1.875]], "J3": [[3.125, 1.45], [-3.126, 1.45], [0.623, -1.451], [-0.626, -1.451]], "L1": [[-0.0, 2.801], [-0.0, -2.799]], "Q1": [[0.999, 0.949], [0.999, -0.951], [-1.0, -0.001]], "Q2": [[0.999, 0.95], [0.999, -0.95], [-1.0, -0.0]], "Q3": [[-0.951, 0.999], [0.949, 0.999], [-0.001, -1.0]], "Q4": [[-0.951, 0.999], [0.949, 0.999], [-0.001, -1.0]], "Q5": [[-0.951, 0.999], [0.949, 0.999], [-0.001, -1.0]], "R1": [[0.433, -0.001], [-0.433, -0.001]], "R10": [[0.433, -0.001], [-0.433, -0.001]], "R11": [[0.433, 0.001], [-0.433, 0.001]], "R12": [[0.433, 0.001], [-0.433, 0.001]], "R13": [[0.434, 0.0], [-0.433, 0.0]], "R14": [[0.433, -0.001], [-0.433, -0.001]], "R15": [[0.433, -0.001], [-0.433, -0.001]], "R16": [[0.433, -0.0], [-0.433, -0.0]], "R17": [[0.433, -0.0], [-0.433, -0.0]], "R18": [[0.432, -0.0], [-0.432, -0.0]], "R19": [[3.067, -0.0], [-3.065, -0.0]], "R2": [[0.433, 0.0], [-0.433, 0.0]], "R3": [[0.432, -0.001], [-0.434, -0.001]], "R4": [[0.432, 0.0], [-0.432, 0.0]], "R40": [[0.433, -0.001], [-0.434, -0.001]], "R41": [[0.433, -0.001], [-0.433, -0.001]], "R42": [[0.432, 0.0], [-0.432, 0.0]], "R43": [[0.434, 0.0], [-0.432, 0.0]], "R44": [[0.432, 0.0], [-0.432, 0.0]], "R45": [[0.434, 0.0], [-0.432, 0.0]], "R46": [[0.434, -0.001], [-0.432, -0.001]], "R47": [[0.432, 0.001], [-0.432, 0.001]], "R5": [[0.432, 0.0], [-0.432, 0.0]], "R6": [[0.432, -0.001], [-0.432, -0.001]], "R60": [[0.432, 0.0], [-0.432, 0.0]], "R61": [[0.432, -0.001], [-0.432, -0.001]], "R62": [[0.434, -0.001], [-0.432, -0.001]], "R63": [[0.434, 0.0], [-0.433, 0.0]], "R64": [[0.433, 0.0], [-0.433, 0.0]], "R65": [[0.434, -0.0], [-0.432, -0.0]], "R7": [[0.434, -0.001], [-0.432, -0.001]], "R8": [[0.434, 0.001], [-0.432, 0.001]], "R9": [[0.433, -0.001], [-0.433, -0.001]], "Rs1": [[3.066, -0.0], [-3.065, -0.0]], "U1": [[1.0, -1.726], [0.499, -1.726], [-0.001, -1.726], [-0.499, -1.726], [-0.999, -1.726], [1.0, 1.724], [0.499, 1.724], [-0.001, 1.724], [-0.499, 1.724], [-0.999, 1.724], [-1.726, 0.751], [-1.726, -0.75], [1.724, 0.751], [1.724, -0.75], [0.0, -0.708], [0.0, -0.28], [0.0, 0.0], [0.0, 0.28], [0.0, 0.708]], "U2": [[-0.001, 0.0013], [-1.251, -1.4997], [-0.75, -1.4997], [-0.25, -1.4997], [0.251, -1.4997], [0.751, -1.4997], [1.249, -1.4997], [1.249, 1.5003], [0.751, 1.5003], [0.251, 1.5003], [-0.25, 1.5003], [-0.75, 1.5003], [-1.251, 1.5003], [-2.0, 0.7503], [-2.0, 0.2503], [-2.0, -0.2497], [-2.0, -0.7507], [2.001, -0.7507], [2.001, -0.2497], [2.001, 0.2503], [2.001, 0.7503]], "U3": [[0.999, -2.111], [0.499, -2.111], [0.001, -2.111], [-0.499, -2.111], [-1.0, -2.111], [0.999, 2.111], [0.499, 2.111], [0.001, 2.111], [-0.499, 2.111], [-1.0, 2.111]], "U4": [[0.325, -3.023], [0.975, -3.023], [1.626, -3.023], [2.276, -3.023], [2.924, -3.023], [3.574, -3.023], [-0.325, -3.023], [-3.574, -3.023], [-2.926, -3.023], [-2.276, -3.023], [-1.626, -3.023], [-0.975, -3.023], [0.975, 3.023], [1.626, 3.023], [2.276, 3.023], [2.926, 3.023], [3.574, 3.023], [0.325, 3.023], [-3.574, 3.023], [-2.926, 3.023], [-2.276, 3.023], [-1.626, 3.023], [-0.975, 3.023], [-0.325, 3.023]], "U5": [[1.349, -0.95], [1.349, -0.0], [1.349, 0.95], [-1.351, 0.95], [-1.351, -0.0], [-1.351, -0.95]], "U6": [[1.349, -0.95], [1.349, 0.0], [1.349, 0.95], [-1.351, 0.95], [-1.351, 0.0], [-1.351, -0.95]], "U7": [[-0.951, 1.148], [-0.001, 1.148], [0.949, 1.148], [0.949, -1.148], [-0.001, -1.148], [-0.951, -1.148]]}
|
||||
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
@@ -0,0 +1 @@
|
||||
[[17.5, 13.0], [22.5, 13.0], [82.5, 13.0], [82.5, 18.0], [52.5, 23.0], [57.5, 23.0], [62.5, 23.0], [77.5, 23.0], [82.5, 23.0], [12.5, 28.0], [7.5, 33.0], [12.5, 33.0], [22.5, 33.0], [27.5, 33.0], [47.5, 33.0], [2.5, 38.0], [7.5, 38.0], [12.5, 38.0], [2.5, 43.0], [7.5, 43.0], [12.5, 43.0], [22.5, 43.0], [27.5, 43.0], [32.5, 43.0], [37.5, 43.0], [47.5, 43.0], [52.5, 43.0], [57.5, 43.0], [62.5, 43.0], [67.5, 43.0], [72.5, 43.0], [82.5, 43.0], [7.5, 48.0], [12.5, 48.0], [87.5, 48.0], [2.5, 53.0], [7.5, 53.0], [12.5, 53.0], [17.5, 53.0], [22.5, 53.0], [27.5, 53.0], [32.5, 53.0], [37.5, 53.0], [47.5, 53.0], [52.5, 53.0], [57.5, 53.0], [62.5, 53.0], [67.5, 53.0], [72.5, 53.0], [2.5, 58.0], [7.5, 58.0], [12.5, 58.0], [17.5, 58.0], [22.5, 58.0], [27.5, 58.0], [32.5, 58.0], [37.5, 58.0], [47.5, 58.0], [52.5, 58.0], [57.5, 58.0], [62.5, 58.0], [67.5, 58.0], [72.5, 58.0], [2.5, 63.0], [7.5, 63.0], [12.5, 63.0], [17.5, 63.0], [22.5, 63.0], [27.5, 63.0], [32.5, 63.0], [37.5, 63.0], [42.5, 63.0], [47.5, 63.0], [52.5, 63.0], [62.5, 63.0], [67.5, 63.0], [72.5, 63.0], [87.5, 63.0]]
|
||||
@@ -0,0 +1,412 @@
|
||||
{
|
||||
"K1": [
|
||||
6.22,
|
||||
27.5,
|
||||
0
|
||||
],
|
||||
"K2": [
|
||||
4.92,
|
||||
28.71,
|
||||
0
|
||||
],
|
||||
"K3": [
|
||||
4.99,
|
||||
26.08,
|
||||
0
|
||||
],
|
||||
"K4": [
|
||||
3.55,
|
||||
27.33,
|
||||
0
|
||||
],
|
||||
"J5": [
|
||||
25.27,
|
||||
25.34,
|
||||
0
|
||||
],
|
||||
"J4": [
|
||||
55.08,
|
||||
26.34,
|
||||
0
|
||||
],
|
||||
"C60": [
|
||||
38.05,
|
||||
16.501,
|
||||
0
|
||||
],
|
||||
"J3": [
|
||||
68.626,
|
||||
15.951,
|
||||
270
|
||||
],
|
||||
"J1": [
|
||||
45.0,
|
||||
0.0,
|
||||
0
|
||||
],
|
||||
"U1": [
|
||||
81.0,
|
||||
50.9,
|
||||
270
|
||||
],
|
||||
"C4": [
|
||||
81.0,
|
||||
53.975,
|
||||
180
|
||||
],
|
||||
"C1": [
|
||||
81.0,
|
||||
55.6,
|
||||
180
|
||||
],
|
||||
"C2": [
|
||||
81.0,
|
||||
57.6,
|
||||
180
|
||||
],
|
||||
"C3": [
|
||||
81.0,
|
||||
59.6,
|
||||
180
|
||||
],
|
||||
"F1": [
|
||||
81.001,
|
||||
62.946,
|
||||
90
|
||||
],
|
||||
"L1": [
|
||||
80.75,
|
||||
43.475,
|
||||
270
|
||||
],
|
||||
"C6": [
|
||||
80.75,
|
||||
38.16,
|
||||
0
|
||||
],
|
||||
"C7": [
|
||||
80.75,
|
||||
35.78,
|
||||
0
|
||||
],
|
||||
"C8": [
|
||||
80.75,
|
||||
33.965,
|
||||
0
|
||||
],
|
||||
"U2": [
|
||||
81.0,
|
||||
29.0,
|
||||
180
|
||||
],
|
||||
"Rs1": [
|
||||
76.675,
|
||||
23.0,
|
||||
0
|
||||
],
|
||||
"U3": [
|
||||
76.0,
|
||||
16.861,
|
||||
180
|
||||
],
|
||||
"C5": [
|
||||
75.58,
|
||||
44.5,
|
||||
0
|
||||
],
|
||||
"C9": [
|
||||
84.45,
|
||||
49.18,
|
||||
0
|
||||
],
|
||||
"D1": [
|
||||
76.75,
|
||||
53.875,
|
||||
90
|
||||
],
|
||||
"U4": [
|
||||
12.5,
|
||||
15.6,
|
||||
0
|
||||
],
|
||||
"R3": [
|
||||
77.825,
|
||||
48.751,
|
||||
90
|
||||
],
|
||||
"C13": [
|
||||
78.829,
|
||||
17.999,
|
||||
0
|
||||
],
|
||||
"U7": [
|
||||
51.501,
|
||||
30.602,
|
||||
0
|
||||
],
|
||||
"Q3": [
|
||||
5.5,
|
||||
23.24,
|
||||
90
|
||||
],
|
||||
"Q4": [
|
||||
2.001,
|
||||
23.251,
|
||||
90
|
||||
],
|
||||
"Q5": [
|
||||
5.501,
|
||||
19.501,
|
||||
90
|
||||
],
|
||||
"C40": [
|
||||
7.185,
|
||||
12.577,
|
||||
0
|
||||
],
|
||||
"R41": [
|
||||
7.185,
|
||||
14.777,
|
||||
0
|
||||
],
|
||||
"R40": [
|
||||
7.185,
|
||||
16.977,
|
||||
0
|
||||
],
|
||||
"R19": [
|
||||
22.065,
|
||||
31.0,
|
||||
0
|
||||
],
|
||||
"Q2": [
|
||||
29.501,
|
||||
31.0,
|
||||
0
|
||||
],
|
||||
"C14": [
|
||||
85.715,
|
||||
29.251,
|
||||
0
|
||||
],
|
||||
"R11": [
|
||||
85.567,
|
||||
31.749,
|
||||
0
|
||||
],
|
||||
"R12": [
|
||||
85.567,
|
||||
26.749,
|
||||
0
|
||||
],
|
||||
"R13": [
|
||||
85.566,
|
||||
24.25,
|
||||
0
|
||||
],
|
||||
"U6": [
|
||||
67.601,
|
||||
23.75,
|
||||
0
|
||||
],
|
||||
"R63": [
|
||||
66.316,
|
||||
30.0,
|
||||
0
|
||||
],
|
||||
"C69": [
|
||||
66.33,
|
||||
27.5,
|
||||
0
|
||||
],
|
||||
"Q1": [
|
||||
67.251,
|
||||
33.251,
|
||||
0
|
||||
],
|
||||
"R10": [
|
||||
73.817,
|
||||
38.001,
|
||||
0
|
||||
],
|
||||
"R14": [
|
||||
73.817,
|
||||
35.501,
|
||||
0
|
||||
],
|
||||
"R15": [
|
||||
73.817,
|
||||
26.501,
|
||||
0
|
||||
],
|
||||
"R16": [
|
||||
73.817,
|
||||
30.5,
|
||||
0
|
||||
],
|
||||
"R17": [
|
||||
70.317,
|
||||
27.5,
|
||||
0
|
||||
],
|
||||
"R18": [
|
||||
69.818,
|
||||
30.0,
|
||||
0
|
||||
],
|
||||
"R9": [
|
||||
73.817,
|
||||
33.001,
|
||||
0
|
||||
],
|
||||
"C10": [
|
||||
87.545,
|
||||
57.999,
|
||||
0
|
||||
],
|
||||
"R1": [
|
||||
86.317,
|
||||
44.001,
|
||||
0
|
||||
],
|
||||
"R2": [
|
||||
84.59,
|
||||
56.0,
|
||||
0
|
||||
],
|
||||
"R4": [
|
||||
87.59,
|
||||
56.0,
|
||||
0
|
||||
],
|
||||
"R5": [
|
||||
85.09,
|
||||
54.0,
|
||||
0
|
||||
],
|
||||
"R6": [
|
||||
87.09,
|
||||
49.501,
|
||||
0
|
||||
],
|
||||
"R7": [
|
||||
87.566,
|
||||
60.001,
|
||||
0
|
||||
],
|
||||
"R8": [
|
||||
86.09,
|
||||
45.999,
|
||||
0
|
||||
],
|
||||
"C11": [
|
||||
85.09,
|
||||
52.001,
|
||||
0
|
||||
],
|
||||
"C12": [
|
||||
84.59,
|
||||
58.0,
|
||||
0
|
||||
],
|
||||
"R42": [
|
||||
8.59,
|
||||
24.0,
|
||||
0
|
||||
],
|
||||
"R43": [
|
||||
8.59,
|
||||
21.5,
|
||||
0
|
||||
],
|
||||
"R44": [
|
||||
2.09,
|
||||
17.5,
|
||||
0
|
||||
],
|
||||
"R45": [
|
||||
2.09,
|
||||
20.0,
|
||||
0
|
||||
],
|
||||
"R46": [
|
||||
9.09,
|
||||
26.501,
|
||||
0
|
||||
],
|
||||
"R47": [
|
||||
9.09,
|
||||
28.999,
|
||||
0
|
||||
],
|
||||
"C61": [
|
||||
34.61,
|
||||
24.001,
|
||||
0
|
||||
],
|
||||
"C62": [
|
||||
35.11,
|
||||
28.0,
|
||||
0
|
||||
],
|
||||
"C63": [
|
||||
35.11,
|
||||
32.0,
|
||||
0
|
||||
],
|
||||
"C64": [
|
||||
34.61,
|
||||
35.999,
|
||||
0
|
||||
],
|
||||
"R60": [
|
||||
40.068,
|
||||
28.0,
|
||||
0
|
||||
],
|
||||
"R61": [
|
||||
39.568,
|
||||
23.501,
|
||||
0
|
||||
],
|
||||
"C65": [
|
||||
40.079,
|
||||
30.5,
|
||||
0
|
||||
],
|
||||
"U5": [
|
||||
56.351,
|
||||
31.0,
|
||||
0
|
||||
],
|
||||
"C66": [
|
||||
57.749,
|
||||
35.001,
|
||||
0
|
||||
],
|
||||
"R62": [
|
||||
61.09,
|
||||
30.001,
|
||||
0
|
||||
],
|
||||
"R64": [
|
||||
62.09,
|
||||
32.5,
|
||||
0
|
||||
],
|
||||
"R65": [
|
||||
55.09,
|
||||
38.0,
|
||||
0
|
||||
],
|
||||
"C67": [
|
||||
58.58,
|
||||
37.999,
|
||||
0
|
||||
],
|
||||
"C68": [
|
||||
62.08,
|
||||
34.999,
|
||||
0
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R2 刀片 PCB(M4):板框 + 分区参考 + 摆位常量。
|
||||
|
||||
/c/Python314/python .claude/r2/pcb_blade.py # 只算 + 自检
|
||||
/c/Python314/python .claude/r2/pcb_blade.py --outline # 画/重画板框(幂等)
|
||||
/c/Python314/python .claude/r2/pcb_blade.py --verify # 读回校验板框 + 挖槽区域
|
||||
/c/Python314/python .claude/r2/pcb_blade.py --slots # 只重画两条排线槽(layer 12 填充)
|
||||
|
||||
## 坐标系
|
||||
|
||||
**板左下角 = (0, 0),x 向右、y 向上**(EasyEDA PCB 里 y 向上为正,见方案 §22.5)。
|
||||
这正好是方案 §3.1 那张分区图的坐标,图里的数可以直接抄。
|
||||
|
||||
⛔ **2026-09-11 用户拍板:98×76 → 90×68**(方案 §34)。手机原点 (3, 29) → **(1.5, 22.34)**:
|
||||
左边距压到 H2 螺钉头离板边 0.6;服务带 18 → 11.34(C60 仍夹在两槽之间,上下各余 0.17);
|
||||
手机顶离板边只剩 1.0(顶边导轨只能咬 ≤ 1 mm);功率列 26 → 19.4。
|
||||
与手机绑定的一切(七个孔 / 两条槽 / J4 / J5 / K1~K4)都从 `PHONE_ORG` 推,不许写死。
|
||||
|
||||
```
|
||||
y 68.00 ┬──────────────────────────────────┬─────────────┐
|
||||
│ │ 功率列 19.4 │
|
||||
│ 小米8 主板 67.09 × 44.66 │ Buck/L/Cin │
|
||||
y 22.34 ┼──── 接口边 ───────────────────────┤ eFuse/Rs │
|
||||
│ 服务带 11.34 C60 / 两条排线槽 │ INA │
|
||||
y 11.00 ┼──────────────────────────────────┴─────────────┤
|
||||
│ 金手指区 11mm(槽体 ~9mm 要让开,这一带不许放件) │
|
||||
y 0.00 ┴───────────────── 板边(卡边)──────────────────┘
|
||||
x 0 1.5 ─── 手机主板 ─── 68.59 70.59 ─ 功率列 ─ 90
|
||||
```
|
||||
|
||||
## 板框的底边是**断开**的
|
||||
|
||||
金手指的防呆缺口画在封装 `EDGE-64P-P1.00_PCIE-X4-GF` 的板框层里(方案 §22.3),
|
||||
所以这里画的底边必须在缺口口部让开一段:断点 x 由 `fp_edge.notch_mouth()` + `J1_X` 算出,
|
||||
**不要手抄**。两段拼起来才是一个闭合板框;拼不上 DRC 会报开口。
|
||||
|
||||
## ⛔ 不用 pcb_PrimitiveArc
|
||||
|
||||
实测(方案 §22.4):`pcb_PrimitiveArc.create()` 在**封装编辑器里画得出但读不回、删不掉**,
|
||||
在 **PCB 文档里干脆什么都不画**(返回值仍然是成功的)。⇒ 四角 R2 用折线近似,
|
||||
弦高误差 < 0.01 mm。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
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 fp_edge # noqa: E402
|
||||
|
||||
PCB = '3b9c4e39c8d164db' # BLADE-PCB
|
||||
MIL = fp_edge.MIL
|
||||
|
||||
# ── 板框(mm,出处 方案 §3.1)─────────────────────────────────────────
|
||||
BW, BH = 90.0, 68.0 # 2026-09-11 由 98×76 压缩(用户拍板)
|
||||
CORNER_R = 2.0
|
||||
CORNER_SEG = 8 # 每个 R2 圆角用几段折线
|
||||
OUTLINE_W = 0.15
|
||||
|
||||
# 金手指阵列中心。**居中**:刀片 98 宽、插槽体只有 39,居中让两侧悬臂等长
|
||||
# (左边是手机主板、右边是功率列,两头都不轻)。⚠ 底板的 5 个插槽必须按同一个数摆。
|
||||
J1_X, J1_Y, J1_ROT = BW / 2.0, 0.0, 0
|
||||
|
||||
# ── 小米8 主板的落点 + 定位孔(板级事实,摆位/布线/铺铜都从这里取)────────
|
||||
# 主板 67.09 × 44.66,长边水平、**接口边(v=0,J600/J1401/侧键都在上面)朝下**。
|
||||
# ⚠ 2026-09-11:(3.0, 29.0) → (1.5, 22.34)。y 的取法:C60 本体 11 要整个落在
|
||||
# 金手指区上沿 11.0 与接口边之间(B 面服务带才有 18.7 的净空),11 + 0.17 + 11 + 0.17 = 22.34;
|
||||
# 板高 68 ⇒ 手机顶离板边 68 − 22.34 − 44.66 = 1.0。x 的取法:H2(u=0.7)离板边 2.2,
|
||||
# M1.6 尼龙螺钉头 Ø3.2 还剩 0.6。
|
||||
PHONE_ORG = (1.5, 22.34)
|
||||
PHONE_WH = (67.09, 44.66)
|
||||
|
||||
# ⛔ **主板定位孔**。整个 §3.2 的高度预算 / §21.1 的槽距结论都建立在
|
||||
# 「螺丝把主板压在刀片上、螺柱间隙 1.0 mm」(R1 §22.3e 的中框式焊盘)之上 ——
|
||||
# 没有这几个孔,`K1~K4` 那四个裸铜盘就没有压力,整套接触方式不成立。
|
||||
# 孔位是实测值(`.claude/mi8_mech.json` 的 `holes`,u/v 本体坐标)。
|
||||
# 直径沿用 R1 已经定稿的一组(`pcb2_place.py`):
|
||||
# 主板自己的孔 **Φ2.0**(用户卡尺)⇒ 配 **M1.6 尼龙立柱**(M2 螺杆塞 Φ2.0 没余量),
|
||||
# 刀片这一侧是螺钉过孔,取 M1.6 的通孔 **Φ1.8 NPTH**。
|
||||
# ⚠ R1 载板上 `H5` 没打(撞 EDL 座),**刀片上七个孔全都够得开**(实测最近焊盘 3.06 mm
|
||||
# 是 H1 到 K2),所以七个全打。
|
||||
HOLE_D = 1.8
|
||||
HOLE_KEEP = 2.2 # 螺钉头避让(M1.6 尼龙螺钉头 Ø3.2 + 余量)
|
||||
HOLE_VIA_KEEP = 1.5 # 孔到过孔/铜的最小中心距(孔半径 0.9 + 过孔半径 0.3 + 净距)
|
||||
|
||||
|
||||
def phone_holes():
|
||||
u"""{名字: (x, y)},刀片坐标。"""
|
||||
m = json.load(io.open(os.path.join(os.path.dirname(HERE), 'mi8_mech.json'),
|
||||
encoding='utf-8'))
|
||||
return {k: (round(PHONE_ORG[0] + u, 3), round(PHONE_ORG[1] + v, 3))
|
||||
for k, (u, v) in m['holes'].items()}
|
||||
|
||||
|
||||
# ── 排线穿板槽(2026-09-09 用户拍板:屏蔽窗去掉、器件全到 B 面、给两条排线开槽)──
|
||||
# ⛔ **屏蔽罩避让窗已删**。它是「屏蔽罩轮廓没实测所以全挖」的保守项,而 R1 §22.3e 的
|
||||
# 定稿是「载板不挖空:用户实测贴合面元件全低于 1 mm」。去掉之后 L2 地平面整块、
|
||||
# 板子恢复刚度,B 面手机投影 67×44 全部可放件(高度预算见 pcb_place.H_B_*)。
|
||||
#
|
||||
# **排线走法**(照搬 R1 `pcb2_place.SLOTS`,2026-09-06 实测排线:电池 8×38、尾插 18×55):
|
||||
# J600/J1401 在主板朝上那一面 → 朝接口边(y=29)走 → 翻过板边落到刀片 A 面
|
||||
# → 到接口边外侧 2.7 mm 的槽 → 穿板 → **U 形折回,贴着 B 面往 +y 走**到手机投影下方的
|
||||
# `J5` / `J4`。折回是绕 x 轴翻 180°:x 不变、y 镜像 —— 而立创底层正是「先 y 镜像再转」
|
||||
# (方案 §25.3),所以 `J5`/`J4` 在底层 **rot 0** 就是对的(R1 §25.16 v4 同一结论)。
|
||||
# ⚠ 槽是 **layer 12 (MULTI) 的填充 = 挖槽区域**(`pcb_PrimitiveFill`),不是板框内环。
|
||||
# DRC 认它(「挖槽区域 到 导线」),内层铺铜也会给它让反焊盘。
|
||||
# ⚠ J1401 的 x 取设计文档 §22.3b 的**本体中心 53.58**(±0.3),不是 `mi8_mech.json`
|
||||
# `contacts_up.J1401 = 48.76` —— 那是座子西端(两者差 4.82 ≈ 半本体 6.1 − 1.3)。
|
||||
# TP1204/1205 两份数据吻合到 0.1,说明坐标系相同、只是 J1401 取的点不同。
|
||||
# 按 53.58 定,尾插车道西沿 47.6 正好让开 H4 螺钉头 (44.9 + 2.2),与 R1 自洽。
|
||||
J600_U = 23.77 # 电池座中心(两份数据一致;§22.3b 标 ±2,槽放宽到 12)
|
||||
J1401_U = 53.58 # 尾插座本体中心(§22.3b,±0.3)
|
||||
# 槽的 y 区间:接口边外侧 1.2~4.2,中心离边 2.7(R1 同)。**跟着手机原点走**(原来写死 24.8~27.8)
|
||||
SLOT_Y = (round(PHONE_ORG[1] - 4.2, 3), round(PHONE_ORG[1] - 1.2, 3))
|
||||
SLOT_W = {'BAT': 12.0, 'TAIL': 20.0} # 排线 8 / 18,各留余量
|
||||
FLEX_W = {'BAT': 8.0, 'TAIL': 18.0}
|
||||
SLOT_R = 1.0 # 槽内圆角(铣刀半径,嘉立创内槽最小 R0.5)
|
||||
|
||||
|
||||
def flex_x():
|
||||
u"""两条排线的中心 x(刀片坐标)。"""
|
||||
return {'BAT': round(PHONE_ORG[0] + J600_U, 3), 'TAIL': round(PHONE_ORG[0] + J1401_U, 3)}
|
||||
|
||||
|
||||
def flex_slots():
|
||||
u"""{名字: (x0, y0, x1, y1)},刀片坐标,铣穿。"""
|
||||
out = {}
|
||||
for k, cx in flex_x().items():
|
||||
w = SLOT_W[k]
|
||||
out[k] = (round(cx - w / 2, 3), SLOT_Y[0], round(cx + w / 2, 3), SLOT_Y[1])
|
||||
return out
|
||||
|
||||
|
||||
def mill_cuts():
|
||||
u"""板上全部铣穿的内槽 [(x0,y0,x1,y1)] —— 布线 / 铺铜 / 摆位都从这里取障碍。"""
|
||||
return list(flex_slots().values())
|
||||
|
||||
|
||||
def slot_check():
|
||||
u"""槽不许啃到螺钉孔避让圈,两条槽之间要留肋,槽不许进手机投影。"""
|
||||
bad = []
|
||||
for k, w in flex_slots().items():
|
||||
for hn, (hx, hy) in phone_holes().items():
|
||||
dx = max(w[0] - hx, 0.0, hx - w[2])
|
||||
dy = max(w[1] - hy, 0.0, hy - w[3])
|
||||
if math.hypot(dx, dy) < HOLE_KEEP:
|
||||
bad.append(u'槽 %s 离螺钉孔 %s 只有 %.2f < %.1f' % (k, hn, math.hypot(dx, dy), HOLE_KEEP))
|
||||
if w[3] > PHONE_ORG[1]:
|
||||
bad.append(u'槽 %s 顶 %.1f 进了手机投影(接口边 %.1f)' % (k, w[3], PHONE_ORG[1]))
|
||||
if w[1] < ZONES['finger'][3] + 1.0:
|
||||
bad.append(u'槽 %s 底 %.1f 压到金手指区' % (k, w[1]))
|
||||
s = flex_slots()
|
||||
rib = s['TAIL'][0] - s['BAT'][2]
|
||||
if rib < 3.0:
|
||||
bad.append(u'两槽之间只剩 %.1f mm 肋(要 >= 3)' % rib)
|
||||
return bad
|
||||
|
||||
|
||||
# 分区(只是参考矩形,画在文档层,摆位脚本从这里取)
|
||||
PHONE_X1 = round(PHONE_ORG[0] + PHONE_WH[0], 3) # 手机右沿 68.59
|
||||
POWER_X0 = round(PHONE_X1 + 2.0, 3) # 功率列西沿 70.59(离手机 2 mm)
|
||||
ZONES = {
|
||||
'finger': (0.0, 0.0, BW, 11.0), # 金手指区:不许放件(槽体 ~9mm)
|
||||
'service': (0.0, 11.0, POWER_X0, PHONE_ORG[1]), # 服务带:C60 / 两条排线槽
|
||||
'phone': (PHONE_ORG[0], PHONE_ORG[1], PHONE_X1, round(PHONE_ORG[1] + PHONE_WH[1], 3)),
|
||||
'power': (POWER_X0, 11.0, BW, BH), # 功率列
|
||||
}
|
||||
|
||||
|
||||
def corner(cx, cy, a0, a1):
|
||||
u"""圆角折线点列(角度 deg,逆时针)。"""
|
||||
pts = []
|
||||
for k in range(CORNER_SEG + 1):
|
||||
a = math.radians(a0 + (a1 - a0) * k / CORNER_SEG)
|
||||
pts.append((cx + CORNER_R * math.cos(a), cy + CORNER_R * math.sin(a)))
|
||||
return pts
|
||||
|
||||
|
||||
def outline_pts():
|
||||
u"""板框点列(逆时针,从底边左端 (r,0) 起,回到 (r,0) 闭合)。
|
||||
|
||||
⚠ 相邻重合点必须去掉:圆角的首点和前一条直边的终点是同一个点,留着就会生成
|
||||
一条**零长度线段**,编辑器会静默把它吃掉(下发 41 段读回 37 段),
|
||||
于是「读回条数 != 期望」变成一个没完没了的假警报。这正是 CLAUDE.md 里
|
||||
原理图那条「零长度导线」坑的 PCB 版。
|
||||
"""
|
||||
r = CORNER_R
|
||||
pts = [(r, 0.0), (BW - r, 0.0)]
|
||||
pts += corner(BW - r, r, 270, 360) # 右下
|
||||
pts += [(BW, BH - r)]
|
||||
pts += corner(BW - r, BH - r, 0, 90) # 右上
|
||||
pts += [(r, BH)]
|
||||
pts += corner(r, BH - r, 90, 180) # 左上
|
||||
pts += [(0.0, r)]
|
||||
pts += corner(r, r, 180, 270) # 左下,收回 (r,0)
|
||||
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)
|
||||
assert abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9, u'板框点列没有闭合'
|
||||
return out
|
||||
|
||||
|
||||
def notch_break():
|
||||
u"""底边要断开的区间(板坐标 mm)。"""
|
||||
ml, mr = fp_edge.notch_mouth()
|
||||
return round(J1_X + ml, 4), round(J1_X + mr, 4)
|
||||
|
||||
|
||||
def segments():
|
||||
u"""板框全部线段。底边在缺口口部断开。"""
|
||||
pts = outline_pts()
|
||||
segs = []
|
||||
for a, b in zip(pts, pts[1:]):
|
||||
segs.append((a[0], a[1], b[0], b[1]))
|
||||
# 第一段是底边 (r,0)->(BW-r,0),切成两段
|
||||
bl, br = notch_break()
|
||||
x0, y0, x1, y1 = segs[0]
|
||||
assert (y0, y1) == (0.0, 0.0) and x0 < bl < br < x1, u'底边不是第一段或断点跑出去了'
|
||||
segs[0:1] = [(x0, 0.0, bl, 0.0), (br, 0.0, x1, 0.0)]
|
||||
# ⚠ 排线槽**不在这里**:它们是 layer 12 的填充(挖槽区域),见 push_slots()
|
||||
return segs
|
||||
|
||||
|
||||
def notch_pts():
|
||||
u"""缺口的点列(板坐标),**自左向右**:口部左 → 导入角 → 左壁 → 顶半圆 → 右壁 → 口部右。
|
||||
|
||||
几何仍然只有 `fp_edge` 一个出处,这里只做「封装坐标 + J1_X」的平移和顺序调整。
|
||||
"""
|
||||
hw = fp_edge.NOTCH_W / 2.0
|
||||
ml, mr = notch_break()
|
||||
cx = J1_X + fp_edge.NOTCH_CX
|
||||
top = fp_edge.NOTCH_D - hw
|
||||
pts = [(ml, 0.0), (cx - hw, fp_edge.LEAD_IN), (cx - hw, top)]
|
||||
for k in range(fp_edge.ARC_SEG, -1, -1): # 半圆从左(π)走到右(0)
|
||||
a = math.pi * k / fp_edge.ARC_SEG
|
||||
pts.append((cx + hw * math.cos(a), top + hw * math.sin(a)))
|
||||
pts += [(cx + hw, fp_edge.LEAD_IN), (mr, 0.0)]
|
||||
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)
|
||||
return out
|
||||
|
||||
|
||||
def outline_polygon():
|
||||
u"""**一条闭合折线**的完整板框点列:圆角矩形 + 底边中间的金手指防呆缺口。
|
||||
|
||||
⛔ 为什么不用 `segments()` 那 37 段线(2026-09-09,M7):
|
||||
`pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**,不是图形线 —— 选中它属性面板写的是
|
||||
「导线」,还带网络 / 网络长度,`图层` 那一格 `data-value=11` 但**显示是空的**
|
||||
(下拉里只有铜层,板框层不在选项里)。渲染成品红、DRC 也不报,
|
||||
所以四道几何校验全过,而**客户端根本不把它当板框**:
|
||||
自动布线一路弹「请先绘制板框!」。板框要用 `pcb_PrimitivePolyline`(折线)画。
|
||||
"""
|
||||
pts = outline_pts() # 闭合,第一段是底边 (r,0)->(BW-r,0)
|
||||
bl, br = notch_break()
|
||||
assert pts[0][1] == 0.0 and pts[1][1] == 0.0 and pts[0][0] < bl < br < pts[1][0], u'底边不是第一段或断点跑出去了'
|
||||
return [pts[0]] + notch_pts() + list(pts[1:])
|
||||
|
||||
|
||||
def polygon_src(pts):
|
||||
u"""点列 -> pcb_MathPolygon.createPolygon 的源数组(mil,显式闭合)。"""
|
||||
a = [mil(pts[0][0]), mil(pts[0][1])]
|
||||
for q in pts[1:]:
|
||||
a += ['L', mil(q[0]), mil(q[1])]
|
||||
if pts[0] != pts[-1]:
|
||||
a += ['L', mil(pts[0][0]), mil(pts[0][1])]
|
||||
return a
|
||||
|
||||
|
||||
def rrect_pts(x0, y0, x1, y1, r, seg=6):
|
||||
u"""圆角矩形闭合点列。内槽必须圆角:铣刀下不去直角,嘉立创内槽最小 R0.5。"""
|
||||
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 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 selfcheck():
|
||||
bl, br = notch_break()
|
||||
assert round(br - bl, 4) == 2.9, (bl, br)
|
||||
# 金手指必须整个落在板内,且不侵占服务带
|
||||
for pin in ('A1', 'A32'):
|
||||
x, y, w, h = fp_edge.pad_geom(pin)
|
||||
ax = J1_X + x
|
||||
assert 0.5 < ax < BW - 0.5, u'%s 跑出板外: x=%.2f' % (pin, ax)
|
||||
top = fp_edge.EDGE_CLR + fp_edge.LONG_L
|
||||
assert top < ZONES['finger'][3], u'手指顶到服务带'
|
||||
assert fp_edge.NOTCH_D < ZONES['finger'][3], u'缺口比金手指区还深'
|
||||
# 分区不许互相重叠(除了 finger/service 只共边)
|
||||
z = ZONES
|
||||
assert z['service'][3] == z['phone'][1] == PHONE_ORG[1]
|
||||
assert z['phone'][2] <= z['power'][0], u'手机投影压进功率列'
|
||||
assert z['service'][2] <= z['power'][0], u'服务带压进功率列'
|
||||
assert z['phone'][3] <= BH, u'手机投影超出板顶'
|
||||
bad = slot_check()
|
||||
assert not bad, u'排线槽校核不过: %s' % bad
|
||||
return True
|
||||
|
||||
|
||||
def call(js, timeout=180):
|
||||
r = run(js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800])
|
||||
return r['result']
|
||||
|
||||
|
||||
def mil(v):
|
||||
return round(v * MIL, 4)
|
||||
|
||||
|
||||
def push_outline():
|
||||
u"""画板框:**一条闭合折线**(`pcb_PrimitivePolyline`),含金手指缺口。
|
||||
|
||||
顺带把层 11 上遗留的**导线**全删掉 —— M4b~M6 一直是用 `pcb_PrimitiveLine`
|
||||
画的,那是铜,客户端不认(见 `outline_polygon()` 的说明)。
|
||||
"""
|
||||
src = polygon_src(outline_polygon())
|
||||
js = ("await eda.dmt_EditorControl.openDocument('%s');" % PCB +
|
||||
"await new Promise(r=>setTimeout(r,2500));" +
|
||||
"const oldL = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11)"
|
||||
".map(l=>l.primitiveId).filter(i=>i.length<=20);" +
|
||||
"if (oldL.length) await eda.pcb_PrimitiveLine.delete(oldL);" +
|
||||
"const oldP = (await eda.pcb_PrimitivePolyline.getAll()||[])"
|
||||
".filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" +
|
||||
"if (oldP.length) await eda.pcb_PrimitivePolyline.delete(oldP);" +
|
||||
"const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) +
|
||||
"const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % mil(OUTLINE_W) +
|
||||
"await eda.pcb_Document.save();" +
|
||||
"return {delLine: oldL.length, delPoly: oldP.length, made: !!r,"
|
||||
" now: (await eda.pcb_PrimitivePolyline.getAll()||[])"
|
||||
".filter(p=>p.getState_Layer()===11).length};")
|
||||
r = call(js, timeout=300)
|
||||
print(u'板框折线: 建 %s(删旧导线 %d / 旧折线 %d);层 11 上现有折线 %d 条'
|
||||
% (u'成功' if r['made'] else u'失败', r['delLine'], r['delPoly'], r['now']))
|
||||
if not r['made'] or r['now'] != 1:
|
||||
raise SystemExit(u'!! 板框没画成')
|
||||
return r
|
||||
|
||||
|
||||
PUSH_SLOTS_JS = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,2500));
|
||||
const S = %s;
|
||||
let del = 0;
|
||||
for (const f of await eda.pcb_PrimitiveFill.getAll())
|
||||
if (f.getState_Layer() === 12) { await eda.pcb_PrimitiveFill.delete(f.getState_PrimitiveId()); del++; }
|
||||
let made = 0, fail = [];
|
||||
for (const [name, src] of S) {
|
||||
const poly = await eda.pcb_MathPolygon.createPolygon(src);
|
||||
const r = await eda.pcb_PrimitiveFill.create(12, poly);
|
||||
if (r) made++; else fail.push(name);
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
const now = (await eda.pcb_PrimitiveFill.getAll()).filter(f => f.getState_Layer() === 12).length;
|
||||
return {del, made, fail, now};
|
||||
"""
|
||||
|
||||
|
||||
def slot_src(x0, y0, x1, y1):
|
||||
u"""圆角矩形 -> pcb_MathPolygon 的点列源(mil,显式闭合)。照抄 R1 pcb2_place.slot_poly。"""
|
||||
pts = rrect_pts(x0, y0, x1, y1, SLOT_R)
|
||||
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 push_slots():
|
||||
S = [[k, slot_src(*w)] for k, w in sorted(flex_slots().items())]
|
||||
r = call(PUSH_SLOTS_JS % (PCB, json.dumps(S)), timeout=300)
|
||||
print(u'排线槽(layer 12 挖槽区域): 建 %d / 失败 %s(删旧 %d);板上现有 %d 个'
|
||||
% (r['made'], r['fail'] or u'无', r['del'], r['now']))
|
||||
for k, w in sorted(flex_slots().items()):
|
||||
print(u' %-5s x %.2f~%.2f y %.2f~%.2f (%.1f × %.1f,排线 %.0f 宽)'
|
||||
% (k, w[0], w[2], w[1], w[3], w[2] - w[0], w[3] - w[1], FLEX_W[k]))
|
||||
if r['now'] != len(S):
|
||||
raise SystemExit(u'!! 槽没建全')
|
||||
return r
|
||||
|
||||
|
||||
def verify():
|
||||
u"""读回板框折线,逐点和 `outline_polygon()` 对账 + 校验闭合与外接矩形。"""
|
||||
r = call("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2000));"
|
||||
"const ps = (await eda.pcb_PrimitivePolyline.getAll()||[])"
|
||||
".filter(p=>p.getState_Layer()===11);"
|
||||
"const line11 = (await eda.pcb_PrimitiveLine.getAll()||[])"
|
||||
".filter(l=>l.layer===11).length;"
|
||||
"const fills = (await eda.pcb_PrimitiveFill.getAll()).filter(f=>f.getState_Layer()===12).length;"
|
||||
"return {n: ps.length, poly: ps.length ? ps[0].getState_Polygon() : null,"
|
||||
" line11, fills};" % PCB, timeout=180)
|
||||
bad = []
|
||||
if r['n'] != 1:
|
||||
bad.append(u'层 11 上有 %d 条折线(应为 1 条)' % r['n'])
|
||||
# ⚠ 层 11 上**允许**留 16 条导线:那是 J1 金手指封装内部的缺口线(fp_edge 画的),
|
||||
# 删不掉也不必删 —— 板框已经由折线自己闭合,它们只是重复的图形。
|
||||
if r['line11'] not in (0, 16):
|
||||
bad.append(u'层 11 上还有 %d 条导线(只该剩 J1 封装那 16 条或 0 条)' % r['line11'])
|
||||
if r['fills'] != len(flex_slots()):
|
||||
bad.append(u'layer 12 挖槽区域 %d 个 != %d' % (r['fills'], len(flex_slots())))
|
||||
pts = []
|
||||
src = r['poly']
|
||||
if isinstance(src, dict):
|
||||
src = src.get('pointArr') or src.get('points') or src.get('polygon') or src
|
||||
if isinstance(src, list):
|
||||
i, cur = 0, None
|
||||
while i < len(src):
|
||||
if src[i] == 'L' or src[i] == 'l':
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(src[i], (int, float)) and i + 1 < len(src) and isinstance(src[i + 1], (int, float)):
|
||||
pts.append((src[i] / MIL, src[i + 1] / MIL))
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
want = outline_polygon()
|
||||
if pts:
|
||||
xs = [p[0] for p in pts]
|
||||
ys = [p[1] for p in pts]
|
||||
if abs(min(xs)) > 0.01 or abs(max(xs) - BW) > 0.01 or abs(min(ys)) > 0.01 or abs(max(ys) - BH) > 0.01:
|
||||
bad.append(u'板框外接矩形 %.2f~%.2f x %.2f~%.2f != 0~%.1f x 0~%.1f'
|
||||
% (min(xs), max(xs), min(ys), max(ys), BW, BH))
|
||||
if abs(pts[0][0] - pts[-1][0]) > 0.01 or abs(pts[0][1] - pts[-1][1]) > 0.01:
|
||||
bad.append(u'折线首尾不重合:%s vs %s' % (pts[0], pts[-1]))
|
||||
if abs(len(pts) - len(want)) > 1:
|
||||
bad.append(u'点数 %d != %d' % (len(pts), len(want)))
|
||||
print(u'读回板框折线 1 条 / %d 点,外接 %.2f~%.2f x %.2f~%.2f;'
|
||||
u'层 11 遗留导线 %d 条;挖槽区域 %d 个'
|
||||
% (len(pts), min(xs), max(xs), min(ys), max(ys), r['line11'], r['fills']))
|
||||
else:
|
||||
bad.append(u'读不回折线点列(原始值 %s)' % str(r['poly'])[:120])
|
||||
if bad:
|
||||
print(u'[X] 校验不过:')
|
||||
for b in bad:
|
||||
print(u' ' + b)
|
||||
else:
|
||||
print(u'[OK] 板框是一条闭合折线(缺口在里面),外接矩形对上,挖槽区域齐')
|
||||
return bad
|
||||
|
||||
|
||||
PUSH_HOLES_JS = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,2500));
|
||||
const H = %s, D = %s;
|
||||
for (const p of await eda.pcb_PrimitivePad.getAll()) {
|
||||
const num = p.getState_PadNumber();
|
||||
if (typeof num === 'string' && /^H[1-7]$/.test(num))
|
||||
await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
|
||||
}
|
||||
let k = 0;
|
||||
for (const [name, x, y] of H) {
|
||||
// layer 12 = 多层;metallization=false ⇒ NPTH。参数位序照抄 R1 pcb2_place.py
|
||||
await eda.pcb_PrimitivePad.create(12, name, x, y, 0, ['ELLIPSE', D, D], '',
|
||||
['ROUND', D], 0, 0, 0, false);
|
||||
k++;
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
return {made: k, now: (await eda.pcb_PrimitivePad.getAll())
|
||||
.filter(p => /^H[1-7]$/.test(String(p.padNumber))).length};
|
||||
"""
|
||||
|
||||
|
||||
def push_holes():
|
||||
hs = phone_holes()
|
||||
arr = [[k, round(v[0] * MIL, 4), round(v[1] * MIL, 4)] for k, v in sorted(hs.items())]
|
||||
r = call(PUSH_HOLES_JS % (PCB, json.dumps(arr), round(HOLE_D * MIL, 4)), timeout=300)
|
||||
print(u'主板定位孔: 下发 %d 个 / 读回 %d 个,NPTH Φ%.1f(⚠ 必须配 M1.6 尼龙立柱)'
|
||||
% (r['made'], r['now'], HOLE_D))
|
||||
return r
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
selfcheck()
|
||||
if '--holes' in sys.argv:
|
||||
push_holes()
|
||||
for k, v in sorted(phone_holes().items()):
|
||||
print(u' %-3s (%6.2f, %6.2f)' % (k, v[0], v[1]))
|
||||
sys.exit(0)
|
||||
bl, br = notch_break()
|
||||
print(u'刀片 %.0f x %.0f,四角 R%.1f;金手指中心 x=%.2f' % (BW, BH, CORNER_R, J1_X))
|
||||
print(u'底边断点: %.2f ~ %.2f(缺口口部)' % (bl, br))
|
||||
print(u'金手指跨度: x %.2f ~ %.2f,指顶 y=%.2f,缺口顶 y=%.2f'
|
||||
% (J1_X + fp_edge.PIN_X['A1'] - fp_edge.FW / 2,
|
||||
J1_X + fp_edge.PIN_X['A32'] + fp_edge.FW / 2,
|
||||
fp_edge.EDGE_CLR + fp_edge.LONG_L, fp_edge.NOTCH_D))
|
||||
for k, z in ZONES.items():
|
||||
print(u' %-8s x %6.2f~%6.2f y %6.2f~%6.2f' % (k, z[0], z[2], z[1], z[3]))
|
||||
for k, w in sorted(flex_slots().items()):
|
||||
print(u' 槽 %-5s x %6.2f~%6.2f y %6.2f~%6.2f' % (k, w[0], w[2], w[1], w[3]))
|
||||
if '--outline' in sys.argv:
|
||||
push_outline()
|
||||
push_slots()
|
||||
verify()
|
||||
elif '--slots' in sys.argv:
|
||||
push_slots()
|
||||
verify()
|
||||
elif '--verify' in sys.argv:
|
||||
verify()
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,404 @@
|
||||
# -*- 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 # 外层铜面的下沿(金手指区一点外层铜都不许有)
|
||||
NOTCH_KEEP = 0.5 # 缺口周围再让 0.5
|
||||
|
||||
# L3 两个岛(x0, y0, x1, y1)—— y 段直接对着 pcb_place 的功能带切,见 --bands
|
||||
# ⛔ 2026-09-11 刀片 90×68:三个岛跟着新板框 / 手机原点 / 功率区西移 8 改(原值见注释)。
|
||||
VO_ISLAND = (17.5, 11.5, B.BW - 0.5, 28.8) # eFuse/检流带 + 服务带:VO_P 4 A 的主通道(原 20~97.5);
|
||||
# 西沿 17.5 = 泄放支路 DIS 区挪到电池槽正下方(x 17.9 起)之后,R19 的 VO_P 脚要就地扎孔进岛
|
||||
# 2026-09-09 M6:J5 / 阻尼组挪到电池槽北边之后岛要跟过去 —— 往北伸一个叶(L 形单环,
|
||||
# 不是第二块铜)。东沿让开尾插槽(槽西沿 − 0.58);北沿跟手机走(原 46 − 6.66)。
|
||||
VO_LOBE = (17.5, 28.8, round(B.flex_slots()['TAIL'][0] - 0.58, 3), round(46.0 + B.PHONE_ORG[1] - 29.0, 3))
|
||||
V12_ISLAND = (B.BW - 14.0, 55.0, B.BW - 0.5, B.BH - 0.5) # 12V 入口(F1 的 +12V 脚)及以上(原 84~97.5 × 55~74)
|
||||
|
||||
|
||||
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()]
|
||||
xs = sorted(set([x0, 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
|
||||
ys = [y_lo]
|
||||
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 vo_poly():
|
||||
u"""VO_P 岛:主体矩形 + 北叶,拼成一个 L 形**单环**(逆时针,显式闭合)。"""
|
||||
a, b = VO_ISLAND, VO_LOBE
|
||||
assert abs(a[3] - b[1]) < 1e-9 and a[0] <= b[0] and b[2] <= a[2], u'北叶必须骑在主体上沿'
|
||||
pts = [(a[0], a[1]), (a[2], a[1]), (a[2], a[3]), (b[2], a[3]),
|
||||
(b[2], b[3]), (b[0], b[3]), (b[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(rrect(*V12_ISLAND))], 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 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))
|
||||
# 内层铜面必须绕开缺口
|
||||
nx0, _a, nx1, ny1 = notch_cut()
|
||||
if nx1 - nx0 < 2.9:
|
||||
bad.append(u'缺口挖得比口部还窄')
|
||||
# 两个岛不许互相重叠(同层,优先级只有 1/2 之差,重叠就说不清谁裁谁)
|
||||
b = V12_ISLAND
|
||||
for a in (VO_ISLAND, VO_LOBE):
|
||||
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 上重叠')
|
||||
# 北叶不许伸进尾插槽/车道
|
||||
for (x0, y0, x1, y1) in B.mill_cuts():
|
||||
if x0 > VO_LOBE[0] + 1.0 and VO_LOBE[0] < x1 and x0 < VO_LOBE[2] and y0 < VO_LOBE[3] and VO_LOBE[1] < y1:
|
||||
bad.append(u'VO 北叶压到槽 (%.1f~%.1f)' % (x0, x1))
|
||||
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()
|
||||
文件差异内容过多而无法显示
加载差异
在新工单中引用
屏蔽一个用户