ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手搓EDA原理图编辑器:核心难点在数据建模而非画图

手搓EDA原理图编辑器:核心难点在数据建模而非画图 写这篇汇报之前先交代一下背景。过去几周我在做一个完全从零开始的手搓 EDA 项目——不调用任何现成的原理图/PCB 开源内核所有数据模型、交互逻辑、渲染流程都自己写。上一期汇报了项目的整体规划和底层渲染方案这一期轮到核心部分原理图编辑器。整个过程比我预期的要反直觉得多。打开嘉立创EDA 或者 KiCad拖一个电阻到画布拉一根线连到另一个引脚点一下编译看到没有错误整套流程熟练到手指有肌肉记忆。但当你决定自己动手实现这套编辑器你会发现那些平时顺畅到无感的操作背后藏着一整套你从未真正注意过的设计决策。这篇文章的核心判断先放在这里原理图编辑器真正难的不是画而是建模。画线、画矩形、画文本三分钟就能跑通但让软件知道你画的那根线连接了哪两个引脚、这个连接在网络层面意味着什么、器件移动之后连线为什么能跟着走、撤销重做为什么能保持连接关系不变——这才是整个编辑器的分水岭。1. 先想清楚一个问题原理图编辑器到底在编辑什么很多第一次动手做编辑器的人会把它理解成一个矢量画图工具用鼠标在画布上画矩形代表电阻画线条代表导线把图形画得像电路图就行。这个理解会把你带进一个巨大的坑。1.1 表面是图形底层是连接关系如果只做视觉层原理图编辑器就是一个画板和画流程图、画架构图的工具没有本质区别。但真正开始用你会发现差着十万八千里器件移动时连在它引脚上的线要跟着走而且不能把网络关系弄丢。两根线在画布上交叉需要判断是电气连接还是只是跨过。网络标签改名整张图纸上所有相关联的引脚和连线语义上要同步更新。编译检查时程序要能知道某个引脚到底连到了哪个网络有没有漏连、悬空、短路。这些行为都指向同一个事实原理图编辑器内部维护的不是一张图形对象列表而是一张电路连接关系模型。图形只是这个模型的可视化投影。我自己的实现里把整个编辑器分为三层层级内容典型问题图形层器件符号、引脚、导线、文本的几何信息坐标、缩放、旋转、选中高亮逻辑层器件实例、引脚映射、网络归属、连接关系连线是否有效、节点是否合并、位号分配工程层图纸编号、层次结构、编译规则、网表导出层次引用、ERC 检查、输出网表如果一开始就把所有信息塞进一个图形对象里前两三天写起来飞快但每加一个功能都要把所有代码翻一遍。这个坑我踩得很实在后来重构才把三层拆干净。1.2 核心数据模型是三张表不是一棵树参考常见 EDA 工具的通用做法我把逻辑层的数据模型收束成三张核心表# 器件实例表图纸上每一个被放置的元件 component_instance { id: CMP_0001, designator: R1, # 位号 symbol_id: RES_0805, # 引用的符号定义 sheet_id: SHEET_MAIN, # 所在图纸 position: {x: 1520, y: 880}, rotation: 0, # 0/90/180/270 mirrored: False, attributes: {value: 10k, package: 0805} } # 引脚映射表器件实例的引脚与网络的关系 pin_connection { id: PIN_0001, component_id: CMP_0001, symbol_pin_id: 1, # 符号定义里的引脚序号 net_id: NET_0042 # 属于哪个网络 } # 网络表一个网络包含哪些引脚 net { id: NET_0042, name: NET_0042, ref_name: VCC, # 如果连接到电源符号会分配名称 pin_connection_ids: [PIN_0001, PIN_0007, PIN_0031] }这段是简化后的常见写法不是某个官方标准但思路很有代表性先有器件再有引脚然后通过连线建立网络。图形层只是这个模型的投影投影在画布上的信息可以重新布局、重新渲染但底层表始终是权威来源。为什么必须这样设计因为后续的 ERC 检查、网表导出、导入到 PCB 编辑器都依赖这张逻辑表。如果只靠图形坐标去判断连接关系图纸一复杂两个引脚之间隔了七八根线、还跨了子图程序基本无法判断。1.3 位号分配一个不起眼但必须早做的模块早期我把位号直接写死在器件实例上结果删除一个电阻后编号断了档。后来才改成独立的位号分配器每次创建器件时从编号池里按序取号删除器件后编号不会回填避免网表里出现重复位号。位号分配虽然小但它很能说明手搓 EDA 的一个规律越是看起来简单的功能越需要放在数据模型层面统一考虑。位号看似只是一个字符串但它决定了网表能否被下游工具正确解析。2. 渲染方案选型Canvas、SVG 还是直接操作 DOM这一节本来是上一期的主场但原理图编辑器打开之后渲染方案会直接决定交互手感必须再深入展开一次。2.1 三种方案的取舍我先后尝试过三种渲染方式最终留在了 Canvas 2D。方案优点缺点适合场景DOM / 内联 SVG浏览器原生事件、CSS 样式方便、调试直观器件数量上千时有明显卡顿缩放平移需要频繁更新 DOM少量器件、教学演示Canvas 2D绘制性能稳定坐标变换由自己控制容易做脏矩形重绘需要自己实现命中检测、事件映射中大型编辑器大多数 Web EDA 的常见选择WebGL绘制上限极高适合超大图纸开发量大文本渲染和命中检测都更麻烦交互逻辑复杂封装密度极高的 PCB 编辑器原理图编辑器里器件数量通常在几百到几千Canvas 2D 是比较均衡的选择。嘉立创EDA 的网页版原理图编辑也走了类似路线底层渲染细节各家实现不同这里只是说浏览器端编辑器的通用方向。如果只是做一个学习用的小工具DOMSVG 完全够用不必一上来就上 Canvas。我选择 Canvas 主要是为了后面 PCB 编辑器铺路——PCB 编辑器的图元数量和重绘频率会比原理图高一个量级。2.2 坐标变换和网格吸附是最基础的交互地基画布上发生的每一件事几乎都离不开坐标变换。原理图编辑器里存在三层坐标世界坐标器件在电路图中的真实位置单位通常是一个抽象的单位不是像素。视图坐标经过平移和缩放后用户看到的画布坐标系。屏幕坐标鼠标点击时从事件对象里拿到的像素坐标。鼠标点击 → 屏幕坐标 → 逆变换到视图坐标 → 再换算成世界坐标这一步如果算错后面所有操作都会漂移。网格吸附也非常关键。没有网格吸附用户画的线永远对不齐引脚永远差半个格点电气连接根本无法判断。我这里的做法是所有器件放置、引脚移动、连线拐点都强制吸附到网格上网格间距和世界坐标的单位强绑定。// 世界坐标、视图坐标、屏幕坐标的换算常见写法 function screenToWorld(screenX, screenY, viewport) { return { x: (screenX - viewport.offsetX) / viewport.zoom viewport.minX, y: (screenY - viewport.offsetY) / viewport.zoom viewport.minY }; } function snapToGrid(worldX, worldY, gridSize 10) { return { x: Math.round(worldX / gridSize) * gridSize, y: Math.round(worldY / gridSize) * gridSize }; }代码是常见写法不是某个产品的实现。落地时无非是你把 gridSize 设成多少的问题一般取 10 或 100 这种容易心算的整数。2.3 命中检测你怎么知道用户点中了引脚DOM 方案里命中检测是浏览器帮你做的。换成 Canvas 之后这件事必须自己做而且要做两层图元级命中判断鼠标点是否落在某个矩形、线段或文本附近。引脚级命中判断点是否落在引脚的热区内并且要区分是引脚本体、连线端点还是器件本体。我的做法是维护一颗空间索引把所有引脚位置提前按网格桶存好。鼠标移动时只查当前位置附近的几个桶而不是遍历全部图元。看似是优化实际上是必需品图纸里有两千个引脚时遍历检测勉强能用图纸里有两万个引脚时不用索引的版本会明显掉帧。引脚热区不要做得太小。实际使用中用户很少能精确点中那个 1 像素的引脚端点。常见的做法是给一个 8 到 16 像素的吸附半径让用户点到附近就能连上。3. 连线机制看起来是画线其实是在构建网络连线是原理图编辑器里交互最密集、也是最容易翻车的部分。它不像器件的拖放——拖放是无状态的连线是有状态的过程一旦开始用户可能有无数种中断方式。3.1 连线的最小可运行流程我实现的连线流程分五个状态类似有限状态机空闲等待用户点击引脚或连线端点。起点已定用户点击了一个引脚进入待连线状态鼠标移动时显示橡皮筋预览线。拐点规划用户点击画布空白处添加拐点线开始折线化。终点命中用户点击另一个引脚或已有连线连线落定弹出连接确认。取消按 Esc 或右键放弃这次连线。每个状态都要处理边界情况。比如用户在起点已定状态下拖到了画布之外或者在拐点规划时不小心双击这些都要有合理的兜底。这段逻辑我重写了两次。第一次是把它写成了一长串事件回调结果每个回调里都夹着一堆 if完全没法维护。第二次改成状态机每个状态一个类事件进来先转状态再转动作代码量少了一半。3.2 网络合并一根线背后发生了什么连线结束的一瞬间真正要做的事是网络合并。用户看到的是画了一根线但程序要做的是读取起点引脚所属的网络 ID读取终点引脚所属的网络 ID如果两者都是空创建一个新网络把两个引脚加进去如果其中一个有网络把另一个引脚合并进已有网络如果两端的网络都存在但不是同一个要判断是直接合并还是产生一段连接警告。这个判断必须放在数据模型层不能放到 UI 层。否则你画了一条看起来连上的线输出网表时却发现它根本没有电气连接。网络合并是画线和建模的分水岭。手动实现一遍之后你再去看嘉立创EDA 里连线的顺畅感会明白那不是魔法是一整套数据逻辑的支撑。3.3 网络标签和电源符号原理图里除了用线连接还有大量通过网络标签完成的隐式连接。两个引脚分别放在两张子图上标签都写VCC它们就算连上了。实现这个逻辑需要在编译阶段做一个网络匹配操作先把所有飞线物理连接产生的网络整理出来。再把每个网络里出现的标签文本提取出来。最后把具有相同标签文本的网络合并成一个网络。这一套做下来我才真正理解为什么很多 EDA 教学视频里反复强调网络标签要命名规范——因为标签名就是网络身份的字符串签名一个多余的空格就会在逻辑层产生两个不同的网络。4. 从手搓视角看成熟 EDA 的隐藏设计把编辑器做到这里再回头去看嘉立创EDA 的相关教程和热门问题会有完全不一样的体感。4.1 用户遇到的问题往往都是模型边界问题看了一圈热词和常见问题用户问得比较多的包括嘉立创EDA 里怎么画等长线、怎么给 PCB 开窗、怎么用 STP 模型生成封装、怎么布杜邦焊盘孔、怎么加 3D 模型。这些看起来是操作问题但往深一层看每一个都对应编辑器的一个底层能力用户问题底层能力怎么布等长线PCB 编辑器中的长度约束、蛇形线布线算法怎么开窗阻焊层、助焊层的数据结构支持怎么用 STP 生成 PCB 封装3D 模型导入、BOM 坐标换算、封装向导怎么添加 5 个 2.54 间距的杜邦焊盘孔焊盘数组、网格复制、库封装编辑怎么在原理图里与 PCB 联动网表同步、交叉选择、前后端映射对普通用户来说这些是点击某个菜单的问题对正在手搓编辑器的人来说每一个都是一个模块级的功能。比如添加 5 个 2.54 间距的焊盘孔在封装编辑器里至少要支持引脚阵列的批量化生成、按间距排列、自动编号。看起来只是复制个数组但编号规则、原点对称、封装库更新这些细节都是独立的小工程。4.2 成熟工具教会我的四件小事手搓项目的意义不一定是要做出一个能替代嘉立创EDA 的产品而是通过复刻来理解工具背后的设计逻辑。我在这个过程中最有收获的是下面四件事第一快捷键设计不是锦上添花而是建模工具的核心交互范式。原理图编辑器里用户的手别离开键盘太远。方向键微调、空格旋转、X/Y 镜像、R 翻转、Del 删除这些操作如果能顺手整个工作流速度会翻倍。我在自己的编辑器里把快捷键做成一张配置表所有操作都注册成命令而不是散落在事件回调里。第二属性面板是数据模型的可视化编辑入口。选中一个器件右侧面板显示位号、值、封装、旋转状态修改值底层数据更新画布重绘。这个链路本身不难但属性面板和画布的同步机制要一开始就设计好否则会出现改了属性画布没反应、画布改了属性面板没更新的经典不同步问题。第三撤销/重做必须基于命令而不是基于快照。最简单的方式是每执行一个操作把整个模型序列化存一份快照。撤销就是恢复快照。但这种方案在图纸变大后内存消耗很夸张。更常见也更持久的做法是命令模式每个操作都实现 execute 和 undo操作的历史栈只存命令不存完整模型。第四编译检查是从图形世界走向电路世界的出口。原理图编辑器最终的目的不是画得好看而是让下游工具比如 PCB 布局布线拿到正确的电路描述。ERC 检查项至少要包括悬空引脚、单端网络、重复位号、未命名的网络、电源引脚未连接。每一条检查都是对数据模型的一次遍历和校验。写到这里我越来越确信一个观点EDA 工具的本质不是画图软件而是电路信息的录入和转换系统。图形界面只是这个系统的输入法。4.3 手搓项目的适用边界也要说清楚边界。这个项目适合用来学习适合用来让自己的代码能力接受一次系统性的检验但不建议在重要生产任务里完全依赖自研编辑器。成熟 EDA 工具的护城河在于它不仅有编辑器还有一套完整的生态元器件库、PCB 设计规则、制造文件输出、与厂商的对接、海量社区教程。这些不是一个人短期能补齐的。如果你在做类似的实验项目我会建议这几条先定一个足够窄的目标比如只支持基本的 2D 原理图绘制不支持层次设计、不支持仿真。数据模型用 JSON 序列化方便调试和断点恢复不要一开始就设计复杂二进制格式。不要试图复刻成熟工具的所有功能。把最核心的流程跑通比做一堆半成品工具栏更有价值。5. 新手最容易踩的坑和我的排查顺序这部分我用自己的真实翻车经历来写作为一份排查清单也算对自己这段工作的复盘。5.1 最常见的 5 个坑坑现象根因坐标偏移点击器件外侧一段距离才能选中世界坐标、视图坐标、屏幕坐标换算忘了一层连线无效明明画了线编译时引脚还是悬空连线只写进了图形层没有更新网络表撤销异常撤销连线后引脚归属错乱撤销操作没有同时恢复网络表的变更网格错位器件和引脚总差半个格点器件放置和引脚坐标使用了不同 gridSize缩放卡顿拖动滑块后画面明显延迟没有做视图变换裁剪所有图元都在缩放时重绘如果你也在手搓编辑器遇到问题先按这个顺序排查不要急着改渲染代码先看数据模型这一步在调试器里检查器件实例、引脚连接、网络的字段是否和预期一致。数据错了后面全是白忙。再看坐标变换用一个已知坐标的引脚手动算一次 screenToWorld、worldToScreen确认没有偏差。再看命中检测是否受缩放到某个比例后热区变小影响命中半径是否按视图坐标计算。再看事件顺序mousedown、mousemove、mouseup 的顺序是否被打断是否有第三方库拦截了事件。最后才看渲染性能如果是性能问题优先做可视区域裁剪再考虑是否换渲染方案。5.2 一个让我花了两天的问题最典型的一个问题器件旋转 90 度后引脚的连接点还是旧坐标。问题出在旋转变换的顺序上。旋转一个器件时正确的做法是围绕器件原心先旋转引脚的世界坐标再更新图形层的绘制转换。我当时的实现只更新了绘制转换没有同步更新引脚的世界坐标导致连线时命中检测用的还是旧坐标。看起来器件转了但底层引脚还在原地连的线全部连到了错误位置。这个问题的排查过程非常现实一开始怀疑渲染层检查绘制代码好几遍都对后来怀疑命中检测看了热区判断也对最后打开调试器看引脚的实际坐标才发现旋转逻辑漏同步了。这类问题在手搓项目里特别常见因为它不是语法错误而是状态不同步。编辑器里有图形层、逻辑层、工程层三层状态任何一个操作没有同步更新所有层就会出现看起来对、模型错的诡异现象。所以现在我在设计任何操作时都会强制过一遍三层清单这个操作要不要改图形层要不要改逻辑层要不要改动工程层三层都改了才算做完。6. 一路手搓下来我对原理图编辑器有了哪些新理解做完这一期之后我对原理图编辑器的理解发生了几处明显的转变。第一我越来越觉得原理图编辑器是一个电路信息录入系统不是画图软件。它最大的价值在于把人的设计意图转成机器可以理解的结构化数据。这个理念会直接决定架构、模块划分和代码风格。第二交互细节的重要性被严重低估了。网格吸附、引脚热区、连线拐点、旋转镜像、撤销重做每一个都是小事合在一起就是有没有专业感的分水岭。用户感知不到某个功能的存在恰恰说明它做对了。第三工具链和生态比编辑器本身难得多。手搓到一定程度你会发现把图纸导出成制造文件、对接元件库、处理大量封装数据这些环节的工作量远超编辑器本体。这也是为什么最终产品还是要回到像嘉立创EDA 这样成熟的工具上不是因为它不可替代而是因为它的生态足够厚。如果你也打算启动一个类似的实验项目我的建议是先做一个只支持 50 个器件、3 张图纸的迷你原理图编辑器把器件放置、引脚连接、网络合并、简单 ERC 检查完整跑通。不要一上来就做层次设计、仿真、网表导入导出。先把最小闭环走完你会获得比看一百篇教程都更深刻的理解。原理图编辑器从来不是画布上的那几根线。它是一整套关于如何表达电路连接关系的设计决策排列在画布背后的逻辑空间里。手搓一次你会把这些决策全部重新经历一遍然后真正看明白那些每天点来点去的按钮背后到底发生了什么。
返回列表