R2 底板:按用户布局重布风扇窗口 + L1 走廊重开 + 位号丝印重排
布线(窗口整体重布,已下发): - 拆窗口内 43 段(用户手布的那批,用户选「拆掉重来」),补 139 段 / 7 孔, GND 回流孔 10 个;fan_check 对板上真实快照逐项净距达标(含「新孔 vs 内层线」)。 - ⛔ 根因:pcb_base.PD_L1 里的 L1 走廊是按**旧摆位**开的(注释坐标全是旧的), 用户重摆后与件对不上 ⇒ 四条长途网必然布不通。重开六条走廊后未完成 4 → 2。 走廊必须盖住焊盘正上方,只盖沿途不行。断面:几何 11.80 / 铺铜后实测 9.82(要 8.0)。 - 与拆线前逐网对比:没引入任何新的连通性问题(GND 未接 58→48、+3V3 2→0、 两路 TACH 2/1→0、+12V 一字不差)。 定版:板上铜以**用户自己布好的那版**为准,不再改动。 丝印(silk_base.py,已下发并读回核对 150/150): - 压焊盘 128→4、压过孔 48→8、越框 8→3、互相重叠 34 对→0;字号不变。 - 三条坑:rotation 是度且不归一化(900=180°);别拿「本体≥5mm」当保留白名单 (会豁免掉 R9A/D3/U13 这些该重排的件);「离本体」必须设硬上限 + 降级策略。 新工具:silk_base.py(丝印重排)、l1_section.py(L1 12V 主块逐 y 断面核算)、 fan_rip.py(按窗口拆线,拆前后比对全板线/孔总数防连带删除)。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
+564
-408
文件差异内容过多而无法显示
加载差异
+321
-165
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"C1": [
|
||||
10.25,
|
||||
18.75,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C10": [
|
||||
12.5,
|
||||
71.75,
|
||||
@@ -17,17 +11,95 @@
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C12": [
|
||||
26.9,
|
||||
15.5,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C13": [
|
||||
24.85,
|
||||
23.86,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"C14": [
|
||||
2.95,
|
||||
28.45,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C15": [
|
||||
8.25,
|
||||
28.45,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C16": [
|
||||
13.55,
|
||||
28.45,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C17": [
|
||||
18.85,
|
||||
28.45,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C18": [
|
||||
19.2,
|
||||
24.85,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C19": [
|
||||
19.75,
|
||||
23.6,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C2": [
|
||||
5.5,
|
||||
35.0,
|
||||
180,
|
||||
1
|
||||
],
|
||||
"C20": [
|
||||
19.75,
|
||||
17.8,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C21": [
|
||||
19.2,
|
||||
15.1,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"C22": [
|
||||
23.9,
|
||||
15.0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C23": [
|
||||
23.9,
|
||||
26.3,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C24": [
|
||||
23.9,
|
||||
28.3,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C3": [
|
||||
19.5,
|
||||
35.0,
|
||||
0,
|
||||
2
|
||||
1
|
||||
],
|
||||
"C4": [
|
||||
17.5,
|
||||
@@ -36,9 +108,9 @@
|
||||
2
|
||||
],
|
||||
"C5": [
|
||||
9.0,
|
||||
28.5,
|
||||
90,
|
||||
1.5,
|
||||
21.75,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C6": [
|
||||
@@ -156,9 +228,9 @@
|
||||
2
|
||||
],
|
||||
"C738": [
|
||||
8.5,
|
||||
83.5,
|
||||
180,
|
||||
7.9248,
|
||||
78.1729,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C739": [
|
||||
@@ -168,75 +240,75 @@
|
||||
2
|
||||
],
|
||||
"C740": [
|
||||
38.0,
|
||||
43.75,
|
||||
180,
|
||||
42.926,
|
||||
41.578,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"C741": [
|
||||
36.25,
|
||||
40.5,
|
||||
41.148,
|
||||
37.338,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C742": [
|
||||
58.25,
|
||||
38.25,
|
||||
270,
|
||||
57.7,
|
||||
38.2,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C743": [
|
||||
42.0,
|
||||
38.5,
|
||||
270,
|
||||
45.3,
|
||||
41.64,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"C744": [
|
||||
43.5,
|
||||
34.5,
|
||||
270,
|
||||
43.18,
|
||||
36.449,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"C745": [
|
||||
42.0,
|
||||
36.5,
|
||||
270,
|
||||
43.18,
|
||||
37.465,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"C746": [
|
||||
40.25,
|
||||
41.0,
|
||||
270,
|
||||
45.3,
|
||||
44.06,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"C747": [
|
||||
58.25,
|
||||
43.0,
|
||||
270,
|
||||
52.95,
|
||||
45.25,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"C748": [
|
||||
58.25,
|
||||
40.25,
|
||||
90,
|
||||
57.7,
|
||||
39.41,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C749": [
|
||||
44.75,
|
||||
34.25,
|
||||
90,
|
||||
48.75,
|
||||
32.42,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"C750": [
|
||||
40.25,
|
||||
43.0,
|
||||
270,
|
||||
49.2,
|
||||
45.91,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"C751": [
|
||||
43.75,
|
||||
37.5,
|
||||
270,
|
||||
65.5,
|
||||
36.5,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"C752": [
|
||||
@@ -270,33 +342,33 @@
|
||||
2
|
||||
],
|
||||
"D2": [
|
||||
20.75,
|
||||
94.25,
|
||||
12.192,
|
||||
77.724,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"DZ1": [
|
||||
16.25,
|
||||
21.75,
|
||||
90,
|
||||
"D3": [
|
||||
19.25,
|
||||
4.0,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"DZ3": [
|
||||
25.75,
|
||||
90.25,
|
||||
1.397,
|
||||
82.296,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"F1": [
|
||||
12.5,
|
||||
17.75,
|
||||
11.8,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"J1": [
|
||||
12.5,
|
||||
10.63,
|
||||
90,
|
||||
5.25,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"J23": [
|
||||
@@ -306,22 +378,22 @@
|
||||
2
|
||||
],
|
||||
"J26": [
|
||||
13.5,
|
||||
94.5,
|
||||
12.446,
|
||||
94.289,
|
||||
0,
|
||||
2
|
||||
1
|
||||
],
|
||||
"J27": [
|
||||
13.5,
|
||||
88.0,
|
||||
7.112,
|
||||
84.443,
|
||||
0,
|
||||
2
|
||||
1
|
||||
],
|
||||
"J33": [
|
||||
80.0,
|
||||
95.5,
|
||||
0,
|
||||
2
|
||||
1
|
||||
],
|
||||
"J34": [
|
||||
49.0,
|
||||
@@ -365,15 +437,15 @@
|
||||
0,
|
||||
2
|
||||
],
|
||||
"Q1": [
|
||||
12.5,
|
||||
24.5,
|
||||
"L4": [
|
||||
10.4,
|
||||
20.7,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"Q10": [
|
||||
26.0,
|
||||
81.75,
|
||||
17.018,
|
||||
81.153,
|
||||
180,
|
||||
2
|
||||
],
|
||||
@@ -384,33 +456,51 @@
|
||||
2
|
||||
],
|
||||
"Q6": [
|
||||
21.25,
|
||||
88.25,
|
||||
0,
|
||||
22.1032,
|
||||
86.3586,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"Q7": [
|
||||
24.75,
|
||||
95.75,
|
||||
270,
|
||||
4.318,
|
||||
79.629,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"Q8": [
|
||||
14.0,
|
||||
83.0,
|
||||
0,
|
||||
4.2148,
|
||||
76.2031,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"Q9": [
|
||||
26.0,
|
||||
77.25,
|
||||
90,
|
||||
13.208,
|
||||
89.916,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R1": [
|
||||
8.75,
|
||||
21.75,
|
||||
270,
|
||||
"R10": [
|
||||
24.85,
|
||||
22.64,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R11": [
|
||||
24.85,
|
||||
21.42,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R12": [
|
||||
24.2,
|
||||
16.58,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R13": [
|
||||
24.2,
|
||||
17.8,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R2": [
|
||||
@@ -426,20 +516,20 @@
|
||||
2
|
||||
],
|
||||
"R4": [
|
||||
1.25,
|
||||
42.5,
|
||||
180,
|
||||
3.937,
|
||||
42.418,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R5": [
|
||||
4.0,
|
||||
42.25,
|
||||
90,
|
||||
4.318,
|
||||
45.085,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R6": [
|
||||
6.75,
|
||||
43.25,
|
||||
3.937,
|
||||
41.402,
|
||||
0,
|
||||
2
|
||||
],
|
||||
@@ -545,6 +635,12 @@
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R7": [
|
||||
19.0,
|
||||
11.25,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R706": [
|
||||
16.75,
|
||||
71.25,
|
||||
@@ -558,39 +654,39 @@
|
||||
2
|
||||
],
|
||||
"R708": [
|
||||
40.25,
|
||||
38.0,
|
||||
270,
|
||||
45.3,
|
||||
42.85,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R709": [
|
||||
35.75,
|
||||
37.25,
|
||||
270,
|
||||
51.181,
|
||||
29.845,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R710": [
|
||||
41.75,
|
||||
34.0,
|
||||
50.165,
|
||||
29.845,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R711": [
|
||||
39.5,
|
||||
34.25,
|
||||
90,
|
||||
54.229,
|
||||
29.845,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R712": [
|
||||
38.0,
|
||||
37.25,
|
||||
90,
|
||||
53.213,
|
||||
29.845,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R716": [
|
||||
37.25,
|
||||
34.25,
|
||||
90,
|
||||
52.197,
|
||||
29.845,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R717": [
|
||||
@@ -606,15 +702,15 @@
|
||||
2
|
||||
],
|
||||
"R719": [
|
||||
64.75,
|
||||
34.25,
|
||||
90,
|
||||
55.245,
|
||||
29.845,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R720": [
|
||||
38.5,
|
||||
41.0,
|
||||
90,
|
||||
49.071,
|
||||
29.847,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R721": [
|
||||
@@ -629,88 +725,136 @@
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R723": [
|
||||
42.926,
|
||||
39.751,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"R724": [
|
||||
43.2,
|
||||
35.35,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R725": [
|
||||
43.2,
|
||||
34.15,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R726": [
|
||||
45.3,
|
||||
33.6,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R727": [
|
||||
57.7,
|
||||
34.18,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R728": [
|
||||
57.7,
|
||||
35.4,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R729": [
|
||||
57.7,
|
||||
42.45,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R733": [
|
||||
22.25,
|
||||
85.0,
|
||||
0,
|
||||
24.5752,
|
||||
84.9776,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R734": [
|
||||
22.25,
|
||||
82.75,
|
||||
22.5712,
|
||||
83.6586,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R746": [
|
||||
19.75,
|
||||
78.5,
|
||||
4.064,
|
||||
82.042,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R747": [
|
||||
25.75,
|
||||
86.0,
|
||||
180,
|
||||
5.9356,
|
||||
78.0532,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R748": [
|
||||
17.0,
|
||||
77.0,
|
||||
90,
|
||||
5.08,
|
||||
73.914,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R749": [
|
||||
17.5,
|
||||
83.5,
|
||||
90,
|
||||
3.175,
|
||||
73.914,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"R750": [
|
||||
7.0,
|
||||
89.25,
|
||||
270,
|
||||
9.906,
|
||||
91.313,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R751": [
|
||||
4.75,
|
||||
89.25,
|
||||
10.541,
|
||||
89.408,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R752": [
|
||||
2.5,
|
||||
88.75,
|
||||
90,
|
||||
12.7,
|
||||
87.884,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R753": [
|
||||
22.75,
|
||||
76.25,
|
||||
270,
|
||||
15.621,
|
||||
89.535,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R754": [
|
||||
17.75,
|
||||
80.75,
|
||||
0,
|
||||
12.573,
|
||||
82.55,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R755": [
|
||||
14.75,
|
||||
79.75,
|
||||
0,
|
||||
14.5288,
|
||||
80.645,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"R756": [
|
||||
14.25,
|
||||
77.5,
|
||||
0,
|
||||
16.51,
|
||||
78.867,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R757": [
|
||||
22.5,
|
||||
80.0,
|
||||
90,
|
||||
18.542,
|
||||
83.566,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R8": [
|
||||
24.85,
|
||||
20.2,
|
||||
180,
|
||||
2
|
||||
],
|
||||
"R9A": [
|
||||
@@ -750,9 +894,9 @@
|
||||
2
|
||||
],
|
||||
"RT602": [
|
||||
19.5,
|
||||
8.0,
|
||||
0,
|
||||
26.9,
|
||||
19.3,
|
||||
90,
|
||||
2
|
||||
],
|
||||
"Rs6": [
|
||||
@@ -780,9 +924,9 @@
|
||||
2
|
||||
],
|
||||
"U29": [
|
||||
2.25,
|
||||
45.75,
|
||||
180,
|
||||
2.032,
|
||||
45.085,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"U30": [
|
||||
@@ -809,6 +953,18 @@
|
||||
0,
|
||||
2
|
||||
],
|
||||
"U36": [
|
||||
23.7,
|
||||
10.4,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"U37": [
|
||||
21.0,
|
||||
20.7,
|
||||
270,
|
||||
2
|
||||
],
|
||||
"U6": [
|
||||
56.0,
|
||||
59.16,
|
||||
@@ -822,8 +978,8 @@
|
||||
2
|
||||
],
|
||||
"Y1": [
|
||||
42.8,
|
||||
41.68,
|
||||
39.878,
|
||||
40.64,
|
||||
270,
|
||||
2
|
||||
]
|
||||
|
||||
文件差异因一行或多行过长而隐藏
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"window": [
|
||||
0.5,
|
||||
73.5,
|
||||
26.84,
|
||||
97.09
|
||||
],
|
||||
"nets": [
|
||||
"$6N45",
|
||||
"$6N48",
|
||||
"$6N49",
|
||||
"$6N55",
|
||||
"$6N64",
|
||||
"$6N74",
|
||||
"+3V3",
|
||||
"FAN_12V_SW",
|
||||
"FAN_EN",
|
||||
"FAN_IN_TACH_RAW",
|
||||
"FAN_OUT_TACH_RAW",
|
||||
"FAN_PWM",
|
||||
"FAN_PWM_L",
|
||||
"FAN_TACH_IN",
|
||||
"FAN_TACH_OUT"
|
||||
],
|
||||
"out_pen": 12,
|
||||
"priority": [
|
||||
"$6N49",
|
||||
"$6N64",
|
||||
"$6N45",
|
||||
"$6N74",
|
||||
"$6N48",
|
||||
"$6N55",
|
||||
"FAN_IN_TACH_RAW",
|
||||
"FAN_OUT_TACH_RAW",
|
||||
"FAN_PWM_L",
|
||||
"FAN_12V_SW",
|
||||
"FAN_TACH_IN",
|
||||
"FAN_TACH_OUT",
|
||||
"FAN_PWM",
|
||||
"FAN_EN",
|
||||
"+3V3"
|
||||
],
|
||||
"_note": "插槽方向在 repair_window._pen 里是 block_rect 封死,不是过路费;slot_pen 已证明无效(A* 宁可付 200 也抄近路),故删除 | pd_l1_extra = 按用户 2026-09-12 新摆位重开的 L1 走廊(旧的四条是按旧摆位开的,与件对不上 ⇒ 长途网必然布不通);逐 y 断面 11.80 ≥ 8.0,见 l1_section.py",
|
||||
"pd_l1_extra": [
|
||||
[
|
||||
"FAN_EN",
|
||||
[
|
||||
12.0,
|
||||
76.1,
|
||||
27.5,
|
||||
77.1
|
||||
]
|
||||
],
|
||||
[
|
||||
"FAN_TACH_IN",
|
||||
[
|
||||
16.0,
|
||||
75.15,
|
||||
27.5,
|
||||
76.05
|
||||
]
|
||||
],
|
||||
[
|
||||
"FAN_TACH_IN",
|
||||
[
|
||||
13.7,
|
||||
76.05,
|
||||
16.2,
|
||||
90.4
|
||||
]
|
||||
],
|
||||
[
|
||||
"FAN_TACH_OUT",
|
||||
[
|
||||
17.4,
|
||||
80.7,
|
||||
27.5,
|
||||
81.7
|
||||
]
|
||||
],
|
||||
[
|
||||
"FAN_TACH_OUT",
|
||||
[
|
||||
17.4,
|
||||
81.7,
|
||||
18.8,
|
||||
84.1
|
||||
]
|
||||
],
|
||||
[
|
||||
"FAN_PWM_L",
|
||||
[
|
||||
10.3,
|
||||
83.9,
|
||||
22.5,
|
||||
87.3
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
文件差异因一行或多行过长而隐藏
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""按坐标**拆掉窗口内待布网的旧铜**(下发新布线之前的第一步)。
|
||||
|
||||
/c/Python314/python .claude/r2/fan_rip.py # 只列清单,不动板
|
||||
/c/Python314/python .claude/r2/fan_rip.py --push # 真删
|
||||
|
||||
⛔ 2026-09-12 实测:**删外层段会连带清掉过孔和它在内层的延续** —— 点名删 6 段,读回发现线少 12 段、
|
||||
孔少 8 个,多掉的全在 L15/L16。⇒ 拆前后都要比对**全板**线 / 孔总数,不能只数自己点名的那几段。
|
||||
这里每次都先读回全板、删完再读回,逐项打印差值。
|
||||
|
||||
判据与 `repair_window` 同口径:`_base_window.json` 的窗口 + 网名,**两个端点都在窗口内**才算窗口内的铜
|
||||
(跨窗边界的段是主干接口,必须留着)。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
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
|
||||
|
||||
MIL = 0.0254
|
||||
CFG = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
|
||||
WIN = CFG['window']
|
||||
NETS = set(n.replace('@', '$') for n in CFG['nets'])
|
||||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2500));" % B.PCB)
|
||||
READ = OPEN + """
|
||||
const L=(await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))
|
||||
.map(l=>[l.primitiveId,l.net,l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth]);
|
||||
const V=(await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.net,v.x,v.y]);
|
||||
return {L, V};
|
||||
"""
|
||||
|
||||
|
||||
def inw(x, y):
|
||||
return WIN[0] <= x <= WIN[2] and WIN[1] <= y <= WIN[3]
|
||||
|
||||
|
||||
def read():
|
||||
r = run(READ, timeout=300)
|
||||
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:400]
|
||||
return r['result']
|
||||
|
||||
|
||||
def main():
|
||||
d = read()
|
||||
L, V = d['L'], d['V']
|
||||
outer = [l for l in L if l[2] in (1, 2)]
|
||||
inner = [l for l in L if l[2] in (15, 16)]
|
||||
print(u'板上:外层 %d 段 / 内层 %d 段 / 过孔 %d' % (len(outer), len(inner), len(V)))
|
||||
kill = []
|
||||
for pid, net, lay, x1, y1, x2, y2, w in outer:
|
||||
if net not in NETS:
|
||||
continue
|
||||
if inw(x1 * MIL, y1 * MIL) and inw(x2 * MIL, y2 * MIL):
|
||||
kill.append((pid, net, lay, x1 * MIL, y1 * MIL, x2 * MIL, y2 * MIL))
|
||||
killv = [(v[0], v[1]) for v in V if v[1] in NETS and inw(v[2] * MIL, v[3] * MIL)]
|
||||
print(u'要拆:%d 段 / %d 孔' % (len(kill), len(killv)))
|
||||
by = {}
|
||||
for k in kill:
|
||||
by[k[1]] = by.get(k[1], 0) + 1
|
||||
for n in sorted(by):
|
||||
print(u' %-18s %d 段' % (n, by[n]))
|
||||
io.open(os.path.join(HERE, '_base_fan_ripped.json'), 'w', encoding='utf-8').write(
|
||||
json.dumps({'segs': [[k[2], k[3], k[4], k[5], k[6], 0, k[1]] for k in kill],
|
||||
'vias': []}, ensure_ascii=False))
|
||||
if '--push' not in sys.argv:
|
||||
print(u'(只列清单;加 --push 才真删)')
|
||||
return
|
||||
ids = [k[0] for k in kill] + [v[0] for v in killv]
|
||||
r = run(OPEN + """
|
||||
const ids = %s;
|
||||
let n = 0;
|
||||
for (const id of ids) { try { await eda.pcb_PrimitiveLine.delete(id); n++; } catch (e) {
|
||||
try { await eda.pcb_PrimitiveVia.delete(id); n++; } catch (e2) {} } }
|
||||
await eda.pcb_Document.save();
|
||||
return n;
|
||||
""" % json.dumps(ids), timeout=600)
|
||||
print(u'删除返回:%s' % json.dumps(r.get('result'), ensure_ascii=False))
|
||||
d2 = read()
|
||||
o2 = [l for l in d2['L'] if l[2] in (1, 2)]
|
||||
i2 = [l for l in d2['L'] if l[2] in (15, 16)]
|
||||
print(u'拆后:外层 %d 段(−%d,点名 %d)/ 内层 %d 段(−%d)/ 过孔 %d(−%d)'
|
||||
% (len(o2), len(outer) - len(o2), len(kill), len(i2), len(inner) - len(i2),
|
||||
len(d2['V']), len(V) - len(d2['V'])))
|
||||
if len(outer) - len(o2) != len(kill) or len(inner) != len(i2):
|
||||
print(u'⛔ 连带删除!实际掉的和点名的对不上,检查内层 / 过孔')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""L1 `+12V` 主块的**逐 y 水平断面**核算(§6.4 要 ≥ 8.0 mm)。
|
||||
|
||||
/c/Python314/python .claude/r2/l1_section.py # 核 pcb_base.PD_L1 现状
|
||||
/c/Python314/python .claude/r2/l1_section.py --cfg # 核 _base_window.json 里的 pd_l1_extra
|
||||
|
||||
主块 x 0.5~27.8(宽 27.30),电流南北流 ⇒ 断面是水平线,线上被走廊占掉的 x 跨度要扣掉。
|
||||
横带(沿 x 长)很贵、竖带(沿 y 长)几乎免费 —— 旧注释「一根 0.15 的线占约 0.5」说的就是竖线。
|
||||
槽指头在 x ≥ 27.5,走廊 x 一律 ≤ 27.5 ⇒ 天然不碰任何一槽的供电路径。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
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)
|
||||
import pcb_base as B # noqa: E402
|
||||
|
||||
MIN_SECTION = 8.0
|
||||
MAIN = B.l1_12v_rects()[0]
|
||||
WIDTH = MAIN[2] - MAIN[0]
|
||||
|
||||
|
||||
def check(corridors, step=0.05):
|
||||
worst = (1e9, None)
|
||||
y = MAIN[1]
|
||||
while y <= MAIN[3]:
|
||||
iv = []
|
||||
for _n, r in corridors:
|
||||
if r[1] - 1e-9 <= y <= r[3] + 1e-9:
|
||||
iv.append((max(r[0], MAIN[0]), min(r[2], MAIN[2])))
|
||||
iv.sort()
|
||||
used, spans = 0.0, []
|
||||
for a, b in iv:
|
||||
if b <= a:
|
||||
continue
|
||||
if spans and a <= spans[-1][1]:
|
||||
spans[-1] = (spans[-1][0], max(spans[-1][1], b))
|
||||
else:
|
||||
spans.append((a, b))
|
||||
for a, b in spans:
|
||||
used += b - a
|
||||
left = WIDTH - used
|
||||
if left < worst[0]:
|
||||
worst = (left, y)
|
||||
y += step
|
||||
return worst
|
||||
|
||||
|
||||
def main():
|
||||
cor = [(n, r) for n, r in B.PD_L1 if n.startswith('FAN')]
|
||||
if '--cfg' in sys.argv:
|
||||
cfg = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
|
||||
cor = [(n, tuple(r)) for n, r in (cfg.get('pd_l1_extra') or [])]
|
||||
print(u'主块 x %.2f~%.2f(宽 %.2f);走廊 %d 条' % (MAIN[0], MAIN[2], WIDTH, len(cor)))
|
||||
for n, r in cor:
|
||||
kind = u'横带' if (r[2] - r[0]) >= (r[3] - r[1]) else u'竖带'
|
||||
print(u' %-14s (%6.2f, %6.2f, %6.2f, %6.2f) %5.2f×%5.2f %s'
|
||||
% (n, r[0], r[1], r[2], r[3], r[2] - r[0], r[3] - r[1], kind))
|
||||
left, y = check(cor)
|
||||
ok = left >= MIN_SECTION
|
||||
print(u'%s 逐 y 最窄断面 %.2f mm(y=%.2f),判据 ≥ %.1f' %
|
||||
(u'[OK]' if ok else u'[X]', left, y, MIN_SECTION))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+614
-556
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,303 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""**窗口内整体重布**:把一块矩形区域里的几个网连同周边 `+3V3` / `GND` 局部一起重布。
|
||||
|
||||
bash .claude/r2/remote_repair_window.sh # 入口(本机)
|
||||
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/repair_window.py
|
||||
|
||||
## 和 `repair_base.py` 的区别(为什么要单独一份)
|
||||
|
||||
`repair_base` 是**按网**补:给它 `+3V3`,它就在**全板**范围重布这个网 —— 2026-09-12 实测,
|
||||
它把新的 `+3V3` 线从 **J34~J37 插槽针排的焊盘中间穿过去**(`fan_check` 十几处 −0.2,
|
||||
还压 U13.7 −0.449)。插槽 1 mm 间距、手焊,那是致命的。
|
||||
|
||||
这一份改成**按窗口**补,三条约束缺一不可:
|
||||
|
||||
1. **`bypad` 只装窗口内的脚**。窗口外的同网脚一概不参与 ⇒ 布线器不会去动全板的主干。
|
||||
2. **窗口外的已有铜照常留在占用图里**。它们是「本网已有的铜」,A* 从窗口内的脚出发,
|
||||
碰到就算到达 —— 这就是和外面主干的接口,不需要显式指定接口点。
|
||||
3. **窗口外加高过路费**(`OUT_PEN`)。不是封死:万一窗口内绕不出去,它还能出去一点点,
|
||||
但代价高到不会乱跑。封死会让本来能绕的网直接报「没布通」。
|
||||
|
||||
拆线(窗口内目标网的旧铜)由本机 `remote_repair_window.sh` 先做掉,这里只读拆完的板面快照。
|
||||
|
||||
输入:`_base_fan_input.json`(板上现有铜)+ `_base_window.json`(窗口与网名,本机写)。
|
||||
输出:`_base_window_repair.json`(只有新增的段与孔)。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
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)
|
||||
|
||||
if not os.environ.get('DCDC_ROUTE_SERVER'):
|
||||
sys.exit(u'⛔ 布线(A*)只在 10.5.5.13 上跑(CLAUDE.md):bash .claude/r2/remote_repair_window.sh')
|
||||
|
||||
import router as RT # noqa: E402
|
||||
import route_base as RB # noqa: E402
|
||||
|
||||
CFG = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
|
||||
WIN = tuple(CFG['window']) # (x0, y0, x1, y1)
|
||||
NETS = [n.replace('@', '$') for n in CFG['nets']]
|
||||
OUT_PEN = int(CFG.get('out_pen', 40))
|
||||
# 朝插槽那一侧的过路费:必须高到 A* 不会为了抄近路钻进槽区(槽体 x 28~70,手焊 1 mm 针排)
|
||||
SLOT_PEN = int(CFG.get('slot_pen', 200))
|
||||
# 虚拟落点沿主干往窗内退多少(避开压在主干线上导致起步格点为 0)
|
||||
RETREAT = float(CFG.get('retreat', 0.75))
|
||||
# 窗口内「别的网」的焊盘外扩多少封死(收费挡不住,见 _pen 的注释)
|
||||
PAD_KEEP = float(CFG.get('pad_keep', 0.10))
|
||||
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
|
||||
|
||||
# ── L1 走廊(`pcb_base.PD_L1` 的运行时追加)─────────────────────────────────
|
||||
# ⛔ A6 那一带 L1 整片是 `+12V` 主块(x 0.5~27.8)⇒ 过孔上得去但走不动,等于单层,
|
||||
# 而 L4 已被 22 件占满。`PD_L1` 里那几条走廊是**按旧摆位**开的(注释里写的坐标就是旧的),
|
||||
# 用户重摆之后与件对不上 ⇒ 长途网(FAN_EN / 两路 TACH / FAN_PWM_L)必然布不通。
|
||||
# 这里让走廊跟着 `_base_window.json` 走,免得每试一组就改 `pcb_base.py`。
|
||||
# ⚠ 判据(§6.4):逐 y 的水平断面上,Σ 走廊 x 跨度 ≤ 27.30 − 8.00 = 19.30 mm。
|
||||
# 本机 `l1_section.py` 核;走廊 x 一律 ≤ 27.5,槽指头在 x ≥ 27.5,天然不碰。
|
||||
_extra = CFG.get('pd_l1_extra')
|
||||
if _extra is not None:
|
||||
_keep = [(n, r) for n, r in RB.B.PD_L1 if not n.startswith('FAN')]
|
||||
RB.B.PD_L1 = _keep + [(n, tuple(r)) for n, r in _extra]
|
||||
print(u'L1 走廊:保留 %d 条非风扇 + 本轮 %d 条 %s'
|
||||
% (len(_keep), len(_extra), ' '.join(sorted(set(n for n, _ in _extra)))))
|
||||
|
||||
pads = RB.load_pads()
|
||||
|
||||
|
||||
def in_win(x, y, m=0.0):
|
||||
return WIN[0] - m <= x <= WIN[2] + m and WIN[1] - m <= y <= WIN[3] + m
|
||||
|
||||
|
||||
# ── 窗口外加过路费:包住整块板,挖掉窗口 ──────────────────────────────────
|
||||
_orig_pen = RT.pen_pads
|
||||
|
||||
|
||||
def _toll_pads():
|
||||
u"""窗口内**不属待布网**的焊盘 —— 必须一起收过路费,否则新线会贴着它们走。
|
||||
|
||||
2026-09-12 实测:不收的话,`+3V3` 贴 `Q8.3` 只剩 **0.003**、`FAN_OUT_TACH_RAW` 贴
|
||||
`Q10.2` 只剩 **0.027**(判据 0.15),`fan_check` 一跑就拦下来、整版不能下发。
|
||||
`route_base._pen` 本来就是靠 `GND_PEN_PADS` / `LATER_PEN_PADS` 收这笔费的,
|
||||
我覆盖 `RT.pen_pads` 时漏了这一步。
|
||||
|
||||
⛔ 判据是「**不是当前正在布的那个网**」,不是「不在待布网集合里」。第一版写成后者,
|
||||
于是 `Q8.3`(网 `$6N48`,本身也在待布名单里)被漏掉 —— 实测它周围过路费始终是 0,
|
||||
而 `+3V3` 的新线就贴着它走到 **0.003**。同属待布名单的别的网,对当前这个网来说一样是障碍。
|
||||
"""
|
||||
out = []
|
||||
for p in pads:
|
||||
if p[6] not in RT.LAY or str(p[1]).endswith(RB.TH_SUFFIX):
|
||||
continue
|
||||
if in_win(p[2], p[3]):
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def _pen(g, ps, halfw):
|
||||
u"""窗口外收过路费。⛔ **不能各向同性**(2026-09-12 实测)。
|
||||
|
||||
朝插槽那一侧(x > 窗口东界)必须近乎封死:`OUT_PEN` 从 40 降到 12 的那一轮,
|
||||
`$6N64` / `FAN_EN` 立刻跑到 x 28.75~32.50 —— **正是槽体 x 28~70 的插槽区**,
|
||||
183 段里 50 段跑出窗口(之前每轮都是 0)。而降它本是为了让跨窗口的网够得着 U20 那边的主干。
|
||||
⇒ 分开定价:东侧(插槽方向)用 `SLOT_PEN`,其余三面用 `OUT_PEN`。
|
||||
"""
|
||||
_orig_pen(g, list(ps) + _toll_pads(), halfw)
|
||||
# ⛔ 光收过路费**挡不住**(2026-09-12 第二次印证,同「插槽方向」那一条):
|
||||
# 实测 `Q8.3` 周围过路费确实是 10,A* 照样贴着它走到 **0.003**;`Q10.2` 同理 0.027。
|
||||
# `penalize` 只是定价,`block_rect` 才是墙 ⇒ 窗口内**别的网**的焊盘按 `PAD_KEEP` 外扩封死。
|
||||
# 代价实测很小:外扩 0.10 时窗口内可走格点 7297 → 6802(72% → 68%,少 495 个)。
|
||||
# ⛔ 外扩量必须按**当前这一档的线宽**算,不能写死。2026-09-12 实测:按 0.15 细线定的
|
||||
# `PAD_KEEP=0.10` 挡不住 0.40 的 `+3V3`(它半宽就有 0.20)—— 照样贴 `Q8.3` 到 **0.003**。
|
||||
# `route_groups` 是按线宽分档建图的,每档传进来的 `halfw` 正是「半宽 + 净距」,直接用它。
|
||||
# (一刀切取最粗的 0.56 会把细线通道也削掉 1/3:窗口内可走格点 68% → 52%。)
|
||||
r = max(PAD_KEEP, halfw)
|
||||
for p in _toll_pads():
|
||||
g.block_rect(p[6], p[2] - p[4] / 2 - r, p[3] - p[5] / 2 - r,
|
||||
p[2] + p[4] / 2 + r, p[3] + p[5] / 2 + r, net=p[7])
|
||||
for l in RT.LAY:
|
||||
for r in RB.B.rect_minus((0.0, 0.0, RB.B.BW, RB.B.BH), [WIN]):
|
||||
if r[0] >= WIN[2] - 1e-6:
|
||||
continue # 东侧(插槽方向)下面单独封死,不是加价
|
||||
g.penalize(l, r[0], r[1], r[2], r[3], OUT_PEN)
|
||||
# ⛔ 插槽方向必须**封死**,不能只加价:本机实测过路费确实加到了 200
|
||||
# (窗口内 0 / 插槽区 200),A* 照样钻 —— `$6N64` 宁可付 200×若干格也要从
|
||||
# x 25.75 抄到 27.75,因为窗口内绕不过去。`penalize` 只是定价,`block_rect` 才是墙。
|
||||
# 槽体 x 28~70 是 1 mm 间距的手焊针排,走线进去是致命的。
|
||||
g.block_rect(l, WIN[2], 0.0, RB.B.BW, RB.B.BH, net='#slotwall')
|
||||
|
||||
|
||||
RT.pen_pads = _pen
|
||||
|
||||
segs = [tuple(s) for s in cp['segs']]
|
||||
vias = [tuple(v) for v in cp['vias']]
|
||||
board_vias = [tuple(v) for v in cp['board_vias']]
|
||||
|
||||
# ⛔ 只装**窗口内**的脚;窗口外的同网铜留在 segs 里当「已有铜」。
|
||||
bypad = {}
|
||||
outside = {}
|
||||
for p in pads:
|
||||
if not p[7] or p[7] not in NETS or str(p[1]).endswith(RB.TH_SUFFIX):
|
||||
continue
|
||||
if in_win(p[2], p[3]):
|
||||
bypad.setdefault(p[7], []).append(p)
|
||||
else:
|
||||
outside[p[7]] = outside.get(p[7], 0) + 1
|
||||
|
||||
# ⛔⛔ 2026-09-12 实测(第一版**漏了这一步,是设计缺陷**):
|
||||
# 「窗口外的同网铜留在占用图里,A* 碰到就算到达」——**不成立**。
|
||||
# `route_groups` 布的是 `bypad[net]` 里的脚**两两相连**,窗外主干根本不是它的终点判据。
|
||||
# 于是「窗外还有脚」的网全军覆没:`FAN_PWM` / `FAN_EN` 窗内只剩 1 只脚 ⇒ 跨度 0、无事可做;
|
||||
# `FAN_TACH_IN` / `FAN_TACH_OUT` / `+3V3` 窗内那两只脚彼此并不需要连(各自要去的是窗外主干)
|
||||
# ⇒ 实测这 5 个网**一段都没产出**,而窗外脚数为 0 的 8 个网全部正常产出。
|
||||
# ⇒ 把主干在**窗口边界处的端点**做成虚拟焊盘塞进 bypad,A* 才有终点可去
|
||||
# (同 `route_base` 用 `finger_escape()` 的 launch 点喂金手指的办法)。
|
||||
def _clip(v, lo, hi):
|
||||
return max(lo, min(hi, v))
|
||||
|
||||
|
||||
edge_pts = {}
|
||||
for s in cp['segs']:
|
||||
lay, x1, y1, x2, y2, w, net = s[0], s[1], s[2], s[3], s[4], s[5], s[6]
|
||||
if net not in NETS or lay not in RT.LAY:
|
||||
continue
|
||||
a, b = in_win(x1, y1), in_win(x2, y2)
|
||||
if a == b:
|
||||
continue # 整段在窗内或整段在窗外,都不是接口
|
||||
if lay != RB.B.BOTTOM:
|
||||
# ⛔ 落点不能取在 **L1**:那一层在这一带整片是 `+12V` 大铜,格点是 255(多网叠加,
|
||||
# 谁都不能走)。实测 `FAN_EN` 的落点取到 L1 之后「起步格点 0 个」,直接报「出不来」。
|
||||
continue
|
||||
ix, iy = (x1, y1) if a else (x2, y2) # 取落在窗内的那一头
|
||||
# ⛔ 端点正好压在主干线上(`FAN_PWM` 实测净距 −0.127)⇒ 起步格点也是 0。
|
||||
# 沿这一段往**窗内**退 RETREAT,让出一个能起步的格点。
|
||||
ox, oy = (x2, y2) if a else (x1, y1) # 段的另一头(窗外那一侧)
|
||||
seg_len = math.hypot(ix - ox, iy - oy)
|
||||
if seg_len > 1e-6:
|
||||
ux, uy = (ix - ox) / seg_len, (iy - oy) / seg_len
|
||||
ix, iy = ix + ux * min(RETREAT, seg_len * 0.4), iy + uy * min(RETREAT, seg_len * 0.4)
|
||||
edge_pts.setdefault(net, []).append((lay, round(ix, 4), round(iy, 4), w))
|
||||
# 主干整条都在窗外的网:取离窗口最近的那个端点,夹到窗边上当落点
|
||||
for net in NETS:
|
||||
if net in edge_pts or not outside.get(net):
|
||||
continue
|
||||
best = None
|
||||
for s in cp['segs']:
|
||||
# ⛔ 同样只在 L4 找:窗缘落点分支第一版漏了这条,`FAN_EN` 的落点取到 **L1**,
|
||||
# 而那一层这一带整片是 `+12V` 大铜、格点是 255(多网叠加,谁都不能走),
|
||||
# 于是「起步格点 0 个 ⇒ 出不来」,本机单独验那个坐标却有 16 个可起步 —— 差别就在层。
|
||||
if s[6] != net or s[0] != RB.B.BOTTOM:
|
||||
continue
|
||||
for (px, py) in ((s[1], s[2]), (s[3], s[4])):
|
||||
cx = _clip(px, WIN[0], WIN[2])
|
||||
cy = _clip(py, WIN[1], WIN[3])
|
||||
d = ((px - cx) ** 2 + (py - cy) ** 2) ** 0.5
|
||||
if best is None or d < best[0]:
|
||||
best = (d, s[0], px, py, s[5])
|
||||
if best and best[0] <= 6.0: # 主干就在窗边上:直接拿它当落点
|
||||
edge_pts.setdefault(net, []).append((best[1], round(best[2], 4), round(best[3], 4), best[4]))
|
||||
elif best:
|
||||
# ⛔ 主干远在窗外(`FAN_EN` / `FAN_TACH_OUT` 的最近端点在 x≈42~43,**在槽区里面**,
|
||||
# 离窗 14.5~15.6 mm)。**不能**放宽上限去接它 —— 那等于让 A* 从窗口里拉一条 15 mm 的线
|
||||
# 横穿插槽区,正是设窗口要避免的事(上一版 `+3V3` 穿针排就是这么来的)。
|
||||
# ⇒ 在**窗口边缘**给它一个落点:窗内这一段布到边上为止,窗外那一小段单独接。
|
||||
ex = _clip(best[2], WIN[0] + 0.3, WIN[2] - 0.3)
|
||||
ey = _clip(best[3], WIN[1] + 0.3, WIN[3] - 0.3)
|
||||
edge_pts.setdefault(net, []).append((best[1], round(ex, 4), round(ey, 4), best[4]))
|
||||
print(u' ⚠ %s 的主干离窗 %.1f mm(在槽区里),只布到窗缘 (%.2f, %.2f),窗外那段要另接'
|
||||
% (net, best[0], ex, ey))
|
||||
|
||||
n_virt = 0
|
||||
for net, pts in edge_pts.items():
|
||||
pts.sort(key=lambda q: -q[3]) # 宽的(主干)优先
|
||||
for (lay, px, py, w) in pts[:2]: # 每个网最多给两个落点,够 A* 挑
|
||||
bypad.setdefault(net, []).append(
|
||||
['#trunk', 'edge%d' % n_virt, px, py, 0.3, 0.3, lay, net, 'RECT'])
|
||||
n_virt += 1
|
||||
print(u'给跨窗口的网补了 %d 个「主干接口」虚拟落点:%s'
|
||||
% (n_virt, ' '.join(sorted(edge_pts)) or u'无'))
|
||||
print(u'窗口 x %.2f~%.2f y %.2f~%.2f;窗口外过路费 %d' % (WIN[0], WIN[2], WIN[1], WIN[3], OUT_PEN))
|
||||
for n in NETS:
|
||||
print(u' %-18s 窗口内 %d 脚 / 窗口外 %d 脚(外面的不参与,只当已有铜的接口)'
|
||||
% (n, len(bypad.get(n, [])), outside.get(n, 0)))
|
||||
drop = [n for n in NETS if not bypad.get(n)]
|
||||
if drop:
|
||||
print(u' (这些网窗口内一只脚都没有,跳过:%s)' % ' '.join(drop))
|
||||
|
||||
print(u'现有铜 %d 段 / %d 孔' % (len(segs), len(vias)))
|
||||
n0, v0 = len(segs), len(vias)
|
||||
RT.AUTO_CACHE.clear()
|
||||
|
||||
# ⛔ 诊断:`$6N49` / `$6N74` / `$6N45` 这几条链一路报「一根铜都没有」,而窗口空着 85%、
|
||||
# 焊盘尺寸与 width_of 都正常 —— 看着不像拥塞,像**布线器压根没处理它们**。
|
||||
# `route_groups` 按线宽分档、逐档调 A*,这里把「每一档收了哪些网」和每档前后的段数打出来,
|
||||
# 一眼就能看出某个网是不是从头到尾没进过任何一档。
|
||||
_orig_rg = RT.route_groups
|
||||
|
||||
|
||||
def _rg(bp, pd, sg, vi, bv, auto, priority=(), only=None):
|
||||
want = set(only) if only is not None else set(bp)
|
||||
seen = {}
|
||||
for net, ps in bp.items():
|
||||
if only is not None and net not in only:
|
||||
continue
|
||||
seen[net] = round(RT.width_of(net, auto, ps), 3)
|
||||
buckets = {}
|
||||
for net, w in seen.items():
|
||||
buckets.setdefault(w, []).append(net)
|
||||
print(u' 分档(线宽 → 网):')
|
||||
for w in sorted(buckets, reverse=True):
|
||||
print(u' %.3f mm: %s' % (w, ' '.join(sorted(buckets[w]))))
|
||||
miss = sorted(want - set(seen))
|
||||
if miss:
|
||||
print(u' ⚠ 这些网在 only 里却没进任何一档:%s' % ' '.join(miss))
|
||||
before = len(sg)
|
||||
out = _orig_rg(bp, pd, sg, vi, bv, auto, priority=priority, only=only)
|
||||
print(u' route_groups 共产出 %d 段' % (len(sg) - before))
|
||||
by = {}
|
||||
for s in sg[before:]:
|
||||
by[s[6]] = by.get(s[6], 0) + 1
|
||||
for net in sorted(seen):
|
||||
print(u' %-18s %s' % (net, (u'%d 段' % by[net]) if net in by else u'⛔ 0 段'))
|
||||
return out
|
||||
|
||||
|
||||
RT.route_groups = _rg
|
||||
# ⛔ 顺序决定成败:`route_groups` 按「出口数少 → 跨度大」排,`$6N49` 排在 `FAN_12V_SW`(37 段) 之后,
|
||||
# 走廊被先布的线占掉 ⇒ 它四只脚**两两直连全被挡**(本机实测 −0.499 ~ +0.139,最好的一对差 0.011),
|
||||
# 报「一根铜都没有」,看着像板子挤,其实是顺序。router 自己的注释里记过同一个坑
|
||||
# ("同样的摆位,先布它就通")。⇒ 把这几条挤在一起的本地链**顶到 priority 最前面**。
|
||||
PRI = tuple(CFG.get('priority') or ()) + tuple(n for n in NETS if n not in (CFG.get('priority') or ()))
|
||||
print(u'布线优先级:%s' % ' '.join(PRI[:6]))
|
||||
logs = RT.route_groups(bypad, pads, segs, vias, board_vias, {},
|
||||
priority=PRI, only=set(bypad))
|
||||
if logs and '--repair' in sys.argv:
|
||||
print(u' 还剩 %d 条没布通,拆线重布(只拆本次新布的)' % len(logs))
|
||||
segs, vias, logs = RT.repair(bypad, pads, segs, vias, logs, board_vias, {}, n0, v0)
|
||||
|
||||
new_s = [list(s) for s in segs[n0:]]
|
||||
new_v = [list(v) for v in vias[v0:]]
|
||||
print(u'新增 %d 段 / %d 孔' % (len(new_s), len(new_v)))
|
||||
for m in logs:
|
||||
print(u' [!] ' + m)
|
||||
|
||||
# 连通性只看窗口内的脚(窗口外的主干本来就通)
|
||||
chk = [q for q in RB.check_pads(pads) if q[7] in set(bypad) and in_win(q[2], q[3])]
|
||||
left = RT.check_connected(chk, segs, vias)
|
||||
print(u'连通性(窗口内口径):%s' % (u'全通' if not left else left))
|
||||
|
||||
# 跑出窗口的新铜要点名 —— 这是这套办法最该盯的失败模式
|
||||
stray = [s for s in new_s if not (in_win(s[1], s[2], 0.5) and in_win(s[3], s[4], 0.5))]
|
||||
print(u'跑出窗口的新线段:%d%s' % (len(stray), u' ⚠' if stray else u' ✓'))
|
||||
for s in stray[:8]:
|
||||
print(u' %s L%s (%.2f,%.2f)->(%.2f,%.2f)' % (s[6], s[0], s[1], s[2], s[3], s[4]))
|
||||
|
||||
json.dump({'window': list(WIN), 'nets': NETS, 'segs': new_s, 'vias': new_v,
|
||||
'logs': list(logs), 'unconnected': list(left), 'stray': len(stray)},
|
||||
io.open(os.path.join(HERE, '_base_window_repair.json'), 'w', encoding='utf-8'),
|
||||
ensure_ascii=False)
|
||||
print(u'已写 _base_window_repair.json')
|
||||
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""底板**位号丝印**的重排与下发。
|
||||
|
||||
/c/Python314/python .claude/r2/silk_base.py # 只算 + 体检(改前 / 改后对照)
|
||||
/c/Python314/python .claude/r2/silk_base.py --push # 下发(pcb_PrimitiveAttribute.modify)+ 读回核对
|
||||
|
||||
## 判据
|
||||
|
||||
位号文字框(字高 `fs`、字宽 ≈ 0.62×字高×字符数,`alignMode=3` 居中)不许:
|
||||
① 压同面焊盘(顶层丝印 layer 3 对 L1 + 通孔盘;底层 layer 4 对 L4 + 通孔盘)
|
||||
② 压过孔 ③ 越板框(留 `EDGE` 0.5) ④ 与别的位号重叠
|
||||
|
||||
⚠ **大件(本体 ≥ 5 mm)的位号保留原位**:五个插槽的位号故意放在槽体西侧 x=28(离本体 21 mm),
|
||||
那是设计,不是问题;U20 / 电解 / 座子同理。它们先占位,小件绕开它们。
|
||||
|
||||
⚠ 位号必须**紧挨器件**才有意义 ⇒ 离本体外框有硬上限 `MAXD`。够不着就降级:
|
||||
理想位 → 允许压过孔 → 允许压焊盘 → 保留原位。单纯"找个空地"会把位号甩到十几毫米外。
|
||||
|
||||
⚠ `rotation` 字段单位是**度且不归一化**(900 = 180°),读回要 `%% 360`。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
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
|
||||
|
||||
MIL = 0.0254
|
||||
SNAP = os.path.join(HERE, '_base_silk.json')
|
||||
PLAN = os.path.join(HERE, '_base_silk_plan.json')
|
||||
CHAR_W, EDGE, GAP, MAXD = 0.62, 0.5, 0.15, 2.2
|
||||
# ⛔ 保留原位的**显式名单**(位号故意远离本体,不是问题):
|
||||
# 五个插槽的位号放在槽体西侧 x=28(离本体 21 mm)、U20 让开天线净空、
|
||||
# 顶面件与板边 USB-C 的位号只能朝板内放。
|
||||
# ⚠ 别用「本体 ≥ 5 mm」当判据:那会把 R9A(7.4×3.5) / D3(6.8×2.2) / U13(2.3×5.8)
|
||||
# 这些**该重排**的中等件也豁免掉 —— 实测残留 25 个里有 18 个是这么来的。
|
||||
KEEP = {'J34', 'J35', 'J36', 'J37', 'J38', 'U20',
|
||||
'J26', 'J27', 'J33', 'C2', 'C3', 'J1', 'J23'}
|
||||
|
||||
READ = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,2500));
|
||||
const M = 0.0254, nrm = v => ((v %% 360) + 360) %% 360;
|
||||
const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator')
|
||||
.map(a=>({ref:a.value, lay:a.layer, x:a.x*M, y:a.y*M, rot:nrm(a.rotation),
|
||||
fs:a.fontSize*M, vis:a.valueVisible?1:0, id:a.primitiveId}));
|
||||
const C = (await eda.pcb_PrimitiveComponent.getAll()||[])
|
||||
.map(c=>({ref:c.designator, x:c.x*M, y:c.y*M, rot:nrm(c.rotation||0), lay:c.layer}));
|
||||
const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x*M, v.y*M, String(v.net), v.diameter*M]);
|
||||
return {attrs:A, comps:C, vias:V};
|
||||
""" % B.PCB
|
||||
|
||||
|
||||
def snap():
|
||||
r = run(READ, timeout=600)
|
||||
d = r.get('result')
|
||||
assert d, json.dumps(r, ensure_ascii=False)[:400]
|
||||
io.open(SNAP, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
|
||||
return d
|
||||
|
||||
|
||||
def tbox(ref, x, y, rot, fs):
|
||||
h = fs
|
||||
w = CHAR_W * h * len(ref)
|
||||
if rot in (90, 270):
|
||||
w, h = h, w
|
||||
return (x - w / 2., y - h / 2., x + w / 2., y + h / 2.)
|
||||
|
||||
|
||||
def ov(p, q):
|
||||
return not (p[2] <= q[0] or q[2] <= p[0] or p[3] <= q[1] or q[3] <= p[1])
|
||||
|
||||
|
||||
def plan(d, pads, sz):
|
||||
comp = {c['ref']: c for c in d['comps']}
|
||||
V = d['vias']
|
||||
padsL = {3: [p for p in pads if p[6] in (1, 12)], 4: [p for p in pads if p[6] in (2, 12)]}
|
||||
|
||||
def hit_pad(bb, lay):
|
||||
return any(ov(bb, (p[2] - p[4] / 2., p[3] - p[5] / 2., p[2] + p[4] / 2., p[3] + p[5] / 2.))
|
||||
for p in padsL[lay])
|
||||
|
||||
def hit_via(bb):
|
||||
return any(ov(bb, (v[0] - v[3] / 2., v[1] - v[3] / 2., v[0] + v[3] / 2., v[1] + v[3] / 2.))
|
||||
for v in V)
|
||||
|
||||
def outside(bb):
|
||||
return bb[0] < EDGE or bb[1] < EDGE or bb[2] > B.BW - EDGE or bb[3] > B.BH - EDGE
|
||||
|
||||
placed, out, stat = [], {}, {}
|
||||
for a in d['attrs']:
|
||||
if a['vis'] and a['ref'] in KEEP:
|
||||
placed.append((a['lay'], tbox(a['ref'], a['x'], a['y'], a['rot'], a['fs'])))
|
||||
stat[u'白名单保留'] = stat.get(u'白名单保留', 0) + 1
|
||||
small = [a for a in d['attrs'] if a['vis'] and a['ref'] not in KEEP]
|
||||
small.sort(key=lambda a: -(sz.get(a['ref'], (1, 1))[0] * sz.get(a['ref'], (1, 1))[1]))
|
||||
for a in small:
|
||||
ref, c, lay, fs = a['ref'], comp.get(a['ref']), a['lay'], a['fs']
|
||||
if not c:
|
||||
continue
|
||||
w, h = sz.get(ref, (1.0, 1.0))
|
||||
if int(round(c['rot'])) % 180:
|
||||
w, h = h, w
|
||||
best, level = None, None
|
||||
for lv in (0, 1, 2):
|
||||
r = 0.0
|
||||
while r <= MAXD and best is None:
|
||||
for ang in (90, 270, 0, 180, 45, 135, 225, 315):
|
||||
for rot in ((0, 90) if ang in (0, 180) else (0,)):
|
||||
tw, th = CHAR_W * fs * len(ref), fs
|
||||
if rot == 90:
|
||||
tw, th = th, tw
|
||||
ca, sa = math.cos(math.radians(ang)), math.sin(math.radians(ang))
|
||||
if abs(ca) <= 0.7 and abs(sa) <= 0.7:
|
||||
off = math.hypot(w / 2. + tw / 2., h / 2. + th / 2.) + GAP + r
|
||||
elif abs(ca) > 0.7:
|
||||
off = w / 2. + tw / 2. + GAP + r
|
||||
else:
|
||||
off = h / 2. + th / 2. + GAP + r
|
||||
x, y = c['x'] + off * ca, c['y'] + off * sa
|
||||
bb = tbox(ref, x, y, rot, fs)
|
||||
if outside(bb) or any(b[0] == lay and ov(bb, b[1]) for b in placed):
|
||||
continue
|
||||
if lv < 2 and hit_pad(bb, lay):
|
||||
continue
|
||||
if lv < 1 and hit_via(bb):
|
||||
continue
|
||||
best, level = (round(x, 4), round(y, 4), rot, bb), lv
|
||||
break
|
||||
if best:
|
||||
break
|
||||
r += 0.25
|
||||
if best:
|
||||
break
|
||||
if best:
|
||||
placed.append((lay, best[3]))
|
||||
out[ref] = {'x': best[0], 'y': best[1], 'rot': best[2], 'id': a['id'], 'lay': lay}
|
||||
k = [u'理想位', u'允许压过孔', u'允许压焊盘'][level]
|
||||
stat[k] = stat.get(k, 0) + 1
|
||||
else:
|
||||
stat[u'无解(保留原位)'] = stat.get(u'无解(保留原位)', 0) + 1
|
||||
return out, stat
|
||||
|
||||
|
||||
def main():
|
||||
d = snap()
|
||||
pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8'))
|
||||
sz = PL.sizes()
|
||||
p, stat = plan(d, pads, sz)
|
||||
io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(p, ensure_ascii=False, indent=0))
|
||||
print(u'位号 %d(可见 %d);重排 %d 个' % (len(d['attrs']), sum(a['vis'] for a in d['attrs']), len(p)))
|
||||
for k, v in sorted(stat.items(), key=lambda t: -t[1]):
|
||||
print(u' %-14s %d' % (k, v))
|
||||
if '--push' not in sys.argv:
|
||||
print(u'(只算;加 --push 下发)')
|
||||
return
|
||||
rows = [[v['id'], v['x'] / MIL, v['y'] / MIL, v['rot']] for v in p.values()]
|
||||
n = 0
|
||||
for k in range(0, len(rows), 60):
|
||||
r = run(u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,1500));
|
||||
const R = %s;
|
||||
let n = 0;
|
||||
for (const [id, x, y, rot] of R) {
|
||||
try { await eda.pcb_PrimitiveAttribute.modify(id, {x: x, y: y, rotation: rot}); n++; } catch (e) {}
|
||||
}
|
||||
return n;
|
||||
""" % (B.PCB, json.dumps(rows[k:k + 60])), timeout=600)
|
||||
n += r.get('result') or 0
|
||||
run(u"await eda.pcb_Document.save(); return 1;", timeout=300)
|
||||
print(u' modify 成功 %d / %d' % (n, len(rows)))
|
||||
# 读回核对
|
||||
d2 = snap()
|
||||
now = {a['ref']: a for a in d2['attrs']}
|
||||
bad = [k for k, v in p.items()
|
||||
if k not in now or abs(now[k]['x'] - v['x']) > 0.02 or abs(now[k]['y'] - v['y']) > 0.02
|
||||
or int(now[k]['rot']) % 360 != v['rot'] % 360]
|
||||
print(u'读回核对:%d 个对上,%d 个没对上 %s' % (len(p) - len(bad), len(bad), bad[:10]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户