# -*- coding: utf-8 -*- u"""刀片布线缓存的**离线连通性**:按铜重叠做并查集(线段 / 过孔 / 焊盘,挨着就算连),纯几何校核。 /c/Python314/python .claude/r2/blade_conn.py # _blade_routes.json /c/Python314/python .claude/r2/blade_conn.py _blade_routes.var0.json _blade_routes.var1.json 为什么要有:`router.py` 自报的「[连通性] XXX: 铜分成 2 块」不可靠 —— 2026-09-11 第二轮服务器结果 自报 8 条,按铜判只有 3 条是真断(+3V3 / VBUS / SCL / SDA / $1N136 都是误报)。比较变体、决定攻哪几个网, 看这里的数。平面网 GND / VO_P 不判(靠铺铜 / L3 岛成网);+3V3 当普通网查。 """ import json, math, sys sys.stdout.reconfigure(encoding='utf-8') R2 = r'D:/DCDC/.claude/r2/' pads = json.load(open(R2 + '_blade_pads.json', encoding='utf-8')) VR, TOL = 0.3, 0.02 PLANE = ('GND', 'VO_P', '+12V') # 2026-09-11 晚起 +12V 也靠 L3 岛成网(方案 §35) def pt_seg(px, py, ax, ay, bx, by): dx, dy = bx-ax, by-ay; L = dx*dx+dy*dy t = 0 if L == 0 else max(0, min(1, ((px-ax)*dx+(py-ay)*dy)/L)) return math.hypot(px-(ax+t*dx), py-(ay+t*dy)) def seg_seg(a, b): (x1,y1,x2,y2),(x3,y3,x4,y4) = a, b d = (x2-x1)*(y4-y3)-(y2-y1)*(x4-x3) if abs(d) > 1e-12: t = ((x3-x1)*(y4-y3)-(y3-y1)*(x4-x3))/d; u = ((x3-x1)*(y2-y1)-(y3-y1)*(x2-x1))/d if 0 <= t <= 1 and 0 <= u <= 1: return 0.0 return min(pt_seg(x1,y1,x3,y3,x4,y4), pt_seg(x2,y2,x3,y3,x4,y4), pt_seg(x3,y3,x1,y1,x2,y2), pt_seg(x4,y4,x1,y1,x2,y2)) def pt_rect(px, py, r): return math.hypot(max(r[0]-px, 0, px-r[2]), max(r[1]-py, 0, py-r[3])) def seg_rect(s, r): if pt_rect(s[0], s[1], r) == 0 or pt_rect(s[2], s[3], r) == 0: return 0.0 e = [(r[0],r[1],r[2],r[1]),(r[2],r[1],r[2],r[3]),(r[2],r[3],r[0],r[3]),(r[0],r[3],r[0],r[1])] return min(seg_seg(s, x) for x in e) def check(path): c = json.load(open(path, encoding='utf-8')) bynet = {} for p in pads: if p[7]: bynet.setdefault(p[7], {'P': [], 'S': [], 'V': []})['P'].append(p) for s in c['segs']: bynet.setdefault(s[6], {'P': [], 'S': [], 'V': []})['S'].append(s) for v in c['vias']: bynet.setdefault(v[2], {'P': [], 'S': [], 'V': []})['V'].append(v) bad = {} for net, g in sorted(bynet.items()): if net in PLANE or len(g['P']) < 2: continue items = [('P', p) for p in g['P']] + [('S', s) for s in g['S']] + [('V', v) for v in g['V']] par = list(range(len(items))) def f(i): while par[i] != i: par[i] = par[par[i]]; i = par[i] return i def touch(a, b): ka, A = a; kb, B = b if ka == 'P' and kb == 'P': return False if ka == 'V' and kb == 'V': return math.hypot(A[0]-B[0], A[1]-B[1]) <= 2*VR + TOL if ka == 'S' and kb == 'S': return A[0] == B[0] and seg_seg(A[1:5], B[1:5]) <= (A[5]+B[5])/2 + TOL if {ka, kb} == {'S', 'P'}: s, p = (A, B) if ka == 'S' else (B, A) r = (p[2]-p[4]/2, p[3]-p[5]/2, p[2]+p[4]/2, p[3]+p[5]/2) return s[0] == p[6] and seg_rect(s[1:5], r) <= s[5]/2 + TOL if {ka, kb} == {'V', 'P'}: v, p = (A, B) if ka == 'V' else (B, A) r = (p[2]-p[4]/2, p[3]-p[5]/2, p[2]+p[4]/2, p[3]+p[5]/2) return pt_rect(v[0], v[1], r) <= VR + TOL v, s = (A, B) if ka == 'V' else (B, A) return pt_seg(v[0], v[1], *s[1:5]) <= VR + s[5]/2 + TOL for i in range(len(items)): for j in range(i+1, len(items)): if touch(items[i], items[j]): a, b = f(i), f(j) if a != b: par[a] = b grp = {} for i, (k, it) in enumerate(items): if k == 'P': grp.setdefault(f(i), []).append('%s.%s' % (it[0], it[1])) if len(grp) > 1: gs = sorted(grp.values(), key=len, reverse=True) bad[net] = gs return bad files = [a for a in sys.argv[1:]] or ['_blade_routes.json'] for fn in files: b = check(fn if (':' in fn or fn.startswith('/')) else R2 + fn) print('===== %s:离线真断开 %d 个网' % (fn, len(b))) for n, gs in b.items(): print(' %-10s %d 块:主块 %d 脚;其余 %s' % (n, len(gs), len(gs[0]), ' | '.join(' '.join(g) for g in gs[1:])[:160]))