# -*- coding: utf-8 -*- u"""载板 PCB2-CARRIER 设计规则 + D± 差分对(2026-09-05)。 做的事和下板的 `pcb_rules.py` 一模一样,只是网络表和对数不同 —— **两个坑、两条陷阱全部照抄那边**,那边的文件头是唯一的说明书,改规则前先读它: · `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装 · `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**, 两者不能混用(混了会被静默吃掉) · 下发之后**别再去 GUI 换“配置”下拉框**,换一次整套重置、网络指派全部悬空 · 写进去的 mm 会被换算成 mil 再换回来,`0.2` 回读是 `0.19999959999999997`, 任何几何比较都留 1e-4 容差 ## ⚠⚠ 必须在「从原理图导入变更」**之后**跑(2026-09-08 实测) 网络指派(差分对 / 线宽 / 铺铜连接方式)是**按网络名**挂上去的, 而网络是元件导入时才在 PCB 上出现的。在空板上跑本脚本: 差分对 0 / 11 对 线宽指派 0 / 12 个网络 铺铜全连接指派 0 / 5 个网络 —— **而“规则配置已下发”那几行照常打印成功**,看上去像做完了。 导入元件后重跑一次就是 11/11、12/12、5/5。 ⇒ 重建板子的顺序是:**切 4 层 → 导入元件 → 下规则 → 摆位**。 判据就是上面那三个分数,不是“没报错”。 ## 和下板不同的三处 1. **差分对 11 对,不是 7 对。** 载板上每一路 D± 被 ESD 器件 `U5n` 切成两段: `USBn_DP/DM`(U32 → U5n,长段,走底层)与 `USBn_DPJ/DMJ`(U5n → J7n,约 5 mm,走顶层)。 两段都是同一条 90 Ω 链路,**两段都要建对**,否则短段会按普通信号走、 在 ESD 器件那头出现一截不受控阻抗。加上 `UPSTREAM` 共 11 对。 载板没有 `DEBUG` 那一对(ESP32 在下板)。 2. **几何 W0.28 / S0.18 直接照搬,不重算**(§25.6 ①)。嘉立创四层 1.6 mm 标准叠层上下对称(L1-L2 与 L3-L4 都是 0.2104 mm 介质),而载板的 层角色是镜像的(Inner1 = 电源面 / Inner2 = 完整地平面), 所以底层 D± 以 Inner2 为参考,介质厚度与下板顶层 D± 以 Inner1(地) 为参考完全相同。 ⚠️ 顶层那 5 段 `*J` 的参考面是 **Inner1(分割的电源面)**,不是完整地。 段长只有 ~5 mm、又紧挨 ESD 器件,可以接受;但铺铜时要保证 每个 `U5n`↔`J7n` 下方的 Inner1 是**同一块**铜(VBUSn 岛),别让分割缝横穿这 5 mm。 3. **功率网络分三档**,不是两档: | 规则 | 默认线宽 | 网络 | 依据 | |---|---|---|---| | `PWR4A` | 1.5 mm | `VO1_P`~`VO5_P` | 每路 4 A(IPC-2221 外层 1 oz,ΔT≈17 °C)| | `PWR5V` | 1.2 mm | `+5V` | §9.4 最坏 2.8 A(1.2 mm 可载 3.6 A)| | `PWR1A` | 0.6 mm | `VBUS1`~`5`、`+3V3_HUB` | 下行 VBUS 每口 0.5 A;Hub 3V3 最坏 0.42 A | **最小值一律 0.25~0.3 mm**,理由同下板:规则是**按网络**生效的, `+5V` / `VBUS` 上挂着 0402 去耦(焊盘才 0.57 mm 宽), 卡大线宽会让几十个焊盘扇不出去、刷出一屏假 DRC。 **载流靠灌铜保证,不靠 DRC**(§10.1)。 用法: /c/Python314/python .claude/pcb2_rules.py [--push | --check] """ import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run PCB = '942ed1560d81c1e7' # PCB2-CARRIER # ── 线宽规则(mm)────────────────────────────────────────────────────── TRACK_RULES = { 'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0}, 'PWR5V': {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0}, # ⚠️ `strokeWidthMin` 2026-09-06 从 0.25 降到 **0.15**(和 DIFF 那次同一条道理): # `+3V3_HUB` 要在 Hub 卫星圈 1.2 mm 网格之间的 0.659 mm 缝里穿过去, # 0.25 的线要 0.65 + 量化余量,过不去;收到 0.15 才通。 # 载流上完全够:这一路最坏 420 mA,0.15 mm 外层 1 oz 在 ΔT=10 °C 下可载 ~0.8 A, # 而且窄的只是几段几毫米长的颈,干路仍然是 0.35。 # **规则是按网络生效的**,最小值只能按最窄的那一段定 —— 同 §10.1。 'PWR1A': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0}, } NET_TRACK = {} for _n in range(1, 6): NET_TRACK['VO%d_P' % _n] = 'PWR4A' NET_TRACK['VBUS%d' % _n] = 'PWR1A' NET_TRACK['+5V'] = 'PWR5V' NET_TRACK['+3V3_HUB'] = 'PWR1A' # ── 差分对几何(mm)── 与下板同一组数,理由见文件头第 2 条 # ⚠️ `strokeWidthMin` 2026-09-06 从 0.20 降到 **0.10**:BM24(0.35 间距)的 D± 出线 # 必须收窄到 0.11 mm 才能对相邻 GND 焊盘留住 0.20 的安全间距(算式见 pcb2_route_dp.W_ARR)。 # 和 §10.1 里 PWR4A/PWR12V 把最小值降到 0.3 是同一条道理:**规则是按网络生效的**, # 一个网络上既有 0.28 的主干也有进细间距焊盘的颈,最小值只能按最窄的那一段定。 # 默认值仍是 0.28,交互布线画出来还是 0.28。 DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.10, 'strokeWidthMax': 0.60, 'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15, 'differentailPairLenTolerMax': 5.0} PAIR_RULE = 'USBPAIR' PAIR_CLEARANCE = 0.15 PAIRS = [('UPSTREAM', 'UP_DP', 'UP_DM')] PAIRS += [('USB%d' % n, 'USB%d_DP' % n, 'USB%d_DM' % n) for n in range(1, 6)] PAIRS += [('USB%dJ' % n, 'USB%d_DPJ' % n, 'USB%d_DMJ' % n) for n in range(1, 6)] MIN_CLEARANCE = 0.2 # ── 铺铜的焊盘连接方式(Plane / Copper Zone)──────────────────────────── # 默认条目 `copperRegion` 的 `connectMode` 是 **0 = 十字花**,辐条宽 0.254 mm。 # 2026-09-05 实测(改前/改后各铺一次、读回 `getState_PourFills()` 对比): # connectMode 0 -> 每个本网通孔焊盘周围一圈反焊盘 + 4 条 `fill:false` 的描边辐条, # VO4 那块铜面吐出 18 个 fill(1 个填充区 + 17 条辐条); # connectMode 1 -> **全连接**,焊盘并进铜里,同一块铜面只剩 1 个 fill。 # 给五路 VOn_P 单独挂 `PLANE-DIRECT`,GND 留默认的十字花: # · VOn_P 是 4 A 主路,每路在内层只有 5 个通孔焊盘(3 个排母针 + TP8n1 + C8n1.1), # 而且只贴 Inner1 一层,全连接之后手焊仍可行,铜面几何也才量得准 # (十字花时铜面多边形把自己的焊盘挖空,`inner1_verify.py` 量到的断面会是 0.3 mm, # 那是**度量假象**不是真断,但真假分不出来 —— 全连接之后这个歧义就没了); # · GND 有 ~200 个通孔焊盘(光排母就 160 针),且同时贴 Inner1 底填和 Inner2 平面, # 全连接会让手焊变得很难受;而回流被 26 根排母针分掉,辐条完全够 —— 见下。 # # ⚠️ 「辐条太细扛不住 4 A」是个**看着对但错**的直觉:IPC-2221 的线宽表是给**长导体**的, # 辐条长度只有 0.254 mm(就是净距那一段),两端都焊在大铜面上。 # 单条辐条 R = ρL/(Wt) = 1.72e-8 × 0.254e-3 / (0.254e-3 × 17.4e-6) ≈ 0.99 mΩ; # 4 条并联 0.25 mΩ,4 A 上压降 1 mV / 发热 16 mW, # 两端定温的均匀发热棒 ΔT ≈ PL/(8kA) ≈ **0.02 K**。所以十字花在电气上不是瓶颈, # 这里改全连接是为了**几何可验证 + 少一处不确定**,不是因为十字花会烧。 PLANE_RULE = 'PLANE-DIRECT' PLANE_CONNECT_DIRECT = 1 # 0 = 十字花(默认), 1 = 全连接(实测) PLANE_NETS = ['VO%d_P' % _n for _n in range(1, 6)] OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,1500));" % PCB) def call(body, timeout=240): r = run(OPEN + body, timeout=timeout) if not r.get('success'): print(u'失败:', json.dumps(r, ensure_ascii=False)[:600]) sys.exit(1) return r['result'] CAP_JS = u""" const D = eda.pcb_Drc; const cur = await D.getCurrentRuleConfiguration(); const c = JSON.parse(JSON.stringify(cur.config)); // 嵌套形状,原样送回 const multi = (await D.getRuleConfiguration( 'JLCPCB Capability(Multiple Layers Board)')).config; // 平形状,只当数据源 const TRACKS = %s, DW = %s, MINC = %s, PAIRRULE = %s, PAIRC = %s; const PLANERULE = %s, PLANEMODE = %s; const setTrack = (e, mn, df, mx) => { for (const k of Object.keys(e.form.data)) Object.assign(e.form.data[k], {minValue: mn, defaultValue: df, maxValue: mx}); }; const T = c['Physics']['Track'], SS = c['Spacing']['Safe Spacing']; // ── 1) 多层板能力集:三个放宽值 + 两条内层条目 ── setTrack(T['copperThickness1oz'], 0.102, 0.254, 2.54); if (!T['copperThickness0.5oz']) { const e = JSON.parse(JSON.stringify(T['copperThickness1oz'])); e.editName = 'copperThickness0.5oz'; e.isSetDefault = false; setTrack(e, 0.089, 0.254, 2.54); T['copperThickness0.5oz'] = e; } Object.assign(c['Physics']['Via Size']['viaSize'].form, {viaOuterdiameterMin: 0.3, viaInnerdiameterMin: 0.2}); if (!SS['copperThicknessInner0.5oz']) { const e = JSON.parse(JSON.stringify(SS['copperThickness1oz'])); e.editName = 'copperThicknessInner0.5oz'; e.isSetDefault = false; const src = multi['Spacing']['Safe Spacing']['copperThicknessInner0.5oz'].table; for (const k of Object.keys(e.tables)) e.tables[k].content = JSON.parse(JSON.stringify(src)); SS['copperThicknessInner0.5oz'] = e; } // ── 2) 功率线宽规则 ── for (const name of Object.keys(TRACKS)) { const f = TRACKS[name]; const e = JSON.parse(JSON.stringify(T['copperThickness1oz'])); e.editName = name; e.isSetDefault = false; e.unit = 'mm'; setTrack(e, f.strokeWidthMin, f.strokeWidthDefault, f.strokeWidthMax); T[name] = e; } // ── 3) 差分对几何 ── const dp = c['Physics']['Differential Pair']['differentialPair'].form; for (const k of Object.keys(dp.strokeWidthTables.data)) Object.assign(dp.strokeWidthTables.data[k], {minValue: DW.strokeWidthMin, defaultValue: DW.strokeWidthDefault, maxValue: DW.strokeWidthMax}); for (const k of Object.keys(dp.diffPairSpacingTables.data)) Object.assign(dp.diffPairSpacingTables.data[k], {minValue: DW.differentailPairSpacingMin, defaultValue: DW.differentailPairSpacingDefault}); dp.differentailPairLenTolerMax = DW.differentailPairLenTolerMax; // ── 3b) 差分对对内间距条目(只给 net-by-net 用,不设为默认)── if (!SS[PAIRRULE]) { const e = JSON.parse(JSON.stringify(SS['copperThickness1oz'])); e.editName = PAIRRULE; e.isSetDefault = false; for (const tk of Object.keys(e.tables || {})) for (const row of (e.tables[tk].content || [])) for (let i = 0; i < row.length; i++) if (typeof row[i] === 'number') row[i] = PAIRC; SS[PAIRRULE] = e; } // ── 4) 安全间距全表抬到下限 ── let bumped = 0; for (const key of Object.keys(SS)) if (key !== PAIRRULE) for (const tk of Object.keys(SS[key].tables || {})) for (const row of (SS[key].tables[tk].content || [])) for (let i = 0; i < row.length; i++) if (typeof row[i] === 'number' && row[i] < MINC) { row[i] = MINC; bumped++; } // ── 5) 铺铜焊盘连接方式:给功率网络单独开一条「全连接」条目 ── const CZ = c['Plane']['Copper Zone']; if (!CZ[PLANERULE]) { const e = JSON.parse(JSON.stringify(CZ['copperRegion'])); e.editName = PLANERULE; e.isSetDefault = false; CZ[PLANERULE] = e; } for (const m of ['singleLayerPadModel', 'multiLayerPadModel']) for (const k of Object.keys(CZ[PLANERULE].form[m].data)) CZ[PLANERULE].form[m].data[k].connectMode = PLANEMODE; const ok = await D.overwriteCurrentRuleConfiguration(c); // ⚠️ 传本体 return {ok: ok === true, bumped: bumped, name: cur.name, track: Object.keys(T), spacing: Object.keys(SS), plane: Object.keys(CZ)}; """ def push_config(): r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE, json.dumps(PAIR_RULE), PAIR_CLEARANCE, json.dumps(PLANE_RULE), PLANE_CONNECT_DIRECT)) if not r.get('ok'): print(u' [!!] overwriteCurrentRuleConfiguration 返回非 true,没生效') sys.exit(1) print(u' 规则配置已下发:线宽条目 %s' % ' '.join(r['track'])) print(u' 安全间距抬高 %d 格,间距条目 %s' % (r['bumped'], ' '.join(r['spacing']))) print(u' 铺铜连接方式条目 %s' % ' '.join(r['plane'])) print(u' 配置名现在是 “%s”' % r['name']) def caveat(): print(u'') print(u'──── 一条必须记住的事 ────') print(u'⚠️ 别去 GUI 的 设计 → 设计规则… 里换“配置”下拉框。') print(u' 换一下整套重置,PWR4A / PWR5V / PWR1A 会被冲掉,') print(u' %d 个网络的线宽指派立刻悬空(DRC 不报错,线宽也就不生效)。' % len(NET_TRACK)) print(u' 要改规则就改 pcb2_rules.py 再 --push。') def _nums(o): if isinstance(o, bool): return [] if isinstance(o, (int, float)): return [float(o)] if isinstance(o, dict): return [v for x in o.values() for v in _nums(x)] if isinstance(o, list): return [v for x in o for v in _nums(x)] return [] def _find(o, key_sub): if isinstance(o, dict): for k, v in o.items(): if key_sub.lower() in str(k).lower() and isinstance(v, (int, float)) \ and not isinstance(v, bool): return float(v) for v in o.values(): r = _find(v, key_sub) if r is not None: return r elif isinstance(o, list): for v in o: r = _find(v, key_sub) if r is not None: return r return None AUDIT_JS = """ await new Promise(r=>setTimeout(r,1500)); const c = await eda.pcb_Drc.getCurrentRuleConfiguration(); const rules = await eda.pcb_Drc.getNetRules(); const nbn = await eda.pcb_Drc.getNetByNetRules(); return { cfgName: c && c.name, layers: await eda.pcb_Layer.getTheNumberOfCopperLayers(), track: Object.keys(c.config.Physics['Track']), cap: { track1oz: c.config.Physics['Track']['copperThickness1oz'], via: c.config.Physics['Via Size']['viaSize'], half: !!c.config.Physics['Track']['copperThickness0.5oz'], inner: !!c.config.Spacing['Safe Spacing']['copperThicknessInner0.5oz'], }, diff: c.config.Physics['Differential Pair'], spacing: c.config.Spacing['Safe Spacing'], pairs: (await eda.pcb_Drc.getAllDifferentialPairs()).map(d => d.name).sort(), assigned: rules.filter(r => r['Track'] && r['Track'] !== 'default') .map(r => [r.name, r['Track']]).sort(), planeEntries: Object.keys(c.config['Plane']['Copper Zone']), planeMode: ((((c.config['Plane']['Copper Zone'][%s] || {}).form || {}) .multiLayerPadModel || {}).data || {})['1'], planeAssigned: rules.filter(r => r['Copper Zone'] && r['Copper Zone'] !== 'default') .map(r => [r.name, r['Copper Zone']]).sort(), nbn: ((nbn && nbn['Safe Spacing']) || []).length, }; """ def audit(): r = call(AUDIT_JS % json.dumps(PLANE_RULE)) bad = [] def chk(cond, good, msg): print((u' [OK] ' if cond else u' [!!] ') + (good if cond else msg)) if not cond: bad.append(msg) print(u'──── 审计 ────') chk(r['layers'] == 4, u'铜层数 4', u'铜层数是 %s,应为 4' % r['layers']) cap = r['cap'] tmin = _find(cap['track1oz'], 'minValue') if tmin is None: tmin = _find(cap['track1oz'], 'strokeWidthMin') vout = _find(cap['via'], 'viaOuterdiameterMin') vin = _find(cap['via'], 'viaInnerdiameterMin') okcap = (tmin is not None and abs(tmin - 0.102) < 5e-4 and vout is not None and abs(vout - 0.3) < 5e-4 and vin is not None and abs(vin - 0.2) < 5e-4 and cap['half'] and cap['inner']) chk(okcap, u'四层板能力集已就位(最小线宽 %.3f / 过孔 %.2f-%.2f,含 0.5oz 内层条目)' % (tmin or -1, vout or -1, vin or -1), u'还是两层板能力集:最小线宽 %.3f(应 0.102)/ 过孔 %.2f-%.2f(应 0.30-0.20)' u' / 0.5oz 线宽条目 %s / 内层间距条目 %s' % (tmin if tmin is not None else -1, vout if vout is not None else -1, vin if vin is not None else -1, u'有' if cap['half'] else u'无', u'有' if cap['inner'] else u'无')) print(u' (配置名 “%s”)' % r['cfgName']) for name in sorted(TRACK_RULES): chk(name in r['track'], u'线宽规则 %s 存在' % name, u'线宽规则 %s **不存在** —— 有 %d 个网络指着它,现在是悬空的' % (name, sum(1 for _, t in r['assigned'] if t == name))) w = _find(r['diff'], 'strokeWidthDefault') if w is None: w = _find(r['diff'], 'defaultValue') sp = _find(r['diff'], 'PairSpacingDefault') if sp is None: d2 = (r['diff'] or {}).get('differentialPair', {}).get('form', {}) \ .get('diffPairSpacingTables') sp = _find(d2, 'defaultValue') chk(w is not None and abs(w - DIFF['strokeWidthDefault']) < 5e-3 and sp is not None and abs(sp - DIFF['differentailPairSpacingDefault']) < 5e-3, u'差分对几何 W %.3f / S %.3f mm' % (w or -1, sp or -1), u'差分对几何是 W %.3f / S %.3f,应为 W %.2f / S %.2f mm' % (w if w is not None else -1, sp if sp is not None else -1, DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'])) # ⚠️ 容差留 1e-4:mm→mil→mm 往返,0.2 回读是 0.19999959999999997 vals = [v for v in _nums(r['spacing']) if 0 < v < 10] mn = min(vals) if vals else -1 chk(mn >= PAIR_CLEARANCE - 1e-4, u'安全间距下限 %.4f mm(对内那条 %.2f)' % (mn, PAIR_CLEARANCE), u'安全间距下限只有 %.4f mm' % mn) others = [v for k in r['spacing'] if k != PAIR_RULE for v in _nums(r['spacing'][k]) if 0 < v < 10] mno = min(others) if others else -1 chk(mno >= MIN_CLEARANCE - 1e-4, u'非差分对间距下限 %.4f mm' % mno, u'非差分对间距下限只有 %.4f mm,应抬到 %.2f mm' % (mno, MIN_CLEARANCE)) want = sorted(n for n, _, _ in PAIRS) chk(r['pairs'] == want, u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])), u'差分对对不上:有 %s,应为 %s' % (r['pairs'], want)) pm = r.get('planeMode') or {} chk(PLANE_RULE in (r.get('planeEntries') or []) and pm.get('connectMode') == PLANE_CONNECT_DIRECT, u'铺铜连接方式条目 %s 在(connectMode=%s 全连接)' % (PLANE_RULE, pm.get('connectMode')), u'没有 %s 条目或 connectMode 不是 %d —— 五路电源岛会退回十字花' % (PLANE_RULE, PLANE_CONNECT_DIRECT)) pa = r.get('planeAssigned') or [] chk(sorted(n for n, _ in pa) == sorted(PLANE_NETS) and all(v == PLANE_RULE for _, v in pa), u'铺铜全连接指派 %d 个网络' % len(pa), u'铺铜全连接指派是 %s,应为 %s 全挂 %s' % (pa, PLANE_NETS, PLANE_RULE)) chk(len(r['assigned']) == len(NET_TRACK), u'线宽指派 %d 个网络' % len(r['assigned']), u'线宽指派只有 %d 个,应为 %d' % (len(r['assigned']), len(NET_TRACK))) chk(r['nbn'] == len(PAIRS), u'对内间距规则 %d 条' % r['nbn'], u'对内间距规则 %d 条,应为 %d' % (r['nbn'], len(PAIRS))) if bad: caveat() return not bad def main(): print(u'载板 PCB2 规则:线宽 %d 条(%s),安全间距下限 %.2f mm,四层板能力集' % (len(TRACK_RULES), ' '.join(sorted(TRACK_RULES)), MIN_CLEARANCE)) print(u'差分对:W %.2f / S %.2f / 线长容差 %.1f mm,共 %d 对' % (DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'], DIFF['differentailPairLenTolerMax'], len(PAIRS))) if '--check' in sys.argv: sys.exit(0 if audit() else 1) if '--push' not in sys.argv: print(u'(未加 --push,没有下发;--check 审计当前状态)') return push_config() n = call(""" const P = %s; let k = 0; for (const d of await eda.pcb_Drc.getAllDifferentialPairs()) await eda.pcb_Drc.deleteDifferentialPair(d.name); for (const [name, p, m] of P) { if (await eda.pcb_Drc.createDifferentialPair(name, p, m)) k++; } return k; """ % json.dumps(PAIRS)) print(u' 差分对 %d / %d 对' % (n, len(PAIRS))) if n != len(PAIRS): print(u' [!!] 有对没建上 —— 多半是网络名在这块板上不存在,去 _pcb2_nets.json 核') # ⚠️ `getNetRules()` 回来的是**整张表**,`overwriteNetRules` 整表覆盖 —— # 线宽(Track)和铺铜连接方式(Copper Zone)必须在**同一次读改写**里做完, # 分两次写的话后一次会拿着旧快照把前一次冲掉。 n = call(""" const T = %s, PN = %s, PR = %s; const pset = new Set(PN); const rules = await eda.pcb_Drc.getNetRules(); let k = 0, p = 0; for (const r of rules) { if (r.type !== 'net') continue; if (T[r.name]) { r['Track'] = T[r.name]; k++; } if (pset.has(r.name)) { r['Copper Zone'] = PR; p++; } } await eda.pcb_Drc.overwriteNetRules(rules); return {track: k, plane: p}; """ % (json.dumps(NET_TRACK, ensure_ascii=False), json.dumps(PLANE_NETS), json.dumps(PLANE_RULE))) print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(NET_TRACK))) print(u' 铺铜全连接指派 %d / %d 个网络(%s)' % (n['plane'], len(PLANE_NETS), ' '.join(PLANE_NETS))) if n['plane'] != len(PLANE_NETS): print(u' [!!] 有网络没指派上 —— 没指派 = 退回十字花,4 A 主路的铜面几何就量不准了') # ⚠️ overwriteNetByNetRules 是整表覆盖,先读回再合并 n = call(""" const PAIRS = %s, RULE = %s; const cur = await eda.pcb_Drc.getNetByNetRules(); const list = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'].slice() : []; const key = e => [e.net1.name, e.net2.name].sort().join('|'); const have = new Set(list.map(key)); let k = 0; for (const [name, dp, dm] of PAIRS) { const e = {net1: {type: 'net', name: dp}, net2: {type: 'net', name: dm}, rule: RULE, copperRuleName: null, planeRuleName: null}; if (have.has(key(e))) continue; list.push(e); k++; } const ok = await eda.pcb_Drc.overwriteNetByNetRules( {'Safe Spacing': list, 'Creepage Distance': (cur && cur['Creepage Distance']) || []}); const back = await eda.pcb_Drc.getNetByNetRules(); return {added: k, ok: ok === true, now: (back['Safe Spacing'] || []).length}; """ % (json.dumps(PAIRS), json.dumps(PAIR_RULE))) print(u' 对内间距规则:新增 %d 条,现共 %d 条(%s = %.2f mm)' % (n['added'], n['now'], PAIR_RULE, PAIR_CLEARANCE)) if n['now'] != len(PAIRS): print(u' [!!] 应为 %d 条' % len(PAIRS)) call("await eda.pcb_Document.save(); return 1;") print(u' 已保存') caveat() if __name__ == '__main__': main()