- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
360 行
17 KiB
Python
360 行
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""R2 底板 PCB 的设计规则(M8b)。
|
||
|
||
/c/Python314/python .claude/r2/rules_base.py # 只反查 + 打印,不下发
|
||
/c/Python314/python .claude/r2/rules_base.py --dump # 重抓「位号.引脚 → 网名」
|
||
/c/Python314/python .claude/r2/rules_base.py --push # 下发规则 + 差分对 + 网络指派
|
||
/c/Python314/python .claude/r2/rules_base.py --audit # 读回审计
|
||
|
||
做法与 `pcb_rules.py`(刀片)同源,**那一份的文件头是唯一的说明书**,四个坑一个没变:
|
||
|
||
· `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装
|
||
· `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**
|
||
· 下发之后**别再去 GUI 换「配置」下拉框** —— 换一次整套重置,网络指派全部悬空
|
||
· `overwriteNetRules` / `overwriteNetByNetRules` 都是**整表覆盖**,先读回再合并
|
||
|
||
## 和刀片那份不同的四处
|
||
|
||
1. **两层板**,当前配置是 `JLCPCB Capability(Two Layers Board)`:
|
||
`Track` / `Safe Spacing` 下只有 `copperThickness1oz` / `2oz` 两条,
|
||
**没有内层条目**,所以刀片那段「补 0.5 oz 内层」整块删掉。
|
||
|
||
2. **入口链是 9 A**(方案 §6.4),不是刀片的 1.45 A。线宽规则给到
|
||
`strokeWidthMax = 12`,真正的载流靠 `pour_base` 的断面校核保证,不靠 DRC。
|
||
|
||
3. **七对差分**:五个槽 + ESP32 原生 USB(D13)+ 上行 USB-C。
|
||
刀片那组 `W 0.28 / S 0.18` 是**四层薄介质**的数,两层 1.6 mm 板照搬会变成 ~137 Ω。
|
||
按方案 §6.3 取 **W 1.2 / S 0.2 ⇒ ~94 Ω**。
|
||
⚠ `min` 必须留到 0.25:1.2 的线接不进 U32 那颗 0.5 mm 间距 QFN-64 的焊盘,
|
||
末端一定要缩颈,min 卡在 1.2 会把整条线判成违规。
|
||
|
||
4. **自动命名网一个都不指派**:底板上 22 个 `$xNyy` 全是小信号
|
||
(泄放电阻串、Buck 反馈、DAC 的 RC、风扇 MOS 栅极),走默认线宽即可。
|
||
⇒ 没有「按位号.引脚反查自动网名」那一步,也就没有那一类静默失效。
|
||
"""
|
||
import io
|
||
import json
|
||
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
|
||
|
||
PCB = B.PCB
|
||
LAYERS = 2
|
||
NETS_FILE = os.path.join(HERE, '_base_pcb_nets.json')
|
||
|
||
# ── 线宽规则(mm,外层 1 oz)────────────────────────────────────────────
|
||
# 载流靠灌铜断面保证、不靠 DRC;min 一律压到 0.15~0.3,否则 0402 焊盘扇不出去。
|
||
TRACK_RULES = {
|
||
'PWR9A': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0},
|
||
'PWR12V': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0},
|
||
'FAN12V': {'strokeWidthDefault': 0.8, 'strokeWidthMin': 0.25, 'strokeWidthMax': 3.0},
|
||
'PWR5V': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.20, 'strokeWidthMax': 3.0},
|
||
'PWR3V3': {'strokeWidthDefault': 0.5, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0},
|
||
}
|
||
|
||
# ── 有名字的网络 → 线宽规则 ────────────────────────────────────────────
|
||
# ⚠ `+12V` 是**一个网**:从 Rs6 下游一路到 5 个槽的 12V 针(43 只脚)。
|
||
# 干路 9 A 那一段和「母线 → 单槽」1.5 A 那一段线宽不同,DRC 只能按一条规则判 ⇒
|
||
# 取干路的 `PWR9A`(default 2.0 只是画线的起手宽度),断面由 `pour_base` 逐段校核。
|
||
NET_TRACK = {
|
||
'VIN_RAW': 'PWR9A', # J1 中心极 → F1
|
||
'VIN_FUSED': 'PWR9A', # F1 → Q1 反接保护
|
||
'VIN_SW': 'PWR9A', # Q1 → 储能 → Rs6 上游
|
||
'+12V': 'PWR9A', # Rs6 下游 → 母线 → 5 槽 + 两颗 Buck + 风扇
|
||
'FAN_12V_SW': 'FAN12V', # 风扇下管开关点(R1 §22.6 定的 0.8)
|
||
'+5V': 'PWR5V', # U30 → 5 槽(每槽给刀片 U5 TPS22918)
|
||
'+3V3': 'PWR3V3', # U19 → ESP32 / mux / DAC / 5 槽逻辑
|
||
'+3V3_HUB': 'PWR3V3', # U31 AMS1117 → U32(与 +3V3 只共地)
|
||
}
|
||
|
||
# ── 铺铜焊盘全连接(理由同刀片:不是怕十字花烧,是**铺完之后断面要量得准**)──
|
||
PLANE_RULE = 'PLANE-DIRECT'
|
||
PLANE_CONNECT_DIRECT = 1
|
||
PLANE_NETS = ['VIN_RAW', 'VIN_FUSED', 'VIN_SW', '+12V']
|
||
|
||
# ── 差分对(mm)── 方案 §6.3:两层 1.6 mm 微带,W 1.2 / S 0.2 ⇒ ~94 Ω ──
|
||
DIFF = {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.25, 'strokeWidthMax': 1.6,
|
||
'differentailPairSpacingDefault': 0.20, 'differentailPairSpacingMin': 0.15,
|
||
'differentailPairLenTolerMax': 1.0} # intra-pair skew 目标 ≤1 mm(§6.3)
|
||
PAIR_RULE = 'USBPAIR'
|
||
PAIR_CLEARANCE = 0.20
|
||
PAIRS = [('USB1', 'USB1_DP', 'USB1_DM'),
|
||
('USB2', 'USB2_DP', 'USB2_DM'),
|
||
('USB3', 'USB3_DP', 'USB3_DM'),
|
||
('USB4', 'USB4_DP', 'USB4_DM'),
|
||
('USB5', 'USB5_DP', 'USB5_DM'),
|
||
('USB6', 'USB6_DP', 'USB6_DM'), # D13:Hub 第 6 口 → ESP32 原生 USB
|
||
('USBUP', 'UP_DP', 'UP_DM')] # J23 USB-C → U32 上行
|
||
|
||
# ⚠ 0.2,不是刀片的 0.15。两层板走 JLC 标准工艺(0.2/0.2)最便宜,
|
||
# 而底板上最紧的封装是 U32 QFN-64(0.5 间距)与 J23 USB-C,实测都过得去。
|
||
# 真刷出「贴片焊盘到贴片焊盘」的假错(全落在某个买来的封装内部)再降到 0.15。
|
||
MIN_CLEARANCE = 0.20
|
||
|
||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,2000));" % PCB)
|
||
|
||
|
||
def call(body, timeout=300):
|
||
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']
|
||
|
||
|
||
def dump_nets():
|
||
u"""重抓「位号.引脚 → 网名」。**每次导入变更之后必跑**。"""
|
||
r = call("const out = {};"
|
||
"for (const c of await eda.pcb_PrimitiveComponent.getAll()) {"
|
||
" const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId("
|
||
" c.getState_PrimitiveId());"
|
||
" out[c.designator] = ps.map(p=>[p.padNumber, p.net]); }"
|
||
"return out;")
|
||
io.open(NETS_FILE, 'w', encoding='utf-8').write(json.dumps(r, ensure_ascii=False))
|
||
print(u'已写 _base_pcb_nets.json:%d 件' % len(r))
|
||
return r
|
||
|
||
|
||
def nets():
|
||
if not os.path.exists(NETS_FILE):
|
||
return dump_nets()
|
||
return json.load(io.open(NETS_FILE, encoding='utf-8'))
|
||
|
||
|
||
def check_names():
|
||
u"""下发前先确认:要指派的网名在板上真的存在(写错名字 = 规则悬空,DRC 不报)。"""
|
||
have = set()
|
||
for _ref, pads in nets().items():
|
||
for _pn, net in pads:
|
||
if net:
|
||
have.add(net)
|
||
miss = [n for n in sorted(set(list(NET_TRACK) + PLANE_NETS)) if n not in have]
|
||
pmiss = [p for t in PAIRS for p in t[1:] if p not in have]
|
||
if miss or pmiss:
|
||
print(u' [!!] 板上没有这些网,**不下发**:线宽/铺铜 %s;差分 %s' % (miss, pmiss))
|
||
sys.exit(1)
|
||
return True
|
||
|
||
|
||
CAP_JS = u"""
|
||
const D = eda.pcb_Drc;
|
||
const cur = await D.getCurrentRuleConfiguration();
|
||
const c = JSON.parse(JSON.stringify(cur.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'];
|
||
|
||
setTrack(T['copperThickness1oz'], 0.127, 0.254, 2.54);
|
||
Object.assign(c['Physics']['Via Size']['viaSize'].form,
|
||
{viaOuterdiameterMin: 0.4, viaInnerdiameterMin: 0.3});
|
||
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;
|
||
}
|
||
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;
|
||
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;
|
||
}
|
||
// 安全间距全表**归一到 MINC**:≤0.2 的格子一律赋值成 MINC(不是「只抬不降」);
|
||
// 0.254 / 0.3 那几格是孔/板边相关的,不动。
|
||
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] <= 0.2001 &&
|
||
Math.abs(row[i] - MINC) > 1e-6) { row[i] = MINC; bumped++; }
|
||
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():
|
||
check_names()
|
||
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 格 → %.2f mm;配置名 "%s"' % (r['bumped'], MIN_CLEARANCE, r['name']))
|
||
|
||
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' [!!] 有对没建上 —— 网络名在这块板上不存在')
|
||
|
||
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;
|
||
const seen = [];
|
||
for (const r of rules) {
|
||
if (r.type !== 'net') continue;
|
||
if (T[r.name]) { r['Track'] = T[r.name]; k++; seen.push(r.name); }
|
||
if (pset.has(r.name)) { r['Copper Zone'] = PR; p++; }
|
||
}
|
||
await eda.pcb_Drc.overwriteNetRules(rules);
|
||
return {track: k, plane: p, seen: seen};
|
||
""" % (json.dumps(NET_TRACK, ensure_ascii=False), json.dumps(PLANE_NETS, ensure_ascii=False),
|
||
json.dumps(PLANE_RULE)))
|
||
print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(NET_TRACK)))
|
||
if n['track'] != len(NET_TRACK):
|
||
print(u' [!!] 板上找不到: %s' % ' '.join(sorted(set(NET_TRACK) - set(n['seen']))))
|
||
print(u' 铺铜全连接指派 %d / %d(%s)' % (n['plane'], len(PLANE_NETS), ' '.join(PLANE_NETS)))
|
||
|
||
n = call("""
|
||
const PAIRS = %s, RULE = %s;
|
||
const cur = await eda.pcb_Drc.getNetByNetRules();
|
||
const old = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'] : [];
|
||
const key = e => [e.net1.name, e.net2.name].sort().join('|');
|
||
const want = new Set(PAIRS.map(([n, dp, dm]) => [dp, dm].sort().join('|')));
|
||
const list = [];
|
||
let dropped = 0;
|
||
for (const e of old) {
|
||
if (e.rule === RULE && !want.has(key(e))) { dropped++; continue; }
|
||
list.push(e);
|
||
}
|
||
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, dropped: dropped, ok: ok === true,
|
||
now: (back['Safe Spacing'] || []).length};
|
||
""" % (json.dumps(PAIRS), json.dumps(PAIR_RULE)))
|
||
print(u' 对内间距:新增 %d / 清掉过期 %d,现共 %d 条(%s = %.2f mm)'
|
||
% (n['added'], n['dropped'], n['now'], PAIR_RULE, PAIR_CLEARANCE))
|
||
|
||
call("await eda.pcb_Document.save(); return 1;")
|
||
print(u' 已保存')
|
||
print(u'')
|
||
print(u'⚠ 别去 GUI 的 设计 → 设计规则… 里换「配置」下拉框:换一次整套重置,')
|
||
print(u' %d 个网络的线宽指派立刻悬空,而 DRC 不会报错。' % len(NET_TRACK))
|
||
print(u'⚠ 再导入一次变更之后顺序是:--dump → --push,少跑 --dump 就是拿旧网名指派。')
|
||
|
||
|
||
AUDIT_JS = u"""
|
||
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']),
|
||
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(),
|
||
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)
|
||
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'──── 审计(配置 "%s")────' % r['cfgName'])
|
||
chk(r['layers'] == LAYERS, u'铜层数 %d' % LAYERS, u'铜层数是 %s,应为 %d' % (r['layers'], LAYERS))
|
||
miss = [k for k in TRACK_RULES if k not in r['track']]
|
||
chk(not miss, u'%d 条线宽条目都在' % len(TRACK_RULES), u'缺线宽条目 %s' % miss)
|
||
got = dict(r['assigned'])
|
||
wrong = [(n, got.get(n), v) for n, v in sorted(NET_TRACK.items()) if got.get(n) != v]
|
||
chk(not wrong, u'%d 个网络的线宽指派全对' % len(NET_TRACK), u'指派不对: %s' % wrong[:6])
|
||
chk(sorted(r['pairs']) == sorted([p[0] for p in PAIRS]),
|
||
u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])),
|
||
u'差分对是 %s,应为 %s' % (r['pairs'], [p[0] for p in PAIRS]))
|
||
chk(r['nbn'] == len(PAIRS), u'对内间距 %d 条' % r['nbn'],
|
||
u'对内间距 %d 条,应为 %d' % (r['nbn'], len(PAIRS)))
|
||
gotp = dict(r['planeAssigned'])
|
||
missp = [n for n in PLANE_NETS if gotp.get(n) != PLANE_RULE]
|
||
chk(not missp, u'铺铜全连接 %d 个网络' % len(PLANE_NETS), u'没指派上: %s' % missp)
|
||
print(u' ' + (u'[OK] 全部通过' if not bad else u'[X] %d 条不过' % len(bad)))
|
||
return bad
|
||
|
||
|
||
if __name__ == '__main__':
|
||
print(u'底板 PCB=%s(%d 层)' % (PCB, LAYERS))
|
||
if '--dump' in sys.argv:
|
||
dump_nets()
|
||
if '--push' in sys.argv:
|
||
push()
|
||
audit()
|
||
elif '--audit' in sys.argv:
|
||
audit()
|
||
elif '--dump' not in sys.argv:
|
||
check_names()
|
||
print(u'线宽规则:')
|
||
for k, v in sorted(TRACK_RULES.items()):
|
||
print(u' %-8s 默认 %.2f 最小 %.2f 最大 %.2f'
|
||
% (k, v['strokeWidthDefault'], v['strokeWidthMin'], v['strokeWidthMax']))
|
||
print(u'网络指派:')
|
||
for k, v in sorted(NET_TRACK.items()):
|
||
print(u' %-12s → %s' % (k, v))
|
||
print(u'差分对 %d:%s' % (len(PAIRS), ' '.join(p[0] for p in PAIRS)))
|
||
print(u'安全间距 %.2f;铺铜全连接 %s' % (MIN_CLEARANCE, ' '.join(PLANE_NETS)))
|