# -*- coding: utf-8 -*- u"""按**手定的拓扑**分段 A*:每段 = 一个网从点 A 到点 B(点给坐标 + 层),A* 只管段内细节(2026-09-15)。 DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/opt_legs.py spec:{"base": "_blade_routes.xxx.json", "out": "_blade_routes.legs%d.json", "variants": [[[网, [x, y, 层], [x, y, 层]], ...], ...]} 层 1 = L1 顶、2 = L4 底。点通常是已有的同网孔 / 焊盘中心:起止格点取点周围 0.4 mm 内的空格点, 再用一段直线从精确点接到格点(净距交给 router_check 复核)。整个网的铜同网可穿,所以后一段可以接在前一段的铜上。 为什么不用 route_groups:它按「最近焊盘」贪心连树,SCL/SDA 这种金手指 → 上拉电阻 → U3 → U4 的总线会被连成绕到板西边的树。 """ 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'⛔ 搜索只在 10.5.5.13 上跑(CLAUDE.md)') import router as RT # noqa: E402 W = 0.15 def grid_now(pads, segs, vias, halfw): board_vias = [(v[0], v[1], v[2], RT.VIA_D) for v in vias] g = RT.build_grid(pads, halfw, board_vias) for (l, a, b, c, d, ww, nn) in segs: RT.block_track(g, l, a, b, c, d, halfw + ww / 2.0, nn, extra=max(0.0, RT.VIA_D / 2.0 + RT.CLR - halfw), exact_v=True) for (vx, vy, nn) in vias: rv = RT.VIA_D / 2.0 + RT.CLR + halfw ev = max(0.0, RT.VIA_D / 2.0 - halfw) for l in RT.LAY: g.block_both(l, vx - rv, vy - rv, vx + rv, vy + rv, ev, net=nn, exact_v=True) return g def nodes_near(g, x, y, lay, net, r=0.4, own_r=0.3): u"""⚠ 孔 / 焊盘自己那一圈格点常被判成「两个网共占」(255:旁边别网孔的膨胀框压进来了),A* 起不了步。 点本身就是本网的铜 ⇒ 0.3 mm 以内的格点强制记成本网(两张图都改),出了这一圈照常判。""" k_ = g.nid(net) kk = int(math.ceil(own_r / RT.G)) for di in range(-kk, kk + 1): for dj in range(-kk, kk + 1): i, j = RT.gx(x) + di, RT.gy(y) + dj if math.hypot(RT.wx(i) - x, RT.wy(j) - y) <= own_r + 1e-9 and 0 <= i < RT.NX and 0 <= j < RT.NY: g.own[lay][g.idx(i, j)] = k_ g.ownv[lay][g.idx(i, j)] = k_ out = [] i0, j0 = RT.gx(x), RT.gy(y) k = int(math.ceil(r / RT.G)) for di in range(-k, k + 1): for dj in range(-k, k + 1): i, j = i0 + di, j0 + dj if math.hypot(RT.wx(i) - x, RT.wy(j) - y) <= r + 1e-9 and g.free(lay, i, j, net): out.append((lay, i, j)) return out def main(): spec = json.load(io.open(sys.argv[1], encoding='utf-8')) base = json.load(io.open(os.path.join(HERE, spec['base']), encoding='utf-8')) pads = RT.load_pads() halfw = W / 2.0 + RT.CLR for k, legs in enumerate(spec['variants']): segs = [tuple(s) for s in base['segs']] vias = [tuple(v) for v in base['vias']] L, bad = {}, [] for net, a, b in legs: g = grid_now(pads, segs, vias, halfw) st = nodes_near(g, a[0], a[1], a[2], net) gl = nodes_near(g, b[0], b[1], b[2], net) path = RT.astar(g, st, gl, net, halfw) if st and gl else None if not path: inv = {v: kk for kk, v in g._id.items()} def who(p): return sorted(set(str(inv.get(g.own[p[2]][g.idx(RT.gx(p[0]) + di, RT.gy(p[1]) + dj)], '·')) for di in (-1, 0, 1) for dj in (-1, 0, 1))) bad.append(u'%s %s→%s(起点 %d %s / 终点 %d %s 个格点)' % (net, a, b, len(st), who(a), len(gl), who(b))) if spec.get('debug'): kn = g.nid(net) for lay in RT.LAY: print(u'--- %s L%d 周围(.=空 o=本网 #=别网,行=y 从大到小)' % (net, lay)) for j in range(RT.gy(a[1]) + 14, RT.gy(a[1]) - 8, -1): row = '' for i in range(RT.gx(a[0]) - 16, RT.gx(a[0]) + 16): v = g.own[lay][g.idx(i, j)] vv = g.ownv[lay][g.idx(i, j)] row += '.' if v == 0 else ('o' if v == kn else '#') row += ',' if vv == 0 else ('o' if vv == kn else '#') print('%6.2f %s' % (RT.wy(j), row)) continue sg, vv = RT.to_segments(path) new = [(l, x0, y0, x1, y1, W, net) for (l, x0, y0, x1, y1) in sg] p0, p1 = path[0], path[-1] for (pt, nd) in ((a, p0), (b, p1)): if math.hypot(RT.wx(nd[1]) - pt[0], RT.wy(nd[2]) - pt[1]) > 1e-6: new.append((nd[0], pt[0], pt[1], RT.wx(nd[1]), RT.wy(nd[2]), W, net)) segs += new vias += [(x, y, net) for (x, y) in vv] L[net] = L.get(net, 0.0) + sum(math.hypot(s[3] - s[1], s[4] - s[2]) for s in new) print(u'[%d] %s 未通 %s' % (k, ' '.join('%s=%.1f' % kv for kv in sorted(L.items())), bad)) json.dump({'segs': [list(s) for s in segs], 'vias': [list(v) for v in vias], 'logs': bad}, io.open(os.path.join(HERE, spec['out'] % k), 'w', encoding='utf-8'), ensure_ascii=False) if __name__ == '__main__': main()