# -*- coding: utf-8 -*- u"""服务器上补指定的断网(2026-09-11 混合布线收尾):以板上快照的全部铜为障碍,只把每个网断开的几块接起来。 DCDC_ROUTE_SERVER=1 /root/pypy/bin/pypy3 .claude/r2/route_nets.py NET1 NET2 ... [--grid 0.125] 读 _board_for_nets.json,写 _route_nets.out.json(按网的新增线 / 孔 + 日志) 做法:按板上铜把本网焊盘分连通块(线-线 / 线-孔 / 线-盘 / 孔-盘 相碰即连,内层线也算)→ 每块挑一只离其它块最近的焊盘当代表 → route_base.route_one 把代表连成树(router 只走 L1/L4,12V 大铜只挡导线、焊盘里不落孔等照 route_base._build)。 按给定顺序一个一个布,前面布出的铜进后面的障碍。 """ import io import json import math import os import sys if os.environ.get('DCDC_ROUTE_SERVER') != '1': sys.exit(u'!! 布线运算只在服务器上跑(DCDC_ROUTE_SERVER=1),见 CLAUDE.md') if '--grid' not in sys.argv: sys.argv += ['--grid', '0.125'] HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) import route_base as R # noqa: E402 RT = R.RT WIDTH = {'+3V3_HUB': 0.3, '+5V': 0.5, '+3V3': 0.4} # ⛔ 内层线也要挡过孔(2026-09-11 第一版漏了:只拿 L1/L4 当障碍,补网 A 的孔戳在 EasyEDA 留在 L2/L3 上的线上 —— # DRC:VDAC1 孔压 L3 的 SDA、+3V3_HUB 孔压 L2 的 NTC2,都是 0 mil 短路)。 # router 只有 L1/L4 两张图;过孔要两层的过孔图都空才准落 ⇒ 沿每条内层线在 L1 的过孔图(ownv)上封一圈,走线图不动。 INNER = [] # [(layer, x1, y1, x2, y2, w, net)],main() 每轮刷新 _orig_grid_now = R.grid_now def _grid_now(pads, halfw, segs, vias, board_vias): g, gt, others = _orig_grid_now(pads, halfw, segs, vias, board_vias) for s in INNER: r = RT.VIA_D / 2.0 + RT.CLR + s[5] / 2.0 n = max(1, int(L(s) / (RT.G / 2.0))) for k in range(n + 1): x = s[1] + (s[3] - s[1]) * k / n y = s[2] + (s[4] - s[2]) * k / n g.block_rect(1, x - r, y - r, x + r, y + r, net=s[6], via=True) return g, gt, others R.grid_now = _grid_now L = lambda s: math.hypot(s[3] - s[1], s[4] - s[2]) # noqa: E731 def _pd(px, py, s): x1, y1, x2, y2 = s[1], s[2], s[3], s[4] dx, dy = x2 - x1, y2 - y1 L2 = dx * dx + dy * dy t = 0 if L2 == 0 else max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / L2)) return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)) def _rd(q, x, y): return math.hypot(max(q[2] - q[4] / 2 - x, x - q[2] - q[4] / 2, 0), max(q[3] - q[5] / 2 - y, y - q[3] - q[5] / 2, 0)) def components(net, pads, segs, vias): ps = [q for q in pads if q[7] == net] ss = [s for s in segs if s[6] == net] vs = [v for v in vias if v[2] == net] items = [('p', q) for q in ps] + [('s', s) for s in ss] + [('v', v) for v in vs] par = list(range(len(items))) def f(i): while par[i] != i: par[i] = par[par[i]] i = par[i] return i def seg_seg(a, b): return min(_pd(a[1], a[2], b), _pd(a[3], a[4], b), _pd(b[1], b[2], a), _pd(b[3], b[4], a)) def touch(a, b): (ka, x), (kb, y) = a, b if ka == 'p' and kb == 'p': return False if ka == 'v' and kb == 'v': return math.hypot(x[0] - y[0], x[1] - y[1]) < 0.61 if ka == 's' and kb == 's': return x[0] == y[0] and seg_seg(x, y) < (x[5] + y[5]) / 2 + 0.01 if kb == 's': (ka, x), (kb, y) = b, a if ka == 's' and kb == 'v': return _pd(y[0], y[1], x) < x[5] / 2 + 0.3 if ka == 's' and kb == 'p': n = max(2, int(L(x) / 0.05)) return (y[6] in (x[0], 12)) and min(_rd(y, x[1] + (x[3] - x[1]) * k / n, x[2] + (x[4] - x[2]) * k / n) for k in range(n + 1)) < x[5] / 2 + 0.01 q, v = (x, y) if ka == 'p' else (y, x) return _rd(q, v[0], v[1]) < 0.3 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): if it[0] == 'p': groups.setdefault(f(i), []).append(it[1]) return list(groups.values()) def reps(groups): u"""每块挑一只离其它块最近的焊盘当代表。""" out = [] for k, g in enumerate(groups): others = [q for j, h in enumerate(groups) if j != k for q in h] best = min(g, key=lambda q: min(math.hypot(q[2] - o[2], q[3] - o[3]) for o in others)) out.append(best) return out def main(): # 参数 NET=宽度 覆盖默认线宽(2026-09-11:+5V 0.5 钻不进 J37.19 南侧 0.74 的缝、CFGSEL0 / +3V3_HUB 出不了 U32 脚间) for a in sys.argv[1:]: if '=' in a and not a.startswith('--'): k, v = a.split('=', 1) WIDTH[k] = float(v) # ⚠ 带参数的开关(--cut X、--grid G)后面那个值不是网名(第一版把 VDAC1@x,y 当网名混进来过) skip = {k + 1 for k, a in enumerate(sys.argv) if a in ('--cut', '--grid')} nets = [a for k, a in enumerate(sys.argv) if k >= 1 and k not in skip and not a.startswith('--') and '=' not in a and '@' not in a and not a.replace('.', '').isdigit()] b = json.load(io.open(os.path.join(HERE, '_board_for_nets.json'), encoding='utf-8')) segs = [tuple(s[:7]) for s in b['segs']] # 内层线也要参与连通判断(不当 L1/L4 障碍) vias = [tuple(v[:3]) for v in b['vias']] if '--drop-a' in sys.argv: # 先在数据里拿掉这些网在「补网 A」(_route_nets.A.json)里布的铜,再重布(孔戳内层线那批)。 # 板上的删除等新结果本机复核过、下发时再做。EasyEDA 会合并共线段 ⇒ 按「两端 + 中点都落在同层同网同宽的 A 段上」认。 a = json.load(io.open(os.path.join(HERE, '_route_nets.A.json'), encoding='utf-8')) a_s = [tuple(x) for n in nets for x in a['nets'].get(n, {}).get('segs', [])] a_v = [tuple(x) for n in nets for x in a['nets'].get(n, {}).get('vias', [])] def in_a(s): cand = [t for t in a_s if t[0] == s[0] and t[6] == s[6] and abs(t[5] - s[5]) < 0.01] return cand and all(any(_pd(x, y, t) < 0.01 for t in cand) for x, y in ((s[1], s[2]), (s[3], s[4]), ((s[1] + s[3]) / 2, (s[2] + s[4]) / 2))) n0 = (len(segs), len(vias)) segs = [s for s in segs if not (s[6] in nets and in_a(s))] vias = [v for v in vias if not (v[2] in nets and any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.02 for w in a_v))] print(u'--drop-a:去掉补网 A 的铜 %d 段 / %d 孔(%s)' % (n0[0] - len(segs), n0[1] - len(vias), ' '.join(nets))) # --cut NET@x,y(可多次):只拆这颗孔和端点落在它上面的同网线段 —— 局部修补(VDAC1 补网 A 那条 97 mm 路径本身合理, # 只有两颗孔戳在内层线上;整根重布要绕 273 mm / 25 孔)。被拆开的几块照常「连通块 + 代表 + route_one」接回。 cuts = [sys.argv[k + 1] for k, a in enumerate(sys.argv) if a == '--cut'] for c in cuts: cn, xy = c.split('@') cx, cy = (float(t) for t in xy.split(',')) nv0, ns0 = len(vias), len(segs) vias = [v for v in vias if not (v[2] == cn and math.hypot(v[0] - cx, v[1] - cy) < 0.05)] segs = [s for s in segs if not (s[6] == cn and (math.hypot(s[1] - cx, s[2] - cy) < 0.05 or math.hypot(s[3] - cx, s[4] - cy) < 0.05))] print(u'--cut %s @ (%.3f, %.3f):拆孔 %d、线 %d' % (cn, cx, cy, nv0 - len(vias), ns0 - len(segs))) if cn not in nets: nets.append(cn) pads = R.load_pads() out, logs = {}, [] for net in nets: INNER[:] = [s for s in segs if s[0] in (15, 16)] obst = [s for s in segs if s[0] in (1, 2)] groups = components(net, pads, segs, vias) if len(groups) < 2: print(u'%s:已经连通(%d 块),跳过' % (net, len(groups))) continue ps = reps(groups) w = WIDTH.get(net, 0.2) lg = [] s, v = R.route_one(net, ps, w, pads, obst, vias, [], lg) segs += [tuple(x) for x in s] vias += [tuple(x) for x in v] after = components(net, pads, segs, vias) out[net] = {'segs': [list(x) for x in s], 'vias': [list(x) for x in v], 'before': len(groups), 'after': len(after), 'logs': lg} print(u'%s:%d 块 → %d 块;新增 %d 段 %.1f mm / %d 孔(线宽 %.2f)%s' % (net, len(groups), len(after), len(s), sum(L(x) for x in s), len(v), w, (u';' + ';'.join(lg)) if lg else '')) logs += lg io.open(os.path.join(HERE, '_route_nets.out.json'), 'w', encoding='utf-8').write(json.dumps( {'nets': out, 'logs': logs, 'grid': RT.G, # 下发时照这两份删板上旧铜:drop_a = 整网拿掉「补网 A」的铜再重布;cuts = 只拆这几颗孔 + 端点在孔上的线(局部修补) 'drop_a': ([n for n in nets if n not in {c.split('@')[0] for c in cuts}] if '--drop-a' in sys.argv else []), 'cuts': cuts}, ensure_ascii=False)) if __name__ == '__main__': main()