# -*- coding: utf-8 -*- u"""BLADE-PCB_1 布线收尾:在服务器布线结果上**手工**补最后几个网(2026-09-14)。 /c/Python314/python .claude/r2/hand_blade1.py # 合成 + 自检(碎块 / 精确净距),不写缓存 /c/Python314/python .claude/r2/hand_blade1.py --write # 自检过了才写 _blade_routes.json 为什么手工:服务器 6 轮(含挪 4 颗电阻)稳定剩 ~8 个网,全在 0.5 mm 细间距脚被邻网铜封口的地方; `MODE=repair` 补线又会把已通的 VO_P / +12V_F 当普通线整条重铺(33 对同网过孔叠在一起),不能用。 这里**不做任何搜索**(CLAUDE.md:搜索只在服务器),只放逐段写死的几何,再用两道判据兜底: ① 按铜几何做并查集,逐网数碎块(焊盘 / 线段 / 过孔,同层相交或端点落进焊盘即相连,过孔两层都算); ② `router_check` 的精确净距(0.15)。 底 = `_blade_routes.var3.json`(第六轮 `--nofan --pf`,净距 0 冲突), 并入 `_blade_routes.repaired.json` 里 **SDA 那 6 段 2 孔**(其余网的补线全丢弃,理由见上)。 """ 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) import router as RT # noqa: E402 BASE = os.path.join(HERE, '_blade_routes.blade1_srv6.json') # = 第六轮 v3 的拷贝(var* 会被下一次 remote_blade.sh 覆盖) REPAIRED = os.path.join(HERE, '_blade_routes.blade1_repair_sda.json') # = 第一次 MODE=repair 的拷贝 PADS = json.load(io.open(os.path.join(HERE, '_blade_pads.json'), encoding='utf-8')) TAKE_FROM_REPAIR = ('SDA',) # 手工操作:('del_seg', 层, x0, y0, x1, y1, 网) / ('del_via', x, y, 网) / ('seg', 层, x0, y0, x1, y1, 宽, 网) / ('via', x, y, 网) OPS = [ # ── $1N147:U2.15 出口被 EFUSE_EN 的换层孔 (43.25, 26.75) 正好堵死 ── # 孔上移到 (43.25, 27.5)(仍在 L1 那根 x 43.25 竖线上,离 U2.13 的 VO 线 0.29、离 R10.1 0.5), # U2.15 直线接 R10.1(R10.1 ↔ R9.2 已通)。 ('del_via', 43.25, 26.75, 'EFUSE_EN'), ('del_seg', 2, 42.994, 27.285, 43.25, 26.75, 'EFUSE_EN'), ('via', 43.25, 27.5, 'EFUSE_EN'), ('seg', 2, 42.994, 27.285, 43.25, 27.5, 0.3, 'EFUSE_EN'), ('seg', 2, 42.245, 26.784, 44.069, 26.632, 0.15, '$1N147'), # ── $1N131:U1.8 → R6.1 → C11.1,这片 L4 其实空着,布线器的出线判定没找到口 ── # U1.8 竖直北上到 35.2,45° 斜进 R6.1(离 C9.1 角 0.23); # R6.1 往西到 x 64.7 绕开 R6.2 / C10.1(离 $1N126 那根 0.566 宽的竖线 0.18、离 C10.1 0.27),北上再东进 C11.1。 ('seg', 2, 66.294, 33.978, 66.294, 35.2, 0.15, '$1N131'), ('seg', 2, 66.294, 35.2, 65.375, 36.119, 0.15, '$1N131'), ('seg', 2, 65.375, 36.126, 64.7, 36.126, 0.15, '$1N131'), ('seg', 2, 64.7, 36.126, 64.7, 39.398, 0.15, '$1N131'), ('seg', 2, 64.7, 39.398, 65.293, 39.398, 0.15, '$1N131'), # ── SDA:三块 R40.1 | U4.23 | U3.4+J1.B6 ── # L4:R40.1 先东后北进 U4.23(直斜会蹭 U4.24 焊盘角 0.09); # 在这段横线上 (15.6, 25.832) 打孔上 L1,L1 沿 x 15.6 南下到 y 20.25(西让 +3V3 竖线 0.58、东让 SCL 孔 0.78), # 东去接服务器补线那根 SDA L1 横线的拐点 (20.5, 20.25)(北让 INA_ALERT 孔 0.38)。 ('seg', 2, 14.849, 25.832, 15.872, 25.832, 0.15, 'SDA'), ('seg', 2, 15.872, 25.832, 15.872, 27.457, 0.15, 'SDA'), ('via', 15.6, 25.832, 'SDA'), ('seg', 1, 15.6, 25.832, 15.6, 20.25, 0.15, 'SDA'), ('seg', 1, 15.6, 20.25, 20.5, 20.25, 0.15, 'SDA'), # ── EDL_EN:U4.7 南面被 KEY_VDN 贴着、北面夹在 KEY_VUP 孔 (18.75,35.5) 和 VBUS_EN 孔 (20.0,35.0) 之间 ── # L4 从脚北端微斜到 x 19.3 北上(离 KEY_VUP 孔 0.175、离 VBUS_EN 线 0.196),y 36.144 东去 R63.1(U6.6 已与之相连)。 ('seg', 2, 19.121, 33.503, 19.121, 34.45, 0.15, 'EDL_EN'), ('seg', 2, 19.121, 34.45, 19.3, 34.63, 0.15, 'EDL_EN'), ('seg', 2, 19.3, 34.63, 19.3, 36.144, 0.15, 'EDL_EN'), ('seg', 2, 19.3, 36.144, 24.511, 36.144, 0.15, 'EDL_EN'), # ── $1N164 顺手收直:U2.3 → 孔 → L1 → 孔 → R16.2 这一绕,第二个孔 (38.25, 26.75) 正好堵在 U2.2 出口 ── # 改成 L4 一根直线 U2.3 引出端 → R16.2(离 R16.1 0.43、离 U2.2 0.2),两孔一段 L1 删掉。 ('del_seg', 2, 38.497, 27.285, 38.25, 26.75, '$1N164'), ('del_via', 38.25, 26.75, '$1N164'), ('del_seg', 1, 37.5, 26.75, 38.25, 26.75, '$1N164'), ('del_via', 37.5, 26.75, '$1N164'), ('del_seg', 2, 37.033, 27.112, 37.5, 26.75, '$1N164'), ('seg', 2, 38.497, 27.285, 37.033, 27.112, 0.3, '$1N164'), # ── EFUSE_PG:R15.1 | U2.2 | U4.16 ── # L4:U2.2 往西到 x 38.3 南下进 R15.1;这根竖线上 (38.3, 25.8) 打孔上 L1(北让 EFUSE_EN 那根 y 25.0 的 L1 横线 0.35)。 # L1:x 36.9 北上到 29.25(西让 EFUSE_FLT 孔、东让 $1N165 孔 0.225)→ 西去 26.8(夹在 BUCK_PG y 28.25 与 $1N165 y 30.25 之间) # → 北上到 30.25 → 西去 20.422(夹在 +3V3 y 29.5 与 KEY_VDN y 31.0 之间),打孔下 L4 穿 U4 肚子接 U4.16(离 +3V3 孔 0.3)。 ('seg', 2, 39.246, 26.784, 38.3, 26.784, 0.15, 'EFUSE_PG'), ('seg', 2, 38.3, 26.784, 38.3, 24.864, 0.15, 'EFUSE_PG'), ('via', 38.3, 25.8, 'EFUSE_PG'), ('seg', 1, 38.3, 25.8, 36.9, 25.8, 0.15, 'EFUSE_PG'), ('seg', 1, 36.9, 25.8, 36.9, 29.25, 0.15, 'EFUSE_PG'), ('seg', 1, 36.9, 29.25, 26.8, 29.25, 0.15, 'EFUSE_PG'), ('seg', 1, 26.8, 29.25, 26.8, 30.25, 0.15, 'EFUSE_PG'), ('seg', 1, 26.8, 30.25, 20.422, 30.25, 0.15, 'EFUSE_PG'), ('via', 20.422, 30.25, 'EFUSE_PG'), ('seg', 2, 20.422, 30.25, 20.422, 27.457, 0.15, 'EFUSE_PG'), # ── VO_P:C65.2(INA226 VBUS 采样的去耦)原靠 L3 西南腿打孔、从来没落下孔;腿已拆(pcb_pour.VO_PTS 的说明)── # L4 往西到 x 24.75(离 $1N165 的 Kelvin 孔 (24.0, 21.25) 0.35),北上到 U3.9 高度,西接 U3.8/U3.9 短接线。 # 线宽 0.3 = VO_P 所属 PWR4A 规则的下限(0.2 会报「物理错误:线宽」);U3.8/U3.9 那根短接线同理从 0.2 换成 0.3。 ('seg', 2, 25.781, 21.298, 24.75, 21.298, 0.3, 'VO_P'), ('seg', 2, 24.75, 21.298, 24.75, 22.233, 0.3, 'VO_P'), ('seg', 2, 24.75, 22.233, 24.107, 22.233, 0.3, 'VO_P'), ('del_seg', 2, 24.107, 22.733, 24.107, 22.233, 'VO_P'), ('seg', 2, 24.107, 22.733, 24.107, 22.233, 0.3, 'VO_P'), # ── +5V:两根金手指的 launch 在 L4 上北上出 +12V 横带(顶 18.0),打孔进 L3 的 +5V 岛(pcb_pour.V5_PTS)── # A19 (39.0) 直上;A32 (52.0) 在 +12V 东腿(x ≥ 49.5)上方,上到 19.2 再西去 48.3 才落孔。U5 那头原有两个孔直接在岛里。 ('seg', 2, 39.0, 11.75, 39.0, 19.2, 0.4, '+5V'), ('via', 39.0, 19.2, '+5V'), ('seg', 2, 52.0, 11.75, 52.0, 19.2, 0.4, '+5V'), ('seg', 2, 52.0, 19.2, 48.3, 19.2, 0.4, '+5V'), ('via', 48.3, 19.2, '+5V'), # ── 70×58(2026-09-14 用户要求):J3 顶到 60.1 越出新板边,挪到 (33.07, 53.93),两根信号重接 ── # $3N45:J3.2 北上到 54.3,斜进原换层孔 (31.75, 55.5)(离 KEY_VDN 孔 0.43、离 J3.3 安装脚 0.45)。 # $3N44:J3.1 沿 y 52.481 西去接原 x 28.5 竖线(在 C60.2 上方 1.2、离 H4 孔 1.3),旧的两孔一段 L1 删掉。 ('del_seg', 2, 31.994, 55.7, 31.75, 55.5, '$3N45'), ('seg', 2, 33.693, 52.481, 33.693, 54.3, 0.15, '$3N45'), ('seg', 2, 33.693, 54.3, 31.75, 55.5, 0.15, '$3N45'), ('del_seg', 2, 30.744, 55.7, 30.0, 54.75, '$3N44'), ('del_via', 30.0, 54.75, '$3N44'), ('del_seg', 1, 30.0, 54.75, 28.5, 54.75, '$3N44'), ('del_via', 28.5, 54.75, '$3N44'), ('del_seg', 2, 28.5, 54.75, 28.5, 40.75, '$3N44'), ('seg', 2, 28.5, 52.481, 28.5, 40.75, 0.15, '$3N44'), ('seg', 2, 32.443, 52.481, 28.5, 52.481, 0.15, '$3N44'), ] def load(): base = json.load(io.open(BASE, encoding='utf-8')) segs = [list(s) for s in base['segs']] vias = [list(v) for v in base['vias']] rep = json.load(io.open(REPAIRED, encoding='utf-8')) sa = set(tuple(s) for s in segs) va = set(tuple(v) for v in vias) segs += [list(s) for s in rep['segs'] if s[6] in TAKE_FROM_REPAIR and tuple(s) not in sa] vias += [list(v) for v in rep['vias'] if v[2] in TAKE_FROM_REPAIR and tuple(v) not in va] return segs, vias def near(a, b, tol=1e-3): return abs(a - b) < tol def apply(segs, vias, ops): for op in ops: k = op[0] if k == 'del_seg': _, l, x0, y0, x1, y1, n = op hit = [s for s in segs if s[0] == l and s[6] == n and ( (near(s[1], x0, 2e-3) and near(s[2], y0) and near(s[3], x1) and near(s[4], y1)) or (near(s[1], x1) and near(s[2], y1) and near(s[3], x0) and near(s[4], y0)))] assert hit, u'删线没找到: %s' % (op,) for s in hit: segs.remove(s) elif k == 'del_via': _, x, y, n = op hit = [v for v in vias if v[2] == n and near(v[0], x) and near(v[1], y)] assert hit, u'删孔没找到: %s' % (op,) for v in hit: vias.remove(v) elif k == 'seg': _, l, x0, y0, x1, y1, w, n = op segs.append([l, x0, y0, x1, y1, w, n]) elif k == 'via': _, x, y, n = op vias.append([x, y, n]) else: raise ValueError(op) def _pt_seg(px, py, ax, ay, bx, by): dx, dy = bx - ax, by - ay L = dx * dx + dy * dy t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L)) return math.hypot(px - ax - t * dx, py - ay - t * dy) def _seg_seg(a, b): def ccw(p, q, r): return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]) p1, p2, p3, p4 = (a[1], a[2]), (a[3], a[4]), (b[1], b[2]), (b[3], b[4]) d1, d2, d3, d4 = ccw(p3, p4, p1), ccw(p3, p4, p2), ccw(p1, p2, p3), ccw(p1, p2, p4) if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)): return 0.0 return min(_pt_seg(p1[0], p1[1], *p3, *p4), _pt_seg(p2[0], p2[1], *p3, *p4), _pt_seg(p3[0], p3[1], *p1, *p2), _pt_seg(p4[0], p4[1], *p1, *p2)) def _pt_rect(px, py, p): dx = max(abs(px - p[2]) - p[4] / 2.0, 0.0) dy = max(abs(py - p[3]) - p[5] / 2.0, 0.0) return math.hypot(dx, dy) def pieces(net, segs, vias): u"""按铜几何算这个网碎成几块,返回 [[成员描述...], ...](只列焊盘)。""" items = [('pad', p) for p in PADS if p[7] == net and p[0] != 'J1'] + \ [('pad', p) for p in PADS if p[7] == net and p[0] == 'J1'] items += [('seg', s) for s in segs if s[6] == net] items += [('via', v) for v in vias if v[2] == net] par = list(range(len(items))) def f(i): while par[i] != i: par[i] = par[par[i]] i = par[i] return i def layers(it): t, o = it if t == 'pad': return {1, 2} if o[6] == 12 else {o[6]} if t == 'seg': return {o[0]} return {1, 2} def touch(a, b): if not (layers(a) & layers(b)): return False (ta, oa), (tb, ob) = a, b eps = 0.02 if ta == 'pad' and tb == 'pad': return False if ta == 'pad' or tb == 'pad': p, (t, o) = (oa, b) if ta == 'pad' else (ob, a) if t == 'via': return _pt_rect(o[0], o[1], p) < 0.3 + eps return min(_pt_rect(o[1], o[2], p), _pt_rect(o[3], o[4], p)) < o[5] / 2.0 + eps if ta == 'via' and tb == 'via': return math.hypot(oa[0] - ob[0], oa[1] - ob[1]) < 0.6 if ta == 'via' or tb == 'via': v, s = (oa, ob) if ta == 'via' else (ob, oa) return _pt_seg(v[0], v[1], s[1], s[2], s[3], s[4]) < 0.3 + s[5] / 2.0 + eps return _seg_seg(oa, ob) < (oa[5] + ob[5]) / 2.0 + eps for i in range(len(items)): for j in range(i + 1, len(items)): if f(i) != f(j) and touch(items[i], items[j]): par[f(i)] = f(j) groups = {} for i, it in enumerate(items): groups.setdefault(f(i), []).append(it) out = [] for g in groups.values(): pads = ['%s.%s' % (o[0], o[1]) for t, o in g if t == 'pad'] if pads: out.append(pads) return out # 靠铜面 / L3 岛成网,不按走线判碎块 PLANE = ('GND', 'VO_P', '+12V', '+5V') def plane_islands(): import pcb_pour as PP return {'VO_P': PP.VO_RECTS, '+12V': PP.V12_RECTS, '+5V': PP.V5_RECTS} def plane_report(segs, vias): u"""平面网:按走线算出的每一块,都必须至少有一个本网过孔**落在本网 L3 岛里**(离岛边 ≥ 0.3)。""" isl = plane_islands() bad = {} for n, rects in isl.items(): vin = [v for v in vias if v[2] == n and any(r[0] + 0.3 <= v[0] <= r[2] - 0.3 and r[1] + 0.3 <= v[1] <= r[3] - 0.3 for r in rects)] for g in pieces(n, segs, vias): # 这一块里有没有岛内过孔:把岛内过孔当成本块成员再算一次块数,不增加 = 在块里 ok = any(len(pieces(n, segs, [v] + [w for w in vias if w[2] == n and w is not v])) and _in_piece(n, g, v, segs, vias) for v in vin) if not ok: bad.setdefault(n, []).append(g) return bad def _in_piece(net, g, v, segs, vias): u"""过孔 v 是否和焊盘组 g 在同一块铜里:把 v 换成一个假焊盘,看它和 g 是否同组。""" fake = ['VIA', '%.2f,%.2f' % (v[0], v[1]), v[0], v[1], 0.6, 0.6, 12, net, 'RECT'] PADS.append(fake) try: for grp in pieces(net, segs, vias): if fake[0] + '.' + fake[1] in grp: return any(x in grp for x in g) finally: PADS.remove(fake) return False def report(segs, vias): nets = sorted(set(p[7] for p in PADS if p[7] and p[7] not in PLANE)) bad = {} for n in nets: g = pieces(n, segs, vias) # J1 的 launch 由 finger_escape 的引出脚接,焊盘本身在指上:只要不是「J1 自己单独一块」就算通 if len(g) > 1: bad[n] = g return bad def main(): segs, vias = load() apply(segs, vias, OPS) bad = report(segs, vias) pb = plane_report(segs, vias) print(u'平面网(每块都要有岛内过孔):%s' % (u'全过' if not pb else '')) for n, gs in sorted(pb.items()): print(u' [X] %-6s 没接进岛的块: %s' % (n, ' | '.join(','.join(x) for x in gs))) print(u'段 %d / 孔 %d;碎块网 %d 个' % (len(segs), len(vias), len(bad))) for n, g in sorted(bad.items()): print(u' %-10s %d 块: %s' % (n, len(g), ' | '.join(','.join(x) for x in g))) out = {'segs': segs, 'vias': vias, 'logs': [u'%s: %d 块' % (n, len(g)) for n, g in bad.items()], 'hand': True} tmp = os.path.join(HERE, '_blade_routes.hand.json') json.dump(out, io.open(tmp, 'w', encoding='utf-8'), ensure_ascii=False) print(u'已写 %s(router_check 用 --cache 指过来核净距)' % os.path.basename(tmp)) if '--write' in sys.argv: json.dump(out, io.open(os.path.join(HERE, '_blade_routes.json'), 'w', encoding='utf-8'), ensure_ascii=False) print(u'已写 _blade_routes.json') if __name__ == '__main__': main()