# -*- coding: utf-8 -*- u"""混合布线(2026-09-11):脚本预布关键铜并锁定 → EasyEDA 自带自动布线只补其余(只开 L1/L4)。 /c/Python314/python .claude/r2/ar_hybrid.py --clear # 删四层全部导线、全部过孔、全部铜面(铜面由 pour_base --push 按计划重建) /c/Python314/python .claude/r2/ar_hybrid.py --push # 下发 _base_preroute_0911.json,线 / 孔全部锁定 /c/Python314/python .claude/r2/ar_hybrid.py --verify # 读回:预布的每段 / 每孔都在、都锁着 /c/Python314/python .claude/r2/ar_hybrid.py --route # EasyEDA autoRouting:只开 L1/L4、保留已有、忽略平面网 / 入口链 ⚠ `--route` 在客户端 3.2.149 上用不了:`eda.pcb_Document.autoRouting is not a function`(文档里标 beta,PCB 端没开放; 枚举全部 `eda.*` 只有 `sch_Document.autoRouting`,PCB 端只有 import/getAutoRouteJsonFile / clearRouting)⇒ 自动布线让用户在客户端手动点, 设置与 IGNORE 相同:布线层只勾顶层 / 底层、保留已有导线和过孔、忽略 GND / +12V / VIN_RAW / VIN_FUSED。 预布子集由 r34-v0 挑出:GND / +12V / 入口链 / Kelvin / 晶振 RBIAS VDD18(PLL) / 两颗 Buck 反馈 / +3V3_HUB / USB1~6 整网, UP 两端到第一个孔(中段交给自动布线)。为什么这么分,见 PROGRESS「EasyEDA 自动布线评审」。 ⚠ `--clear` 前先备份板上现状(`_base_board_autoroute_0911.json` 就是 09-11 那版自动布线)。 """ 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 = B.MIL # 基准铜(strip / verify 以它为准):0911 = r34 挑出的预布;0911b = 再去掉 +3V3_HUB 远程线、加 C4.2 改接和服务器中心线版 UP SUBSET = os.path.join(HERE, '_base_preroute_0911b.json') VIA_D, VIA_HOLE = 0.6, 0.3 # 与 router / 缓存一致 IGNORE = ['GND', '+12V', 'VIN_RAW', 'VIN_FUSED'] # 平面网 + 入口链(靠铜面) OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2500));" % B.PCB) def call(js, timeout=600): r = run(OPEN + js, timeout=timeout) if not r.get('success'): sys.exit(u'!! ' + json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def clear(): r = call(u""" const out = {lines: 0, vias: 0, pours: 0, rounds: 0}; for (let k = 0; k < 10; k++) { const ls = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).map(l=>l.primitiveId); const vs = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>v.primitiveId); const ps = (await eda.pcb_PrimitivePour.getAll()||[]).map(p=>p.primitiveId); if (!ls.length && !vs.length && !ps.length) break; if (ls.length) await eda.pcb_PrimitiveLine.delete(ls); if (vs.length) await eda.pcb_PrimitiveVia.delete(vs); for (const p of ps) await eda.pcb_PrimitivePour.delete(p); out.lines += ls.length; out.vias += vs.length; out.pours += ps.length; out.rounds++; } await eda.pcb_Document.save(); out.left = {lines: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).length, vias: (await eda.pcb_PrimitiveVia.getAll()||[]).length, pours: (await eda.pcb_PrimitivePour.getAll()||[]).length, poured: (await eda.pcb_PrimitivePoured.getAll()||[]).length}; return out;""", timeout=900) print(u'清板:删导线 %d / 过孔 %d / 铜面 %d(%d 轮);剩 %s' % (r['lines'], r['vias'], r['pours'], r['rounds'], r['left'])) if any(r['left'].values()): sys.exit(u'!! 没清干净') PUSH_JS = u""" const MIL = %s, S = %s, V = %s, H = %s, D = %s; let ls = 0, vs = 0, bad = []; for (const s of S) { try { const o = await eda.pcb_PrimitiveLine.create(s[6], s[0], s[1]*MIL, s[2]*MIL, s[3]*MIL, s[4]*MIL, s[5]*MIL, true); if (o) ls++; else bad.push(s[6]); } catch (e) { bad.push(s[6] + ' ' + e); } } for (const v of V) { try { const o = await eda.pcb_PrimitiveVia.create(v[2], v[0]*MIL, v[1]*MIL, H*MIL, D*MIL, undefined, null, null, true); if (o) vs++; else bad.push('via ' + v[2]); } catch (e) { bad.push('via ' + v[2] + ' ' + e); } } return {ls, vs, bad: bad.slice(0, 10), nbad: bad.length}; """ def push(): c = json.load(io.open(SUBSET, encoding='utf-8')) segs, vias = c['segs'], c['vias'] nl = nv = nb = 0 for k in range(0, max(len(segs), 1), 120): r = call(PUSH_JS % (MIL, json.dumps(segs[k:k + 120]), json.dumps(vias if k == 0 else []), VIA_HOLE, VIA_D), timeout=900) nl, nv, nb = nl + r['ls'], nv + r['vs'], nb + r['nbad'] if r['bad']: print(u' ⚠ 失败样例: %s' % r['bad']) call(u'await eda.pcb_Document.save(); return 1;') print(u'下发(锁定):导线 %d / %d,过孔 %d / %d,失败 %d' % (nl, len(segs), nv, len(vias), nb)) if nb or nl != len(segs) or nv != len(vias): sys.exit(u'!! 下发不全') def readback(): r = call(u""" return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)) .map(l=>[l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth,l.net||'',!!l.primitiveLock]), V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x,v.y,v.net||'',v.diameter,v.holeDiameter,!!v.primitiveLock])};""") L = [[a[0], a[1] / MIL, a[2] / MIL, a[3] / MIL, a[4] / MIL, a[5] / MIL, a[6], a[7]] for a in r['L']] V = [[a[0] / MIL, a[1] / MIL, a[2], a[3] / MIL, a[4] / MIL, a[5]] for a in r['V']] return L, V def verify(): c = json.load(io.open(SUBSET, encoding='utf-8')) L, V = readback() # ⚠ EasyEDA 会把首尾相接的共线段合并成一段(下发 599 读回 572),端点精确匹配对不上 ⇒ # 判据改成「预布段的两端 + 中点都落在板上同层 / 同网 / 同宽、且锁定的某段线上」 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.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2)) return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)) def covered(s, locked_only): pts = ((s[1], s[2]), (s[3], s[4]), ((s[1] + s[3]) / 2, (s[2] + s[4]) / 2)) cand = [b for b in L if b[0] == s[0] and b[6] == s[6] and abs(b[5] - s[5]) < 0.01 and (b[7] or not locked_only)] return all(any(pd(x, y, b) < 0.01 for b in cand) for x, y in pts) miss = [s for s in c['segs'] if not covered(s, False)] unl = [s for s in c['segs'] if s not in miss and not covered(s, True)] vm = [v for v in c['vias'] if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 and w[5] for w in V)] print(u'读回:导线 %d(锁定 %d)/ 过孔 %d(锁定 %d)' % (len(L), sum(1 for s in L if s[7]), len(V), sum(1 for v in V if v[5]))) print(u'预布 %d 段:没被盖住 %d,盖住但没锁 %d;预布 %d 孔:缺或没锁 %d' % (len(c['segs']), len(miss), len(unl), len(c['vias']), len(vm))) for s in miss[:8]: print(u' 缺段 %s' % s) for v in vm[:8]: print(u' 缺孔 %s' % v) return not miss and not vm def route(): u"""桥接服务端每个请求硬上限 30 s(bridge-server.mjs REQUEST_TIMEOUT_MS),自动布线远超这个数 ⇒ 发起后立刻返回,结果挂在 globalThis.__AR 上,再每 15 s 轮询一次(每次轮询都是短请求)。""" import time r = call(u""" const T = (typeof EPCB_LayerId !== 'undefined') ? EPCB_LayerId.TOP : 1; const Bt = (typeof EPCB_LayerId !== 'undefined') ? EPCB_LayerId.BOTTOM : 2; const KEEP = (typeof EPCB_AutoRoutingExistingPrimitiveMode !== 'undefined') ? EPCB_AutoRoutingExistingPrimitiveMode.KEEP : 'keep'; const t0 = Date.now(); globalThis.__AR = {state: 'running', t0}; eda.pcb_Document.autoRouting({layers: [T, Bt], ignoreNets: %s, existingPrimitiveMode: KEEP}) .then(async res => { globalThis.__AR = {state: 'done', res, ms: Date.now() - t0}; try { await eda.pcb_Document.save(); globalThis.__AR.saved = true; } catch (e) { globalThis.__AR.saveErr = String(e); } }) .catch(e => { globalThis.__AR = {state: 'error', err: String(e && (e.stack || e.message) || e), ms: Date.now() - t0}; }); return {started: true, layers: [T, Bt], keep: KEEP};""" % json.dumps(IGNORE), timeout=60) print(u'发起:%s' % json.dumps(r, ensure_ascii=False)) t0 = time.time() while time.time() - t0 < 3600: time.sleep(15) try: s = run(u"const a = globalThis.__AR || null;" u"const n = (await eda.pcb_PrimitiveLine.getAll()||[]).length;" u"return {a: a && a.state === 'running' ? {state: 'running', sec: (Date.now() - a.t0) / 1000} : a, lines: n};", timeout=60) except Exception as e: # 客户端忙着布线时请求可能超时,下一轮再问 print(u' 轮询异常:%s' % e) continue res = s.get('result') or {} a = res.get('a') or {} print(u' %4.0f s:%s,板上导线 %s' % (time.time() - t0, a.get('state'), res.get('lines'))) sys.stdout.flush() if a.get('state') in ('done', 'error'): print(json.dumps(a, ensure_ascii=False)[:3000]) return print(u'!! 60 分钟还没结束') def set_lock(flag): u"""全部导线(四层)+ 过孔改锁定状态;分块改(桥接每个请求 30 s 上限)。 2026-09-11:锁着 + 「保留已有」时用户点自动布线毫无反应 ⇒ 解锁做对照。""" ids = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).map(l=>l.primitiveId)," u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>v.primitiveId)};") done = 0 for kind, api in (('L', 'pcb_PrimitiveLine'), ('V', 'pcb_PrimitiveVia')): arr = ids[kind] for k in range(0, len(arr), 150): if kind == 'V' and not flag: # ⚠ 过孔的 modify(id, {primitiveLock:false}) 返回成功但**不生效**(3.2.149 实测,读回仍锁), # toAsync().setState_PrimitiveLock() 连新建的普通孔也报「对象参数不正确」⇒ 锁着的孔删掉按 5 参数原位重建(不锁) r = call(u"let n = 0; for (const id of %s) { const v = await eda.pcb_PrimitiveVia.get(id);" u" if (!v || !v.primitiveLock) continue;" u" const a = [v.net, v.x, v.y, v.holeDiameter, v.diameter];" u" if (await eda.pcb_PrimitiveVia.delete(id) && await eda.pcb_PrimitiveVia.create(...a)) n++; }" u" return n;" % json.dumps(arr[k:k + 100]), timeout=120) if len(arr) > k + 100: r += call(u"let n = 0; for (const id of %s) { const v = await eda.pcb_PrimitiveVia.get(id);" u" if (!v || !v.primitiveLock) continue;" u" const a = [v.net, v.x, v.y, v.holeDiameter, v.diameter];" u" if (await eda.pcb_PrimitiveVia.delete(id) && await eda.pcb_PrimitiveVia.create(...a)) n++; }" u" return n;" % json.dumps(arr[k + 100:k + 150]), timeout=120) else: r = call(u"let n = 0; for (const id of %s) { try { if (await eda.%s.modify(id, {primitiveLock: %s})) n++; } catch (e) {} }" u" return n;" % (json.dumps(arr[k:k + 150]), api, 'true' if flag else 'false'), timeout=120) done += r call(u'await eda.pcb_Document.save(); return 1;') L, V = readback() print(u'%s:改了 %d / %d;读回 导线 %d(锁定 %d)/ 过孔 %d(锁定 %d)' % (u'锁定' if flag else u'解锁', done, len(ids['L']) + len(ids['V']), len(L), sum(1 for s in L if s[7]), len(V), sum(1 for v in V if v[5]))) KO_NAME = 'AR-NOWIRE-%s' def keepout(add): u"""L2 / L3 整板「禁止布线」区域(EPCB_PrimitiveRegionRuleType.NO_WIRES = 5;API 没有「禁止过孔」类型)。 2026-09-11 用户实测:EasyEDA 自动布线**不勾内层就不跑**(进度一闪、零产出)⇒ 对话框四层都勾,用这两块区域把导线挡在内层外。 `add=False` 只删这两块。⚠ create 传的 regionName 读回是空(3.2.149 实测)⇒ 按「层 15/16 + ruleType 恰为 [5]」认。""" src = B.polygon_src(B.outline_polygon()) r = call(u""" const mine = g => [15, 16].includes(g.layer) && JSON.stringify(g.ruleType) === '[5]'; const old = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine); for (const g of old) await eda.pcb_PrimitiveRegion.delete(g.primitiveId); const made = []; if (%s) for (const [lay, tag] of [[15, 'L2'], [16, 'L3']]) { const poly = await eda.pcb_MathPolygon.createPolygon(%s); const g = await eda.pcb_PrimitiveRegion.create(lay, poly, [5], '%s'.replace('%%s', tag)); made.push(g ? [g.layer, g.regionName, g.ruleType] : null); } await eda.pcb_Document.save(); const now = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine).map(g => [g.layer, g.ruleType, g.primitiveId]); return {deleted: old.length, made, now};""" % ('true' if add else 'false', json.dumps(src), KO_NAME)) print(u'内层禁布区:删旧 %d,新建 %s;板上现有 %s' % (r['deleted'], r['made'], r['now'])) ZONES = os.path.join(HERE, '_base_inner_keepout_0911.json') def keepout_zones(add): u"""L2 + L3 **关键区**「禁止布线」区域(`_base_inner_keepout_0911.json`:USB1~6 在 L1/L4 上的 D± 段各外扩 0.9 + 晶振 / 两颗 Buck / 入口功率链矩形,shapely 合并)。2026-09-11 用户选「关键区禁布 + 四层全勾」: 布线器只有四层全勾才跑、且把禁布区当连孔也禁 ⇒ 只在这些区域里不许它走内层、不许它打孔,其余随它。 认法与 `keepout()` 相同:层 15/16 + ruleType 恰为 [5](regionName 存不住)。""" zs = json.load(io.open(ZONES, encoding='utf-8')) if add else [] def src(pts): a = [round(pts[0][0] * MIL, 4), round(pts[0][1] * MIL, 4)] for x, y in pts[1:]: a += ['L', round(x * MIL, 4), round(y * MIL, 4)] return a rings = [[src(z['pts'])] + [src(h) for h in z['holes']] for z in zs] r = call(u""" const mine = g => [15, 16].includes(g.layer) && JSON.stringify(g.ruleType) === '[5]'; const old = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine); for (const g of old) await eda.pcb_PrimitiveRegion.delete(g.primitiveId); const R = %s, made = [], bad = []; for (const lay of [15, 16]) for (let i = 0; i < R.length; i++) { const poly = R[i].length > 1 ? eda.pcb_MathPolygon.createComplexPolygon(R[i]) : eda.pcb_MathPolygon.createPolygon(R[i][0]); const p = (poly && typeof poly.then === 'function') ? await poly : poly; if (!p) { bad.push([lay, i, 'poly']); continue; } try { const g = await eda.pcb_PrimitiveRegion.create(lay, p, [5]); if (g) made.push([lay, i]); else bad.push([lay, i, 'create']); } catch (e) { bad.push([lay, i, String(e).slice(0, 80)]); } } await eda.pcb_Document.save(); const now = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine).map(g => g.layer); return {deleted: old.length, made: made.length, bad, now: now.length, byLayer: {L2: now.filter(l => l === 15).length, L3: now.filter(l => l === 16).length}};""" % json.dumps(rings), timeout=120) print(u'内层关键区禁布:删旧 %d,新建 %d(失败 %s);板上现有 %d %s' % (r['deleted'], r['made'], r['bad'], r['now'], r['byLayer'])) 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.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2)) return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)) def _on(seg, pool): u"""seg 的两端 + 中点都落在 pool 里同层 / 同网 / 同宽的某段上(EasyEDA 会合并共线段,所以按「被盖住」判)。""" cand = [b for b in pool if b[0] == seg[0] and b[6] == seg[6] and abs(b[5] - seg[5]) < 0.01] pts = ((seg[1], seg[2]), (seg[3], seg[4]), ((seg[1] + seg[3]) / 2, (seg[2] + seg[4]) / 2)) return all(any(_pd(x, y, b) < 0.01 for b in cand) for x, y in pts) def strip(nets=None): u"""删掉**不属于预布子集**的导线 / 过孔(= 自动布线加的),再把被连带删掉的预布段补回来。 `nets` 给了就只动这些网(2026-09-11:自动布线在已完整的 USB3 / USB6 上又并了一条,只删它们的新增部分)。""" c = json.load(io.open(SUBSET, encoding='utf-8')) r = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))" u".map(l=>[l.primitiveId,l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth,l.net||''])," u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.x,v.y,v.net||''])};") L = [(a[0], [a[1], a[2] / MIL, a[3] / MIL, a[4] / MIL, a[5] / MIL, a[6] / MIL, a[7]]) for a in r['L']] V = [(a[0], a[1] / MIL, a[2] / MIL, a[3]) for a in r['V']] if nets: L = [(i, s) for i, s in L if s[6] in nets] V = [v for v in V if v[3] in nets] kill_l = [i for i, s in L if not _on(s, c['segs'])] kill_v = [i for i, x, y, n in V if not any(v[2] == n and math.hypot(v[0] - x, v[1] - y) < 0.01 for v in c['vias'])] for k in range(0, len(kill_l), 300): call(u"await eda.pcb_PrimitiveLine.delete(%s); return 1;" % json.dumps(kill_l[k:k + 300]), timeout=120) if kill_v: call(u"await eda.pcb_PrimitiveVia.delete(%s); return 1;" % json.dumps(kill_v), timeout=120) call(u'await eda.pcb_Document.save(); return 1;') print(u'清掉非预布:导线 %d / %d,过孔 %d / %d' % (len(kill_l), len(L), len(kill_v), len(V))) Lb, _Vb = readback() # ⚠ 指定了 nets 就只补这些网的预布段(2026-09-11 踩过:apply_up 删了 +3V3_HUB 远程线,随后 strip(UP) 把它们全补了回来,压在新 UP 上) lost = [s for s in c['segs'] if not _on(s, Lb) and (not nets or s[6] in nets)] if lost: r = call(PUSH_JS % (MIL, json.dumps(lost), json.dumps([]), VIA_HOLE, VIA_D), timeout=300) call(u'await eda.pcb_Document.save(); return 1;') print(u'补回被合并连带删掉的预布段:%d / %d(失败 %d)' % (r['ls'], len(lost), r['nbad'])) Lb, Vb = readback() lost = [s for s in c['segs'] if not _on(s, Lb)] vl = [v for v in c['vias'] if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in Vb)] print(u'现在:导线 %d / 过孔 %d;预布没盖住 %d 段、缺孔 %d' % (len(Lb), len(Vb), len(lost), len(vl))) for s in lost[:6]: print(u' 没盖住 %s' % s) # C4.2(VIN_SW 去耦)被 U13.10 → Rs6.2 的 +12V Kelvin 线挡在 VIN_SW 铜北边,L4 上接不过去; # 自动布线绕 L1 18 mm 穿 12V 大铜(L1 12V 主块最窄 13.20 → 8.95)。2026-09-11 服务器扫描(c4_scan,孔不进焊盘、只接宽功率铜 / Rs6.1) # 用户选「接功率铜」:孔 A 在 C4.2 西南外侧,L1 4.5 mm 到孔 B,再 L4 短接 Rs6.1 旁 2.0 宽的 VIN_SW 铜。 C4_FIX_SEGS = [[2, 16.5, 46.0, 15.5, 45.125, 0.3, 'VIN_SW'], [1, 15.5, 45.125, 13.688, 41.0, 0.3, 'VIN_SW'], [2, 13.688, 41.0, 13.625, 40.875, 0.3, 'VIN_SW']] C4_FIX_VIAS = [[15.5, 45.125, 'VIN_SW'], [13.688, 41.0, 'VIN_SW']] def fix_c4(): u"""删自动布线在 VIN_SW 上加的铜(= 去 C4.2 那条 L1 绕线),下发 C4_FIX_*(不锁),读回核对。""" strip({'VIN_SW'}) r = call(PUSH_JS.replace(', true);', ');').replace('null, null, true)', 'null, null, false)') % (MIL, json.dumps(C4_FIX_SEGS), json.dumps(C4_FIX_VIAS), VIA_HOLE, VIA_D), timeout=120) call(u'await eda.pcb_Document.save(); return 1;') L, V = readback() miss = [s for s in C4_FIX_SEGS if not _on(s, L)] vm = [v for v in C4_FIX_VIAS if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in V)] print(u'C4.2 改接:下发线 %d / 孔 %d(失败 %d);读回缺线 %d、缺孔 %d;VIN_SW 现在 %d 段 / %d 孔' % (r['ls'], r['vs'], r['nbad'], len(miss), len(vm), sum(1 for s in L if s[6] == 'VIN_SW'), sum(1 for v in V if v[2] == 'VIN_SW'))) UP_ROUTE = os.path.join(HERE, '_up_route.cl_b52.json') UP_RIP = ('+5V', 'EFUSE_EN4', 'HUB_RST') # 挡 UP 中心线的自动布线网:整网删掉,之后「所选网络」重布 U32_C = (51.5, 38.6) def apply_up(with_ret=True): u"""下发服务器中心线版 UP(up_route.py --centerline --pre-only --free-3v3hub --pitch 0.52): ① 删 UP_RIP 三个网的全部铜 + +3V3_HUB 离 U32 > 8 mm 的远程线 / 孔(它们最后「所选网络」重布); ② UP 恢复成只剩预布两端段(strip);③ 下发 UP 新线 + GND 回流孔(不锁);④ 读回核对。""" u = json.load(io.open(UP_ROUTE, encoding='utf-8')) far = lambda x, y: math.hypot(x - U32_C[0], y - U32_C[1]) > 8.0 # noqa: E731 r = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))" u".map(l=>[l.primitiveId,l.startX,l.startY,l.endX,l.endY,l.net||''])," u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.x,v.y,v.net||''])};") kl = [a[0] for a in r['L'] if a[5] in UP_RIP or (a[5] == '+3V3_HUB' and far(a[1] / MIL, a[2] / MIL) and far(a[3] / MIL, a[4] / MIL))] kv = [a[0] for a in r['V'] if a[3] in UP_RIP or (a[3] == '+3V3_HUB' and far(a[1] / MIL, a[2] / MIL))] for k in range(0, len(kl), 300): call(u"await eda.pcb_PrimitiveLine.delete(%s); return 1;" % json.dumps(kl[k:k + 300]), timeout=120) if kv: call(u"await eda.pcb_PrimitiveVia.delete(%s); return 1;" % json.dumps(kv), timeout=120) call(u'await eda.pcb_Document.save(); return 1;') print(u'腾位置:删线 %d / 孔 %d(%s + +3V3_HUB 远程线)' % (len(kl), len(kv), ' '.join(UP_RIP))) strip({'UP_DP', 'UP_DM'}) vias = [list(v) for v in u['ret']] if with_ret else [] js = PUSH_JS.replace(', true);', ');').replace('null, null, true)', 'null, null, false)') n = 0 for k in range(0, len(u['segs']), 120): res = call(js % (MIL, json.dumps(u['segs'][k:k + 120]), json.dumps(vias if k == 0 else []), VIA_HOLE, VIA_D), timeout=300) n += res['ls'] if res['nbad']: print(u' ⚠ 失败 %d:%s' % (res['nbad'], res['bad'])) call(u'await eda.pcb_Document.save(); return 1;') Lb, Vb = readback() miss = [s for s in u['segs'] if not _on(list(s), Lb)] vm = [v for v in vias if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in Vb)] print(u'UP 下发:线 %d / %d,回流孔 %d;读回缺线 %d、缺孔 %d;板上 UP_DP %d 段 / UP_DM %d 段' % (n, len(u['segs']), len(vias), len(miss), len(vm), sum(1 for s in Lb if s[6] == 'UP_DP'), sum(1 for s in Lb if s[6] == 'UP_DM'))) def inner_type(t): u"""L2 / L3 层类型:'PLANE'(内电层,负片)或 'SIGNAL'。 2026-09-11:EasyEDA 自动布线不勾内层不跑;勾内层 + 整板禁布区又不敢打孔 ⇒ 试把内层改内电层。""" r = call(u""" const T = (typeof EPCB_LayerType !== 'undefined') ? (%s ? EPCB_LayerType.INTERNAL_ELECTRICAL : EPCB_LayerType.SIGNAL) : '%s'; const a = await eda.pcb_Layer.modifyLayer(15, {type: T}); const b = await eda.pcb_Layer.modifyLayer(16, {type: T}); await eda.pcb_Document.save(); return {T, a, b, now: (await eda.pcb_Layer.getAllLayers()||[]).filter(l => [1,2,15,16].includes(l.id)).map(l => [l.id, l.name, l.type])};""" % ('true' if t == 'PLANE' else 'false', t)) print(u'内层类型 → %s:modify %s / %s;读回 %s' % (r['T'], r['a'], r['b'], r['now'])) if __name__ == '__main__': if '--strip' in sys.argv: strip() if '--keepout' in sys.argv: keepout(True) if '--unkeepout' in sys.argv: keepout(False) if '--zones' in sys.argv: keepout_zones(True) if '--unzones' in sys.argv: keepout_zones(False) if '--plane' in sys.argv: inner_type('PLANE') if '--signal' in sys.argv: inner_type('SIGNAL') if '--unlock' in sys.argv: set_lock(False) if '--lock' in sys.argv: set_lock(True) if '--clear' in sys.argv: clear() if '--push' in sys.argv: push() if '--verify' in sys.argv: sys.exit(0 if verify() else 1) if '--route' in sys.argv: route()