电力系统作图工具技术拆解:SVG画布与符号数据模型设计

📅 发布时间:2026/10/18 17:55:14
电力系统作图工具技术拆解:SVG画布与符号数据模型设计
简介这是一款面向电力工程师与系统运维人员的电力系统后台作图软件用于快速绘制电力网络拓扑、设备布局及连接关系内置发电机、变压器、断路器、母线等常用符号库并支持实时状态下的图标闪烁提示便于故障预警与监控。压缩包共108个文件约5.45MB包含C源程序、头文件、目标文件以及位图、图标、工具栏资源等绘图组件适合二次开发或研究电力图形编辑逻辑。已有909人学习下载。除了基础图形编辑、自动布线与图层管理软件还预留了与SCADA等数据源的对接能力可实现动态数据刷新和图表同步帮助使用者从底层理解电力系统可视化工具的构建方式。对于从事电力信息化、配电网设计或监控界面开发的读者这份资源既可直接用作开发基础也能作为学习电力符号建模的参考。1. 电力系统作图软件把单线图绘制时间从两小时压到二十分钟这个项目是一套面向电力系统设计的 Web 端作图工具内置发电机、变压器、断路器、隔离开关、母线、接地等常用电力符号从面板拖到画布上连上线就是一张可存档、可导出的电气主接线图。拆这套代码时最明显的感受是它的核心不是绘图而是把电力符号抽象成了可计算的数据结构。用过通用绘图软件画电力图的人都有体会符号要自己找、端子要对半天、导线要一根根拆成折线这套工具把这些重复劳动全收进了内置符号库。适合电力设计院、变电站后台开发者和需要画系统图的电气专业学生。图纸本质上是一份 JSON改数据就等于改图这为后续做批量出图和参数联动留了很大余地。2. 技术栈与整体架构SVG 画布和符号数据模型的双层设计电力作图工具和通用画图软件的架构思路完全不一样。通用工具追求自由度电力图纸追求的是规范符号必须符合国标图形的比例端子位置必须精确导线必须横平竖直。这些约束决定了技术选型不能只看「能画出来」还要看「能不能约束得住」。2.1 技术选型为什么是 SVG 而不是 Canvas绘制电气主接线图这类场景有很明确的特征画布上的元素量级通常在几十到几百个每个符号需要精确的尺寸和端口坐标导线要随时编辑折点图纸要支持无级缩放。这些需求全部指向 SVG。SVG 的每个符号都是独立的 DOM 节点事件绑定是浏览器原生行为缩放不模糊线宽可以保持恒定Canvas 要实现同样的效果得自己写命中检测和重绘调度符号一多优势又体现不出来。对比维度SVGCanvas元素量级适合几百个以内的节点上千节点更稳定事件绑定每个元素原生 DOM 事件需要手动做命中检测缩放效果矢量不模糊位图会糊要重绘改造成本直接操作节点属性要维护自己的渲染循环上层框架选了 Vue 3 Vite原因也很直接符号库就是一份份结构化的 JS 对象组件层只需要把对象渲染成 SVG 片段属性面板要实时修改符号参数Vue 的响应式系统天然合适。状态管理没有引入重家伙直接用一个集中的 reactive 对象当 store所有图纸操作都走统一的数据修改函数后续做撤销重做也方便。这个组合我认为是这个项目场景下的稳妥选择不是最炫但绝对够用。2.2 目录结构符号库、画布组件和存储层怎么分工这个项目的目录划分很清爽按职责分成四条线组件层负责交互符号层负责定义图形存储层负责图纸数据工具层负责算法。src/ ├── main.js # 应用入口 ├── App.vue # 布局框架左侧符号面板 中部画布 右侧属性面板 ├── components/ │ ├── SymbolPanel.vue # 符号列表按电力设备分类展示 │ ├── CanvasBoard.vue # 画布容器处理拖拽、缩放、框选 │ ├── PropertyPanel.vue # 选中符号的参数编辑区 │ └── ToolBar.vue # 保存、导出、撤销重做按钮 ├── symbols/ │ ├── index.js # 符号注册表聚合所有符号定义 │ ├── generator.js # 发电机符号 │ ├── transformer.js # 双绕组变压器符号 │ ├── breaker.js # 断路器符号 │ ├── disconnector.js # 隔离开关符号 │ ├── busbar.js # 母线符号 │ └── ground.js # 接地符号 ├── store/ │ ├── diagram.js # 图纸数据elements、connections、视图状态 │ └── selection.js # 当前选中元素和端口 └── utils/ ├── export.js # SVG / PNG / JSON 导出 ├── snap.js # 网格吸附计算 ├── connector.js # 导线路径生成 └── id.js # 唯一 id 生成器这里我要多说一句 symbols 目录的设计。它看起来只是放了一堆图形实际上是整个项目的数据源头SymbolPanel 渲染靠遍历这个注册表拖拽创建元素靠读注册表里的定义就连导出图片时查找端口位置也依赖注册表。我一般会把「数据定义」和「渲染逻辑」放得远远的这个项目做到了改符号图形只需要动 symbols 目录不会牵连到画布交互代码。2.3 图纸文件格式一份可读可改的 JSON 规范这个项目最值得借鉴的设计是图纸文件格式。它不是把 SVG 直接序列化存盘而是存了一份结构化的 JSONSVG 只是渲染结果。这样做的好处是一份图纸文件打开之后程序能按类型过滤设备、按属性做统计甚至做简单的拓扑分析。{ version: 1, gridSize: 10, elements: [ { id: node_001, type: generator, x: 120, y: 80, rotation: 0, properties: { ratedVoltage: 10kV, ratedPower: 50MW } }, { id: node_002, type: transformer, x: 200, y: 80, rotation: 0, properties: { capacity: 50MVA, ratio: 110/10kV } } ], connections: [ { id: line_001, from: { elementId: node_001, portId: top }, to: { elementId: node_002, portId: high }, vertices: [] } ] }字段含义说明version 预留了文件迁移的扩展点gridSize 决定吸附粒度每张图纸可以自带不同的网格elements 是画布上的符号实例每个实例记录类型、坐标、旋转角、业务属性connections 是导线记录 from/to 两个端子和 vertices 折点数组。折点为空时由算法自动生成正交路径手动调整过折点后则以手动数据为准。这种结构对二次开发极其友好后面讲批量出图时会再体现一次。3. 核心模块拆解从符号定义到导线连通的完整链路画一张电力图的核心链路其实只有三步从符号库取一个符号定义放到画布上调整位置和属性然后用导线把各个设备的端子连起来。这三步分别对应符号库、画布交互、连接器三个模块下面逐个拆。3.1 符号库设计一个符号就是一个带端子的 SVG 模版先看变压器符号的定义代码这是整个符号库的最小单元// src/symbols/transformer.js export default { type: transformer, name: 双绕组变压器, category: 变压器, width: 80, height: 60, ports: [ { id: high, x: 40, y: 0, direction: top, label: 高压侧 }, { id: low, x: 40, y: 60, direction: bottom, label: 低压侧 } ], render(item) { const c item.properties || {}; return circle cx40 cy18 r14 fill#fff stroke#333 stroke-width2/ text x40 y23 text-anchormiddle font-size12${c.highVoltage || 110kV}/text circle cx40 cy42 r14 fill#fff stroke#333 stroke-width2/ text x40 y47 text-anchormiddle font-size12${c.lowVoltage || 10kV}/text ; } };一个符号定义由五部分组成type 是全局唯一标识name 是面板上显示的名称width/height 是占位尺寸ports 是端子数组render 是渲染函数。ports 是画布交互的关键导线连接的时候找的就是这些端子坐标。render 返回 SVG 字符串传入的 item 携带 properties电压等级这类参数可以直接写进图形文字里。端子 direction 有 top/bottom/left/right 四个取值决定导线从哪个方向接入自动布线时要靠它计算路径起止方向。凡是需要接线的设备端子必须落在图形边缘否则导线会穿进图形内部。内置符号的覆盖面直接决定这个工具好不好用下面是这套项目默认支持的符号清单类型标识显示名称端子数量典型用途generator发电机1电源进线transformer双绕组变压器2主变压器breaker断路器2开关分合disconnector隔离开关2检修隔离busbar母线4汇流排ground接地1中性点接地这个清单不是死的二次开发章节会讲怎么往里面加类型。我的经验是符号库一定要留一个「自定义」分类入口实际交付给电力设计人员时他们总有国标之外的厂标图形需要补充。3.2 画布交互拖放、缩放到网格吸附的实现路径符号从面板拖到画布上要经过三个步骤面板拖起时记录原始符号定义画布 dragover 时计算落点坐标drop 时把符号实例写入 diagram store。落点坐标要做网格吸附不然图纸会越画越乱导线也对不齐。// src/utils/snap.js export function snap(value, gridSize) { return Math.round(value / gridSize) * gridSize; } export function getDropPosition(event, canvasRect, gridSize 10) { const x event.clientX - canvasRect.left; const y event.clientY - canvasRect.top; return { x: snap(x, gridSize), y: snap(y, gridSize) }; }注意 snap 用的是 Math.round 而不是 Math.floor目的是让元素吸附到最近的网格线而不是只往左上角贴。gridSize 默认 10实际可以从图纸文件里读这样每张图都能定义自己的吸附粒度。实现拖放时还有一个常见的坑drop 事件的坐标是相对视口的必须先减去画布容器的 getBoundingClientRect() 偏移才能换算成画布坐标如果画布还有缩放坐标还要再除以 zoom 倍率。忘记这一步符号落点就会和鼠标位置错开一大截。画布上还做了滚轮缩放和平移。缩放实现用的是 transform: scale 加 translate缩放中心对准鼠标位置计算时要把鼠标在画布上的相对坐标和当前缩放比例联动起来。这套交互逻辑不算复杂但写起来容易绕建议单独封装成一个 useCanvasTransform 的 hook别跟拖拽逻辑混在一起。3.3 导线连接和自动布线端子间怎样生成正交路径电力图纸里的导线讲究正交横平竖直、避免斜线交叉这是图纸可读性的来源。连接逻辑分两步先通过端口坐标换算连接点再由连接点生成正交折线路径。// src/utils/connector.js import { snap } from ./snap; export function buildOrthogonalPath(from, to, gridSize 10) { const start { x: snap(from.x, gridSize), y: snap(from.y, gridSize) }; const end { x: snap(to.x, gridSize), y: snap(to.y, gridSize) }; const midY Math.round((start.y end.y) / 2 / gridSize) * gridSize; return [ [start.x, start.y], [start.x, midY], [end.x, midY], [end.x, end.y] ]; }这个函数生成的是先竖后横的 Z 形路径适用于两个端子水平错位不严重的场景。midY 取两个端点的中间值再做一次吸附保证折点也落在网格上。路径生成后写入 connection.vertices用户手动调整折点时替换为自定义折点数组。实际工程里还要考虑符号遮挡当路径中间穿过其他符号时常见做法是检测路径与元素包围盒相交后在相交侧插入偏移点绕行偏移量一般取 40px 左右这个策略实现简单效果也能满足大多数接线场景。导线绘制还有一个细节连接线起点终点要做「端子缩进」即从端子坐标沿方向向量内缩 3 到 5 个像素再开始画线这样导线的端点不会盖住符号的轮廓线视觉上更干净。缩进量不用大大了反而会在密集接线时显得线没接上。4. 运行和部署本地起服务、存图纸、导出图片的完整步骤这部分我把从零开始跑通这个项目的完整流程走一遍包括环境要求、启动命令、图纸保存和图片导出的细节。照着做就能把工具跑起来画第一张图。4.1 环境准备和本地启动项目是标准前端工程依赖 Node.js 环境建议用 18 以上的版本Vite 对旧版本 Node 的支持不友好。启动步骤就三步npm install npm run dev npm run build说明一下每步在干什么npm install 安装依赖这个项目依赖很少核心就是 Vue 全家桶和 Vite装起来很快npm run dev 启动开发服务器默认端口 5173启动后浏览器打开对应地址就能看到左侧符号面板和中间画布npm run build 输出生产版本到 dist 目录后续往后台系统里挂的时候把 dist 配成任意静态目录即可。如果 5173 被占用可以在 vite.config.js 里改 server.port 配置。项目没有后端服务图纸默认保存在浏览器 localStorage 里。第一次上手时建议直接画一张最小测试图拖一个发电机、一个变压器到画布从发电机的端子拉一根线到变压器的端子验证自动布线效果。如果线连不上优先检查端子方向定义这是最常见的配置问题。4.2 图纸保存、导入和图片导出图纸保存就是把我上面说的 JSON 序列化后下载成文件导入则是反序列化并做版本号校验。图片导出是另一个独立流程需要把 SVG 转成 PNG 位图// src/utils/export.js export function exportPNG(svgElement) { const xml new XMLSerializer().serializeToString(svgElement); const svgBlob new Blob([xml], { type: image/svgxml;charsetutf-8 }); const url URL.createObjectURL(svgBlob); const img new Image(); img.onload () { const canvas document.createElement(canvas); canvas.width svgElement.clientWidth * 2; canvas.height svgElement.clientHeight * 2; const ctx canvas.getContext(2d); ctx.fillStyle #ffffff; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob((pngBlob) { const a document.createElement(a); a.href URL.createObjectURL(pngBlob); a.download diagram.png; a.click(); }, image/png); }; img.src url; }这里有两个工程细节值得说。第一canvas.width 取了 SVG 的 2 倍是为了导出图片在打印或缩放展示时保持清晰这个倍率可以做成导出面板里的可选项常见取值是 2 或 3。第二整个流程依赖图片的 onload 回调如果 SVG 内部引用了外部字体或图片资源onload 可能不触发导出就会卡住所以生产环境里这个项目把符号和文字全部内联不依赖外部字体。另外导出前先 fillRect 把画布底色刷白这一步漏了的话透明背景 PNG 在很多看图工具里会显示成黑色。提示导出 SVG 文件本身也是支持的直接把 XMLSerializer 的结果写成 .svg 文件下载即可适合对清晰度要求更高的印刷场景。5. 避坑与常见问题排查符号不显示、连不上线、导出空白这个项目我实际跑下来遇到过不少问题大部分都集中在拖放、连接、导出三条链路上。下面挑四条最有代表性的踩坑记录每条按「现象 → 原因 → 解决」写清楚。5.1 符号拖到画布上不显示控制台也没有报错现象从符号面板拖拽一个发电机到画布画布上什么都没有store 里也没有新增元素记录。原因最常见的是 drop 事件根本没有触发。浏览器默认不允许在普通 div 上放置拖拽内容如果 CanvasBoard 没有在 dragover 里调用 event.preventDefault()drop 事件就不会派发。解决在 CanvasBoard 的 dragover 和 drop 处理器里都调用 preventDefault()并且在 dragover 里设置 dropEffect 为 copy给用户一个可放置的视觉反馈。另外检查拖拽数据是否通过 dataTransfer.setData 写入getData 必须在 drop 事件里同步读取放进 setTimeout 或 Promise 回调里取出来就是空值这一点很多人会踩。5.2 导线端子对不齐连线总是偏一个角现象从变压器高压端拉线到断路器导线的起终点和视觉上的端点明显偏移就像线没有接在设备上。原因端口坐标定义在符号的局部坐标系里但画布上渲染时符号可能被旋转过、或者画布整体有缩放代码里直接用了原始 ports 坐标没有做矩阵换算。解决计算连接点位置时用符号实例的 transform 对端口坐标做一次变换。旋转 90 度时 x/y 要互换并补偿宽高差缩放时坐标要乘以 zoom。最稳妥的做法是拿 SVG 元素的 getScreenCTM() 统一换算但这个 API 性能开销大所以只建议在连接操作和导出的瞬间调用平时拖拽过程不要跑。5.3 导出 PNG 背景全黑或者全白图片里什么都看不见现象导出的 PNG 文件能正常打开但整张图是纯黑色或者纯白色符号完全看不见。原因两种常见因素。黑色背景一般是没有在画布上先填充白色底色透明背景在部分看图软件里显示成黑色白色背景看不到符号则是 XMLSerializer 序列化时漏掉了 SVG 的命名空间浏览器在 img 标签里解析 SVG 时要求 xmlns 属性必须存在。解决序列化之前手动给根 svg 节点补上 xmlnshttp://www.w3.org/2000/svg绘制之前先 ctx.fillStyle #ffffff 并 fillRect 铺满画布。这两处改完导出基本就稳定了。5.4 导入旧图纸文件报 JSON 解析错误现象之前导出过的图纸文件重新拖回页面导入提示 JSON.parse 失败或者部分符号显示错乱。原因可能是文件在传输过程被文本编辑器改坏了编码也可能是项目升级后符号定义里删掉了某个字段旧文件里残留了未注册的类型。解决导入函数里加一层防御逻辑解析 JSON 后逐个校验 elements 里的 type 是否存在于符号注册表未注册的类型给用户弹警告而不是直接让页面崩掉。version 字段低于当前版本时走迁移函数把旧结构转换成新结构迁移函数要一直留在代码里不能因为版本稳定了就删否则老用户的图纸就打不开了。6. 二次开发新增自定义符号和批量出图的改造思路6.1 新增一个自定义符号需要改哪些文件拿到这份代码后最常见的需求是往符号库里加自己单位的特殊设备。完整流程分四步缺一不可在 src/symbols 下新建 js 文件定义符号在 index.js 里注册然后画一张包含该符号的图纸做显示验证最后到导出面板确认图片正常。我一般按这个顺序走和前端的组件开发一样先定义数据、再验证渲染、最后跑通导出链路。注册这一步经常被漏掉漏掉的直接后果是 SymbolPanel 里看不到这个符号但控制台也不报错排查起来要翻一遍注册表才能发现很耽误时间。新增符号时要注意端子坐标和符号尺寸的比例关系。如果符号定义里 width 是 80端口 x 却写了 90这个端口就会跑到图形外面导线连接时会露出明显的断点。6.2 批量出图把台账数据变成图纸的自动化思路如果手上有一份设备台账希望自动生成一批单线图这个项目的数据结构可以让你完全不用碰画布。因为每张图都是 JSON写一个转换脚本就能把台账映射成 elements 和 connections。常见做法是// scripts/generateDiagram.js function buildDiagram(entries) { const elements []; const connections []; entries.forEach((entry, index) { const x 100 index * 220; elements.push({ id: node_${index}, type: entry.deviceType, x, y: 100, properties: entry.params }); }); return { version: 1, gridSize: 10, elements, connections }; }这个脚本只负责生成数据结构不负责渲染。生成完以后把 JSON 导入到工具里画布会自动渲染成图纸端口、导线这些都按符号定义走。如果想要更彻底的自动化可以在脚本里直接 import connector 模块生成连接路径这样导出的 JSON 不需要打开工具就能被下游系统读取做拓扑分析。这是这个项目比直接用绘图软件高明的地方图纸不再是不可编辑的图片而是一份结构化数据可以做设备统计、做接线校验甚至对接后台监控系统。我在给这个项目做扩展时养成的一个习惯是每次改动符号库都强制自己跑一遍「定义、注册、渲染、导出」四步验证再补一张最小图纸做回归确认旧图纸打开正常。这个习惯帮我避开了不少次改完符号导致存量图纸打不开的翻车现场。希望帮到你。本文还有配套的精品资源点击获取