| @@ -25,10 +25,12 @@ SOURCES += \ | |||||
| src/domain/runtime_state.cpp \ | src/domain/runtime_state.cpp \ | ||||
| src/services/project_service.cpp \ | src/services/project_service.cpp \ | ||||
| src/services/hmi_editor_service.cpp \ | src/services/hmi_editor_service.cpp \ | ||||
| src/services/logic_editor_service.cpp \ | |||||
| src/services/hmi_runtime_service.cpp \ | src/services/hmi_runtime_service.cpp \ | ||||
| src/services/runtime_mode_service.cpp \ | src/services/runtime_mode_service.cpp \ | ||||
| src/infrastructure/json_project_storage.cpp \ | src/infrastructure/json_project_storage.cpp \ | ||||
| src/ui/hmi_editor_widget.cpp | |||||
| src/ui/hmi_editor_widget.cpp \ | |||||
| src/ui/logic_editor_widget.cpp | |||||
| HEADERS += \ | HEADERS += \ | ||||
| src/ui/main_window.h \ | src/ui/main_window.h \ | ||||
| @@ -41,10 +43,12 @@ HEADERS += \ | |||||
| src/domain/project_storage.h \ | src/domain/project_storage.h \ | ||||
| src/services/project_service.h \ | src/services/project_service.h \ | ||||
| src/services/hmi_editor_service.h \ | src/services/hmi_editor_service.h \ | ||||
| src/services/logic_editor_service.h \ | |||||
| src/services/hmi_runtime_service.h \ | src/services/hmi_runtime_service.h \ | ||||
| src/services/runtime_mode_service.h \ | src/services/runtime_mode_service.h \ | ||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| src/ui/hmi_editor_widget.h | |||||
| src/ui/hmi_editor_widget.h \ | |||||
| src/ui/logic_editor_widget.h | |||||
| FORMS += \ | FORMS += \ | ||||
| src/ui/main_window.ui | src/ui/main_window.ui | ||||
| @@ -1,6 +1,8 @@ | |||||
| #include "control_logic_model.h" | #include "control_logic_model.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <unordered_map> | |||||
| #include <unordered_set> | |||||
| namespace { | namespace { | ||||
| @@ -86,6 +88,12 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error) | |||||
| } // namespace | } // namespace | ||||
| bool LogicPoint::isValid() const | |||||
| { | |||||
| return x >= kMinimumCoordinate && x <= kMaximumCoordinate | |||||
| && y >= kMinimumCoordinate && y <= kMaximumCoordinate; | |||||
| } | |||||
| bool LogicNode::validate(std::string *error) const | bool LogicNode::validate(std::string *error) const | ||||
| { | { | ||||
| if (id.empty()) | if (id.empty()) | ||||
| @@ -93,6 +101,11 @@ bool LogicNode::validate(std::string *error) const | |||||
| setError(error, "logic node id must not be empty"); | setError(error, "logic node id must not be empty"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (!position.isValid()) | |||||
| { | |||||
| setError(error, "logic node position is outside the supported range"); | |||||
| return false; | |||||
| } | |||||
| // 根据 variant 中实际保存的配置类型调用对应校验函数 | // 根据 variant 中实际保存的配置类型调用对应校验函数 | ||||
| return std::visit( | return std::visit( | ||||
| @@ -115,6 +128,81 @@ bool LogicConnection::validate(std::string *error) const | |||||
| } | } | ||||
| bool ControlLogic::validate(std::string *error) const | bool ControlLogic::validate(std::string *error) const | ||||
| { | |||||
| if (!validateStructure(error)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| if (id.empty() || name.empty()) | |||||
| { | |||||
| setError(error, "control logic id and name must not be empty"); | |||||
| return false; | |||||
| } | |||||
| if (nodes.empty()) | |||||
| { | |||||
| return true; | |||||
| } | |||||
| std::unordered_map<std::string, std::size_t> incoming_count; | |||||
| for (const LogicNode &node : nodes) | |||||
| { | |||||
| incoming_count.emplace(node.id, 0U); | |||||
| } | |||||
| for (const LogicConnection &connection : connections) | |||||
| { | |||||
| ++incoming_count.at(connection.toNodeId); | |||||
| } | |||||
| // 线圈必须由至少一个条件路径驱动,避免孤立线圈在执行时意外写入 M 位 | |||||
| for (const LogicNode &node : nodes) | |||||
| { | |||||
| if (std::holds_alternative<CoilNodeConfig>(node.config) | |||||
| && incoming_count.at(node.id) == 0U) | |||||
| { | |||||
| setError(error, "coil node must have at least one input connection"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| // 从所有线圈沿反向连接遍历,一次找出能够产生动作的全部条件节点 | |||||
| std::unordered_map<std::string, std::vector<std::string>> incoming; | |||||
| std::vector<std::string> pending; | |||||
| for (const LogicNode &node : nodes) | |||||
| { | |||||
| incoming.emplace(node.id, std::vector<std::string>{}); | |||||
| if (std::holds_alternative<CoilNodeConfig>(node.config)) | |||||
| { | |||||
| pending.push_back(node.id); | |||||
| } | |||||
| } | |||||
| for (const LogicConnection &connection : connections) | |||||
| { | |||||
| incoming.at(connection.toNodeId).push_back(connection.fromNodeId); | |||||
| } | |||||
| std::unordered_set<std::string> reaches_coil; | |||||
| while (!pending.empty()) | |||||
| { | |||||
| const std::string current = pending.back(); | |||||
| pending.pop_back(); | |||||
| if (!reaches_coil.insert(current).second) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| for (const std::string &previous : incoming.at(current)) | |||||
| { | |||||
| pending.push_back(previous); | |||||
| } | |||||
| } | |||||
| if (reaches_coil.size() != nodes.size()) | |||||
| { | |||||
| setError(error, "condition node must reach a coil node"); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| bool ControlLogic::validateStructure(std::string *error) const | |||||
| { | { | ||||
| if (id.empty() || name.empty()) | if (id.empty() || name.empty()) | ||||
| { | { | ||||
| @@ -155,5 +243,79 @@ bool ControlLogic::validate(std::string *error) const | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| // 同一对节点只能存在一条有向连接,避免执行语义和删除行为产生歧义 | |||||
| for (auto current = connections.cbegin(); current != connections.cend(); ++current) | |||||
| { | |||||
| const auto duplicate = std::find_if( | |||||
| current + 1, | |||||
| connections.cend(), | |||||
| [current](const LogicConnection &candidate) | |||||
| { | |||||
| return candidate.fromNodeId == current->fromNodeId | |||||
| && candidate.toNodeId == current->toNodeId; | |||||
| }); | |||||
| if (duplicate != connections.cend()) | |||||
| { | |||||
| setError(error, "logic connections must be unique"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| std::unordered_map<std::string, std::vector<std::string>> outgoing; | |||||
| std::unordered_map<std::string, std::size_t> incoming_count; | |||||
| for (const LogicNode &node : nodes) | |||||
| { | |||||
| outgoing.emplace(node.id, std::vector<std::string>{}); | |||||
| incoming_count.emplace(node.id, 0U); | |||||
| } | |||||
| for (const LogicConnection &connection : connections) | |||||
| { | |||||
| const auto from = std::find_if( | |||||
| nodes.cbegin(), | |||||
| nodes.cend(), | |||||
| [&connection](const LogicNode &node) | |||||
| { | |||||
| return node.id == connection.fromNodeId; | |||||
| }); | |||||
| if (std::holds_alternative<CoilNodeConfig>(from->config)) | |||||
| { | |||||
| setError(error, "coil node cannot have outgoing connections"); | |||||
| return false; | |||||
| } | |||||
| outgoing.at(connection.fromNodeId).push_back(connection.toNodeId); | |||||
| ++incoming_count.at(connection.toNodeId); | |||||
| } | |||||
| // 使用迭代拓扑排序校验环路,不改变节点的持久化顺序 | |||||
| std::vector<std::string> pending; | |||||
| for (const auto &entry : incoming_count) | |||||
| { | |||||
| if (entry.second == 0U) | |||||
| { | |||||
| pending.push_back(entry.first); | |||||
| } | |||||
| } | |||||
| std::size_t visited_count = 0U; | |||||
| while (!pending.empty()) | |||||
| { | |||||
| const std::string current = pending.back(); | |||||
| pending.pop_back(); | |||||
| ++visited_count; | |||||
| for (const std::string &next : outgoing.at(current)) | |||||
| { | |||||
| std::size_t &count = incoming_count.at(next); | |||||
| --count; | |||||
| if (count == 0U) | |||||
| { | |||||
| pending.push_back(next); | |||||
| } | |||||
| } | |||||
| } | |||||
| if (visited_count != nodes.size()) | |||||
| { | |||||
| setError(error, "logic connections must not contain a cycle"); | |||||
| return false; | |||||
| } | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -7,6 +7,18 @@ | |||||
| #include <variant> | #include <variant> | ||||
| #include <vector> | #include <vector> | ||||
| // 逻辑节点在编辑画布中的持久化坐标 | |||||
| struct LogicPoint | |||||
| { | |||||
| static constexpr int kMinimumCoordinate = 0; | |||||
| static constexpr int kMaximumCoordinate = 100000; | |||||
| int x = 0; | |||||
| int y = 0; | |||||
| bool isValid() const; | |||||
| }; | |||||
| // 触点工作方式 | // 触点工作方式 | ||||
| enum class ContactMode | enum class ContactMode | ||||
| { | { | ||||
| @@ -72,6 +84,8 @@ struct LogicNode | |||||
| std::string id; | std::string id; | ||||
| // 节点自身的业务配置 | // 节点自身的业务配置 | ||||
| LogicNodeConfig config; | LogicNodeConfig config; | ||||
| // 节点在逻辑编辑画布中的位置 | |||||
| LogicPoint position; | |||||
| // 校验节点配置并通过 error 返回失败原因 | // 校验节点配置并通过 error 返回失败原因 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -105,4 +119,6 @@ struct ControlLogic | |||||
| // 校验控制逻辑并通过 error 返回失败原因 | // 校验控制逻辑并通过 error 返回失败原因 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| // 校验编辑过程所需的结构规则,允许暂时没有完整输出 | |||||
| bool validateStructure(std::string *error = nullptr) const; | |||||
| }; | }; | ||||
| @@ -211,6 +211,38 @@ QJsonObject serializeAddress(const RegisterAddress &address) | |||||
| return object; | return object; | ||||
| } | } | ||||
| QJsonObject serializeLogicPoint(const LogicPoint &position) | |||||
| { | |||||
| QJsonObject object; | |||||
| object.insert(QStringLiteral("x"), position.x); | |||||
| object.insert(QStringLiteral("y"), position.y); | |||||
| return object; | |||||
| } | |||||
| bool parseLogicPoint( | |||||
| const QJsonObject &object, | |||||
| const std::string &context, | |||||
| LogicPoint *position, | |||||
| ParseState *state) | |||||
| { | |||||
| return readInt( | |||||
| object, | |||||
| "x", | |||||
| context, | |||||
| LogicPoint::kMinimumCoordinate, | |||||
| LogicPoint::kMaximumCoordinate, | |||||
| &position->x, | |||||
| state) | |||||
| && readInt( | |||||
| object, | |||||
| "y", | |||||
| context, | |||||
| LogicPoint::kMinimumCoordinate, | |||||
| LogicPoint::kMaximumCoordinate, | |||||
| &position->y, | |||||
| state); | |||||
| } | |||||
| // 解析寄存器地址并校验区域名称及项目允许的索引范围 | // 解析寄存器地址并校验区域名称及项目允许的索引范围 | ||||
| bool parseAddress( | bool parseAddress( | ||||
| const QJsonObject &object, | const QJsonObject &object, | ||||
| @@ -719,6 +751,7 @@ QJsonObject serializeLogicNode(const LogicNode &node) | |||||
| { | { | ||||
| QJsonObject object; | QJsonObject object; | ||||
| object.insert(QStringLiteral("id"), fromUtf8(node.id)); | object.insert(QStringLiteral("id"), fromUtf8(node.id)); | ||||
| object.insert(QStringLiteral("position"), serializeLogicPoint(node.position)); | |||||
| // std::visit 将不同节点配置统一转换为 config JSON 对象 | // std::visit 将不同节点配置统一转换为 config JSON 对象 | ||||
| object.insert( | object.insert( | ||||
| QStringLiteral("config"), | QStringLiteral("config"), | ||||
| @@ -814,9 +847,19 @@ bool parseLogicNode( | |||||
| ParseState *state) | ParseState *state) | ||||
| { | { | ||||
| QJsonObject config; | QJsonObject config; | ||||
| return readString(object, "id", context, &node->id, state) | |||||
| && readObject(object, "config", context, &config, state) | |||||
| && parseNodeConfig(config, context + ".config", &node->config, state); | |||||
| QJsonObject position; | |||||
| if (!readString(object, "id", context, &node->id, state) | |||||
| || !readObject(object, "config", context, &config, state) | |||||
| || !readObject(object, "position", context, &position, state) | |||||
| || !parseNodeConfig(config, context + ".config", &node->config, state)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| return parseLogicPoint( | |||||
| position, | |||||
| context + ".position", | |||||
| &node->position, | |||||
| state); | |||||
| } | } | ||||
| // 将逻辑节点之间的有向连接序列化为 JSON 对象 | // 将逻辑节点之间的有向连接序列化为 JSON 对象 | ||||
| @@ -11,6 +11,7 @@ | |||||
| #include "infrastructure/json_project_storage.h" | #include "infrastructure/json_project_storage.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/logic_editor_service.h" | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| @@ -25,6 +26,7 @@ int main(int argc, char *argv[]) | |||||
| JsonProjectStorage project_storage; | JsonProjectStorage project_storage; | ||||
| ProjectService project_service(project_storage); | ProjectService project_service(project_storage); | ||||
| HmiEditorService hmi_editor_service(project_service); | HmiEditorService hmi_editor_service(project_service); | ||||
| LogicEditorService logic_editor_service(project_service); | |||||
| // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | ||||
| VirtualRegisterRepository virtual_register_repository; | VirtualRegisterRepository virtual_register_repository; | ||||
| HmiRuntimeService hmi_runtime_service(virtual_register_repository); | HmiRuntimeService hmi_runtime_service(virtual_register_repository); | ||||
| @@ -33,6 +35,7 @@ int main(int argc, char *argv[]) | |||||
| runtime_mode_service, | runtime_mode_service, | ||||
| project_service, | project_service, | ||||
| hmi_editor_service, | hmi_editor_service, | ||||
| logic_editor_service, | |||||
| hmi_runtime_service); | hmi_runtime_service); | ||||
| main_window.show(); | main_window.show(); | ||||
| @@ -0,0 +1,395 @@ | |||||
| #include "logic_editor_service.h" | |||||
| #include "project_service.h" | |||||
| #include <algorithm> | |||||
| #include <sstream> | |||||
| #include <type_traits> | |||||
| #include <utility> | |||||
| namespace { | |||||
| bool validateCandidateNode( | |||||
| const std::string &id, | |||||
| const LogicNodeConfig &config, | |||||
| const LogicPoint &position, | |||||
| std::string *error) | |||||
| { | |||||
| LogicNode candidate; | |||||
| candidate.id = id; | |||||
| candidate.config = config; | |||||
| candidate.position = position; | |||||
| return candidate.validate(error); | |||||
| } | |||||
| } // namespace | |||||
| LogicEditorService::LogicEditorService(ProjectService &project_service) | |||||
| : project_service_(project_service) | |||||
| { | |||||
| } | |||||
| const ControlLogic *LogicEditorService::findLogic(const std::string &logic_id) const | |||||
| { | |||||
| const Project &project = project_service_.project(); | |||||
| const auto logic = std::find_if( | |||||
| project.controlLogics.cbegin(), | |||||
| project.controlLogics.cend(), | |||||
| [&logic_id](const ControlLogic &candidate) | |||||
| { | |||||
| return candidate.id == logic_id; | |||||
| }); | |||||
| return logic == project.controlLogics.cend() ? nullptr : &*logic; | |||||
| } | |||||
| const LogicNode *LogicEditorService::findNode( | |||||
| const std::string &logic_id, const std::string &node_id) const | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return nullptr; | |||||
| } | |||||
| const auto node = std::find_if( | |||||
| logic->nodes.cbegin(), | |||||
| logic->nodes.cend(), | |||||
| [&node_id](const LogicNode &candidate) | |||||
| { | |||||
| return candidate.id == node_id; | |||||
| }); | |||||
| return node == logic->nodes.cend() ? nullptr : &*node; | |||||
| } | |||||
| std::string LogicEditorService::firstLogicId() const | |||||
| { | |||||
| const Project &project = project_service_.project(); | |||||
| return project.controlLogics.empty() ? std::string{} : project.controlLogics.front().id; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||||
| { | |||||
| if (!project_service_.project().controlLogics.empty()) | |||||
| { | |||||
| return {true, LogicEditorError::None, {}, firstLogicId()}; | |||||
| } | |||||
| ControlLogic logic; | |||||
| logic.id = "logic-1"; | |||||
| logic.name = "控制逻辑 1"; | |||||
| Project &project = project_service_.editProject(); | |||||
| project.controlLogics.push_back(std::move(logic)); | |||||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::addNode( | |||||
| const std::string &logic_id, | |||||
| const LogicNodeConfig &config, | |||||
| const LogicPoint &position) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| const std::string id = makeUniqueNodeId(*logic, nodePrefix(config)); | |||||
| std::string error; | |||||
| if (!validateCandidateNode(id, config, position, &error)) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidNode, error); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) | |||||
| { | |||||
| return candidate.id == logic_id; | |||||
| }); | |||||
| LogicNode node; | |||||
| node.id = id; | |||||
| node.config = config; | |||||
| node.position = position; | |||||
| target->nodes.push_back(std::move(node)); | |||||
| return {true, LogicEditorError::None, {}, id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::moveNode( | |||||
| const std::string &logic_id, | |||||
| const std::string &node_id, | |||||
| const LogicPoint &position) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| const LogicNode *node = findNode(logic_id, node_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| if (node == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::NodeNotFound, "logic node was not found"); | |||||
| } | |||||
| if (!position.isValid()) | |||||
| { | |||||
| return failure( | |||||
| LogicEditorError::InvalidNode, | |||||
| "logic node position is outside the supported range"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) | |||||
| { | |||||
| return candidate.id == logic_id; | |||||
| }); | |||||
| auto editable = std::find_if( | |||||
| target->nodes.begin(), | |||||
| target->nodes.end(), | |||||
| [&node_id](const LogicNode &candidate) | |||||
| { | |||||
| return candidate.id == node_id; | |||||
| }); | |||||
| editable->position = position; | |||||
| return {true, LogicEditorError::None, {}, node_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::updateNodeConfig( | |||||
| const std::string &logic_id, | |||||
| const std::string &node_id, | |||||
| const LogicNodeConfig &config) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| const LogicNode *node = findNode(logic_id, node_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| if (node == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::NodeNotFound, "logic node was not found"); | |||||
| } | |||||
| if (node->config.index() != config.index()) | |||||
| { | |||||
| return failure( | |||||
| LogicEditorError::UnsupportedNodeChange, | |||||
| "node category cannot be changed after creation"); | |||||
| } | |||||
| std::string error; | |||||
| if (!validateCandidateNode(node_id, config, node->position, &error)) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidNode, error); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &candidate) | |||||
| { | |||||
| return candidate.id == logic_id; | |||||
| }); | |||||
| auto editable = std::find_if( | |||||
| target->nodes.begin(), | |||||
| target->nodes.end(), | |||||
| [&node_id](const LogicNode &candidate) | |||||
| { | |||||
| return candidate.id == node_id; | |||||
| }); | |||||
| editable->config = config; | |||||
| return {true, LogicEditorError::None, {}, node_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::connectNodes( | |||||
| const std::string &logic_id, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| if (findNode(logic_id, from_node_id) == nullptr | |||||
| || findNode(logic_id, to_node_id) == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::NodeNotFound, "logic connection node was not found"); | |||||
| } | |||||
| if (hasConnection(*logic, from_node_id, to_node_id)) | |||||
| { | |||||
| return failure( | |||||
| LogicEditorError::DuplicateConnection, | |||||
| "logic connection already exists"); | |||||
| } | |||||
| ControlLogic candidate = *logic; | |||||
| candidate.connections.push_back({from_node_id, to_node_id}); | |||||
| std::string error; | |||||
| if (!candidate.validateStructure(&error)) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidConnection, error); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &item) | |||||
| { | |||||
| return item.id == logic_id; | |||||
| }); | |||||
| target->connections.push_back({from_node_id, to_node_id}); | |||||
| return {true, LogicEditorError::None, {}, to_node_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::disconnectNodes( | |||||
| const std::string &logic_id, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| const auto connection = std::find_if( | |||||
| logic->connections.cbegin(), | |||||
| logic->connections.cend(), | |||||
| [&from_node_id, &to_node_id](const LogicConnection &candidate) | |||||
| { | |||||
| return candidate.fromNodeId == from_node_id | |||||
| && candidate.toNodeId == to_node_id; | |||||
| }); | |||||
| if (connection == logic->connections.cend()) | |||||
| { | |||||
| return failure(LogicEditorError::InvalidConnection, "logic connection was not found"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &item) | |||||
| { | |||||
| return item.id == logic_id; | |||||
| }); | |||||
| target->connections.erase( | |||||
| std::remove_if( | |||||
| target->connections.begin(), | |||||
| target->connections.end(), | |||||
| [&from_node_id, &to_node_id](const LogicConnection &candidate) | |||||
| { | |||||
| return candidate.fromNodeId == from_node_id | |||||
| && candidate.toNodeId == to_node_id; | |||||
| }), | |||||
| target->connections.end()); | |||||
| return {true, LogicEditorError::None, {}, to_node_id}; | |||||
| } | |||||
| LogicEditorResult LogicEditorService::removeNode( | |||||
| const std::string &logic_id, const std::string &node_id) | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::LogicNotFound, "control logic was not found"); | |||||
| } | |||||
| if (findNode(logic_id, node_id) == nullptr) | |||||
| { | |||||
| return failure(LogicEditorError::NodeNotFound, "logic node was not found"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto target = std::find_if( | |||||
| project.controlLogics.begin(), | |||||
| project.controlLogics.end(), | |||||
| [&logic_id](const ControlLogic &item) | |||||
| { | |||||
| return item.id == logic_id; | |||||
| }); | |||||
| target->nodes.erase( | |||||
| std::remove_if( | |||||
| target->nodes.begin(), | |||||
| target->nodes.end(), | |||||
| [&node_id](const LogicNode &candidate) | |||||
| { | |||||
| return candidate.id == node_id; | |||||
| }), | |||||
| target->nodes.end()); | |||||
| target->connections.erase( | |||||
| std::remove_if( | |||||
| target->connections.begin(), | |||||
| target->connections.end(), | |||||
| [&node_id](const LogicConnection &candidate) | |||||
| { | |||||
| return candidate.fromNodeId == node_id || candidate.toNodeId == node_id; | |||||
| }), | |||||
| target->connections.end()); | |||||
| return {true, LogicEditorError::None, {}, node_id}; | |||||
| } | |||||
| std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | |||||
| { | |||||
| return std::visit( | |||||
| [](const auto &value) -> std::string | |||||
| { | |||||
| using Config = std::decay_t<decltype(value)>; | |||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||||
| { | |||||
| return "contact"; | |||||
| } | |||||
| if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||||
| { | |||||
| return "coil"; | |||||
| } | |||||
| return "compare"; | |||||
| }, | |||||
| config); | |||||
| } | |||||
| std::string LogicEditorService::makeUniqueNodeId( | |||||
| const ControlLogic &logic, const std::string &prefix) | |||||
| { | |||||
| for (std::size_t index = 1;; ++index) | |||||
| { | |||||
| std::ostringstream stream; | |||||
| stream << prefix << '-' << index; | |||||
| const std::string candidate = stream.str(); | |||||
| const auto match = std::find_if( | |||||
| logic.nodes.cbegin(), | |||||
| logic.nodes.cend(), | |||||
| [&candidate](const LogicNode &node) | |||||
| { | |||||
| return node.id == candidate; | |||||
| }); | |||||
| if (match == logic.nodes.cend()) | |||||
| { | |||||
| return candidate; | |||||
| } | |||||
| } | |||||
| } | |||||
| bool LogicEditorService::hasConnection( | |||||
| const ControlLogic &logic, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id) | |||||
| { | |||||
| return std::find_if( | |||||
| logic.connections.cbegin(), | |||||
| logic.connections.cend(), | |||||
| [&from_node_id, &to_node_id](const LogicConnection &candidate) | |||||
| { | |||||
| return candidate.fromNodeId == from_node_id | |||||
| && candidate.toNodeId == to_node_id; | |||||
| }) | |||||
| != logic.connections.cend(); | |||||
| } | |||||
| LogicEditorResult LogicEditorService::failure( | |||||
| LogicEditorError error, const std::string &message) | |||||
| { | |||||
| return {false, error, message, {}}; | |||||
| } | |||||
| @@ -0,0 +1,74 @@ | |||||
| #pragma once | |||||
| #include "domain/control_logic_model.h" | |||||
| #include <string> | |||||
| class ProjectService; | |||||
| enum class LogicEditorError | |||||
| { | |||||
| None, | |||||
| LogicNotFound, | |||||
| NodeNotFound, | |||||
| DuplicateConnection, | |||||
| InvalidNode, | |||||
| InvalidConnection, | |||||
| UnsupportedNodeChange | |||||
| }; | |||||
| struct LogicEditorResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| LogicEditorError error = LogicEditorError::None; | |||||
| std::string message; | |||||
| std::string id; | |||||
| }; | |||||
| class LogicEditorService | |||||
| { | |||||
| public: | |||||
| explicit LogicEditorService(ProjectService &project_service); | |||||
| const ControlLogic *findLogic(const std::string &logic_id) const; | |||||
| const LogicNode *findNode( | |||||
| const std::string &logic_id, const std::string &node_id) const; | |||||
| std::string firstLogicId() const; | |||||
| LogicEditorResult ensureDefaultLogic(); | |||||
| LogicEditorResult addNode( | |||||
| const std::string &logic_id, | |||||
| const LogicNodeConfig &config, | |||||
| const LogicPoint &position); | |||||
| LogicEditorResult moveNode( | |||||
| const std::string &logic_id, | |||||
| const std::string &node_id, | |||||
| const LogicPoint &position); | |||||
| LogicEditorResult updateNodeConfig( | |||||
| const std::string &logic_id, | |||||
| const std::string &node_id, | |||||
| const LogicNodeConfig &config); | |||||
| LogicEditorResult connectNodes( | |||||
| const std::string &logic_id, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id); | |||||
| LogicEditorResult disconnectNodes( | |||||
| const std::string &logic_id, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id); | |||||
| LogicEditorResult removeNode( | |||||
| const std::string &logic_id, const std::string &node_id); | |||||
| private: | |||||
| static std::string nodePrefix(const LogicNodeConfig &config); | |||||
| static std::string makeUniqueNodeId( | |||||
| const ControlLogic &logic, const std::string &prefix); | |||||
| static bool hasConnection( | |||||
| const ControlLogic &logic, | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id); | |||||
| static LogicEditorResult failure( | |||||
| LogicEditorError error, const std::string &message); | |||||
| ProjectService &project_service_; | |||||
| }; | |||||
| @@ -0,0 +1,624 @@ | |||||
| #include "logic_editor_widget.h" | |||||
| #include "services/logic_editor_service.h" | |||||
| #include <QGraphicsPathItem> | |||||
| #include <QGraphicsScene> | |||||
| #include <QGraphicsSceneMouseEvent> | |||||
| #include <QMouseEvent> | |||||
| #include <QPainter> | |||||
| #include <QResizeEvent> | |||||
| #include <QStyleOptionGraphicsItem> | |||||
| #include <algorithm> | |||||
| #include <cmath> | |||||
| #include <functional> | |||||
| #include <map> | |||||
| #include <type_traits> | |||||
| #include <utility> | |||||
| namespace { | |||||
| constexpr qreal kNodeWidth = 168.0; | |||||
| constexpr qreal kNodeHeight = 72.0; | |||||
| constexpr qreal kPortRadius = 6.0; | |||||
| QString nodeTitle(const LogicNodeConfig &config) | |||||
| { | |||||
| return std::visit( | |||||
| [](const auto &value) -> QString | |||||
| { | |||||
| using Config = std::decay_t<decltype(value)>; | |||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||||
| { | |||||
| return value.mode == ContactMode::NormallyOpen | |||||
| ? QStringLiteral("常开触点") | |||||
| : QStringLiteral("常闭触点"); | |||||
| } | |||||
| if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||||
| { | |||||
| if (value.mode == CoilMode::Set) | |||||
| { | |||||
| return QStringLiteral("置位线圈"); | |||||
| } | |||||
| if (value.mode == CoilMode::Reset) | |||||
| { | |||||
| return QStringLiteral("复位线圈"); | |||||
| } | |||||
| return QStringLiteral("普通线圈"); | |||||
| } | |||||
| return QStringLiteral("D 值比较"); | |||||
| }, | |||||
| config); | |||||
| } | |||||
| QString nodeDetail(const LogicNodeConfig &config) | |||||
| { | |||||
| return std::visit( | |||||
| [](const auto &value) -> QString | |||||
| { | |||||
| using Config = std::decay_t<decltype(value)>; | |||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||||
| { | |||||
| return QStringLiteral("M%1").arg(value.address.index()); | |||||
| } | |||||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||||
| { | |||||
| return QStringLiteral("M%1").arg(value.address.index()); | |||||
| } | |||||
| else | |||||
| { | |||||
| return QStringLiteral("D%1 = %2") | |||||
| .arg(value.address.index()) | |||||
| .arg(value.value); | |||||
| } | |||||
| }, | |||||
| config); | |||||
| } | |||||
| bool isCoil(const LogicNodeConfig &config) | |||||
| { | |||||
| return std::holds_alternative<CoilNodeConfig>(config); | |||||
| } | |||||
| QPainterPath makeConnectionPath(const QPointF &from, const QPointF &to) | |||||
| { | |||||
| QPainterPath path(from); | |||||
| const qreal offset = std::max(48.0, std::abs(to.x() - from.x()) * 0.45); | |||||
| path.cubicTo( | |||||
| from + QPointF(offset, 0), | |||||
| to - QPointF(offset, 0), | |||||
| to); | |||||
| return path; | |||||
| } | |||||
| } // namespace | |||||
| class LogicEditorWidget::NodeItem final : public QGraphicsItem | |||||
| { | |||||
| public: | |||||
| NodeItem( | |||||
| const LogicNode &node, | |||||
| std::function<void(const std::string &, const QPointF &)> moved, | |||||
| std::function<void(const std::string &)> position_changed) | |||||
| : node_id_(node.id), | |||||
| config_(node.config), | |||||
| moved_(std::move(moved)), | |||||
| position_changed_(std::move(position_changed)) | |||||
| { | |||||
| setPos(node.position.x, node.position.y); | |||||
| setFlag(ItemIsSelectable, true); | |||||
| setFlag(ItemSendsGeometryChanges, true); | |||||
| setAcceptedMouseButtons(Qt::LeftButton); | |||||
| } | |||||
| QRectF boundingRect() const override | |||||
| { | |||||
| return {0, 0, kNodeWidth, kNodeHeight}; | |||||
| } | |||||
| void paint( | |||||
| QPainter *painter, | |||||
| const QStyleOptionGraphicsItem *option, | |||||
| QWidget *) override | |||||
| { | |||||
| const QRectF rect = boundingRect().adjusted(1, 1, -1, -1); | |||||
| painter->setRenderHint(QPainter::Antialiasing, true); | |||||
| painter->setPen(QPen(QColor(QStringLiteral("#33424d")), 1)); | |||||
| painter->setBrush(isCoil(config_) | |||||
| ? QColor(QStringLiteral("#fff2d8")) | |||||
| : QColor(QStringLiteral("#e8f1f6"))); | |||||
| painter->drawRoundedRect(rect, 5, 5); | |||||
| painter->setPen(QColor(QStringLiteral("#18252e"))); | |||||
| painter->drawText( | |||||
| rect.adjusted(14, 8, -14, -34), | |||||
| Qt::AlignLeft | Qt::AlignVCenter, | |||||
| nodeTitle(config_)); | |||||
| painter->setPen(QColor(QStringLiteral("#586873"))); | |||||
| painter->drawText( | |||||
| rect.adjusted(14, 35, -14, -8), | |||||
| Qt::AlignLeft | Qt::AlignVCenter, | |||||
| nodeDetail(config_)); | |||||
| painter->setPen(QPen(QColor(QStringLiteral("#33424d")), 1)); | |||||
| painter->setBrush(QColor(QStringLiteral("#ffffff"))); | |||||
| painter->drawEllipse(inputPortRect()); | |||||
| if (!isCoil(config_)) | |||||
| { | |||||
| painter->drawEllipse(outputPortRect()); | |||||
| } | |||||
| if ((option->state & QStyle::State_Selected) != 0) | |||||
| { | |||||
| painter->setBrush(Qt::NoBrush); | |||||
| painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2)); | |||||
| painter->drawRoundedRect(boundingRect().adjusted(0, 0, -1, -1), 5, 5); | |||||
| } | |||||
| } | |||||
| const std::string &nodeId() const | |||||
| { | |||||
| return node_id_; | |||||
| } | |||||
| bool hasOutput() const | |||||
| { | |||||
| return !isCoil(config_); | |||||
| } | |||||
| QRectF inputPortRect() const | |||||
| { | |||||
| return {0, kNodeHeight / 2.0 - kPortRadius, | |||||
| 2.0 * kPortRadius, 2.0 * kPortRadius}; | |||||
| } | |||||
| QRectF outputPortRect() const | |||||
| { | |||||
| return {kNodeWidth - 2.0 * kPortRadius, kNodeHeight / 2.0 - kPortRadius, | |||||
| 2.0 * kPortRadius, 2.0 * kPortRadius}; | |||||
| } | |||||
| QPointF inputPortScenePosition() const | |||||
| { | |||||
| return mapToScene(inputPortRect().center()); | |||||
| } | |||||
| QPointF outputPortScenePosition() const | |||||
| { | |||||
| return mapToScene(outputPortRect().center()); | |||||
| } | |||||
| void setInteractionEnabled(bool enabled) | |||||
| { | |||||
| setFlag(ItemIsMovable, enabled); | |||||
| } | |||||
| protected: | |||||
| QVariant itemChange(GraphicsItemChange change, const QVariant &value) override | |||||
| { | |||||
| if (change == ItemPositionHasChanged && position_changed_) | |||||
| { | |||||
| position_changed_(node_id_); | |||||
| } | |||||
| return QGraphicsItem::itemChange(change, value); | |||||
| } | |||||
| void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override | |||||
| { | |||||
| QGraphicsItem::mouseReleaseEvent(event); | |||||
| if (flags().testFlag(ItemIsMovable) && moved_) | |||||
| { | |||||
| moved_(node_id_, pos()); | |||||
| } | |||||
| } | |||||
| private: | |||||
| std::string node_id_; | |||||
| LogicNodeConfig config_; | |||||
| std::function<void(const std::string &, const QPointF &)> moved_; | |||||
| std::function<void(const std::string &)> position_changed_; | |||||
| }; | |||||
| class LogicEditorWidget::ConnectionItem final : public QGraphicsPathItem | |||||
| { | |||||
| public: | |||||
| ConnectionItem( | |||||
| const std::string &from_node_id, | |||||
| const std::string &to_node_id) | |||||
| : from_node_id_(from_node_id), to_node_id_(to_node_id) | |||||
| { | |||||
| setFlag(ItemIsSelectable, true); | |||||
| setZValue(-1.0); | |||||
| setPen(QPen(QColor(QStringLiteral("#5d6f7b")), 2)); | |||||
| } | |||||
| const std::string &fromNodeId() const | |||||
| { | |||||
| return from_node_id_; | |||||
| } | |||||
| const std::string &toNodeId() const | |||||
| { | |||||
| return to_node_id_; | |||||
| } | |||||
| void updatePath(const QPointF &from, const QPointF &to) | |||||
| { | |||||
| setPath(makeConnectionPath(from, to)); | |||||
| } | |||||
| private: | |||||
| std::string from_node_id_; | |||||
| std::string to_node_id_; | |||||
| }; | |||||
| LogicEditorWidget::LogicEditorWidget( | |||||
| LogicEditorService &editor_service, | |||||
| QWidget *parent) | |||||
| : QGraphicsView(parent), editor_service_(editor_service) | |||||
| { | |||||
| scene_ = new QGraphicsScene(this); | |||||
| scene_->setSceneRect(0, 0, 2400, 1400); | |||||
| setScene(scene_); | |||||
| setRenderHint(QPainter::Antialiasing, true); | |||||
| setBackgroundBrush(QColor(QStringLiteral("#ffffff"))); | |||||
| setDragMode(QGraphicsView::RubberBandDrag); | |||||
| setTransformationAnchor(QGraphicsView::AnchorUnderMouse); | |||||
| connect(scene_, &QGraphicsScene::selectionChanged, | |||||
| this, &LogicEditorWidget::handleSelectionChanged); | |||||
| } | |||||
| void LogicEditorWidget::setLogicId(const std::string &logic_id) | |||||
| { | |||||
| if (logic_id_ == logic_id) | |||||
| { | |||||
| return; | |||||
| } | |||||
| logic_id_ = logic_id; | |||||
| reloadLogic(); | |||||
| } | |||||
| void LogicEditorWidget::setEditingEnabled(bool enabled) | |||||
| { | |||||
| editing_enabled_ = enabled; | |||||
| setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag); | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node != nullptr) | |||||
| { | |||||
| node->setInteractionEnabled(enabled); | |||||
| } | |||||
| } | |||||
| if (!enabled) | |||||
| { | |||||
| clearPreview(); | |||||
| } | |||||
| } | |||||
| void LogicEditorWidget::reloadLogic() | |||||
| { | |||||
| clearPreview(); | |||||
| scene_->clear(); | |||||
| if (logic_id_.empty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const ControlLogic *logic = editor_service_.findLogic(logic_id_); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| std::map<std::string, NodeItem *> node_items; | |||||
| for (const LogicConnection &connection : logic->connections) | |||||
| { | |||||
| scene_->addItem(new ConnectionItem(connection.fromNodeId, connection.toNodeId)); | |||||
| } | |||||
| for (const LogicNode &node : logic->nodes) | |||||
| { | |||||
| auto *item = new NodeItem( | |||||
| node, | |||||
| [this](const std::string &node_id, const QPointF &position) | |||||
| { | |||||
| handleNodePositionChanged(node_id); | |||||
| const LogicPoint point{ | |||||
| static_cast<int>(std::lround(position.x())), | |||||
| static_cast<int>(std::lround(position.y()))}; | |||||
| const LogicEditorResult result = editor_service_.moveNode( | |||||
| logic_id_, node_id, point); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| reportFailure(result); | |||||
| reloadLogic(); | |||||
| return; | |||||
| } | |||||
| emit nodeChanged(QString::fromStdString(node_id)); | |||||
| }, | |||||
| [this](const std::string &) | |||||
| { | |||||
| updateConnections(); | |||||
| }); | |||||
| item->setInteractionEnabled(editing_enabled_); | |||||
| scene_->addItem(item); | |||||
| node_items.emplace(node.id, item); | |||||
| } | |||||
| updateConnections(); | |||||
| } | |||||
| void LogicEditorWidget::selectNode(const std::string &node_id) | |||||
| { | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node != nullptr) | |||||
| { | |||||
| node->setSelected(node->nodeId() == node_id); | |||||
| if (node->nodeId() == node_id) | |||||
| { | |||||
| ensureVisible(node); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| std::string LogicEditorWidget::selectedNodeId() const | |||||
| { | |||||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||||
| { | |||||
| const NodeItem *node = dynamic_cast<const NodeItem *>(item); | |||||
| if (node != nullptr) | |||||
| { | |||||
| return node->nodeId(); | |||||
| } | |||||
| } | |||||
| return {}; | |||||
| } | |||||
| LogicEditorResult LogicEditorWidget::addNode(const LogicNodeConfig &config) | |||||
| { | |||||
| if (logic_id_.empty()) | |||||
| { | |||||
| const LogicEditorResult logic_result = editor_service_.ensureDefaultLogic(); | |||||
| if (!logic_result.succeeded) | |||||
| { | |||||
| reportFailure(logic_result); | |||||
| return logic_result; | |||||
| } | |||||
| logic_id_ = logic_result.id; | |||||
| } | |||||
| const LogicEditorResult result = editor_service_.addNode( | |||||
| logic_id_, config, nextNodePosition()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| reportFailure(result); | |||||
| return result; | |||||
| } | |||||
| reloadLogic(); | |||||
| selectNode(result.id); | |||||
| emit nodeChanged(QString::fromStdString(result.id)); | |||||
| emit graphChanged(); | |||||
| return result; | |||||
| } | |||||
| LogicEditorResult LogicEditorWidget::deleteSelected() | |||||
| { | |||||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node != nullptr) | |||||
| { | |||||
| const LogicEditorResult result = editor_service_.removeNode( | |||||
| logic_id_, node->nodeId()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| reportFailure(result); | |||||
| return result; | |||||
| } | |||||
| reloadLogic(); | |||||
| emit nodeSelected({}); | |||||
| emit graphChanged(); | |||||
| return result; | |||||
| } | |||||
| ConnectionItem *connection = dynamic_cast<ConnectionItem *>(item); | |||||
| if (connection != nullptr) | |||||
| { | |||||
| const LogicEditorResult result = editor_service_.disconnectNodes( | |||||
| logic_id_, connection->fromNodeId(), connection->toNodeId()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| reportFailure(result); | |||||
| return result; | |||||
| } | |||||
| reloadLogic(); | |||||
| emit graphChanged(); | |||||
| return result; | |||||
| } | |||||
| } | |||||
| return {false, LogicEditorError::NodeNotFound, "no logic object is selected", {}}; | |||||
| } | |||||
| void LogicEditorWidget::mousePressEvent(QMouseEvent *event) | |||||
| { | |||||
| if (editing_enabled_ && event->button() == Qt::LeftButton) | |||||
| { | |||||
| NodeItem *output = nodeAtPort(mapToScene(event->pos()), true); | |||||
| if (output != nullptr) | |||||
| { | |||||
| connecting_from_node_id_ = output->nodeId(); | |||||
| preview_item_ = scene_->addPath(QPainterPath()); | |||||
| preview_item_->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2, Qt::DashLine)); | |||||
| preview_item_->setZValue(-0.5); | |||||
| updatePreview(mapToScene(event->pos())); | |||||
| event->accept(); | |||||
| return; | |||||
| } | |||||
| } | |||||
| QGraphicsView::mousePressEvent(event); | |||||
| } | |||||
| void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event) | |||||
| { | |||||
| if (!connecting_from_node_id_.empty()) | |||||
| { | |||||
| updatePreview(mapToScene(event->pos())); | |||||
| event->accept(); | |||||
| return; | |||||
| } | |||||
| QGraphicsView::mouseMoveEvent(event); | |||||
| } | |||||
| void LogicEditorWidget::mouseReleaseEvent(QMouseEvent *event) | |||||
| { | |||||
| if (!connecting_from_node_id_.empty()) | |||||
| { | |||||
| NodeItem *input = nodeAtPort(mapToScene(event->pos()), false); | |||||
| const std::string from = connecting_from_node_id_; | |||||
| clearPreview(); | |||||
| if (input != nullptr && input->nodeId() != from) | |||||
| { | |||||
| const LogicEditorResult result = editor_service_.connectNodes( | |||||
| logic_id_, from, input->nodeId()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| reportFailure(result); | |||||
| } | |||||
| else | |||||
| { | |||||
| reloadLogic(); | |||||
| emit graphChanged(); | |||||
| } | |||||
| } | |||||
| event->accept(); | |||||
| return; | |||||
| } | |||||
| QGraphicsView::mouseReleaseEvent(event); | |||||
| } | |||||
| void LogicEditorWidget::resizeEvent(QResizeEvent *event) | |||||
| { | |||||
| QGraphicsView::resizeEvent(event); | |||||
| updateConnections(); | |||||
| } | |||||
| void LogicEditorWidget::handleSelectionChanged() | |||||
| { | |||||
| emit nodeSelected(QString::fromStdString(selectedNodeId())); | |||||
| } | |||||
| void LogicEditorWidget::handleNodePositionChanged(const std::string &) | |||||
| { | |||||
| updateConnections(); | |||||
| } | |||||
| void LogicEditorWidget::updateConnections() | |||||
| { | |||||
| std::map<std::string, NodeItem *> node_items; | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node != nullptr) | |||||
| { | |||||
| node_items.emplace(node->nodeId(), node); | |||||
| } | |||||
| } | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| ConnectionItem *connection = dynamic_cast<ConnectionItem *>(item); | |||||
| if (connection == nullptr) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| const auto from = node_items.find(connection->fromNodeId()); | |||||
| const auto to = node_items.find(connection->toNodeId()); | |||||
| if (from != node_items.end() && to != node_items.end()) | |||||
| { | |||||
| connection->updatePath( | |||||
| from->second->outputPortScenePosition(), | |||||
| to->second->inputPortScenePosition()); | |||||
| } | |||||
| } | |||||
| } | |||||
| void LogicEditorWidget::updatePreview(const QPointF &scene_position) | |||||
| { | |||||
| if (preview_item_ == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node != nullptr && node->nodeId() == connecting_from_node_id_) | |||||
| { | |||||
| preview_item_->setPath( | |||||
| makeConnectionPath(node->outputPortScenePosition(), scene_position)); | |||||
| return; | |||||
| } | |||||
| } | |||||
| } | |||||
| void LogicEditorWidget::clearPreview() | |||||
| { | |||||
| if (preview_item_ != nullptr) | |||||
| { | |||||
| scene_->removeItem(preview_item_); | |||||
| delete preview_item_; | |||||
| preview_item_ = nullptr; | |||||
| } | |||||
| connecting_from_node_id_.clear(); | |||||
| } | |||||
| LogicEditorWidget::NodeItem *LogicEditorWidget::nodeAtPort( | |||||
| const QPointF &scene_position, bool output) const | |||||
| { | |||||
| for (QGraphicsItem *item : scene_->items()) | |||||
| { | |||||
| NodeItem *node = dynamic_cast<NodeItem *>(item); | |||||
| if (node == nullptr || (output && !node->hasOutput())) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| const QPointF port = output | |||||
| ? node->outputPortScenePosition() | |||||
| : node->inputPortScenePosition(); | |||||
| if (QLineF(port, scene_position).length() <= 12.0) | |||||
| { | |||||
| return node; | |||||
| } | |||||
| } | |||||
| return nullptr; | |||||
| } | |||||
| LogicPoint LogicEditorWidget::nextNodePosition() const | |||||
| { | |||||
| const ControlLogic *logic = editor_service_.findLogic(logic_id_); | |||||
| for (std::size_t index = 0;; ++index) | |||||
| { | |||||
| const LogicPoint candidate{ | |||||
| 80 + static_cast<int>(index % 5U) * 230, | |||||
| 80 + static_cast<int>(index / 5U) * 130}; | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return candidate; | |||||
| } | |||||
| const auto occupied = std::find_if( | |||||
| logic->nodes.cbegin(), | |||||
| logic->nodes.cend(), | |||||
| [&candidate](const LogicNode &node) | |||||
| { | |||||
| return std::abs(node.position.x - candidate.x) < 40 | |||||
| && std::abs(node.position.y - candidate.y) < 40; | |||||
| }); | |||||
| if (occupied == logic->nodes.cend()) | |||||
| { | |||||
| return candidate; | |||||
| } | |||||
| } | |||||
| } | |||||
| void LogicEditorWidget::reportFailure(const LogicEditorResult &result) | |||||
| { | |||||
| emit editorError(QString::fromStdString(result.message)); | |||||
| } | |||||
| @@ -0,0 +1,64 @@ | |||||
| #pragma once | |||||
| #include "domain/control_logic_model.h" | |||||
| #include <QGraphicsView> | |||||
| #include <string> | |||||
| class LogicEditorService; | |||||
| struct LogicEditorResult; | |||||
| class QGraphicsPathItem; | |||||
| class QGraphicsScene; | |||||
| class QMouseEvent; | |||||
| class QResizeEvent; | |||||
| class LogicEditorWidget final : public QGraphicsView | |||||
| { | |||||
| Q_OBJECT | |||||
| public: | |||||
| explicit LogicEditorWidget( | |||||
| LogicEditorService &editor_service, | |||||
| QWidget *parent = nullptr); | |||||
| void setLogicId(const std::string &logic_id); | |||||
| void setEditingEnabled(bool enabled); | |||||
| void reloadLogic(); | |||||
| void selectNode(const std::string &node_id); | |||||
| std::string selectedNodeId() const; | |||||
| LogicEditorResult addNode(const LogicNodeConfig &config); | |||||
| LogicEditorResult deleteSelected(); | |||||
| signals: | |||||
| void nodeSelected(const QString &node_id); | |||||
| void nodeChanged(const QString &node_id); | |||||
| void graphChanged(); | |||||
| void editorError(const QString &message); | |||||
| protected: | |||||
| void mousePressEvent(QMouseEvent *event) override; | |||||
| void mouseMoveEvent(QMouseEvent *event) override; | |||||
| void mouseReleaseEvent(QMouseEvent *event) override; | |||||
| void resizeEvent(QResizeEvent *event) override; | |||||
| private: | |||||
| class NodeItem; | |||||
| class ConnectionItem; | |||||
| void handleSelectionChanged(); | |||||
| void handleNodePositionChanged(const std::string &node_id); | |||||
| void updateConnections(); | |||||
| void updatePreview(const QPointF &scene_position); | |||||
| void clearPreview(); | |||||
| NodeItem *nodeAtPort(const QPointF &scene_position, bool output) const; | |||||
| LogicPoint nextNodePosition() const; | |||||
| void reportFailure(const LogicEditorResult &result); | |||||
| LogicEditorService &editor_service_; | |||||
| QGraphicsScene *scene_ = nullptr; | |||||
| std::string logic_id_; | |||||
| bool editing_enabled_ = true; | |||||
| std::string connecting_from_node_id_; | |||||
| QGraphicsPathItem *preview_item_ = nullptr; | |||||
| }; | |||||
| @@ -1,8 +1,10 @@ | |||||
| #include "main_window.h" | #include "main_window.h" | ||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "logic_editor_widget.h" | |||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/logic_editor_service.h" | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "ui_main_window.h" | #include "ui_main_window.h" | ||||
| @@ -18,13 +20,19 @@ | |||||
| #include <QMessageBox> | #include <QMessageBox> | ||||
| #include <QPushButton> | #include <QPushButton> | ||||
| #include <QSpinBox> | #include <QSpinBox> | ||||
| #include <QStackedWidget> | |||||
| #include <QStatusBar> | #include <QStatusBar> | ||||
| #include <QStyle> | #include <QStyle> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <QToolBar> | #include <QToolBar> | ||||
| #include <QTabWidget> | |||||
| #include <QTreeWidget> | #include <QTreeWidget> | ||||
| #include <QTreeWidgetItem> | #include <QTreeWidgetItem> | ||||
| #include <cstdint> | |||||
| #include <limits> | |||||
| #include <type_traits> | |||||
| namespace { | namespace { | ||||
| QString modeText(ApplicationMode mode) | QString modeText(ApplicationMode mode) | ||||
| @@ -124,6 +132,7 @@ MainWindow::MainWindow( | |||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | |||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| QWidget *parent) | QWidget *parent) | ||||
| : QMainWindow(parent), | : QMainWindow(parent), | ||||
| @@ -131,6 +140,7 @@ MainWindow::MainWindow( | |||||
| runtime_mode_service_(runtime_mode_service), | runtime_mode_service_(runtime_mode_service), | ||||
| project_service_(project_service), | project_service_(project_service), | ||||
| hmi_editor_service_(hmi_editor_service), | hmi_editor_service_(hmi_editor_service), | ||||
| logic_editor_service_(logic_editor_service), | |||||
| hmi_runtime_service_(hmi_runtime_service) | hmi_runtime_service_(hmi_runtime_service) | ||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| @@ -138,7 +148,9 @@ MainWindow::MainWindow( | |||||
| configureActions(); // 创建所有菜单 Action、工具栏按钮,绑定点击槽函数 | configureActions(); // 创建所有菜单 Action、工具栏按钮,绑定点击槽函数 | ||||
| configurePropertyEditor(); // 动态构建属性表单:ID、文本、坐标宽高、寄存器区域 (M/D)、地址索引 + 应用按钮 | configurePropertyEditor(); // 动态构建属性表单:ID、文本、坐标宽高、寄存器区域 (M/D)、地址索引 + 应用按钮 | ||||
| configureHmiEditor(); // 创建画布控件HmiEditorWidget嵌入主窗口,绑定画布信号:选中控件、控件拖动修改、编辑器报错;同时启动运行刷新定时器 | configureHmiEditor(); // 创建画布控件HmiEditorWidget嵌入主窗口,绑定画布信号:选中控件、控件拖动修改、编辑器报错;同时启动运行刷新定时器 | ||||
| configureLogicEditor(); | |||||
| hmi_editor_service_.ensureDefaultPage(); | hmi_editor_service_.ensureDefaultPage(); | ||||
| logic_editor_service_.ensureDefaultLogic(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| updateModeUi(tr("系统已进入编辑态")); | updateModeUi(tr("系统已进入编辑态")); | ||||
| } | } | ||||
| @@ -216,10 +228,112 @@ void MainWindow::configureActions() | |||||
| connect(delete_control_action_, &QAction::triggered, | connect(delete_control_action_, &QAction::triggered, | ||||
| this, &MainWindow::deleteSelectedControl); | this, &MainWindow::deleteSelectedControl); | ||||
| logic_tool_bar_ = addToolBar(tr("控制逻辑节点")); | |||||
| logic_tool_bar_->setObjectName(QStringLiteral("logicToolBar")); | |||||
| logic_tool_bar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); | |||||
| add_normally_open_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_ArrowRight), tr("常开")); | |||||
| add_normally_closed_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_ArrowRight), tr("常闭")); | |||||
| add_normal_coil_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_DialogApplyButton), tr("线圈")); | |||||
| add_set_coil_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_DialogYesButton), tr("置位")); | |||||
| add_reset_coil_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_DialogNoButton), tr("复位")); | |||||
| add_compare_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("D 比较")); | |||||
| delete_logic_action_ = logic_tool_bar_->addAction( | |||||
| style()->standardIcon(QStyle::SP_TrashIcon), tr("删除")); | |||||
| add_normally_open_action_->setObjectName(QStringLiteral("addNormallyOpenAction")); | |||||
| add_normally_closed_action_->setObjectName(QStringLiteral("addNormallyClosedAction")); | |||||
| add_normal_coil_action_->setObjectName(QStringLiteral("addNormalCoilAction")); | |||||
| add_set_coil_action_->setObjectName(QStringLiteral("addSetCoilAction")); | |||||
| add_reset_coil_action_->setObjectName(QStringLiteral("addResetCoilAction")); | |||||
| add_compare_action_->setObjectName(QStringLiteral("addCompareAction")); | |||||
| delete_logic_action_->setObjectName(QStringLiteral("deleteLogicAction")); | |||||
| add_normally_open_action_->setToolTip(tr("添加常开触点")); | |||||
| add_normally_closed_action_->setToolTip(tr("添加常闭触点")); | |||||
| add_normal_coil_action_->setToolTip(tr("添加普通线圈")); | |||||
| add_set_coil_action_->setToolTip(tr("添加置位线圈")); | |||||
| add_reset_coil_action_->setToolTip(tr("添加复位线圈")); | |||||
| add_compare_action_->setToolTip(tr("添加 D 值与常量比较")); | |||||
| delete_logic_action_->setToolTip(tr("删除选中的逻辑节点或连接")); | |||||
| connect(add_normally_open_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| ContactMode::NormallyOpen}); | |||||
| }); | |||||
| connect(add_normally_closed_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| ContactMode::NormallyClosed}); | |||||
| }); | |||||
| connect(add_normal_coil_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| CoilMode::Normal}); | |||||
| }); | |||||
| connect(add_set_coil_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| CoilMode::Set}); | |||||
| }); | |||||
| connect(add_reset_coil_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| CoilMode::Reset}); | |||||
| }); | |||||
| connect(add_compare_action_, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| addLogicNode(CompareNodeConfig{ | |||||
| RegisterAddress{RegisterArea::D, 0}, | |||||
| ComparisonOperator::Equal, | |||||
| 0}); | |||||
| }); | |||||
| connect(delete_logic_action_, &QAction::triggered, | |||||
| this, &MainWindow::deleteSelectedLogicObject); | |||||
| connect(ui_->editorTabWidget, &QTabWidget::currentChanged, | |||||
| this, | |||||
| [this](int index) | |||||
| { | |||||
| hmi_tool_bar_->setVisible(index == 0); | |||||
| logic_tool_bar_->setVisible(index == 1); | |||||
| if (index == 0) | |||||
| { | |||||
| showControlProperties(selected_control_id_); | |||||
| } | |||||
| else | |||||
| { | |||||
| showLogicNodeProperties(selected_logic_node_id_); | |||||
| } | |||||
| }); | |||||
| hmi_tool_bar_->setVisible(ui_->editorTabWidget->currentIndex() == 0); | |||||
| logic_tool_bar_->setVisible(ui_->editorTabWidget->currentIndex() == 1); | |||||
| ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction()); | ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction()); | ||||
| ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction()); | ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction()); | ||||
| ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction()); | ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction()); | ||||
| ui_->viewMenu->addAction(hmi_tool_bar_->toggleViewAction()); | ui_->viewMenu->addAction(hmi_tool_bar_->toggleViewAction()); | ||||
| ui_->viewMenu->addAction(logic_tool_bar_->toggleViewAction()); | |||||
| ui_->viewMenu->addSeparator(); | ui_->viewMenu->addSeparator(); | ||||
| ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction()); | ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction()); | ||||
| } | } | ||||
| @@ -306,6 +420,38 @@ void MainWindow::configureHmiEditor() | |||||
| runtime_refresh_timer_->start(); | runtime_refresh_timer_->start(); | ||||
| } | } | ||||
| void MainWindow::configureLogicEditor() | |||||
| { | |||||
| QLayout *layout = ui_->logicCanvasPlaceholder->layout(); | |||||
| delete ui_->logicEmptyLabel; | |||||
| logic_editor_widget_ = new LogicEditorWidget( | |||||
| logic_editor_service_, ui_->logicCanvasPlaceholder); | |||||
| logic_editor_widget_->setObjectName(QStringLiteral("logicEditorWidget")); | |||||
| logic_editor_widget_->setMinimumHeight(320); | |||||
| layout->addWidget(logic_editor_widget_); | |||||
| connect(logic_editor_widget_, &LogicEditorWidget::nodeSelected, | |||||
| this, | |||||
| [this](const QString &id) | |||||
| { | |||||
| showLogicNodeProperties(toUtf8(id)); | |||||
| }); | |||||
| connect(logic_editor_widget_, &LogicEditorWidget::nodeChanged, | |||||
| this, | |||||
| [this](const QString &id) | |||||
| { | |||||
| showLogicNodeProperties(toUtf8(id)); | |||||
| refreshProjectUi(); | |||||
| }); | |||||
| connect(logic_editor_widget_, &LogicEditorWidget::graphChanged, | |||||
| this, &MainWindow::refreshProjectUi); | |||||
| connect(logic_editor_widget_, &LogicEditorWidget::editorError, | |||||
| this, | |||||
| [this](const QString &message) | |||||
| { | |||||
| statusBar()->showMessage(message, 5000); | |||||
| }); | |||||
| } | |||||
| void MainWindow::configurePropertyEditor() | void MainWindow::configurePropertyEditor() | ||||
| { | { | ||||
| control_id_edit_ = new QLineEdit(ui_->propertiesPage); | control_id_edit_ = new QLineEdit(ui_->propertiesPage); | ||||
| @@ -337,18 +483,75 @@ void MainWindow::configurePropertyEditor() | |||||
| binding_index_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); | binding_index_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); | ||||
| apply_properties_button_ = new QPushButton(tr("应用属性"), ui_->propertiesPage); | apply_properties_button_ = new QPushButton(tr("应用属性"), ui_->propertiesPage); | ||||
| apply_properties_button_->setObjectName(QStringLiteral("applyPropertiesButton")); | apply_properties_button_->setObjectName(QStringLiteral("applyPropertiesButton")); | ||||
| QFormLayout *form = ui_->propertiesForm; | |||||
| form->addRow(tr("控件 ID"), control_id_edit_); | |||||
| form->addRow(tr("显示文本"), control_text_edit_); | |||||
| form->addRow(tr("X"), control_x_spin_box_); | |||||
| form->addRow(tr("Y"), control_y_spin_box_); | |||||
| form->addRow(tr("宽度"), control_width_spin_box_); | |||||
| form->addRow(tr("高度"), control_height_spin_box_); | |||||
| form->addRow(tr("绑定区域"), binding_area_combo_box_); | |||||
| form->addRow(tr("绑定地址"), binding_index_spin_box_); | |||||
| form->addRow(apply_properties_button_); | |||||
| property_stack_ = new QStackedWidget(ui_->propertiesPage); | |||||
| property_stack_->setObjectName(QStringLiteral("propertyStack")); | |||||
| hmi_properties_page_ = new QWidget(property_stack_); | |||||
| auto *hmi_form = new QFormLayout(hmi_properties_page_); | |||||
| hmi_form->setContentsMargins(0, 0, 0, 0); | |||||
| hmi_form->addRow(tr("控件 ID"), control_id_edit_); | |||||
| hmi_form->addRow(tr("显示文本"), control_text_edit_); | |||||
| hmi_form->addRow(tr("X"), control_x_spin_box_); | |||||
| hmi_form->addRow(tr("Y"), control_y_spin_box_); | |||||
| hmi_form->addRow(tr("宽度"), control_width_spin_box_); | |||||
| hmi_form->addRow(tr("高度"), control_height_spin_box_); | |||||
| hmi_form->addRow(tr("绑定区域"), binding_area_combo_box_); | |||||
| hmi_form->addRow(tr("绑定地址"), binding_index_spin_box_); | |||||
| hmi_form->addRow(apply_properties_button_); | |||||
| logic_properties_page_ = new QWidget(property_stack_); | |||||
| auto *logic_form = new QFormLayout(logic_properties_page_); | |||||
| logic_form->setContentsMargins(0, 0, 0, 0); | |||||
| logic_node_id_edit_ = new QLineEdit(logic_properties_page_); | |||||
| logic_node_id_edit_->setObjectName(QStringLiteral("logicNodeIdEdit")); | |||||
| logic_node_id_edit_->setReadOnly(true); | |||||
| logic_node_type_label_ = new QLabel(logic_properties_page_); | |||||
| logic_node_type_label_->setObjectName(QStringLiteral("logicNodeTypeLabel")); | |||||
| logic_address_area_label_ = new QLabel(logic_properties_page_); | |||||
| logic_address_area_label_->setObjectName(QStringLiteral("logicAddressAreaLabel")); | |||||
| logic_address_spin_box_ = new QSpinBox(logic_properties_page_); | |||||
| logic_address_spin_box_->setObjectName(QStringLiteral("logicAddressSpinBox")); | |||||
| logic_address_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); | |||||
| logic_mode_combo_box_ = new QComboBox(logic_properties_page_); | |||||
| logic_mode_combo_box_->setObjectName(QStringLiteral("logicModeComboBox")); | |||||
| logic_comparison_combo_box_ = new QComboBox(logic_properties_page_); | |||||
| logic_comparison_combo_box_->setObjectName(QStringLiteral("logicComparisonComboBox")); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("等于"), static_cast<int>(ComparisonOperator::Equal)); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("不等于"), static_cast<int>(ComparisonOperator::NotEqual)); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("小于"), static_cast<int>(ComparisonOperator::LessThan)); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("小于等于"), static_cast<int>(ComparisonOperator::LessThanOrEqual)); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("大于"), static_cast<int>(ComparisonOperator::GreaterThan)); | |||||
| logic_comparison_combo_box_->addItem( | |||||
| tr("大于等于"), static_cast<int>(ComparisonOperator::GreaterThanOrEqual)); | |||||
| logic_value_spin_box_ = new QSpinBox(logic_properties_page_); | |||||
| logic_value_spin_box_->setObjectName(QStringLiteral("logicValueSpinBox")); | |||||
| logic_value_spin_box_->setRange( | |||||
| std::numeric_limits<std::int16_t>::min(), | |||||
| std::numeric_limits<std::int16_t>::max()); | |||||
| apply_logic_properties_button_ = new QPushButton( | |||||
| tr("应用属性"), logic_properties_page_); | |||||
| apply_logic_properties_button_->setObjectName( | |||||
| QStringLiteral("applyLogicPropertiesButton")); | |||||
| logic_form->addRow(tr("节点 ID"), logic_node_id_edit_); | |||||
| logic_form->addRow(tr("节点类型"), logic_node_type_label_); | |||||
| logic_form->addRow(tr("地址区域"), logic_address_area_label_); | |||||
| logic_form->addRow(tr("地址"), logic_address_spin_box_); | |||||
| logic_form->addRow(tr("工作方式"), logic_mode_combo_box_); | |||||
| logic_form->addRow(tr("比较运算"), logic_comparison_combo_box_); | |||||
| logic_form->addRow(tr("比较常量"), logic_value_spin_box_); | |||||
| logic_form->addRow(apply_logic_properties_button_); | |||||
| property_stack_->addWidget(hmi_properties_page_); | |||||
| property_stack_->addWidget(logic_properties_page_); | |||||
| ui_->propertiesForm->addRow(property_stack_); | |||||
| connect(apply_properties_button_, &QPushButton::clicked, | connect(apply_properties_button_, &QPushButton::clicked, | ||||
| this, &MainWindow::applySelectedControlProperties); | this, &MainWindow::applySelectedControlProperties); | ||||
| connect(apply_logic_properties_button_, &QPushButton::clicked, | |||||
| this, &MainWindow::applySelectedLogicNodeProperties); | |||||
| showControlProperties({}); | showControlProperties({}); | ||||
| } | } | ||||
| @@ -361,6 +564,11 @@ void MainWindow::refreshProjectUi() | |||||
| hmi_editor_widget_->setPageId(current_page_id); | hmi_editor_widget_->setPageId(current_page_id); | ||||
| } | } | ||||
| const HmiPage *page = hmi_editor_service_.findPage(current_page_id); | const HmiPage *page = hmi_editor_service_.findPage(current_page_id); | ||||
| const std::string current_logic_id = logic_editor_service_.firstLogicId(); | |||||
| if (logic_editor_widget_ != nullptr) | |||||
| { | |||||
| logic_editor_widget_->setLogicId(current_logic_id); | |||||
| } | |||||
| ui_->projectTree->clear(); | ui_->projectTree->clear(); | ||||
| auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | ||||
| if (page != nullptr) | if (page != nullptr) | ||||
| @@ -375,13 +583,20 @@ void MainWindow::refreshProjectUi() | |||||
| ui_->hmiPageTitleLabel->setText(tr("尚未创建 HMI 页面")); | ui_->hmiPageTitleLabel->setText(tr("尚未创建 HMI 页面")); | ||||
| ui_->hmiPageSizeLabel->clear(); | ui_->hmiPageSizeLabel->clear(); | ||||
| } | } | ||||
| new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); | |||||
| auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); | |||||
| for (const ControlLogic &logic : project_service_.project().controlLogics) | |||||
| { | |||||
| const QString suffix = logic.enabled ? QString{} : tr("(已禁用)"); | |||||
| logic_root->addChild(new QTreeWidgetItem( | |||||
| logic_root, {fromUtf8(logic.name) + suffix})); | |||||
| } | |||||
| ui_->projectTree->expandAll(); | ui_->projectTree->expandAll(); | ||||
| } | } | ||||
| // 根据控件ID加载控件属性到右侧属性面板 | // 根据控件ID加载控件属性到右侧属性面板 | ||||
| void MainWindow::showControlProperties(const std::string &control_id) | void MainWindow::showControlProperties(const std::string &control_id) | ||||
| { | { | ||||
| property_stack_->setCurrentWidget(hmi_properties_page_); | |||||
| // 更新窗口全局状态:记录当前选中控件ID | // 更新窗口全局状态:记录当前选中控件ID | ||||
| selected_control_id_ = control_id; | selected_control_id_ = control_id; | ||||
| @@ -439,6 +654,86 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| control->binding.has_value() ? control->binding->index() : 0); | control->binding.has_value() ? control->binding->index() : 0); | ||||
| } | } | ||||
| void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||||
| { | |||||
| property_stack_->setCurrentWidget(logic_properties_page_); | |||||
| selected_logic_node_id_ = node_id; | |||||
| const LogicNode *node = logic_editor_service_.findNode( | |||||
| logic_editor_service_.firstLogicId(), node_id); | |||||
| const bool has_node = node != nullptr; | |||||
| ui_->selectionValueLabel->setText( | |||||
| has_node ? fromUtf8(node->id) : tr("未选择")); | |||||
| for (QWidget *widget : {static_cast<QWidget *>(logic_node_id_edit_), | |||||
| static_cast<QWidget *>(logic_address_spin_box_), | |||||
| static_cast<QWidget *>(logic_mode_combo_box_), | |||||
| static_cast<QWidget *>(logic_comparison_combo_box_), | |||||
| static_cast<QWidget *>(logic_value_spin_box_), | |||||
| static_cast<QWidget *>(apply_logic_properties_button_)}) | |||||
| { | |||||
| widget->setEnabled(has_node); | |||||
| } | |||||
| logic_node_type_label_->setEnabled(has_node); | |||||
| logic_address_area_label_->setEnabled(has_node); | |||||
| if (!has_node) | |||||
| { | |||||
| logic_node_id_edit_->clear(); | |||||
| logic_node_type_label_->clear(); | |||||
| logic_address_area_label_->clear(); | |||||
| logic_mode_combo_box_->clear(); | |||||
| return; | |||||
| } | |||||
| logic_node_id_edit_->setText(fromUtf8(node->id)); | |||||
| logic_mode_combo_box_->clear(); | |||||
| if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config)) | |||||
| { | |||||
| logic_node_type_label_->setText(tr("触点")); | |||||
| logic_address_area_label_->setText(QStringLiteral("M")); | |||||
| logic_address_spin_box_->setValue(contact->address.index()); | |||||
| logic_mode_combo_box_->addItem( | |||||
| tr("常开"), static_cast<int>(ContactMode::NormallyOpen)); | |||||
| logic_mode_combo_box_->addItem( | |||||
| tr("常闭"), static_cast<int>(ContactMode::NormallyClosed)); | |||||
| logic_mode_combo_box_->setCurrentIndex( | |||||
| logic_mode_combo_box_->findData(static_cast<int>(contact->mode))); | |||||
| logic_mode_combo_box_->setEnabled(true); | |||||
| logic_comparison_combo_box_->setEnabled(false); | |||||
| logic_value_spin_box_->setEnabled(false); | |||||
| } | |||||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config)) | |||||
| { | |||||
| logic_node_type_label_->setText(tr("线圈")); | |||||
| logic_address_area_label_->setText(QStringLiteral("M")); | |||||
| logic_address_spin_box_->setValue(coil->address.index()); | |||||
| logic_mode_combo_box_->addItem( | |||||
| tr("普通"), static_cast<int>(CoilMode::Normal)); | |||||
| logic_mode_combo_box_->addItem( | |||||
| tr("置位"), static_cast<int>(CoilMode::Set)); | |||||
| logic_mode_combo_box_->addItem( | |||||
| tr("复位"), static_cast<int>(CoilMode::Reset)); | |||||
| logic_mode_combo_box_->setCurrentIndex( | |||||
| logic_mode_combo_box_->findData(static_cast<int>(coil->mode))); | |||||
| logic_mode_combo_box_->setEnabled(true); | |||||
| logic_comparison_combo_box_->setEnabled(false); | |||||
| logic_value_spin_box_->setEnabled(false); | |||||
| } | |||||
| else | |||||
| { | |||||
| const auto &compare = std::get<CompareNodeConfig>(node->config); | |||||
| logic_node_type_label_->setText(tr("D 值比较")); | |||||
| logic_address_area_label_->setText(QStringLiteral("D")); | |||||
| logic_address_spin_box_->setValue(compare.address.index()); | |||||
| logic_mode_combo_box_->addItem(tr("不适用")); | |||||
| logic_mode_combo_box_->setEnabled(false); | |||||
| logic_comparison_combo_box_->setEnabled(true); | |||||
| logic_comparison_combo_box_->setCurrentIndex( | |||||
| logic_comparison_combo_box_->findData( | |||||
| static_cast<int>(compare.comparison))); | |||||
| logic_value_spin_box_->setEnabled(true); | |||||
| logic_value_spin_box_->setValue(compare.value); | |||||
| } | |||||
| } | |||||
| void MainWindow::addHmiControl(HmiControlType type) | void MainWindow::addHmiControl(HmiControlType type) | ||||
| { | { | ||||
| const std::string page_id = hmi_editor_service_.firstPageId(); | const std::string page_id = hmi_editor_service_.firstPageId(); | ||||
| @@ -507,6 +802,78 @@ void MainWindow::applySelectedControlProperties() | |||||
| statusBar()->showMessage(tr("控件属性已更新"), 3000); | statusBar()->showMessage(tr("控件属性已更新"), 3000); | ||||
| } | } | ||||
| void MainWindow::addLogicNode(const LogicNodeConfig &config) | |||||
| { | |||||
| const LogicEditorResult result = logic_editor_widget_->addNode(config); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| showProjectResult(tr("添加逻辑节点"), fromUtf8(result.message), false); | |||||
| return; | |||||
| } | |||||
| selected_logic_node_id_ = result.id; | |||||
| showLogicNodeProperties(result.id); | |||||
| refreshProjectUi(); | |||||
| statusBar()->showMessage(tr("已添加逻辑节点"), 3000); | |||||
| } | |||||
| void MainWindow::deleteSelectedLogicObject() | |||||
| { | |||||
| const LogicEditorResult result = logic_editor_widget_->deleteSelected(); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| return; | |||||
| } | |||||
| selected_logic_node_id_.clear(); | |||||
| showLogicNodeProperties({}); | |||||
| refreshProjectUi(); | |||||
| } | |||||
| void MainWindow::applySelectedLogicNodeProperties() | |||||
| { | |||||
| const std::string logic_id = logic_editor_service_.firstLogicId(); | |||||
| const LogicNode *node = logic_editor_service_.findNode( | |||||
| logic_id, selected_logic_node_id_); | |||||
| if (node == nullptr) | |||||
| { | |||||
| return; | |||||
| } | |||||
| LogicNodeConfig config = node->config; | |||||
| if (std::holds_alternative<ContactNodeConfig>(config)) | |||||
| { | |||||
| config = ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, logic_address_spin_box_->value()}, | |||||
| static_cast<ContactMode>(logic_mode_combo_box_->currentData().toInt())}; | |||||
| } | |||||
| else if (std::holds_alternative<CoilNodeConfig>(config)) | |||||
| { | |||||
| config = CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, logic_address_spin_box_->value()}, | |||||
| static_cast<CoilMode>(logic_mode_combo_box_->currentData().toInt())}; | |||||
| } | |||||
| else | |||||
| { | |||||
| config = CompareNodeConfig{ | |||||
| RegisterAddress{RegisterArea::D, logic_address_spin_box_->value()}, | |||||
| static_cast<ComparisonOperator>( | |||||
| logic_comparison_combo_box_->currentData().toInt()), | |||||
| static_cast<std::int16_t>(logic_value_spin_box_->value())}; | |||||
| } | |||||
| const LogicEditorResult result = logic_editor_service_.updateNodeConfig( | |||||
| logic_id, selected_logic_node_id_, config); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false); | |||||
| return; | |||||
| } | |||||
| logic_editor_widget_->reloadLogic(); | |||||
| logic_editor_widget_->selectNode(selected_logic_node_id_); | |||||
| showLogicNodeProperties(selected_logic_node_id_); | |||||
| refreshProjectUi(); | |||||
| statusBar()->showMessage(tr("逻辑节点属性已更新"), 3000); | |||||
| } | |||||
| void MainWindow::createNewProject() | void MainWindow::createNewProject() | ||||
| { | { | ||||
| const QString name = QInputDialog::getText( | const QString name = QInputDialog::getText( | ||||
| @@ -522,9 +889,12 @@ void MainWindow::createNewProject() | |||||
| return; | return; | ||||
| } | } | ||||
| hmi_editor_service_.ensureDefaultPage(); | hmi_editor_service_.ensureDefaultPage(); | ||||
| logic_editor_service_.ensureDefaultLogic(); | |||||
| selected_control_id_.clear(); | selected_control_id_.clear(); | ||||
| selected_logic_node_id_.clear(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| hmi_editor_widget_->reloadPage(); | hmi_editor_widget_->reloadPage(); | ||||
| logic_editor_widget_->reloadLogic(); | |||||
| showControlProperties({}); | showControlProperties({}); | ||||
| statusBar()->showMessage(tr("已创建新工程"), 3000); | statusBar()->showMessage(tr("已创建新工程"), 3000); | ||||
| } | } | ||||
| @@ -570,8 +940,10 @@ void MainWindow::loadProject() | |||||
| return; | return; | ||||
| } | } | ||||
| selected_control_id_.clear(); | selected_control_id_.clear(); | ||||
| selected_logic_node_id_.clear(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| hmi_editor_widget_->reloadPage(); | hmi_editor_widget_->reloadPage(); | ||||
| logic_editor_widget_->reloadLogic(); | |||||
| showControlProperties({}); | showControlProperties({}); | ||||
| showProjectResult(tr("加载工程"), tr("工程已加载"), true); | showProjectResult(tr("加载工程"), tr("工程已加载"), true); | ||||
| } | } | ||||
| @@ -636,12 +1008,21 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); | ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); | ||||
| hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); | hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); | ||||
| hmi_editor_widget_->setRuntimeActive(policy.usesVirtualRegisters); | hmi_editor_widget_->setRuntimeActive(policy.usesVirtualRegisters); | ||||
| logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); | |||||
| hmi_tool_bar_->setEnabled(policy.allowsProjectEditing); | hmi_tool_bar_->setEnabled(policy.allowsProjectEditing); | ||||
| logic_tool_bar_->setEnabled(policy.allowsProjectEditing); | |||||
| add_button_action_->setEnabled(policy.allowsProjectEditing); | add_button_action_->setEnabled(policy.allowsProjectEditing); | ||||
| add_indicator_action_->setEnabled(policy.allowsProjectEditing); | add_indicator_action_->setEnabled(policy.allowsProjectEditing); | ||||
| add_numeric_display_action_->setEnabled(policy.allowsProjectEditing); | add_numeric_display_action_->setEnabled(policy.allowsProjectEditing); | ||||
| add_numeric_input_action_->setEnabled(policy.allowsProjectEditing); | add_numeric_input_action_->setEnabled(policy.allowsProjectEditing); | ||||
| delete_control_action_->setEnabled(policy.allowsProjectEditing); | delete_control_action_->setEnabled(policy.allowsProjectEditing); | ||||
| add_normally_open_action_->setEnabled(policy.allowsProjectEditing); | |||||
| add_normally_closed_action_->setEnabled(policy.allowsProjectEditing); | |||||
| add_normal_coil_action_->setEnabled(policy.allowsProjectEditing); | |||||
| add_set_coil_action_->setEnabled(policy.allowsProjectEditing); | |||||
| add_reset_coil_action_->setEnabled(policy.allowsProjectEditing); | |||||
| add_compare_action_->setEnabled(policy.allowsProjectEditing); | |||||
| delete_logic_action_->setEnabled(policy.allowsProjectEditing); | |||||
| new_project_action_->setEnabled(policy.allowsProjectEditing); | new_project_action_->setEnabled(policy.allowsProjectEditing); | ||||
| save_project_action_->setEnabled(policy.allowsProjectEditing); | save_project_action_->setEnabled(policy.allowsProjectEditing); | ||||
| save_as_project_action_->setEnabled(policy.allowsProjectEditing); | save_as_project_action_->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -9,6 +9,7 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "domain/hmi_model.h" | #include "domain/hmi_model.h" | ||||
| #include "domain/control_logic_model.h" | |||||
| #include "domain/runtime_state.h" | #include "domain/runtime_state.h" | ||||
| #include <QMainWindow> | #include <QMainWindow> | ||||
| @@ -23,6 +24,7 @@ class QLabel; | |||||
| class QLineEdit; | class QLineEdit; | ||||
| class QPushButton; | class QPushButton; | ||||
| class QSpinBox; | class QSpinBox; | ||||
| class QStackedWidget; | |||||
| class QTimer; | class QTimer; | ||||
| class QToolBar; | class QToolBar; | ||||
| @@ -36,6 +38,8 @@ class ProjectService; | |||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class LogicEditorService; | |||||
| class LogicEditorWidget; | |||||
| /** | /** | ||||
| * @brief 组织主界面区域并将用户操作转交给服务层 | * @brief 组织主界面区域并将用户操作转交给服务层 | ||||
| @@ -59,6 +63,7 @@ public: | |||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | |||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| @@ -71,18 +76,28 @@ private: | |||||
| void configureAppearance(); | void configureAppearance(); | ||||
| // 创建并连接 HMI 编辑画布 | // 创建并连接 HMI 编辑画布 | ||||
| void configureHmiEditor(); | void configureHmiEditor(); | ||||
| // 创建并连接控制逻辑编辑画布 | |||||
| void configureLogicEditor(); | |||||
| // 创建并连接控件属性编辑表单 | // 创建并连接控件属性编辑表单 | ||||
| void configurePropertyEditor(); | void configurePropertyEditor(); | ||||
| // 刷新工程树和当前 HMI 页面信息 | // 刷新工程树和当前 HMI 页面信息 | ||||
| void refreshProjectUi(); | void refreshProjectUi(); | ||||
| // 显示指定 HMI 控件的可编辑属性 | // 显示指定 HMI 控件的可编辑属性 | ||||
| void showControlProperties(const std::string &control_id); | void showControlProperties(const std::string &control_id); | ||||
| // 显示指定控制逻辑节点的可编辑属性 | |||||
| void showLogicNodeProperties(const std::string &node_id); | |||||
| // 向当前页面添加指定类型的 HMI 控件 | // 向当前页面添加指定类型的 HMI 控件 | ||||
| void addHmiControl(HmiControlType type); | void addHmiControl(HmiControlType type); | ||||
| // 删除当前选中的 HMI 控件 | // 删除当前选中的 HMI 控件 | ||||
| void deleteSelectedControl(); | void deleteSelectedControl(); | ||||
| // 将属性表单内容应用到当前选中控件 | // 将属性表单内容应用到当前选中控件 | ||||
| void applySelectedControlProperties(); | void applySelectedControlProperties(); | ||||
| // 向当前控制逻辑添加指定配置的节点 | |||||
| void addLogicNode(const LogicNodeConfig &config); | |||||
| // 删除当前选中的逻辑节点或连接 | |||||
| void deleteSelectedLogicObject(); | |||||
| // 将逻辑属性表单内容应用到当前选中节点 | |||||
| void applySelectedLogicNodeProperties(); | |||||
| // 创建新的工程并刷新编辑界面 | // 创建新的工程并刷新编辑界面 | ||||
| void createNewProject(); | void createNewProject(); | ||||
| // 保存当前工程到已关联的路径 | // 保存当前工程到已关联的路径 | ||||
| @@ -123,6 +138,7 @@ private: | |||||
| */ | */ | ||||
| ProjectService &project_service_; | ProjectService &project_service_; | ||||
| HmiEditorService &hmi_editor_service_; | HmiEditorService &hmi_editor_service_; | ||||
| LogicEditorService &logic_editor_service_; | |||||
| HmiRuntimeService &hmi_runtime_service_; | HmiRuntimeService &hmi_runtime_service_; | ||||
| QActionGroup *mode_action_group_ = nullptr; | QActionGroup *mode_action_group_ = nullptr; | ||||
| QAction *new_project_action_ = nullptr; | QAction *new_project_action_ = nullptr; | ||||
| @@ -134,8 +150,17 @@ private: | |||||
| QAction *add_numeric_display_action_ = nullptr; | QAction *add_numeric_display_action_ = nullptr; | ||||
| QAction *add_numeric_input_action_ = nullptr; | QAction *add_numeric_input_action_ = nullptr; | ||||
| QAction *delete_control_action_ = nullptr; | QAction *delete_control_action_ = nullptr; | ||||
| QAction *add_normally_open_action_ = nullptr; | |||||
| QAction *add_normally_closed_action_ = nullptr; | |||||
| QAction *add_normal_coil_action_ = nullptr; | |||||
| QAction *add_set_coil_action_ = nullptr; | |||||
| QAction *add_reset_coil_action_ = nullptr; | |||||
| QAction *add_compare_action_ = nullptr; | |||||
| QAction *delete_logic_action_ = nullptr; | |||||
| QToolBar *hmi_tool_bar_ = nullptr; | QToolBar *hmi_tool_bar_ = nullptr; | ||||
| QToolBar *logic_tool_bar_ = nullptr; | |||||
| HmiEditorWidget *hmi_editor_widget_ = nullptr; | HmiEditorWidget *hmi_editor_widget_ = nullptr; | ||||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | |||||
| QTimer *runtime_refresh_timer_ = nullptr; | QTimer *runtime_refresh_timer_ = nullptr; | ||||
| QLabel *mode_status_label_ = nullptr; | QLabel *mode_status_label_ = nullptr; | ||||
| QLabel *register_status_label_ = nullptr; | QLabel *register_status_label_ = nullptr; | ||||
| @@ -149,5 +174,17 @@ private: | |||||
| QComboBox *binding_area_combo_box_ = nullptr; | QComboBox *binding_area_combo_box_ = nullptr; | ||||
| QSpinBox *binding_index_spin_box_ = nullptr; | QSpinBox *binding_index_spin_box_ = nullptr; | ||||
| QPushButton *apply_properties_button_ = nullptr; | QPushButton *apply_properties_button_ = nullptr; | ||||
| QStackedWidget *property_stack_ = nullptr; | |||||
| QWidget *hmi_properties_page_ = nullptr; | |||||
| QWidget *logic_properties_page_ = nullptr; | |||||
| QLineEdit *logic_node_id_edit_ = nullptr; | |||||
| QLabel *logic_node_type_label_ = nullptr; | |||||
| QLabel *logic_address_area_label_ = nullptr; | |||||
| QSpinBox *logic_address_spin_box_ = nullptr; | |||||
| QComboBox *logic_mode_combo_box_ = nullptr; | |||||
| QComboBox *logic_comparison_combo_box_ = nullptr; | |||||
| QSpinBox *logic_value_spin_box_ = nullptr; | |||||
| QPushButton *apply_logic_properties_button_ = nullptr; | |||||
| std::string selected_control_id_; | std::string selected_control_id_; | ||||
| std::string selected_logic_node_id_; | |||||
| }; | }; | ||||
| @@ -117,6 +117,54 @@ void testLogicNodeConfigurationBoundaries() | |||||
| require(comparison.validate(), "comparison node bound to D address must be valid"); | require(comparison.validate(), "comparison node bound to D address must be valid"); | ||||
| } | } | ||||
| void testLogicGraphBoundaries() | |||||
| { | |||||
| LogicNode start; | |||||
| start.id = "start"; | |||||
| start.config = ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| ContactMode::NormallyOpen}; | |||||
| LogicNode run_contact; | |||||
| run_contact.id = "run-contact"; | |||||
| run_contact.config = ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 1}, | |||||
| ContactMode::NormallyOpen}; | |||||
| LogicNode coil; | |||||
| coil.id = "run-coil"; | |||||
| coil.config = CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 1}, | |||||
| CoilMode::Normal}; | |||||
| ControlLogic logic; | |||||
| logic.id = "hold-logic"; | |||||
| logic.name = "Hold logic"; | |||||
| logic.nodes = {start, run_contact, coil}; | |||||
| logic.connections = { | |||||
| {start.id, coil.id}, | |||||
| {run_contact.id, coil.id}}; | |||||
| require(logic.validate(), "a branched self-hold graph must be valid"); | |||||
| logic.connections.push_back({start.id, coil.id}); | |||||
| require(!logic.validate(), "duplicate logic connections must be rejected"); | |||||
| logic.connections.pop_back(); | |||||
| logic.connections.push_back({coil.id, start.id}); | |||||
| require(!logic.validate(), "coil outgoing connections must be rejected"); | |||||
| logic.connections.pop_back(); | |||||
| logic.connections.push_back({start.id, run_contact.id}); | |||||
| logic.connections.push_back({run_contact.id, start.id}); | |||||
| require(!logic.validate(), "logic connection cycles must be rejected"); | |||||
| logic.connections.pop_back(); | |||||
| logic.connections.pop_back(); | |||||
| logic.connections.clear(); | |||||
| logic.connections.push_back({start.id, coil.id}); | |||||
| require(!logic.validate(), "a disconnected condition node must be rejected"); | |||||
| } | |||||
| void testModelsValidateBindingsAndIdentifiers() | void testModelsValidateBindingsAndIdentifiers() | ||||
| { | { | ||||
| // 聚合验证必须拒绝错误绑定、重复标识和越界控件 | // 聚合验证必须拒绝错误绑定、重复标识和越界控件 | ||||
| @@ -177,6 +225,7 @@ int main() | |||||
| testRegisterAddressBoundaries(); | testRegisterAddressBoundaries(); | ||||
| testRegisterRepositorySeparatesAreas(); | testRegisterRepositorySeparatesAreas(); | ||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testLogicGraphBoundaries(); | |||||
| testModelsValidateBindingsAndIdentifiers(); | testModelsValidateBindingsAndIdentifiers(); | ||||
| testRuntimeStateBoundaries(); | testRuntimeStateBoundaries(); | ||||
| } | } | ||||
| @@ -0,0 +1,102 @@ | |||||
| #include "domain/project_storage.h" | |||||
| #include "services/logic_editor_service.h" | |||||
| #include "services/project_service.h" | |||||
| #include <exception> | |||||
| #include <iostream> | |||||
| #include <stdexcept> | |||||
| namespace { | |||||
| class TestProjectStorage final : public ProjectStorage | |||||
| { | |||||
| public: | |||||
| ProjectSaveResult save(const Project &, const std::string &) override | |||||
| { | |||||
| return {true, ProjectStorageError::None, {}}; | |||||
| } | |||||
| ProjectLoadResult load(const std::string &) override | |||||
| { | |||||
| return {false, {}, ProjectStorageError::FileReadFailed, {}}; | |||||
| } | |||||
| }; | |||||
| void require(bool condition, const std::string &message) | |||||
| { | |||||
| if (!condition) | |||||
| { | |||||
| throw std::runtime_error(message); | |||||
| } | |||||
| } | |||||
| void testEditorOperations() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| LogicEditorService service(project_service); | |||||
| const LogicEditorResult logic_result = service.ensureDefaultLogic(); | |||||
| require(logic_result.succeeded, "default logic must be created"); | |||||
| const std::string logic_id = logic_result.id; | |||||
| const LogicEditorResult start_result = service.addNode( | |||||
| logic_id, | |||||
| ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| ContactMode::NormallyOpen}, | |||||
| {80, 80}); | |||||
| const LogicEditorResult coil_result = service.addNode( | |||||
| logic_id, | |||||
| CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 1}, | |||||
| CoilMode::Normal}, | |||||
| {360, 80}); | |||||
| require(start_result.succeeded && coil_result.succeeded, | |||||
| "contact and coil must be added"); | |||||
| require(service.connectNodes(logic_id, start_result.id, coil_result.id).succeeded, | |||||
| "valid nodes must connect"); | |||||
| require(!service.connectNodes(logic_id, start_result.id, coil_result.id).succeeded, | |||||
| "duplicate node connections must fail"); | |||||
| require(!service.connectNodes(logic_id, coil_result.id, start_result.id).succeeded, | |||||
| "coil outgoing connection must fail"); | |||||
| const LogicNode *start = service.findNode(logic_id, start_result.id); | |||||
| require(start != nullptr && start->position.x == 80, | |||||
| "added node position must be stored"); | |||||
| require(service.moveNode(logic_id, start_result.id, {120, 160}).succeeded, | |||||
| "node position must be editable"); | |||||
| require(service.updateNodeConfig( | |||||
| logic_id, | |||||
| start_result.id, | |||||
| ContactNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 2}, | |||||
| ContactMode::NormallyClosed}) | |||||
| .succeeded, | |||||
| "contact properties must be editable"); | |||||
| require(service.removeNode(logic_id, start_result.id).succeeded, | |||||
| "node deletion must succeed"); | |||||
| require(service.findNode(logic_id, start_result.id) == nullptr, | |||||
| "deleted node must disappear"); | |||||
| require(service.findLogic(logic_id)->connections.empty(), | |||||
| "deleting a node must remove its connections"); | |||||
| } | |||||
| } // namespace | |||||
| int main() | |||||
| { | |||||
| try | |||||
| { | |||||
| testEditorOperations(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "logic editor service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "logic editor service tests passed\n"; | |||||
| return 0; | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| TEMPLATE = app | |||||
| TARGET = logic_editor_service_tests | |||||
| CONFIG += console c++17 testcase warn_on | |||||
| CONFIG -= app_bundle qt | |||||
| INCLUDEPATH += ../src | |||||
| SOURCES += \ | |||||
| logic_editor_service_tests.cpp \ | |||||
| ../src/domain/register_address.cpp \ | |||||
| ../src/domain/control_logic_model.cpp \ | |||||
| ../src/domain/project_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | |||||
| ../src/domain/register_repository.cpp \ | |||||
| ../src/services/project_service.cpp \ | |||||
| ../src/services/logic_editor_service.cpp | |||||
| HEADERS += \ | |||||
| ../src/domain/register_address.h \ | |||||
| ../src/domain/control_logic_model.h \ | |||||
| ../src/domain/project_model.h \ | |||||
| ../src/domain/hmi_model.h \ | |||||
| ../src/domain/register_repository.h \ | |||||
| ../src/domain/project_storage.h \ | |||||
| ../src/services/project_service.h \ | |||||
| ../src/services/logic_editor_service.h | |||||
| @@ -2,9 +2,11 @@ | |||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/logic_editor_service.h" | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/logic_editor_widget.h" | |||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| #include <QAction> | #include <QAction> | ||||
| @@ -57,11 +59,16 @@ void testModeActionsControlEditingAvailability() | |||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| HmiEditorService editor_service(project_service); | HmiEditorService editor_service(project_service); | ||||
| LogicEditorService logic_editor_service(project_service); | |||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| RuntimeModeService mode_service; | RuntimeModeService mode_service; | ||||
| MainWindow window( | MainWindow window( | ||||
| mode_service, project_service, editor_service, runtime_service); | |||||
| mode_service, | |||||
| project_service, | |||||
| editor_service, | |||||
| logic_editor_service, | |||||
| runtime_service); | |||||
| window.resize(1000, 640); | window.resize(1000, 640); | ||||
| window.show(); | window.show(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| @@ -71,6 +78,10 @@ void testModeActionsControlEditingAvailability() | |||||
| QAction *online_action = requiredChild<QAction>(window, "onlineModeAction"); | QAction *online_action = requiredChild<QAction>(window, "onlineModeAction"); | ||||
| QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction"); | QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction"); | ||||
| QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction"); | QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction"); | ||||
| QAction *add_normally_open_action = requiredChild<QAction>( | |||||
| window, "addNormallyOpenAction"); | |||||
| QAction *add_normal_coil_action = requiredChild<QAction>( | |||||
| window, "addNormalCoilAction"); | |||||
| QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | ||||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | ||||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | ||||
| @@ -100,6 +111,13 @@ void testModeActionsControlEditingAvailability() | |||||
| require(editor_service.findPage(page_id)->controls.empty(), | require(editor_service.findPage(page_id)->controls.empty(), | ||||
| "deleting a selected control must update the HMI page model"); | "deleting a selected control must update the HMI page model"); | ||||
| add_normally_open_action->trigger(); | |||||
| add_normal_coil_action->trigger(); | |||||
| const ControlLogic *logic = logic_editor_service.findLogic( | |||||
| logic_editor_service.firstLogicId()); | |||||
| require(logic != nullptr && logic->nodes.size() == 2, | |||||
| "logic actions must add nodes through the logic editor service"); | |||||
| offline_action->trigger(); | offline_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | require(mode_service.mode() == ApplicationMode::OfflineRunning, | ||||
| "offline action must enter offline running"); | "offline action must enter offline running"); | ||||
| @@ -109,6 +127,8 @@ void testModeActionsControlEditingAvailability() | |||||
| "properties dock must be disabled while running"); | "properties dock must be disabled while running"); | ||||
| require(!add_button_action->isEnabled(), | require(!add_button_action->isEnabled(), | ||||
| "HMI add controls must be disabled while running"); | "HMI add controls must be disabled while running"); | ||||
| require(!add_normally_open_action->isEnabled(), | |||||
| "logic add nodes must be disabled while running"); | |||||
| online_action->trigger(); | online_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | require(mode_service.mode() == ApplicationMode::OfflineRunning, | ||||
| @@ -125,6 +145,8 @@ void testModeActionsControlEditingAvailability() | |||||
| "properties dock must be restored after returning to editing"); | "properties dock must be restored after returning to editing"); | ||||
| require(add_button_action->isEnabled(), | require(add_button_action->isEnabled(), | ||||
| "HMI add controls must be restored after returning to editing"); | "HMI add controls must be restored after returning to editing"); | ||||
| require(add_normally_open_action->isEnabled(), | |||||
| "logic add nodes must be restored after returning to editing"); | |||||
| online_action->trigger(); | online_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||||
| main_window_tests.cpp \ | main_window_tests.cpp \ | ||||
| ../src/ui/main_window.cpp \ | ../src/ui/main_window.cpp \ | ||||
| ../src/ui/hmi_editor_widget.cpp \ | ../src/ui/hmi_editor_widget.cpp \ | ||||
| ../src/ui/logic_editor_widget.cpp \ | |||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| @@ -20,12 +21,14 @@ SOURCES += \ | |||||
| ../src/domain/runtime_state.cpp \ | ../src/domain/runtime_state.cpp \ | ||||
| ../src/services/project_service.cpp \ | ../src/services/project_service.cpp \ | ||||
| ../src/services/hmi_editor_service.cpp \ | ../src/services/hmi_editor_service.cpp \ | ||||
| ../src/services/logic_editor_service.cpp \ | |||||
| ../src/services/hmi_runtime_service.cpp \ | ../src/services/hmi_runtime_service.cpp \ | ||||
| ../src/services/runtime_mode_service.cpp | ../src/services/runtime_mode_service.cpp | ||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/ui/main_window.h \ | ../src/ui/main_window.h \ | ||||
| ../src/ui/hmi_editor_widget.h \ | ../src/ui/hmi_editor_widget.h \ | ||||
| ../src/ui/logic_editor_widget.h \ | |||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| @@ -35,6 +38,7 @@ HEADERS += \ | |||||
| ../src/domain/runtime_state.h \ | ../src/domain/runtime_state.h \ | ||||
| ../src/services/project_service.h \ | ../src/services/project_service.h \ | ||||
| ../src/services/hmi_editor_service.h \ | ../src/services/hmi_editor_service.h \ | ||||
| ../src/services/logic_editor_service.h \ | |||||
| ../src/services/hmi_runtime_service.h \ | ../src/services/hmi_runtime_service.h \ | ||||
| ../src/services/runtime_mode_service.h | ../src/services/runtime_mode_service.h | ||||
| @@ -68,6 +68,7 @@ Project makeExampleProject() | |||||
| contact.config = ContactNodeConfig{ | contact.config = ContactNodeConfig{ | ||||
| RegisterAddress{RegisterArea::M, 0}, | RegisterAddress{RegisterArea::M, 0}, | ||||
| ContactMode::NormallyOpen}; | ContactMode::NormallyOpen}; | ||||
| contact.position = {80, 100}; | |||||
| LogicNode compare; | LogicNode compare; | ||||
| compare.id = "temperature-check"; | compare.id = "temperature-check"; | ||||
| @@ -75,12 +76,14 @@ Project makeExampleProject() | |||||
| RegisterAddress{RegisterArea::D, 2}, | RegisterAddress{RegisterArea::D, 2}, | ||||
| ComparisonOperator::GreaterThanOrEqual, | ComparisonOperator::GreaterThanOrEqual, | ||||
| static_cast<std::int16_t>(100)}; | static_cast<std::int16_t>(100)}; | ||||
| compare.position = {320, 100}; | |||||
| LogicNode coil; | LogicNode coil; | ||||
| coil.id = "run-coil"; | coil.id = "run-coil"; | ||||
| coil.config = CoilNodeConfig{ | coil.config = CoilNodeConfig{ | ||||
| RegisterAddress{RegisterArea::M, 1}, | RegisterAddress{RegisterArea::M, 1}, | ||||
| CoilMode::Set}; | CoilMode::Set}; | ||||
| coil.position = {560, 100}; | |||||
| ControlLogic logic; | ControlLogic logic; | ||||
| logic.id = "start-logic"; | logic.id = "start-logic"; | ||||
| @@ -176,6 +179,8 @@ void testExampleProjectRoundTrip() | |||||
| "control logic enabled state must survive round trip"); | "control logic enabled state must survive round trip"); | ||||
| require(project.controlLogics.front().nodes.size() == 3, | require(project.controlLogics.front().nodes.size() == 3, | ||||
| "logic node count must survive round trip"); | "logic node count must survive round trip"); | ||||
| require(project.controlLogics.front().nodes.front().position.x == 80, | |||||
| "logic node position must survive round trip"); | |||||
| const auto &compare = std::get<CompareNodeConfig>( | const auto &compare = std::get<CompareNodeConfig>( | ||||
| project.controlLogics.front().nodes.at(1).config); | project.controlLogics.front().nodes.at(1).config); | ||||
| @@ -2,7 +2,7 @@ | |||||
| - Goal: 完成综合平台编程器的工程管理和后续编辑功能。 | - Goal: 完成综合平台编程器的工程管理和后续编辑功能。 | ||||
| - Branch: `main`。 | - Branch: `main`。 | ||||
| - Current status: 已完成开发顺序第 6 步,HMI 编辑器、工程保存加载闭环和离线 HMI 寄存器适配已完成。 | |||||
| - Current status: 已完成开发顺序第 7 步,控制逻辑图形编辑、领域校验和工程持久化闭环已完成。 | |||||
| - Changed files: | - Changed files: | ||||
| - `app/integrated_platform.pro` | - `app/integrated_platform.pro` | ||||
| - `app/src/main.cpp` | - `app/src/main.cpp` | ||||
| @@ -52,6 +52,12 @@ | |||||
| - `app/tests/main_window_tests.cpp` | - `app/tests/main_window_tests.cpp` | ||||
| - `app/src/ui/hmi_editor_widget.h` | - `app/src/ui/hmi_editor_widget.h` | ||||
| - `app/src/ui/hmi_editor_widget.cpp` | - `app/src/ui/hmi_editor_widget.cpp` | ||||
| - `app/src/services/logic_editor_service.h` | |||||
| - `app/src/services/logic_editor_service.cpp` | |||||
| - `app/src/ui/logic_editor_widget.h` | |||||
| - `app/src/ui/logic_editor_widget.cpp` | |||||
| - `app/tests/logic_editor_service_tests.pro` | |||||
| - `app/tests/logic_editor_service_tests.cpp` | |||||
| - Decisions made: | - Decisions made: | ||||
| - Qt 应用源码位于 `app/`,构建输出位于被忽略的 `build/`。 | - Qt 应用源码位于 `app/`,构建输出位于被忽略的 `build/`。 | ||||
| - 使用 qmake、Qt Widgets、Qt SerialBus 和 MinGW 8.1。 | - 使用 qmake、Qt Widgets、Qt SerialBus 和 MinGW 8.1。 | ||||
| @@ -80,6 +86,12 @@ | |||||
| - 新增控件先以未绑定状态进入编辑器,避免自动分配地址造成误绑定。 | - 新增控件先以未绑定状态进入编辑器,避免自动分配地址造成误绑定。 | ||||
| - 离线运行的按钮采用点击切换 M 位语义,数值输入写入一个带符号 16 位 D 字。 | - 离线运行的按钮采用点击切换 M 位语义,数值输入写入一个带符号 16 位 D 字。 | ||||
| - `HmiRuntimeService` 仅通过 `RegisterRepository` 访问寄存器,不依赖虚拟寄存器、串口或 Modbus。 | - `HmiRuntimeService` 仅通过 `RegisterRepository` 访问寄存器,不依赖虚拟寄存器、串口或 Modbus。 | ||||
| - 控制逻辑使用有向无环图;串联为 AND、多个前驱汇合为 OR、一个输出连接多个后继为并行分支。 | |||||
| - 线圈只能作为终点,保存时要求线圈有输入且每个条件节点最终到达线圈。 | |||||
| - 状态保持通过触点与线圈共享 M 地址表达,不创建图上的反馈环。 | |||||
| - `LogicEditorService` 负责节点和连接编辑,删除节点会同时删除关联连接。 | |||||
| - `LogicEditorWidget` 只保存节点 ID 和连接端点,不访问 HMI、寄存器或 PLC。 | |||||
| - 节点位置保存在 `1.0` 工程文件中,并作为逻辑节点的必填工程字段。 | |||||
| - Validation run and results: | - Validation run and results: | ||||
| - 在 `build/baseline/` 执行 qmake 与 `mingw32-make -j2`,构建成功。 | - 在 `build/baseline/` 执行 qmake 与 `mingw32-make -j2`,构建成功。 | ||||
| - 已启动 `integrated_platform.exe` 并正常退出。 | - 已启动 `integrated_platform.exe` 并正常退出。 | ||||
| @@ -100,6 +112,9 @@ | |||||
| - 在 `build/hmi-editor/` 重新构建 Release 应用,构建成功并完成启动冒烟检查。 | - 在 `build/hmi-editor/` 重新构建 Release 应用,构建成功并完成启动冒烟检查。 | ||||
| - 在 `build/hmi-editor-service-tests/` 构建并运行 HMI 服务测试,输出 `HMI editor service tests passed`。 | - 在 `build/hmi-editor-service-tests/` 构建并运行 HMI 服务测试,输出 `HMI editor service tests passed`。 | ||||
| - 重新构建并运行领域、工程管理和主窗口测试,全部通过。 | - 重新构建并运行领域、工程管理和主窗口测试,全部通过。 | ||||
| - Remaining work: 进入开发顺序第 7 步,实现控制逻辑编辑器。 | |||||
| - 控制逻辑编辑器完成后,领域、逻辑编辑服务、工程管理和主窗口测试全部通过。 | |||||
| - Debug 应用构建成功。 | |||||
| - 启动 Debug 应用确认主窗口创建成功,窗口标题为“综合平台编程器”。 | |||||
| - Remaining work: 进入开发顺序第 8 步,实现离线软件逻辑执行器和 HMI 闭环。 | |||||
| - Known risks / blockers: 无。 | - Known risks / blockers: 无。 | ||||
| - Suggested next command: 为控制逻辑建立独立图形编辑器和服务层编辑用例。 | |||||
| - Suggested next command: 为控制逻辑建立固定周期的软件执行器并按拓扑顺序求值。 | |||||
| @@ -84,6 +84,16 @@ main.cpp -> UI + Services + Infrastructure | |||||
| 常量比较。新增节点时应增加独立配置类型及其校验和执行处理,不得向通用 `LogicNode` 持续 | 常量比较。新增节点时应增加独立配置类型及其校验和执行处理,不得向通用 `LogicNode` 持续 | ||||
| 添加只对单一节点有效的可选字段。定时器不是当前原始需求范围,不提前建立模型或执行逻辑。 | 添加只对单一节点有效的可选字段。定时器不是当前原始需求范围,不提前建立模型或执行逻辑。 | ||||
| 控制逻辑使用有向无环图表达。没有前驱的条件节点从隐式左母线取得真值,条件节点输出为 | |||||
| 前驱输入与自身条件的逻辑与;多个前驱在汇合处执行逻辑或,一个输出连接多个后继表示并行 | |||||
| 分支。线圈只能作为终点。状态保持通过触点和线圈绑定同一个 M 地址表达,不建立从线圈返回 | |||||
| 触点的环形连接。上述语义是后续软件逻辑执行器的确定性求值契约。 | |||||
| `services/LogicEditorService` 负责逻辑节点的添加、移动、属性更新、连接、断开和删除。编辑服务 | |||||
| 只要求每次操作后的结构合法,允许用户暂时保留尚未连接完成的中间状态;工程保存仍调用完整 | |||||
| 领域校验,拒绝无输入线圈和不能到达线圈的条件节点。`ui/LogicEditorWidget` 只投影节点标识和 | |||||
| 连接端点,通过服务提交修改,不访问 HMI、寄存器仓库或 PLC。节点画布位置随工程保存。 | |||||
| ## 运行模式边界 | ## 运行模式边界 | ||||
| | 模式 | 允许编辑工程 | 寄存器来源 | 软件逻辑执行器 | 进入约束 | | | 模式 | 允许编辑工程 | 寄存器来源 | 软件逻辑执行器 | 进入约束 | | ||||