简介面向电力系统绘图开发与接线图设计人员的SVG图元规范文档聚焦单线图中隔离开关、断路器等电力设备符号的标准化定义帮助解决图符方向多、坐标易偏移、跨平台复用难等问题。压缩包内共1个doc文件约5.2MB系统整理竖直与水平隔离开关在刀上/刀下、分/合等状态下的Symbol ID、示意图形及SVG代码并说明以基础图元配合旋转、镜像生成多方向图符的思路。文档给出可直接引用的symbol、viewBox、circle、line等定义示例便于在支持SVG的绘图软件或C、Java等环境中导入复用减少重复绘制并提升图元精度与一致性。已有2980人学习适合需要统一电力图元库、完善图形交换规范的开发者参考。1. 电力图元为什么要从位图搬到 SVG一张单线图背后的三次返工电力图元和电力图符说的是一回事一次接线图上的断路器、隔离开关、变压器、母线、接地符号。不少团队起步时用 PNG 切图或图标字体凑一套小屏看着还行一上指挥中心大屏边缘全是毛刺改一条线宽要重新导一遍图点设备看详情只能靠另一份坐标 JSON 硬对。某公司的能源管理平台在这上面返工了三轮最后把整套图符重做成 SVG 定义。后面要讲的就是这套图元怎么定坐标、怎么留端口、怎么切状态、怎么批量校验——给正在做电力监控组态、单线图编辑器、巡检大屏的前端和平台工程师看。2. 拆开一个电力图符SVG 里到底该画什么、该留什么能用的电力图元不是一张画得好看的图而是一段带约定的几何数据。同一套图元有人拿去做组态编辑器有人拿去做静态单线图还有人拿去喂给拓扑分析——如果图元里只有路径后面两种需求都会被堵死。所以定义图元时要把三件事分开看几何viewBox 与线宽、复用symbol 与 use、语义端口与设备属性。这三样定下来图元库才具备后续扩展空间。2.1 viewBox 与 64 网格图元缩放的锚点定在哪第一件要拍板的事情是画布尺寸。常见做法是每个图元统一viewBox0 0 64 64不跟设备实际尺寸挂钩。理由很实际单线图上一台断路器和一个接地符号在纸面上就是一回事大小由排版决定图元本身只需要一个稳定的内部坐标系。用 64 做基准边长好处是能被 2、4、8、16 整除端口落在 32、16、48 这些位置时不会出现半像素。线宽跟着画布走别写绝对值。约定stroke-width 边长 / 32也就是 64 网格下取 2。这条规则一旦定下来图元放大到 256 或缩到 24视觉重量都一致反过来说从 CAD 导出的 SVG 里那种stroke-width0.0352的写法必须洗掉否则整张图看着像褪了色。另一个常被忽略的是preserveAspectRatio。图元大多是正方形默认值xMidYMid meet就够但如果你的组态画布里允许拖拽拉伸设备框就得显式固定成meet让内容居中留白而不是被压扁成椭圆。母线这类需要横向拉长的图元单独处理用preserveAspectRationone只对矩形填充安全带圆角的符号会变形一般还是做成可平铺的分段件。约定项取值为什么这么定viewBox0 0 64 64能被 2、4、8、16 整除端口落整点基准线宽2 边长 / 32缩放后视觉重量一致端口位置边框中点或 8 的倍数排版时天然对齐栅格端点样式stroke-linecapbutt电气符号要锐利圆头会糊住触头间隙填充默认none实心点单独填便于整体换色文字图元内不出现text字体缺失、设备名动态变化最后一行值得单独说一句。设备名、编号、电压等级这些标注一定是运行时数据写进图元就等于把变化的东西焊死。图元只管形状标注单独一层渲染这是后面所有自动化的前提。2.2 symbol 与 use一份定义撑起整张单线图图元定义放在symbol里不放在g里。区别在于symbol自带viewBox被use引用时可以用width/height重新定尺寸等于自带一次自适应缩放g没有这个能力引用出来就是原尺寸想放大只能在外面套transform套多了坐标系就乱了。一份典型定义长这样!-- assets/symbols/pwr-breaker.svg -- svg xmlnshttp://www.w3.org/2000/svg width0 height0 aria-hiddentrue defs symbol idpwr-breaker viewBox0 0 64 64 g classpwr-shape fillnone strokecurrentColor stroke-width2 stroke-linecapbutt stroke-linejoinmiter path dM4 32 H18 / path dM18 32 L46 16 / path dM46 32 H60 / circle cx18 cy32 r2.5 fillcurrentColor strokenone / circle cx46 cy32 r2.5 fillcurrentColor strokenone / /g circle classpwr-port>{ pwr-breaker: { viewBox: [0, 0, 64, 64], ports: [ { id: in, x: 4, y: 32 }, { id: out, x: 60, y: 32 } ] } }这段 JSON 由构建流程生成、不手工维护图元改了它自动跟着变。前端做接线吸附时读它后端做拓扑连通性判断时也读它两边拿到的是同一份数据。端口命名也要有约定。两点设备用in/out就够了三绕组变压器用hv/mv/lv母线这类多端口设备用p1…pn顺序按从左到右、从上到下排。名字一旦定下来就别改它是拓扑数据的连接键。3.3 做状态分位、合位、接地位的切换实现状态切换最容易做得很不稳定。见过不少实现是在 JS 里改path的d属性字符串拼一拼看着能用等图元加到五十个、状态加到四种代码就变成没人敢碰的黑匣子。稳的做法是把每个状态画成独立的一组用显示隐藏切换symbol idpwr-breaker viewBox0 0 64 64 !-- 公共部分引线与触头支点任何状态都在 -- g classpwr-base fillnone strokecurrentColor stroke-width2 stroke-linecapbutt path dM4 32 H18 M46 32 H60 / circle cx18 cy32 r2.5 fillcurrentColor strokenone / circle cx46 cy32 r2.5 fillcurrentColor strokenone / /g !-- 合位斜线与静触头连通 -- g classpwr-state pwr-state--closed fillnone strokecurrentColor stroke-width2 path dM18 32 H46 / /g !-- 分位斜线抬起 -- g classpwr-state pwr-state--open fillnone strokecurrentColor stroke-width2 path dM18 32 L46 16 / /g !-- 接地位分位基础上加接地符号 -- g classpwr-state pwr-state--grounded fillnone strokecurrentColor stroke-width2 path dM18 32 L46 16 / path dM46 16 V40 M40 40 H52 M42 45 H50 M44 50 H48 / /g circle classpwr-port>.pwr-state { display: none; } [data-stateclosed] .pwr-state--closed, [data-stateopen] .pwr-state--open, [data-stategrounded] .pwr-state--grounded { display: inline; }># tools/check_symbols.py import re import sys import pathlib import xml.etree.ElementTree as ET SVG http://www.w3.org/2000/svg GRID 64 # 统一网格边长 ID_RE re.compile(r^pwr-[a-z0-9](-[a-z0-9])*$) SHAPES {path, line, polyline, polygon, circle, rect} def check(path: pathlib.Path) - list[str]: errs: list[str] [] root ET.parse(path).getroot() for sym in root.iter(f{{{SVG}}}{{}}.format().replace({}, symbol)): sid sym.get(id, ) if not ID_RE.match(sid): errs.append(f{path.name}: id 不合规 - {sid}) vb (sym.get(viewBox) or ).replace(,, ).split() if len(vb) ! 4: errs.append(f{path.name}#{sid}: 缺少 viewBox) elif (float(vb[2]), float(vb[3])) ! (GRID, GRID): errs.append(f{path.name}#{sid}: viewBox 非 {GRID}x{GRID} - {vb[2]}x{vb[3]}) if sym.get(width) or sym.get(height): errs.append(f{path.name}#{sid}: symbol 上不要写 width/height) if not list(sym.iter(f{{{SVG}}}circle)): errs.append(f{path.name}#{sid}: 一个端口都没有) for e in sym.iter(f{{{SVG}}}text): errs.append(f{path.name}#{sid}: 图元内出现 text 元素) for e in sym.iter(): tag e.tag.replace(f{{{SVG}}}, ) if tag in SHAPES: sw e.get(stroke-width) if sw is not None and not (0.8 float(sw) 4): errs.append(f{path.name}#{sid}: 线宽异常 {sw}) return errs if __name__ __main__: bad 0 for f in sorted(pathlib.Path(assets/symbols).rglob(*.svg)): for msg in check(f): print(msg) bad 1 sys.exit(1 if bad else 0)注意上面那段root.iter(...)的写法被我写得很绕写正式脚本时直接用root.iter({http://www.w3.org/2000/svg}symbol)更清楚这里是为了提醒一件事ElementTree匹配带命名空间的标签必须带花括号前缀直接写symbol一个都匹配不到脚本会静默通过所有检查。我第一次写这个校验器就是让这个问题悄悄通过了直到某天发现所有图元都没被检查才发现问题——所以校验脚本本身也要验证故意放一个坏图元进去看它报不报。线宽区间取 0.8 到 4是经验值。CAD 导出的0.0352会被拦下误写成20的粗线也会被拦下。真需要更粗的强调线用class单独处理不走这条规则。脚本退码要正确有错误返回 1CI 才会红。返回 0 而只打印警告等于没接。4.2 从参数表批量生成图符形状规整的图元完全没必要手写用一张 CSV 描述几何脚本生成 SVG。适合批量做的是接地符号、端子、熔断器、电容器这些几何固定的件。symbols.csv长这样id,w,h,sw,paths,ports pwr-ground,64,64,2,M32 16 V36;M20 36 H44;M24 42 H40;M28 48 H36,in:32,16 pwr-fuse,64,64,2,M4 32 H22;M42 32 H60,M22 32 H42;in:4,32;out:60,32生成脚本# tools/gen_symbols.py import csv import pathlib from xml.sax.saxutils import escape TPL svg xmlnshttp://www.w3.org/2000/svg width0 height0 aria-hiddentrue defs symbol id{sid} viewBox0 0 {w} {h} g classpwr-shape fillnone strokecurrentColor stroke-width{sw} stroke-linecapbutt {paths} /g {ports} /symbol /defs /svg PORT_TPL circle classpwr-port>const SVG_NS http://www.w3.org/2000/svg; const GRID 8; // 图面吸附栅格单位是 SVG 用户坐标 // 屏幕坐标 - SVG 用户坐标唯一可靠的方式 function toUserSpace(svg, evt) { const pt svg.createSVGPoint(); pt.x evt.clientX; pt.y evt.clientY; return pt.matrixTransform(svg.getScreenCTM().inverse()); } // 吸附到栅格避免图元落在半像素上 function snap(v, grid GRID) { return Math.round(v / grid) * grid; } function place(svg, symbolId, clientX, clientY, state open) { const p toUserSpace(svg, { clientX, clientY }); const use document.createElementNS(SVG_NS, use); use.setAttribute(href, #${symbolId}); use.setAttribute(x, snap(p.x) - 32); // 减半边长让鼠标落在图元中心 use.setAttribute(y, snap(p.y) - 32); use.setAttribute(width, 64); use.setAttribute(height, 64); use.dataset.state state; use.dataset.symbol symbolId; use.style.color var(--pwr-line, #1f2937); svg.appendChild(use); return use; }getScreenCTM().inverse()返回的是当前渲染矩阵的逆矩阵画布怎么缩放、怎么平移、viewBox怎么设它都算进去了这是唯一不会随实现变化出错的路子。use的x/y定位的是 symbol 左上角所以要减掉 32 才是中心对齐。端口命中做吸附时遍历所有已放置use元素的端口换算成画布坐标后找最近的一个距离阈值给 12 个用户单位。端口坐标从symbol里的circle读cx/cy再加上use的x/y偏移即可。这套算下来接线体验能接近专业组态软件而代码不到一百行。5. 避坑电力图元落地时最容易翻车的五件事第一件CAD 导出的图元一进页面就占满整屏。现象是拖一个断路器进画布整个视口被它盖住线宽细得看不见。原因是导出文件用的是毫米单位viewBox0 0 210 297而容器按 64 逻辑尺寸给了宽度等比放大后自然占满整个屏幕。解决是把图元重绘进 64 网格或者写一个归一化脚本读原viewBox用transform缩放平移到 64×64 再重新计算stroke-width。不做归一化后面所有尺寸对齐的事情都无从谈起。第二件图元缩放后线宽忽粗忽细。现象是同一张图上放大的设备看着比原尺寸的粗一圈。原因是stroke-width用的是用户单位跟着缩放一起变可不同图元的原始尺寸不一致视觉重量就对不上。解决办法是强制所有图元统一 64 网格、统一线宽 2靠一致性抵消缩放差异。vector-effectnon-scaling-stroke能让线宽恒定在屏幕像素但它会让放大后的图元显得单薄电气图慎用。第三件use hrefxxx.svg#id在部分环境里引用不到外部符号。现象是图元位置留白控制台也没有明显报错。原因是跨文件引用受同源与加载时机影响use加载外部资源并非所有环境都稳。稳妥做法是构建期把所有symbol内联成一张 sprite 塞进页面顶部运行时只引用页面内的#id。构建期多做一步运行期就少一类不稳定的失败。第四件端口坐标表和图元不同步接线接到空白处。现象是用户点在图元端子上连接线却连到旁边的位置偏移量还随着缩放变化。原因是端口写在一份手工维护的 JSON 里图元改了几何没回头改 JSON。解决办法只有一个方向端口只写在 SVG 里坐标表由脚本抽取生成构建流程里强制重新生成不让手工编辑。第五件图元里写死设备名换一台设备就显示错。现象是复制出来的断路器还带着上一台的名字改都改不掉。原因是标注用了text直接嵌进symbol。symbol里放文字一是字体不一定有二是内容注定要变三是文字会跟着缩放糊掉。图元只管形状标注层用独立text落在同一个坐标系里渲染字号不随缩放变化。这五条基本都是同一类问题把该由程序管的东西交给了手工。血泪经验就一句话——凡是能由脚本生成的别留在文件里手工调。6. 进阶把图元做成可校验的数据契约图元库真正成熟是它变成上游和下游之间的一份约定前端按它排版后端按它算拓扑设计按它评审。落地方式是一份 manifest构建时从 SVG 里抽出来随包发布python tools/check_symbols.py \ python tools/gen_symbols.py \ python tools/build_manifest.py assets/symbols.manifest.jsonbuild_manifest.py干的事很朴素扫一遍assets/symbols对每个symbol记录viewBox、端口列表、状态 id 列表输出 JSON。前端拿它做吸附和接线校验后端拿它做设备类型到端子数的映射两边不再各存一份。仓库里加一条断言manifest 里的每个端口 id 必须能在symbol里找到对应元素缺一个就让 CI 失败。这条断言运行时间很短却能挡掉绝大多数「改了图元忘了同步」的问题。交付物谁消费变更频率assets/symbols/*.svg前端渲染低需评审symbols.csv生成脚本低需评审symbols.manifest.json前后端共用随图元自动变不手改校验脚本CI稳定后基本不动最后一条习惯是我的私货新加一个图元之前先写它的端口名和设备类型再动手画形状。顺序反过来画完再想端口在哪儿十有八九会画出一个接线接不进去的图元。这套图元定义方式不复杂难的是从第一天就把约定当契约别等到图元表到两百个才回头补校验。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?