#pragma once #include "domain/control_logic_model.h" #include "editor_history.h" #include #include #include class ProjectService; // 梯形图编辑失败分类 enum class LogicEditorError { None, // 操作成功或没有错误 LogicNotFound, // 控制逻辑不存在 RungNotFound, // 网络不存在 ExpressionNotFound, // 条件表达式不存在 NodeNotFound, // 节点不存在 InvalidNode, // 节点配置或节点位置无效 InvalidOperation, // 操作参数或当前结构不允许该操作 UnsupportedNodeChange, // 不允许把条件节点改成输出节点,或反向修改 DuplicateName, // 控制逻辑名称与其他逻辑重复 LastLogicRequired // 删除后不能少于一个控制逻辑 }; // 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID struct LogicEditorResult { bool succeeded = false; // 操作是否成功 LogicEditorError error = LogicEditorError::None; // 失败时的分类 std::string message; // 面向用户的 UTF-8 成功说明或失败原因 std::string id; // 成功时返回新建或更新对象的稳定 ID }; enum class LogicConditionPasteTargetKind { Append, EmptyColumn, BranchEmptyColumn, AfterNode, ReplaceWire, ReplaceWireColumn }; struct LogicConditionPasteTarget { LogicConditionPasteTargetKind kind = LogicConditionPasteTargetKind::Append; std::string expressionId; int column = 0; }; // 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做 class LogicEditorService { public: /** * @brief 创建梯形图编辑服务 * @param project_service 用于读取和修改当前工程的项目服务 */ explicit LogicEditorService(ProjectService &project_service); // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 /** @brief 按 ID 查找控制逻辑,未找到时返回空指针 */ const ControlLogic *findLogic(const std::string &logic_id) const; /** @brief 在指定逻辑中按 ID 查找网络,未找到时返回空指针 */ const LadderRung *findRung( const std::string &logic_id, const std::string &rung_id) const; /** @brief 在指定逻辑的所有网络中按 ID 查找节点,未找到时返回空指针 */ const LogicNode *findNode( const std::string &logic_id, const std::string &node_id) const; /** @brief 在指定网络中按 ID 查找条件表达式,未找到时返回空指针 */ const ConditionExpression *findExpression( const std::string &logic_id, const std::string &rung_id, const std::string &expression_id) const; /** @brief 返回工程中第一个控制逻辑 ID,没有逻辑时返回空字符串 */ std::string firstLogicId() const; /** @brief 返回指定逻辑中第一个网络 ID,没有网络或逻辑不存在时返回空字符串 */ std::string firstRungId(const std::string &logic_id) const; /** @brief 查找节点所属网络 ID,未找到时返回空字符串 */ std::string rungIdForNode( const std::string &logic_id, const std::string &node_id) const; /** @brief 返回指定 M/D 地址的工程注释,没有注释时返回空字符串 */ std::string registerCommentFor(const RegisterAddress &address) const; /** * @brief 确保工程至少有一组可编辑逻辑 * @return 已有或新建逻辑的成功结果及其 ID * * 新建逻辑时允许暂时没有网络,第一次实际编辑网络内容时再创建网络 */ LogicEditorResult ensureDefaultLogic(); /** @brief 添加控制逻辑;名称不能为空且必须唯一 */ LogicEditorResult addLogic(const std::string &name); /** @brief 修改控制逻辑名称;名称不能为空且必须唯一 */ LogicEditorResult renameLogic( const std::string &logic_id, const std::string &name); /** @brief 删除控制逻辑;工程至少保留一组逻辑 */ LogicEditorResult removeLogic(const std::string &logic_id); /** @brief 将逻辑在工程列表中上移或下移一位,offset 只能为 -1 或 1 */ LogicEditorResult moveLogic(const std::string &logic_id, int offset); /** @brief 启用或停用指定控制逻辑 */ LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); /** @brief 在指定逻辑末尾添加空网络 */ LogicEditorResult addRung(const std::string &logic_id); /** @brief 删除指定网络 */ LogicEditorResult removeRung( const std::string &logic_id, const std::string &rung_id); /** @brief 修改网络注释;注释长度和换行规则由领域校验约束 */ LogicEditorResult updateRungComment( const std::string &logic_id, const std::string &rung_id, const std::string &comment); // 条件区编辑:串联、按列插入、横线和并联分支 /** @brief 在网络条件末尾追加一个条件节点 */ LogicEditorResult appendCondition( const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config); /** * @brief 按绝对条件列插入条件节点 * @param column 从 0 开始的条件列号;插入位置必须位于允许的条件区 */ LogicEditorResult insertConditionAtColumn( const std::string &logic_id, const std::string &rung_id, int column, const LogicNodeConfig &config); /** * @brief 在直属并联分支的指定视觉列插入条件节点 * * 目标列可以是分支已有横线或 UI 投影出的补线格,服务会原子补齐必要横线 */ LogicEditorResult insertConditionInBranchAtColumn( const std::string &logic_id, const std::string &rung_id, const std::string &branch_expression_id, int column, const LogicNodeConfig &config); /** @brief 在网络条件末尾追加指定列宽的横线 */ LogicEditorResult appendWire( const std::string &logic_id, const std::string &rung_id, int column_span = 1); /** @brief 在目标节点后串联插入条件节点 */ LogicEditorResult insertConditionAfter( const std::string &logic_id, const std::string &rung_id, const std::string &target_node_id, const LogicNodeConfig &config); /** @brief 在目标表达式后串联插入横线 */ LogicEditorResult insertWireAfter( const std::string &logic_id, const std::string &rung_id, const std::string &target_expression_id, int column_span = 1); /** @brief 用条件节点整体替换一条横线表达式 */ LogicEditorResult replaceWireWithCondition( const std::string &logic_id, const std::string &rung_id, const std::string &wire_expression_id, const LogicNodeConfig &config); /** @brief 只替换横线表达式中的一个指定列单元格 */ LogicEditorResult replaceWireColumnWithCondition( const std::string &logic_id, const std::string &rung_id, const std::string &wire_expression_id, int column_offset, const LogicNodeConfig &config); /** * @brief 将选中的条件节点建立为并联分支 * @param selected_node_ids 同一网络中按视觉连续范围选择的条件节点 ID */ LogicEditorResult addParallelBranch( const std::string &logic_id, const std::string &rung_id, const std::vector &selected_node_ids, const LogicNodeConfig &config); /** @brief 将选中的横线表达式建立为并联旁路 */ LogicEditorResult addParallelWireBranch( const std::string &logic_id, const std::string &rung_id, const std::vector &selected_expression_ids); /** * @brief 按选中的横线网格建立并联旁路 * * 所有网格必须来自同一条横线,且列号连续;操作失败时不保留部分修改 */ LogicEditorResult addParallelWireBranchAtCells( const std::string &logic_id, const std::string &rung_id, const std::vector> &selected_wire_cells); /** * @brief 设置网络唯一的输出节点 * @param configured 是否将新输出标记为已完成配置 * * 输出节点固定位于网络输出槽,替换已有输出时保持一次原子编辑 */ LogicEditorResult setOutput( const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config, bool configured = false); // 节点属性和删除操作 /** @brief 更新节点配置;条件节点和输出节点不能互相改型 */ LogicEditorResult updateNodeConfig( const std::string &logic_id, const std::string &node_id, const LogicNodeConfig &config); /** * @brief 批量粘贴条件节点到目标网络末尾 * @param logic_id 目标控制逻辑 * @param rung_id 目标网络;为空时自动创建网络 * @param nodes 待复制节点,只读取配置和 configured 状态 * @return 成功时返回第一个新节点 ID,失败时整批回滚 */ LogicEditorResult pasteConditionNodes( const std::string &logic_id, const std::string &rung_id, const std::vector &nodes, const LogicConditionPasteTarget &target = {}); /** 判断所选条件是否位于同一串联层级并且视觉连续 */ bool areConditionNodesContiguous( const std::string &logic_id, const std::string &rung_id, const std::vector &node_ids) const; /** * @brief 将整条网络复制到目标控制逻辑末尾 * @param logic_id 目标控制逻辑 * @param source 要复制的网络 * @return 成功时返回新网络 ID,所有节点和表达式都会获得新 ID */ LogicEditorResult pasteRung( const std::string &logic_id, const LadderRung &source); /** @brief 删除指定节点并归一化受影响的表达式树 */ LogicEditorResult removeNode( const std::string &logic_id, const std::string &node_id); /** * @brief 批量删除节点 * @param node_ids 节点 ID 列表,不能为空且不能包含重复或不存在的 ID * @return 成功时作为一次编辑记录,失败时整体回滚 */ LogicEditorResult removeNodes( const std::string &logic_id, const std::vector &node_ids); /** @brief 删除指定条件表达式并归一化表达式树 */ LogicEditorResult removeExpression( const std::string &logic_id, const std::string &rung_id, const std::string &expression_id); /** @brief 批量删除条件表达式,失败时整体回滚 */ LogicEditorResult removeExpressions( const std::string &logic_id, const std::string &rung_id, const std::vector &expression_ids); // 当前编辑会话的撤销/重做 /** @brief 判断是否存在可撤销的梯形图编辑操作 */ bool canUndo() const; /** @brief 判断是否存在可重做的梯形图编辑操作 */ bool canRedo() const; /** @brief 撤销最近一次成功的梯形图编辑操作 */ LogicEditorResult undo(); /** @brief 重做最近一次被撤销的梯形图编辑操作 */ LogicEditorResult redo(); /** @brief 清空撤销/重做历史,不修改当前工程内容 */ void clearHistory(); private: // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 struct HistoryState { std::vector logics; // 编辑前或编辑后的完整逻辑集合 }; // 捕获当前工程中的全部控制逻辑 HistoryState captureState() const; // 比较编辑前后状态并记录实际发生的修改 void recordHistory(HistoryState before); // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 void rollbackEdit(HistoryState before, bool modified_before); // 比较两个历史快照是否完全相同 static bool statesEqual( const HistoryState &left, const HistoryState &right); static bool logicsEqual( const ControlLogic &left, const ControlLogic &right); static bool rungsEqual( const LadderRung &left, const LadderRung &right); static bool expressionsEqual( const ConditionExpression &left, const ConditionExpression &right); static bool nodesEqual(const LogicNode &left, const LogicNode &right); static bool configsEqual( const LogicNodeConfig &left, const LogicNodeConfig &right); static LogicEditorResult historyFailure(const std::string &message); // 判断节点配置是否属于条件节点 static bool isConditionConfig(const LogicNodeConfig &config); // 判断节点配置是否属于输出节点 static bool isOutputConfig(const LogicNodeConfig &config); // 返回节点配置对应的稳定 ID 前缀 static std::string nodePrefix(const LogicNodeConfig &config); // 在一个控制逻辑的全部网络中生成唯一节点 ID static std::string makeUniqueNodeId( const ControlLogic &logic, const std::string &prefix); // 生成唯一横线表达式 ID static std::string makeUniqueWireId(const ControlLogic &logic); // 生成唯一容器表达式 ID static std::string makeUniqueExpressionId(const ControlLogic &logic); // 生成唯一网络 ID static std::string makeUniqueRungId(const ControlLogic &logic); // 创建统一的失败结果 static LogicEditorResult failure( LogicEditorError error, const std::string &message); ProjectService &project_service_; // 不拥有的工程服务依赖 EditorHistory history_; // 当前编辑会话的撤销/重做历史 bool suppress_history_ = false; // 复合粘贴期间由外层统一记录一次历史 };