OpenPLC Editor v4 FBD 与 SFC 图形编辑器
OpenPLC 编辑器通过构建在 ReactFlow (@xyflow/react) 之上的节点与边画布架构,实现了图形化 PLC 编程。功能块图 (FBD) 编辑器是成熟且功能完备的实现;顺序功能图 (SFC) 编辑器目前仅作为结构占位符,等待后续实现。这两个编辑器均由统一的图形编辑器调度器分发,该调度器会根据 POU 声明的语言(fbd、sfc或ld)选择相应的组件。本页将介绍定义这些图形编辑器的架构模式、节点类型分类、数据流以及调试集成。
图形编辑器分发与语言路由
图形编辑器系统使用基于语言的区分联合来选择正确的编辑器组件。GraphicalEditor组件接收language作为属性(取值为'ld' | 'sfc' | 'fbd'之一),并通过简单的查找对象将其映射到相应的编辑器组件。这种模式保持了分发逻辑的声明性,并确保每种语言变体都是独立、自包含的 React 组件。在查看历史提交时,会应用一个只读覆盖层,使用cursor-not-allowed指示器拦截所有指针事件。
| 语言 | 组件 | 状态 |
|---|---|---|
fbd | FbdEditor | 已完全实现 |
sfc | SfcEditor | 未实现 |
ld | LadderEditor | 已完全实现 |
FBD 节点类型分类与构建器模式
FBD 编辑器定义了七种不同的节点类型,每种类型都有专用的构建器函数和可视化组件。节点类型注册表集中存储在customNodeTypes映射中,ReactFlow 使用它来解析哪个组件渲染哪个节点。并行的nodesBuilder映射提供了在构建新节点(拖放、复制粘贴或编程式创建)时使用的工厂函数。
| 节点类型 | 组件 | 构建器 | 描述 |
|---|---|---|---|
block | BlockNodeElement | buildBlockNode | 带有类型化输入/输出句柄的函数/功能块调用 |
input-variable | VariableElement | buildVariableNode | 向图表输入数据的只读变量 |
output-variable | VariableElement | buildVariableNode | 从图表接收数据的写入目标变量 |
inout-variable | VariableElement | buildVariableNode | 同时具有输入和输出句柄的双向变量 |
connector | ConnectionElement | buildConnectionNode | 入站连接点(左侧句柄,目标) |
continuation | ConnectionElement | buildConnectionNode | 出站延续点(右侧句柄,源) |
comment | CommentElement | buildCommentNode | 无电气行为的自由文本注释 |
构建器模式至关重要:每个构建器函数接受一个BuilderBasicProps(id + 位置)以及特定类型的参数,并返回一个完整成型的 ReactFlowNode对象,其中包含计算好的句柄位置、尺寸和默认数据。buildGenericNode函数充当多态分发器,根据nodeType参数路由到正确的构建器——这是拖放处理程序和剪贴板粘贴逻辑使用的单一入口点。
FBD 数据模型:梯级状态与句柄几何结构
FBD 编辑器的状态被捕获在FBDRungState类型中,它封装了完整的 ReactFlow 图——一个Node对象数组、一个Edge对象数组以及一个selectedNodes子集。此梯级状态持久化在 Zustandfbd存储 slice 中,并通过防抖本地状态镜像模式与FBDBody分子组件进行双向同步:该组件维护一个rungLocal副本以实现即时的 ReactFlow 响应,然后通过fbdFlowActions.setRung将更改防抖(200ms)回写到存储中。这避免了在每次像素级拖动事件时写入存储的性能开销,同时保证了完成时的正确序列化。
每个节点的data字段携带一个BasicNodeData结构,其中包含定义节点连接点的句柄拓扑。句柄在构建时使用全局(相对于流面板)和相对(节点内部)位置进行预计算,并存储为CustomHandleProps。这种双坐标系使编辑器能够在不查询 DOM 的情况下计算边路由和连接验证,这对于无头序列化至关重要。
| BasicNodeData 字段 | 用途 |
|---|---|
handles | 所有句柄(输入 + 输出 + 连接器的联合) |
inputHandles/outputHandles | 用于连接验证的方向子集 |
inputConnector/outputConnector | 主电气连接器(第一个句柄) |
numericId | 用于编译器临时变量命名的数字 UUID |
executionOrder | IEC 61131-3 执行顺序索引 |
variable | 关联的 PLC 变量引用 |
draggable/selectable/deletable | 交互控制标志(调试期间锁定) |
FbdEditor特性组件将 FBD 流状态桥接到项目数据模型。当flow.updated变为true时,它会根据zodFBDFlowSchema验证流,然后调用updatePou将 FBD 图持久化到项目 JSON 中。这确保了图形状态在写入项目文件之前始终是可序列化且符合模式规范的。
FBD 拖放元素创建
新的 FBD 元素通过拖放管道添加到画布中,该管道将浏览器拖动事件转换为 ReactFlow 节点构建。FBDBody组件检测被拖动元素何时进入视口(insideViewport),捕获鼠标位置,并在放置时调用handleAddElementByDropping。此处理程序从拖放负载中确定节点类型(使用拖放检测工具中的getFbdBlockType和isFbdBlockDrag),通过newGraphicalEditorNodeID生成唯一的节点 ID,通过reactFlowInstance.screenToFlowPosition将屏幕位置转换为流坐标,并委托给buildGenericNode进行实际的节点实例化。
对于块类型节点,处理程序从拖放负载中解析库源。库字符串的结构为system|user/{libraryName}/{pouName},处理程序解析它以在libraries存储中定位正确的函数或功能块定义。解析出的变体被传递给buildGenericNode→buildBlockNode,后者根据变体的变量签名计算块的尺寸和句柄布局。然后,新节点通过fbdFlowActions.setRung插入到梯级中,并捕获快照以支持撤销/重做。
FBD 调试集成:实时边着色与变量强制
FBD 编辑器实现了实时调试可视化:当承载的布尔信号为 TRUE 时,将边染成绿色(#00FF00),并在变量节点上叠加调试值徽章。这是在styledEdgesmemo 中计算的,它执行递归图遍历以确定每条边的逻辑状态。
determineEdgeState函数在图中递归工作:对于每条边,它检查源节点是否是直通节点(connector/continuation)——如果是,则传播传入边的状态;否则,查询getNodeOutputState以解析实际的信号值。对于变量节点,该函数通过复合键查找调试值。对于块节点,它区分功能块实例(使用{instanceName}.{outputName}键格式)和函数调用(使用_TMP_{BLOCKNAME}{numericId}_{OUTPUT}临时变量命名约定)。这种双重命名解析是必要的,因为 IEC 61131-3 编译管道为函数和功能块分配了不同的变量策略。
当调试器激活时,节点交互被锁定:styledNodes将所有节点的draggable、selectable和deletable覆盖为false,防止在实时执行期间修改图表。变量节点还通过上下文菜单和模态框支持强制/解强制操作,使用直接写入 Modbus 调试协议的forceDebugVariable和releaseDebugVariable服务。
函数调用输出的_TMP_{BLOCKNAME}{numericId}_{OUTPUT}命名约定是一个关键的实现细节——每个节点data上的numericId字段是在构建时通过generateNumericUUID()生成的,并且必须在序列化周期中保持稳定,调试值解析才能正确工作。
FBD 剪贴板:复制、剪切与粘贴
useFBDClipboard钩子使用浏览器原生的剪贴板事件实现了一个类型区分的剪贴板。复制和剪切操作将选定的节点及其连接的边序列化为一个标记为language: 'fbd'的ClipboardType对象,该对象通过event.clipboardData.setData存储在自定义 MIME 类型fbd:nodes下。这种类型区分防止了跨语言粘贴错误(例如,将梯形图节点粘贴到 FBD 画布中)。
粘贴处理程序解析剪贴板数据,验证language字段,然后通过reactFlowInstance.screenToFlowPosition将屏幕空间鼠标位置转换为流空间坐标——如果光标在视口内,粘贴的节点将以光标为中心;否则默认为当前视口原点。pasteNodesAtFBD工具(来自存储的 fbd slice)重新映射节点 ID 和边引用以避免冲突,然后结果通过fbdFlowActions.setNodes和fbdFlowActions.addEdge合并到梯级中。
FBD 库差异检测
FbdEditor特性组件计算库差异——即块节点的变体签名与当前库定义发生漂移的情况。当用户定义的函数或功能块在实例被放置到画布上之后被修改时,就会发生这种情况。检测算法将原始 POU 的接口变量与块节点存储的variant.variables进行比较,将每个变量格式化为{name}|{class}|{definition}|{value}以进行比较。具有差异签名的节点通过 ID 标记在nodeDivergences数组中,该数组传递给FBDBody并传播到每个节点的data.hasDivergence标志以进行视觉指示。
对于函数类型的 POU,算法还检查OUT变量的类型是否与 POU 声明的返回类型匹配——这是一种微妙但关键的验证,因为编译管道从OUT派生函数的返回类型。
FBD 视口状态持久化
当用户在编辑器标签页之间切换时,FBD 编辑器通过saveEditorViewState操作持久化 ReactFlow 视口(平移偏移和缩放级别)。这被实现为一个订阅editor.meta.name的 Zustand 订阅——当活动编辑器改变时,当前视口在新编辑器加载之前以先前编辑器的名称保存。挂载时,FBDBody组件通过reactFlowInstance.setViewport恢复保存的视口,确保用户返回到他们离开时完全相同的视图。保存操作使用零持续时间动画以避免可见的视口过渡。
SFC 编辑器:当前实现状态
SFC 编辑器目前作为一个结构占位符存在——一个渲染静态<div>且无功能行为的裸组件。GraphicalEditor中的分发路由已配置为在language === 'sfc'时渲染SfcEditor,因此集成点已经建立。实现 SFC 编辑器需要:
新的节点类型分类用于 SFC 元素(步、转换、动作、发散、收敛)
专用的 Zustand 存储 slice(类似于
fbd/ladder)用于 SFC 图状态Zod 模式用于序列化验证(类似于
zodFBDFlowSchema)SFC 专用工具箱位于工作区活动栏中
FBD 编辑器的架构——特别是构建器模式、带防抖同步的梯级状态模式以及调试边着色引擎——为 SFC 实现提供了经过验证的蓝图。
架构总结:FBD 组件分层
FBD 编辑器跨越三个原子设计层进行组织,每层在渲染和状态管理管道中承担不同的职责:
| 层级 | 组件 | 职责 |
|---|---|---|
| 特性 | FbdEditor | 流与项目同步,差异检测,Zod 验证,编排FBDBody |
| 分子 | FBDBody | ReactFlow 画布,拖放处理,调试边着色,剪贴板,视口持久化,防抖存储同步 |
| 原子 | BlockNodeElement,VariableElement,ConnectionElement,CommentElement | 单个节点渲染,内联编辑,句柄渲染,调试徽章 |
| 原子 | buildGenericNode,buildBlockNode,buildVariableNode,buildConnectionNode,buildCommentNode | 计算句柄几何和默认数据的节点工厂函数 |
FBDBody中的防抖本地状态镜像模式是关键的性能优化——没有它,每次拖动移动都会触发完整的存储写入、Zod 验证和项目 JSON 序列化。200ms 的防抖确保仅持久化稳定的状态,而dragging标志在主动拖动期间完全抑制同步。