From 0fb0efc9bca0d0624ea7a3f269f597b5c78f277d Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Wed, 12 Aug 2026 10:13:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=A2=AF=E5=BD=A2?= =?UTF-8?q?=E5=9B=BE=E6=8E=A7=E5=88=B6=E9=80=BB=E8=BE=91=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/domain/control_logic_model.cpp | 292 +++---- app/src/domain/control_logic_model.h | 93 +-- .../infrastructure/json_project_storage.cpp | 223 +++--- app/src/services/logic_editor_service.cpp | 543 ++++++++----- app/src/services/logic_editor_service.h | 48 +- app/src/ui/logic_editor_widget.cpp | 727 ++++++++---------- app/src/ui/logic_editor_widget.h | 29 +- app/src/ui/main_window.cpp | 84 +- app/src/ui/main_window.h | 12 +- app/tests/domain_tests.cpp | 74 +- app/tests/logic_editor_service_tests.cpp | 79 +- app/tests/main_window_tests.cpp | 13 +- app/tests/project_management_tests.cpp | 43 +- docs/ai/handoff.md | 14 +- docs/architecture.md | 18 +- docs/开发顺序.md | 3 +- 16 files changed, 1210 insertions(+), 1085 deletions(-) diff --git a/app/src/domain/control_logic_model.cpp b/app/src/domain/control_logic_model.cpp index a9ff78f..0b9da98 100644 --- a/app/src/domain/control_logic_model.cpp +++ b/app/src/domain/control_logic_model.cpp @@ -1,12 +1,9 @@ #include "control_logic_model.h" #include -#include -#include namespace { -// 在提供错误字符串时写入校验失败原因 void setError(std::string *error, const std::string &message) { if (error != nullptr) @@ -15,30 +12,6 @@ void setError(std::string *error, const std::string &message) } } -// 判断触点工作方式是否受支持 -bool isSupportedMode(ContactMode mode) -{ - return mode == ContactMode::NormallyOpen || mode == ContactMode::NormallyClosed; -} - -// 判断线圈工作方式是否受支持 -bool isSupportedMode(CoilMode mode) -{ - return mode == CoilMode::Normal || mode == CoilMode::Set || mode == CoilMode::Reset; -} - -// 判断数值比较运算符是否受支持 -bool isSupportedComparison(ComparisonOperator comparison) -{ - return comparison == ComparisonOperator::Equal - || comparison == ComparisonOperator::NotEqual - || comparison == ComparisonOperator::LessThan - || comparison == ComparisonOperator::LessThanOrEqual - || comparison == ComparisonOperator::GreaterThan - || comparison == ComparisonOperator::GreaterThanOrEqual; -} - -// 校验触点节点配置 bool validateConfig(const ContactNodeConfig &config, std::string *error) { if (!config.address.isValid() || config.address.area() != RegisterArea::M) @@ -46,7 +19,8 @@ bool validateConfig(const ContactNodeConfig &config, std::string *error) setError(error, "contact node requires a valid M address"); return false; } - if (!isSupportedMode(config.mode)) + if (config.mode != ContactMode::NormallyOpen + && config.mode != ContactMode::NormallyClosed) { setError(error, "contact node has an unsupported mode"); return false; @@ -54,7 +28,6 @@ bool validateConfig(const ContactNodeConfig &config, std::string *error) return true; } -// 校验线圈节点配置 bool validateConfig(const CoilNodeConfig &config, std::string *error) { if (!config.address.isValid() || config.address.area() != RegisterArea::M) @@ -62,7 +35,9 @@ bool validateConfig(const CoilNodeConfig &config, std::string *error) setError(error, "coil node requires a valid M address"); return false; } - if (!isSupportedMode(config.mode)) + if (config.mode != CoilMode::Normal + && config.mode != CoilMode::Set + && config.mode != CoilMode::Reset) { setError(error, "coil node has an unsupported mode"); return false; @@ -70,7 +45,6 @@ bool validateConfig(const CoilNodeConfig &config, std::string *error) return true; } -// 校验数值比较节点配置 bool validateConfig(const CompareNodeConfig &config, std::string *error) { if (!config.address.isValid() || config.address.area() != RegisterArea::D) @@ -78,7 +52,12 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error) setError(error, "comparison node requires a valid D address"); return false; } - if (!isSupportedComparison(config.comparison)) + if (config.comparison != ComparisonOperator::Equal + && config.comparison != ComparisonOperator::NotEqual + && config.comparison != ComparisonOperator::LessThan + && config.comparison != ComparisonOperator::LessThanOrEqual + && config.comparison != ComparisonOperator::GreaterThan + && config.comparison != ComparisonOperator::GreaterThanOrEqual) { setError(error, "comparison node has an unsupported operator"); return false; @@ -86,14 +65,27 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error) return true; } -} // namespace - -bool LogicPoint::isValid() const +template +bool hasDuplicateId(const std::vector &items) { - return x >= kMinimumCoordinate && x <= kMaximumCoordinate - && y >= kMinimumCoordinate && y <= kMaximumCoordinate; + for (auto current = items.cbegin(); current != items.cend(); ++current) + { + if (std::find_if( + current + 1, + items.cend(), + [¤t](const TItem &candidate) + { + return candidate.id == current->id; + }) != items.cend()) + { + return true; + } + } + return false; } +} // namespace + bool LogicNode::validate(std::string *error) const { if (id.empty()) @@ -101,220 +93,166 @@ bool LogicNode::validate(std::string *error) const setError(error, "logic node id must not be empty"); return false; } - if (!position.isValid()) - { - setError(error, "logic node position is outside the supported range"); - return false; - } - - // 根据 variant 中实际保存的配置类型调用对应校验函数 return std::visit( - [error](const auto &nodeConfig) + [error](const auto &config) { - return validateConfig(nodeConfig, error); + return validateConfig(config, error); }, config); } -bool LogicConnection::validate(std::string *error) const +bool LogicNode::isCondition() const { - // 连接两端必须存在且不能指向同一个节点 - if (fromNodeId.empty() || toNodeId.empty() || fromNodeId == toNodeId) - { - setError(error, "logic connection must connect two different nodes"); - return false; - } - return true; + return !std::holds_alternative(config); } -bool ControlLogic::validate(std::string *error) const +bool LogicNode::isOutput() const { - if (!validateStructure(error)) + return std::holds_alternative(config); +} + +bool LadderStage::validate(std::string *error) const +{ + if (id.empty() || branches.empty()) { + setError(error, "ladder stage id and branches must not be empty"); return false; } - if (id.empty() || name.empty()) + if (hasDuplicateId(branches)) { - setError(error, "control logic id and name must not be empty"); + setError(error, "parallel branch node ids must be unique"); return false; } - - if (nodes.empty()) - { - return true; - } - - std::unordered_map 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) + for (const LogicNode &node : branches) { - if (std::holds_alternative(node.config) - && incoming_count.at(node.id) == 0U) + if (!node.validate(error)) { - setError(error, "coil node must have at least one input connection"); return false; } - } - - // 从所有线圈沿反向连接遍历,一次找出能够产生动作的全部条件节点 - std::unordered_map> incoming; - std::vector pending; - for (const LogicNode &node : nodes) - { - incoming.emplace(node.id, std::vector{}); - if (std::holds_alternative(node.config)) + if (!node.isCondition()) { - pending.push_back(node.id); + setError(error, "ladder stage may contain condition nodes only"); + return false; } } - for (const LogicConnection &connection : connections) + return true; +} + +bool LadderRung::validate(std::string *error) const +{ + if (!validateStructure(error)) { - incoming.at(connection.toNodeId).push_back(connection.fromNodeId); + return false; } - std::unordered_set reaches_coil; - while (!pending.empty()) + if (stages.empty() && !output.has_value()) { - 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); - } + return true; } - if (reaches_coil.size() != nodes.size()) + if (stages.empty() || !output.has_value()) { - setError(error, "condition node must reach a coil node"); + setError(error, "non-empty ladder rung requires conditions and an output coil"); return false; } return true; } -bool ControlLogic::validateStructure(std::string *error) const +bool LadderRung::validateStructure(std::string *error) const { if (id.empty() || name.empty()) { - setError(error, "control logic id and name must not be empty"); + setError(error, "ladder rung id and name must not be empty"); + return false; + } + if (hasDuplicateId(stages)) + { + setError(error, "ladder stage ids must be unique within a rung"); return false; } - - // 校验所有节点并收集节点标识用于后续检查连接引用 std::vector node_ids; - node_ids.reserve(nodes.size()); - for (const LogicNode &node : nodes) + for (const LadderStage &stage : stages) { - if (!node.validate(error)) + if (!stage.validate(error)) { return false; } - if (std::find(node_ids.cbegin(), node_ids.cend(), node.id) != node_ids.cend()) + for (const LogicNode &node : stage.branches) { - setError(error, "logic node ids must be unique within a logic"); - return false; + node_ids.push_back(node.id); } - node_ids.push_back(node.id); } - - // 每条连接只能引用当前控制逻辑中已经定义的节点 - for (const LogicConnection &connection : connections) + if (output.has_value()) { - if (!connection.validate(error)) + if (!output->validate(error)) { return false; } - if (std::find(node_ids.cbegin(), node_ids.cend(), connection.fromNodeId) - == node_ids.cend() - || std::find(node_ids.cbegin(), node_ids.cend(), connection.toNodeId) - == node_ids.cend()) + if (!output->isOutput()) { - setError(error, "logic connection references an unknown node"); + setError(error, "ladder rung output must be a coil node"); return false; } + node_ids.push_back(output->id); } - - // 同一对节点只能存在一条有向连接,避免执行语义和删除行为产生歧义 - for (auto current = connections.cbegin(); current != connections.cend(); ++current) + std::sort(node_ids.begin(), node_ids.end()); + if (std::adjacent_find(node_ids.cbegin(), node_ids.cend()) != node_ids.cend()) { - 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; - } + setError(error, "logic node ids must be unique within a rung"); + return false; } + return true; +} - std::unordered_map> outgoing; - std::unordered_map incoming_count; - for (const LogicNode &node : nodes) +bool ControlLogic::validate(std::string *error) const +{ + if (!validateStructure(error)) { - outgoing.emplace(node.id, std::vector{}); - incoming_count.emplace(node.id, 0U); + return false; } - for (const LogicConnection &connection : connections) + for (const LadderRung &rung : rungs) { - const auto from = std::find_if( - nodes.cbegin(), - nodes.cend(), - [&connection](const LogicNode &node) - { - return node.id == connection.fromNodeId; - }); - if (std::holds_alternative(from->config)) + if (!rung.validate(error)) { - setError(error, "coil node cannot have outgoing connections"); return false; } - outgoing.at(connection.fromNodeId).push_back(connection.toNodeId); - ++incoming_count.at(connection.toNodeId); } + return true; +} - // 使用迭代拓扑排序校验环路,不改变节点的持久化顺序 - std::vector pending; - for (const auto &entry : incoming_count) +bool ControlLogic::validateStructure(std::string *error) const +{ + if (id.empty() || name.empty()) { - if (entry.second == 0U) - { - pending.push_back(entry.first); - } + setError(error, "control logic id and name must not be empty"); + return false; + } + if (hasDuplicateId(rungs)) + { + setError(error, "ladder rung ids must be unique within a logic"); + return false; } - std::size_t visited_count = 0U; - while (!pending.empty()) + std::vector node_ids; + for (const LadderRung &rung : rungs) { - const std::string current = pending.back(); - pending.pop_back(); - ++visited_count; - for (const std::string &next : outgoing.at(current)) + if (!rung.validateStructure(error)) + { + return false; + } + for (const LadderStage &stage : rung.stages) { - std::size_t &count = incoming_count.at(next); - --count; - if (count == 0U) + for (const LogicNode &node : stage.branches) { - pending.push_back(next); + node_ids.push_back(node.id); } } + if (rung.output.has_value()) + { + node_ids.push_back(rung.output->id); + } } - if (visited_count != nodes.size()) + std::sort(node_ids.begin(), node_ids.end()); + if (std::adjacent_find(node_ids.cbegin(), node_ids.cend()) != node_ids.cend()) { - setError(error, "logic connections must not contain a cycle"); + setError(error, "logic node ids must be unique within a logic"); return false; } return true; diff --git a/app/src/domain/control_logic_model.h b/app/src/domain/control_logic_model.h index 43a6f4a..4bf5e0b 100644 --- a/app/src/domain/control_logic_model.h +++ b/app/src/domain/control_logic_model.h @@ -3,122 +3,93 @@ #include "register_address.h" #include +#include #include #include #include -// 逻辑节点在编辑画布中的持久化坐标 -struct LogicPoint -{ - static constexpr int kMinimumCoordinate = 0; - static constexpr int kMaximumCoordinate = 100000; - - int x = 0; - int y = 0; - - bool isValid() const; -}; - -// 触点工作方式 enum class ContactMode { - NormallyOpen, // 常开触点 - NormallyClosed // 常闭触点 + NormallyOpen, + NormallyClosed }; -// 线圈工作方式 enum class CoilMode { - Normal, // 普通线圈 - Set, // 置位线圈 - Reset // 复位线圈 + Normal, + Set, + Reset }; -// 数值比较运算符 enum class ComparisonOperator { - Equal, // 等于 - NotEqual, // 不等于 - LessThan, // 小于 - LessThanOrEqual, // 小于等于 - GreaterThan, // 大于 - GreaterThanOrEqual // 大于等于 + Equal, + NotEqual, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual }; -// 描述读取 M 区位状态的触点节点配置 struct ContactNodeConfig { - // 触点绑定的 M 区地址 RegisterAddress address{RegisterArea::M, 0}; - // 触点工作方式 ContactMode mode = ContactMode::NormallyOpen; }; -// 描述向 M 区写入状态的线圈节点配置 struct CoilNodeConfig { - // 线圈绑定的 M 区地址 RegisterAddress address{RegisterArea::M, 0}; - // 线圈工作方式 CoilMode mode = CoilMode::Normal; }; -// 描述 D 区数值与固定值的比较节点配置 struct CompareNodeConfig { - // 被比较的 D 区地址 RegisterAddress address{RegisterArea::D, 0}; - // 比较运算符 ComparisonOperator comparison = ComparisonOperator::Equal; - // 参与比较的固定值 std::int16_t value = 0; }; -// 控制逻辑节点支持的配置类型 using LogicNodeConfig = std::variant; - -// 描述控制逻辑中的一个功能节点 + struct LogicNode { - // 节点唯一标识 std::string id; - // 节点自身的业务配置 LogicNodeConfig config; - // 节点在逻辑编辑画布中的位置 - LogicPoint position; - // 校验节点配置并通过 error 返回失败原因 bool validate(std::string *error = nullptr) const; + bool isCondition() const; + bool isOutput() const; }; -// 描述两个控制逻辑节点之间的连接关系 -struct LogicConnection +// 同一个串联级内的条件互为并联关系,任一条件成立即通过该级 +struct LadderStage { - // 连接起点的节点标识 - std::string fromNodeId; - // 连接终点的节点标识 - std::string toNodeId; + std::string id; + std::vector branches; - // 校验连接配置并通过 error 返回失败原因 bool validate(std::string *error = nullptr) const; }; -// 描述一组完整的控制逻辑及其节点和连接 +// 梯级中的各级按顺序串联,输出线圈固定在最右侧 +struct LadderRung +{ + std::string id; + std::string name; + std::vector stages; + std::optional output; + + bool validate(std::string *error = nullptr) const; + bool validateStructure(std::string *error = nullptr) const; +}; + struct ControlLogic { - // 控制逻辑唯一标识 std::string id; - // 控制逻辑显示名称 std::string name; - // 控制逻辑包含的节点集合 - std::vector nodes; - // 控制逻辑包含的连接集合 - std::vector connections; - // 控制逻辑是否启用 + std::vector rungs; bool enabled = true; - // 校验控制逻辑并通过 error 返回失败原因 bool validate(std::string *error = nullptr) const; - // 校验编辑过程所需的结构规则,允许暂时没有完整输出 bool validateStructure(std::string *error = nullptr) const; }; diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index 6b36573..476adc1 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -211,38 +211,6 @@ QJsonObject serializeAddress(const RegisterAddress &address) 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( const QJsonObject &object, @@ -751,7 +719,6 @@ QJsonObject serializeLogicNode(const LogicNode &node) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(node.id)); - object.insert(QStringLiteral("position"), serializeLogicPoint(node.position)); // std::visit 将不同节点配置统一转换为 config JSON 对象 object.insert( QStringLiteral("config"), @@ -847,124 +814,184 @@ bool parseLogicNode( ParseState *state) { QJsonObject config; - 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); + return true; } -// 将逻辑节点之间的有向连接序列化为 JSON 对象 -QJsonObject serializeConnection(const LogicConnection &connection) +QJsonObject serializeLadderStage(const LadderStage &stage) { + QJsonArray branches; + for (const LogicNode &node : stage.branches) + { + branches.append(serializeLogicNode(node)); + } QJsonObject object; - object.insert(QStringLiteral("fromNodeId"), fromUtf8(connection.fromNodeId)); - object.insert(QStringLiteral("toNodeId"), fromUtf8(connection.toNodeId)); + object.insert(QStringLiteral("id"), fromUtf8(stage.id)); + object.insert(QStringLiteral("branches"), branches); return object; } -// 解析逻辑连接的起点和终点节点标识 -bool parseConnection( +bool parseLadderStage( const QJsonObject &object, const std::string &context, - LogicConnection *connection, + LadderStage *stage, ParseState *state) { - return readString(object, "fromNodeId", context, &connection->fromNodeId, state) - && readString(object, "toNodeId", context, &connection->toNodeId, state); + QJsonArray branches; + if (!readString(object, "id", context, &stage->id, state) + || !readArray(object, "branches", context, &branches, state)) + { + return false; + } + stage->branches.reserve(static_cast(branches.size())); + for (int index = 0; index < branches.size(); ++index) + { + if (!branches.at(index).isObject()) + { + return state->fail( + ProjectStorageError::InvalidField, + context + ".branches items must be objects"); + } + LogicNode node; + if (!parseLogicNode( + branches.at(index).toObject(), + context + ".branches[" + std::to_string(index) + ']', + &node, + state)) + { + return false; + } + stage->branches.push_back(std::move(node)); + } + return true; } -// 将一套控制逻辑及其节点和连接序列化为 JSON 对象 -QJsonObject serializeControlLogic(const ControlLogic &logic) +QJsonObject serializeLadderRung(const LadderRung &rung) { - QJsonArray nodes; - for (const LogicNode &node : logic.nodes) + QJsonArray stages; + for (const LadderStage &stage : rung.stages) { - nodes.append(serializeLogicNode(node)); + stages.append(serializeLadderStage(stage)); } + QJsonObject object; + object.insert(QStringLiteral("id"), fromUtf8(rung.id)); + object.insert(QStringLiteral("name"), fromUtf8(rung.name)); + object.insert(QStringLiteral("stages"), stages); + object.insert( + QStringLiteral("output"), + rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output)) + : QJsonValue(QJsonValue::Null)); + return object; +} - QJsonArray connections; - for (const LogicConnection &connection : logic.connections) +bool parseLadderRung( + const QJsonObject &object, + const std::string &context, + LadderRung *rung, + ParseState *state) +{ + QJsonArray stages; + QJsonValue output; + if (!readString(object, "id", context, &rung->id, state) + || !readString(object, "name", context, &rung->name, state) + || !readArray(object, "stages", context, &stages, state) + || !readValue(object, "output", context, &output, state)) + { + return false; + } + rung->stages.reserve(static_cast(stages.size())); + for (int index = 0; index < stages.size(); ++index) + { + if (!stages.at(index).isObject()) + { + return state->fail( + ProjectStorageError::InvalidField, + context + ".stages items must be objects"); + } + LadderStage stage; + if (!parseLadderStage( + stages.at(index).toObject(), + context + ".stages[" + std::to_string(index) + ']', + &stage, + state)) + { + return false; + } + rung->stages.push_back(std::move(stage)); + } + if (output.isNull()) + { + rung->output.reset(); + return true; + } + if (!output.isObject()) + { + return state->fail( + ProjectStorageError::InvalidField, + context + ".output must be an object or null"); + } + LogicNode node; + if (!parseLogicNode(output.toObject(), context + ".output", &node, state)) { - connections.append(serializeConnection(connection)); + return false; } + rung->output = std::move(node); + return true; +} +QJsonObject serializeControlLogic(const ControlLogic &logic) +{ + QJsonArray rungs; + for (const LadderRung &rung : logic.rungs) + { + rungs.append(serializeLadderRung(rung)); + } QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(logic.id)); object.insert(QStringLiteral("name"), fromUtf8(logic.name)); object.insert(QStringLiteral("enabled"), logic.enabled); - object.insert(QStringLiteral("nodes"), nodes); - object.insert(QStringLiteral("connections"), connections); + object.insert(QStringLiteral("rungs"), rungs); return object; } -// 解析一套控制逻辑,并逐项构造节点和连接集合 bool parseControlLogic( const QJsonObject &object, const std::string &context, ControlLogic *logic, ParseState *state) { - QJsonArray nodes; - QJsonArray connections; + QJsonArray rungs; if (!readString(object, "id", context, &logic->id, state) || !readString(object, "name", context, &logic->name, state) || !readBool(object, "enabled", context, &logic->enabled, state) - || !readArray(object, "nodes", context, &nodes, state) - || !readArray(object, "connections", context, &connections, state)) + || !readArray(object, "rungs", context, &rungs, state)) { return false; } - - // 数组元素必须是对象,错误上下文包含下标以便定位损坏数据 - logic->nodes.reserve(static_cast(nodes.size())); - for (int index = 0; index < nodes.size(); ++index) - { - if (!nodes.at(index).isObject()) - { - return state->fail( - ProjectStorageError::InvalidField, - context + ".nodes items must be objects"); - } - LogicNode node; - if (!parseLogicNode( - nodes.at(index).toObject(), - context + ".nodes[" + std::to_string(index) + ']', - &node, - state)) - { - return false; - } - logic->nodes.push_back(std::move(node)); - } - - logic->connections.reserve(static_cast(connections.size())); - for (int index = 0; index < connections.size(); ++index) + logic->rungs.reserve(static_cast(rungs.size())); + for (int index = 0; index < rungs.size(); ++index) { - if (!connections.at(index).isObject()) + if (!rungs.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, - context + ".connections items must be objects"); + context + ".rungs items must be objects"); } - LogicConnection connection; - if (!parseConnection( - connections.at(index).toObject(), - context + ".connections[" + std::to_string(index) + ']', - &connection, - state)) + LadderRung rung; + if (!parseLadderRung( + rungs.at(index).toObject(), + context + ".rungs[" + std::to_string(index) + ']', + &rung, + state)) { return false; } - logic->connections.push_back(std::move(connection)); + logic->rungs.push_back(std::move(rung)); } return true; } diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index 152d69c..44b2384 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -9,17 +9,12 @@ namespace { -bool validateCandidateNode( - const std::string &id, - const LogicNodeConfig &config, - const LogicPoint &position, - std::string *error) +LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) { - LogicNode candidate; - candidate.id = id; - candidate.config = config; - candidate.position = position; - return candidate.validate(error); + LogicNode node; + node.id = id; + node.config = config; + return node; } } // namespace @@ -31,15 +26,33 @@ LogicEditorService::LogicEditorService(ProjectService &project_service) const ControlLogic *LogicEditorService::findLogic(const std::string &logic_id) const { - const Project &project = project_service_.project(); + const auto &logics = project_service_.project().controlLogics; const auto logic = std::find_if( - project.controlLogics.cbegin(), - project.controlLogics.cend(), + logics.cbegin(), + logics.cend(), [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); - return logic == project.controlLogics.cend() ? nullptr : &*logic; + return logic == logics.cend() ? nullptr : &*logic; +} + +const LadderRung *LogicEditorService::findRung( + const std::string &logic_id, const std::string &rung_id) const +{ + const ControlLogic *logic = findLogic(logic_id); + if (logic == nullptr) + { + return nullptr; + } + const auto rung = std::find_if( + logic->rungs.cbegin(), + logic->rungs.cend(), + [&rung_id](const LadderRung &candidate) + { + return candidate.id == rung_id; + }); + return rung == logic->rungs.cend() ? nullptr : &*rung; } const LogicNode *LogicEditorService::findNode( @@ -50,20 +63,99 @@ const LogicNode *LogicEditorService::findNode( { return nullptr; } - const auto node = std::find_if( - logic->nodes.cbegin(), - logic->nodes.cend(), - [&node_id](const LogicNode &candidate) + for (const LadderRung &rung : logic->rungs) + { + for (const LadderStage &stage : rung.stages) { - return candidate.id == node_id; - }); - return node == logic->nodes.cend() ? nullptr : &*node; + const auto node = std::find_if( + stage.branches.cbegin(), + stage.branches.cend(), + [&node_id](const LogicNode &candidate) + { + return candidate.id == node_id; + }); + if (node != stage.branches.cend()) + { + return &*node; + } + } + if (rung.output.has_value() && rung.output->id == node_id) + { + return &*rung.output; + } + } + return nullptr; } std::string LogicEditorService::firstLogicId() const { - const Project &project = project_service_.project(); - return project.controlLogics.empty() ? std::string{} : project.controlLogics.front().id; + const auto &logics = project_service_.project().controlLogics; + return logics.empty() ? std::string{} : logics.front().id; +} + +std::string LogicEditorService::firstRungId(const std::string &logic_id) const +{ + const ControlLogic *logic = findLogic(logic_id); + return logic == nullptr || logic->rungs.empty() + ? std::string{} : logic->rungs.front().id; +} + +std::string LogicEditorService::rungIdForNode( + const std::string &logic_id, const std::string &node_id) const +{ + const ControlLogic *logic = findLogic(logic_id); + if (logic == nullptr) + { + return {}; + } + for (const LadderRung &rung : logic->rungs) + { + if (rung.output.has_value() && rung.output->id == node_id) + { + return rung.id; + } + for (const LadderStage &stage : rung.stages) + { + if (std::any_of( + stage.branches.cbegin(), + stage.branches.cend(), + [&node_id](const LogicNode &node) + { + return node.id == node_id; + })) + { + return rung.id; + } + } + } + return {}; +} + +std::string LogicEditorService::stageIdForNode( + const std::string &logic_id, const std::string &node_id) const +{ + const ControlLogic *logic = findLogic(logic_id); + if (logic == nullptr) + { + return {}; + } + for (const LadderRung &rung : logic->rungs) + { + for (const LadderStage &stage : rung.stages) + { + if (std::any_of( + stage.branches.cbegin(), + stage.branches.cend(), + [&node_id](const LogicNode &node) + { + return node.id == node_id; + })) + { + return stage.id; + } + } + } + return {}; } LogicEditorResult LogicEditorService::ensureDefaultLogic() @@ -72,33 +164,25 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() { return {true, LogicEditorError::None, {}, firstLogicId()}; } - ControlLogic logic; logic.id = "logic-1"; logic.name = "控制逻辑 1"; + logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt}); 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) +LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) { 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); - } - + LadderRung rung; + rung.id = makeUniqueRungId(*logic); + rung.name = "网络 " + std::to_string(logic->rungs.size() + 1U); Project &project = project_service_.editProject(); auto target = std::find_if( project.controlLogics.begin(), @@ -107,230 +191,266 @@ LogicEditorResult LogicEditorService::addNode( { 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}; + target->rungs.push_back(std::move(rung)); + return {true, LogicEditorError::None, {}, target->rungs.back().id}; } -LogicEditorResult LogicEditorService::moveNode( - const std::string &logic_id, - const std::string &node_id, - const LogicPoint &position) +LogicEditorResult LogicEditorService::removeRung( + const std::string &logic_id, const std::string &rung_id) { 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) + if (findRung(logic_id, rung_id) == nullptr) { - return failure(LogicEditorError::NodeNotFound, "logic node was not found"); + return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); } - if (!position.isValid()) + if (logic->rungs.size() == 1U) { - return failure( - LogicEditorError::InvalidNode, - "logic node position is outside the supported range"); + return failure(LogicEditorError::InvalidOperation, "control logic requires one rung"); } - 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}; + project.controlLogics.begin(), project.controlLogics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); + target->rungs.erase( + std::remove_if( + target->rungs.begin(), target->rungs.end(), + [&rung_id](const LadderRung &rung) { return rung.id == rung_id; }), + target->rungs.end()); + return {true, LogicEditorError::None, {}, rung_id}; } -LogicEditorResult LogicEditorService::updateNodeConfig( +LogicEditorResult LogicEditorService::appendCondition( const std::string &logic_id, - const std::string &node_id, + const std::string &rung_id, const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); - const LogicNode *node = findNode(logic_id, node_id); + const LadderRung *rung = findRung(logic_id, rung_id); if (logic == nullptr) { return failure(LogicEditorError::LogicNotFound, "control logic was not found"); } - if (node == nullptr) + if (rung == nullptr) { - return failure(LogicEditorError::NodeNotFound, "logic node was not found"); + return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); } - if (node->config.index() != config.index()) + if (!isConditionConfig(config)) { - return failure( - LogicEditorError::UnsupportedNodeChange, - "node category cannot be changed after creation"); + return failure(LogicEditorError::InvalidNode, "ladder condition cannot be a coil"); } - + const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); + LogicNode node = makeNode(node_id, config); std::string error; - if (!validateCandidateNode(node_id, config, node->position, &error)) + if (!node.validate(&error)) { return failure(LogicEditorError::InvalidNode, error); } - + LadderStage stage; + stage.id = makeUniqueStageId(*rung); + stage.branches.push_back(std::move(node)); 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; + auto &logics = project.controlLogics; + auto target_logic = std::find_if( + logics.begin(), logics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); + auto target_rung = std::find_if( + target_logic->rungs.begin(), target_logic->rungs.end(), + [&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; }); + target_rung->stages.push_back(std::move(stage)); return {true, LogicEditorError::None, {}, node_id}; } -LogicEditorResult LogicEditorService::connectNodes( +LogicEditorResult LogicEditorService::addParallelCondition( const std::string &logic_id, - const std::string &from_node_id, - const std::string &to_node_id) + const std::string &rung_id, + const std::string &stage_id, + const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); + const LadderRung *rung = findRung(logic_id, rung_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) + if (rung == nullptr) { - return failure(LogicEditorError::NodeNotFound, "logic connection node was not found"); + return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); } - if (hasConnection(*logic, from_node_id, to_node_id)) + const auto stage = std::find_if( + rung->stages.cbegin(), rung->stages.cend(), + [&stage_id](const LadderStage &candidate) { return candidate.id == stage_id; }); + if (stage == rung->stages.cend()) { - return failure( - LogicEditorError::DuplicateConnection, - "logic connection already exists"); + return failure(LogicEditorError::StageNotFound, "ladder stage was not found"); } - - ControlLogic candidate = *logic; - candidate.connections.push_back({from_node_id, to_node_id}); + if (!isConditionConfig(config)) + { + return failure(LogicEditorError::InvalidNode, "parallel branch requires a condition"); + } + const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); + LogicNode node = makeNode(node_id, config); std::string error; - if (!candidate.validateStructure(&error)) + if (!node.validate(&error)) { - return failure(LogicEditorError::InvalidConnection, 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 &item) - { - return item.id == logic_id; - }); - target->connections.push_back({from_node_id, to_node_id}); - return {true, LogicEditorError::None, {}, to_node_id}; + auto target_logic = std::find_if( + project.controlLogics.begin(), project.controlLogics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); + auto target_rung = std::find_if( + target_logic->rungs.begin(), target_logic->rungs.end(), + [&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; }); + auto target_stage = std::find_if( + target_rung->stages.begin(), target_rung->stages.end(), + [&stage_id](const LadderStage &candidate) { return candidate.id == stage_id; }); + target_stage->branches.push_back(std::move(node)); + return {true, LogicEditorError::None, {}, node_id}; } -LogicEditorResult LogicEditorService::disconnectNodes( +LogicEditorResult LogicEditorService::setOutput( const std::string &logic_id, - const std::string &from_node_id, - const std::string &to_node_id) + const std::string &rung_id, + const LogicNodeConfig &config) { 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()) + if (findRung(logic_id, rung_id) == nullptr) { - return failure(LogicEditorError::InvalidConnection, "logic connection was not found"); + return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); } + if (!isOutputConfig(config)) + { + return failure(LogicEditorError::InvalidNode, "ladder output must be a coil"); + } + const LadderRung *rung = findRung(logic_id, rung_id); + const std::string node_id = rung->output.has_value() + ? rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); + LogicNode node = makeNode(node_id, config); + std::string error; + if (!node.validate(&error)) + { + return failure(LogicEditorError::InvalidNode, error); + } + Project &project = project_service_.editProject(); + auto target_logic = std::find_if( + project.controlLogics.begin(), project.controlLogics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); + auto target_rung = std::find_if( + target_logic->rungs.begin(), target_logic->rungs.end(), + [&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; }); + target_rung->output = std::move(node); + return {true, LogicEditorError::None, {}, node_id}; +} +LogicEditorResult LogicEditorService::updateNodeConfig( + const std::string &logic_id, + const std::string &node_id, + const LogicNodeConfig &config) +{ + const LogicNode *node = findNode(logic_id, node_id); + 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"); + } + LogicNode candidate = makeNode(node_id, config); + std::string error; + if (!candidate.validate(&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 &item) + for (ControlLogic &logic : project.controlLogics) + { + if (logic.id != logic_id) { - 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) + continue; + } + for (LadderRung &rung : logic.rungs) + { + for (LadderStage &stage : rung.stages) + { + for (LogicNode &editable : stage.branches) + { + if (editable.id == node_id) + { + editable.config = config; + return {true, LogicEditorError::None, {}, node_id}; + } + } + } + if (rung.output.has_value() && rung.output->id == node_id) { - return candidate.fromNodeId == from_node_id - && candidate.toNodeId == to_node_id; - }), - target->connections.end()); - return {true, LogicEditorError::None, {}, to_node_id}; + rung.output->config = config; + return {true, LogicEditorError::None, {}, node_id}; + } + } + } + return failure(LogicEditorError::NodeNotFound, "logic node was not found"); } 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) + for (ControlLogic &logic : project.controlLogics) + { + if (logic.id != logic_id) { - return item.id == logic_id; - }); - target->nodes.erase( - std::remove_if( - target->nodes.begin(), - target->nodes.end(), - [&node_id](const LogicNode &candidate) + continue; + } + for (LadderRung &rung : logic.rungs) + { + if (rung.output.has_value() && rung.output->id == node_id) { - 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) + rung.output.reset(); + return {true, LogicEditorError::None, {}, node_id}; + } + for (LadderStage &stage : rung.stages) { - return candidate.fromNodeId == node_id || candidate.toNodeId == node_id; - }), - target->connections.end()); + stage.branches.erase( + std::remove_if( + stage.branches.begin(), stage.branches.end(), + [&node_id](const LogicNode &node) { return node.id == node_id; }), + stage.branches.end()); + } + rung.stages.erase( + std::remove_if( + rung.stages.begin(), rung.stages.end(), + [](const LadderStage &stage) { return stage.branches.empty(); }), + rung.stages.end()); + } + } return {true, LogicEditorError::None, {}, node_id}; } +bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) +{ + return !std::holds_alternative(config); +} + +bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) +{ + return std::holds_alternative(config); +} + std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) { return std::visit( @@ -341,11 +461,14 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) { return "contact"; } - if constexpr (std::is_same_v) + else if constexpr (std::is_same_v) { return "coil"; } - return "compare"; + else + { + return "compare"; + } }, config); } @@ -355,37 +478,51 @@ std::string LogicEditorService::makeUniqueNodeId( { 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) + const std::string candidate = prefix + '-' + std::to_string(index); + bool found = false; + for (const LadderRung &rung : logic.rungs) + { + found = (rung.output.has_value() && rung.output->id == candidate); + for (const LadderStage &stage : rung.stages) { - return node.id == candidate; - }); - if (match == logic.nodes.cend()) + found = found || std::any_of( + stage.branches.cbegin(), stage.branches.cend(), + [&candidate](const LogicNode &node) { return node.id == candidate; }); + } + } + if (!found) { return candidate; } } } -bool LogicEditorService::hasConnection( - const ControlLogic &logic, - const std::string &from_node_id, - const std::string &to_node_id) +std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic) { - 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(); + for (std::size_t index = 1;; ++index) + { + const std::string candidate = "rung-" + std::to_string(index); + if (std::none_of( + logic.rungs.cbegin(), logic.rungs.cend(), + [&candidate](const LadderRung &rung) { return rung.id == candidate; })) + { + return candidate; + } + } +} + +std::string LogicEditorService::makeUniqueStageId(const LadderRung &rung) +{ + for (std::size_t index = 1;; ++index) + { + const std::string candidate = "stage-" + std::to_string(index); + if (std::none_of( + rung.stages.cbegin(), rung.stages.cend(), + [&candidate](const LadderStage &stage) { return stage.id == candidate; })) + { + return candidate; + } + } } LogicEditorResult LogicEditorService::failure( diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index 1d3334f..ab11d26 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -10,10 +10,11 @@ enum class LogicEditorError { None, LogicNotFound, + RungNotFound, + StageNotFound, NodeNotFound, - DuplicateConnection, InvalidNode, - InvalidConnection, + InvalidOperation, UnsupportedNodeChange }; @@ -31,42 +32,49 @@ public: explicit LogicEditorService(ProjectService &project_service); const ControlLogic *findLogic(const std::string &logic_id) const; + const LadderRung *findRung( + const std::string &logic_id, const std::string &rung_id) const; const LogicNode *findNode( const std::string &logic_id, const std::string &node_id) const; std::string firstLogicId() const; + std::string firstRungId(const std::string &logic_id) const; + std::string rungIdForNode( + const std::string &logic_id, const std::string &node_id) const; + std::string stageIdForNode( + const std::string &logic_id, const std::string &node_id) const; LogicEditorResult ensureDefaultLogic(); - LogicEditorResult addNode( + LogicEditorResult addRung(const std::string &logic_id); + LogicEditorResult removeRung( + const std::string &logic_id, const std::string &rung_id); + LogicEditorResult appendCondition( const std::string &logic_id, - const LogicNodeConfig &config, - const LogicPoint &position); - LogicEditorResult moveNode( + const std::string &rung_id, + const LogicNodeConfig &config); + LogicEditorResult addParallelCondition( const std::string &logic_id, - const std::string &node_id, - const LogicPoint &position); + const std::string &rung_id, + const std::string &stage_id, + const LogicNodeConfig &config); + LogicEditorResult setOutput( + const std::string &logic_id, + const std::string &rung_id, + const LogicNodeConfig &config); 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 bool isConditionConfig(const LogicNodeConfig &config); + static bool isOutputConfig(const LogicNodeConfig &config); 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 std::string makeUniqueRungId(const ControlLogic &logic); + static std::string makeUniqueStageId(const LadderRung &rung); static LogicEditorResult failure( LogicEditorError error, const std::string &message); diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index dd2eba3..99043c9 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -1,95 +1,68 @@ #include "logic_editor_widget.h" -#include "services/logic_editor_service.h" - -#include +#include #include -#include -#include #include #include #include #include #include -#include -#include #include -#include namespace { -constexpr qreal kNodeWidth = 168.0; -constexpr qreal kNodeHeight = 72.0; -constexpr qreal kPortRadius = 6.0; +constexpr qreal kMinimumSceneWidth = 1280.0; +constexpr qreal kLeftRailX = 72.0; +constexpr qreal kStageWidth = 184.0; +constexpr qreal kNodeWidth = 118.0; +constexpr qreal kBranchSpacing = 58.0; +constexpr qreal kRungGap = 30.0; -QString nodeTitle(const LogicNodeConfig &config) +QString comparisonText(ComparisonOperator comparison) { - return std::visit( - [](const auto &value) -> QString - { - using Config = std::decay_t; - if constexpr (std::is_same_v) - { - return value.mode == ContactMode::NormallyOpen - ? QStringLiteral("常开触点") - : QStringLiteral("常闭触点"); - } - if constexpr (std::is_same_v) - { - if (value.mode == CoilMode::Set) - { - return QStringLiteral("置位线圈"); - } - if (value.mode == CoilMode::Reset) - { - return QStringLiteral("复位线圈"); - } - return QStringLiteral("普通线圈"); - } - return QStringLiteral("D 值比较"); - }, - config); + switch (comparison) + { + case ComparisonOperator::Equal: return QStringLiteral("="); + case ComparisonOperator::NotEqual: return QStringLiteral("<>"); + case ComparisonOperator::LessThan: return QStringLiteral("<"); + case ComparisonOperator::LessThanOrEqual: return QStringLiteral("<="); + case ComparisonOperator::GreaterThan: return QStringLiteral(">"); + case ComparisonOperator::GreaterThanOrEqual: return QStringLiteral(">="); + default: return QStringLiteral("?"); + } } -QString nodeDetail(const LogicNodeConfig &config) +QString nodeAddressText(const LogicNodeConfig &config) { return std::visit( [](const auto &value) -> QString { using Config = std::decay_t; - if constexpr (std::is_same_v) - { - return QStringLiteral("M%1").arg(value.address.index()); - } - else if constexpr (std::is_same_v) + if constexpr (std::is_same_v + || std::is_same_v) { return QStringLiteral("M%1").arg(value.address.index()); } else { - return QStringLiteral("D%1 = %2") + return QStringLiteral("D%1 %2 %3") .arg(value.address.index()) + .arg(comparisonText(value.comparison)) .arg(value.value); } }, config); } -bool isCoil(const LogicNodeConfig &config) +qreal rungHeight(const LadderRung &rung) { - return std::holds_alternative(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; + std::size_t maximum_branches = 1U; + for (const LadderStage &stage : rung.stages) + { + maximum_branches = std::max(maximum_branches, stage.branches.size()); + } + return 100.0 + static_cast(maximum_branches - 1U) * kBranchSpacing; } } // namespace @@ -99,22 +72,21 @@ class LogicEditorWidget::NodeItem final : public QGraphicsItem public: NodeItem( const LogicNode &node, - std::function moved, - std::function position_changed) + const std::string &rung_id, + const std::string &stage_id, + const QPointF ¢er) : node_id_(node.id), - config_(node.config), - moved_(std::move(moved)), - position_changed_(std::move(position_changed)) + rung_id_(rung_id), + stage_id_(stage_id), + config_(node.config) { - setPos(node.position.x, node.position.y); + setPos(center); setFlag(ItemIsSelectable, true); - setFlag(ItemSendsGeometryChanges, true); - setAcceptedMouseButtons(Qt::LeftButton); } QRectF boundingRect() const override { - return {0, 0, kNodeWidth, kNodeHeight}; + return {-kNodeWidth / 2.0, -27.0, kNodeWidth, 54.0}; } void paint( @@ -122,135 +94,115 @@ public: 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_)) + const bool selected = (option->state & QStyle::State_Selected) != 0; + if (selected) { - painter->drawEllipse(outputPortRect()); + painter->fillRect( + boundingRect(), QColor(QStringLiteral("#dceef7"))); } + painter->setPen(QPen( + selected ? QColor(QStringLiteral("#1677a8")) + : QColor(QStringLiteral("#1f2b33")), + selected ? 2.0 : 1.6)); - if ((option->state & QStyle::State_Selected) != 0) + if (const auto *contact = std::get_if(&config_)) { - painter->setBrush(Qt::NoBrush); - painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2)); - painter->drawRoundedRect(boundingRect().adjusted(0, 0, -1, -1), 5, 5); + painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); + painter->drawLine(QPointF(18, -15), QPointF(18, 15)); + if (contact->mode == ContactMode::NormallyClosed) + { + painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); + } } - } - - 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_) + else if (const auto *coil = std::get_if(&config_)) { - position_changed_(node_id_); + painter->drawArc(QRectF(-29, -20, 30, 40), -80 * 16, 160 * 16); + painter->drawArc(QRectF(-1, -20, 30, 40), 100 * 16, 160 * 16); + if (coil->mode == CoilMode::Set || coil->mode == CoilMode::Reset) + { + painter->drawText( + QRectF(-12, -14, 24, 28), + Qt::AlignCenter, + coil->mode == CoilMode::Set ? QStringLiteral("S") + : QStringLiteral("R")); + } } - return QGraphicsItem::itemChange(change, value); - } - - void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override - { - QGraphicsItem::mouseReleaseEvent(event); - if (flags().testFlag(ItemIsMovable) && moved_) + else { - moved_(node_id_, pos()); + painter->drawRect(QRectF(-43, -18, 86, 36)); } + painter->setPen(QColor(QStringLiteral("#263640"))); + painter->drawText( + QRectF(-kNodeWidth / 2.0, -42, kNodeWidth, 18), + Qt::AlignCenter, + nodeAddressText(config_)); } + const std::string &nodeId() const { return node_id_; } + const std::string &rungId() const { return rung_id_; } + const std::string &stageId() const { return stage_id_; } + private: std::string node_id_; + std::string rung_id_; + std::string stage_id_; LogicNodeConfig config_; - std::function moved_; - std::function position_changed_; }; -class LogicEditorWidget::ConnectionItem final : public QGraphicsPathItem +class LogicEditorWidget::RungItem final : public QGraphicsItem { 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) - { + RungItem( + const LadderRung &rung, + int number, + qreal top, + qreal height, + qreal scene_width) + : rung_id_(rung.id), + name_(QString::fromUtf8(rung.name.data(), static_cast(rung.name.size()))), + number_(number), + height_(height), + scene_width_(scene_width) + { + setPos(0, top); setFlag(ItemIsSelectable, true); - setZValue(-1.0); - setPen(QPen(QColor(QStringLiteral("#5d6f7b")), 2)); + setZValue(-2.0); } - const std::string &fromNodeId() const + QRectF boundingRect() const override { - return from_node_id_; + return {20, 0, scene_width_ - 40, height_}; } - const std::string &toNodeId() const + void paint( + QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *) override { - return to_node_id_; + if ((option->state & QStyle::State_Selected) != 0) + { + painter->fillRect( + boundingRect(), QColor(QStringLiteral("#f0f7fa"))); + } + painter->setPen(QColor(QStringLiteral("#62717b"))); + painter->drawText( + QRectF(28, 6, 260, 22), + Qt::AlignLeft | Qt::AlignVCenter, + QStringLiteral("%1 %2").arg(number_).arg(name_)); + painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); + painter->drawLine( + QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); } - void updatePath(const QPointF &from, const QPointF &to) - { - setPath(makeConnectionPath(from, to)); - } + const std::string &rungId() const { return rung_id_; } private: - std::string from_node_id_; - std::string to_node_id_; + std::string rung_id_; + QString name_; + int number_ = 0; + qreal height_ = 0; + qreal scene_width_ = 0; }; LogicEditorWidget::LogicEditorWidget( @@ -259,12 +211,11 @@ LogicEditorWidget::LogicEditorWidget( : 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); + setAlignment(Qt::AlignLeft | Qt::AlignTop); connect(scene_, &QGraphicsScene::selectionChanged, this, &LogicEditorWidget::handleSelectionChanged); } @@ -276,75 +227,124 @@ void LogicEditorWidget::setLogicId(const std::string &logic_id) return; } logic_id_ = logic_id; + current_rung_id_.clear(); reloadLogic(); } void LogicEditorWidget::setEditingEnabled(bool enabled) { editing_enabled_ = enabled; - setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag); - for (QGraphicsItem *item : scene_->items()) - { - NodeItem *node = dynamic_cast(item); - if (node != nullptr) - { - node->setInteractionEnabled(enabled); - } - } - if (!enabled) - { - clearPreview(); - } + setInteractive(enabled); } void LogicEditorWidget::reloadLogic() { - clearPreview(); scene_->clear(); - if (logic_id_.empty()) - { - return; - } const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { + scene_->setSceneRect(0, 0, kMinimumSceneWidth, 400); return; } - std::map node_items; - for (const LogicConnection &connection : logic->connections) + std::size_t maximum_stage_count = 0U; + for (const LadderRung &rung : logic->rungs) { - scene_->addItem(new ConnectionItem(connection.fromNodeId, connection.toNodeId)); + maximum_stage_count = std::max(maximum_stage_count, rung.stages.size()); } - for (const LogicNode &node : logic->nodes) + const qreal scene_width = std::max( + kMinimumSceneWidth, + kLeftRailX + static_cast(maximum_stage_count) * kStageWidth + 300.0); + const qreal right_rail_x = scene_width - 72.0; + if (current_rung_id_.empty() && !logic->rungs.empty()) { - auto *item = new NodeItem( - node, - [this](const std::string &node_id, const QPointF &position) + current_rung_id_ = logic->rungs.front().id; + } + + qreal top = 24.0; + int rung_number = 1; + for (const LadderRung &rung : logic->rungs) + { + const qreal height = rungHeight(rung); + scene_->addItem(new RungItem(rung, rung_number, top, height, scene_width)); + const qreal main_y = top + 62.0; + + scene_->addLine( + kLeftRailX, top + 32.0, kLeftRailX, top + height - 18.0, + QPen(QColor(QStringLiteral("#202a31")), 3)); + scene_->addLine( + right_rail_x, top + 32.0, right_rail_x, top + height - 18.0, + QPen(QColor(QStringLiteral("#202a31")), 3)); + + qreal cursor_x = kLeftRailX; + for (const LadderStage &stage : rung.stages) + { + const qreal next_x = cursor_x + kStageWidth; + const qreal left_join = cursor_x + 18.0; + const qreal right_join = next_x - 18.0; + scene_->addLine( + cursor_x, main_y, left_join, main_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + scene_->addLine( + right_join, main_y, next_x, main_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + if (stage.branches.size() > 1U) { - handleNodePositionChanged(node_id); - const LogicPoint point{ - static_cast(std::lround(position.x())), - static_cast(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 &) + const qreal bottom_y = main_y + + static_cast(stage.branches.size() - 1U) * kBranchSpacing; + scene_->addLine( + left_join, main_y, left_join, bottom_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + scene_->addLine( + right_join, main_y, right_join, bottom_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + } + for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) { - updateConnections(); - }); - item->setInteractionEnabled(editing_enabled_); - scene_->addItem(item); - node_items.emplace(node.id, item); + const qreal branch_y = main_y + static_cast(branch) * kBranchSpacing; + scene_->addLine( + left_join, branch_y, right_join, branch_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + scene_->addItem(new NodeItem( + stage.branches.at(branch), + rung.id, + stage.id, + QPointF((left_join + right_join) / 2.0, branch_y))); + } + cursor_x = next_x; + } + + const qreal output_x = right_rail_x - 116.0; + scene_->addLine( + cursor_x, main_y, output_x, main_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + scene_->addLine( + output_x, main_y, right_rail_x, main_y, + QPen(QColor(QStringLiteral("#202a31")), 2)); + if (rung.output.has_value()) + { + scene_->addItem(new NodeItem( + *rung.output, + rung.id, + {}, + QPointF(output_x + 50.0, main_y))); + } + else + { + auto *placeholder = scene_->addText(QStringLiteral("< 输出线圈 >")); + placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); + placeholder->setPos(output_x + 4.0, main_y - 14.0); + } + if (rung.stages.empty()) + { + auto *placeholder = scene_->addText(QStringLiteral("< 添加条件 >")); + placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); + placeholder->setPos(kLeftRailX + 32.0, main_y - 14.0); + } + top += height + kRungGap; + ++rung_number; } - updateConnections(); + scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + 20.0)); } void LogicEditorWidget::selectNode(const std::string &node_id) @@ -376,249 +376,182 @@ std::string LogicEditorWidget::selectedNodeId() const 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() +std::string LogicEditorWidget::selectedRungId() const { for (QGraphicsItem *item : scene_->selectedItems()) { - NodeItem *node = dynamic_cast(item); + const NodeItem *node = dynamic_cast(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; + return node->rungId(); } - ConnectionItem *connection = dynamic_cast(item); - if (connection != nullptr) + const RungItem *rung = dynamic_cast(item); + if (rung != 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 rung->rungId(); } } - return {false, LogicEditorError::NodeNotFound, "no logic object is selected", {}}; + return {}; } -void LogicEditorWidget::mousePressEvent(QMouseEvent *event) +std::string LogicEditorWidget::selectedStageId() const { - if (editing_enabled_ && event->button() == Qt::LeftButton) + for (QGraphicsItem *item : scene_->selectedItems()) { - NodeItem *output = nodeAtPort(mapToScene(event->pos()), true); - if (output != nullptr) + const NodeItem *node = dynamic_cast(item); + if (node != 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; + return node->stageId(); } } - QGraphicsView::mousePressEvent(event); + return {}; } -void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event) +LogicEditorResult LogicEditorWidget::addRung() { - if (!connecting_from_node_id_.empty()) + const LogicEditorResult result = editor_service_.addRung(logic_id_); + if (result.succeeded) { - updatePreview(mapToScene(event->pos())); - event->accept(); - return; + current_rung_id_ = result.id; + reloadLogic(); + emit graphChanged(); } - QGraphicsView::mouseMoveEvent(event); -} - -void LogicEditorWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (!connecting_from_node_id_.empty()) + else { - 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; + reportFailure(result); } - 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(); + return result; } -void LogicEditorWidget::updateConnections() +LogicEditorResult LogicEditorWidget::appendCondition(const LogicNodeConfig &config) { - std::map node_items; - for (QGraphicsItem *item : scene_->items()) + const LogicEditorResult result = editor_service_.appendCondition( + logic_id_, currentRungId(), config); + if (result.succeeded) { - NodeItem *node = dynamic_cast(item); - if (node != nullptr) - { - node_items.emplace(node->nodeId(), node); - } + reloadLogic(); + selectNode(result.id); + emit graphChanged(); } - for (QGraphicsItem *item : scene_->items()) + else { - ConnectionItem *connection = dynamic_cast(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()); - } + reportFailure(result); } + return result; } -void LogicEditorWidget::updatePreview(const QPointF &scene_position) +LogicEditorResult LogicEditorWidget::addParallelCondition( + const LogicNodeConfig &config) { - if (preview_item_ == nullptr) + const std::string rung_id = selectedRungId(); + const std::string stage_id = selectedStageId(); + LogicEditorResult result; + if (rung_id.empty() || stage_id.empty()) { - return; + result = {false, + LogicEditorError::InvalidOperation, + "select a condition before adding a parallel branch", + {}}; } - for (QGraphicsItem *item : scene_->items()) + else { - NodeItem *node = dynamic_cast(item); - if (node != nullptr && node->nodeId() == connecting_from_node_id_) - { - preview_item_->setPath( - makeConnectionPath(node->outputPortScenePosition(), scene_position)); - return; - } + result = editor_service_.addParallelCondition( + logic_id_, rung_id, stage_id, config); + } + if (result.succeeded) + { + reloadLogic(); + selectNode(result.id); + emit graphChanged(); } + else + { + reportFailure(result); + } + return result; } -void LogicEditorWidget::clearPreview() +LogicEditorResult LogicEditorWidget::setOutput(const LogicNodeConfig &config) { - if (preview_item_ != nullptr) + const LogicEditorResult result = editor_service_.setOutput( + logic_id_, currentRungId(), config); + if (result.succeeded) { - scene_->removeItem(preview_item_); - delete preview_item_; - preview_item_ = nullptr; + reloadLogic(); + selectNode(result.id); + emit graphChanged(); + } + else + { + reportFailure(result); } - connecting_from_node_id_.clear(); + return result; } -LogicEditorWidget::NodeItem *LogicEditorWidget::nodeAtPort( - const QPointF &scene_position, bool output) const +LogicEditorResult LogicEditorWidget::deleteSelected() { - for (QGraphicsItem *item : scene_->items()) + const std::string node_id = selectedNodeId(); + LogicEditorResult result; + if (!node_id.empty()) { - NodeItem *node = dynamic_cast(item); - if (node == nullptr || (output && !node->hasOutput())) + result = editor_service_.removeNode(logic_id_, node_id); + } + else + { + const std::string rung_id = selectedRungId(); + if (rung_id.empty()) { - continue; + return {false, LogicEditorError::InvalidOperation, "no ladder object is selected", {}}; } - const QPointF port = output - ? node->outputPortScenePosition() - : node->inputPortScenePosition(); - if (QLineF(port, scene_position).length() <= 12.0) + result = editor_service_.removeRung(logic_id_, rung_id); + if (result.succeeded && current_rung_id_ == rung_id) { - return node; + current_rung_id_ = editor_service_.firstRungId(logic_id_); } } - return nullptr; + if (result.succeeded) + { + reloadLogic(); + emit nodeSelected({}); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; } -LogicPoint LogicEditorWidget::nextNodePosition() const +void LogicEditorWidget::resizeEvent(QResizeEvent *event) { - const ControlLogic *logic = editor_service_.findLogic(logic_id_); - for (std::size_t index = 0;; ++index) + QGraphicsView::resizeEvent(event); +} + +void LogicEditorWidget::handleSelectionChanged() +{ + const std::string rung_id = selectedRungId(); + if (!rung_id.empty()) { - const LogicPoint candidate{ - 80 + static_cast(index % 5U) * 230, - 80 + static_cast(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; - } + current_rung_id_ = rung_id; } + emit nodeSelected(QString::fromStdString(selectedNodeId())); } void LogicEditorWidget::reportFailure(const LogicEditorResult &result) { emit editorError(QString::fromStdString(result.message)); } + +std::string LogicEditorWidget::currentRungId() const +{ + const std::string selected = selectedRungId(); + if (!selected.empty()) + { + return selected; + } + if (!current_rung_id_.empty()) + { + return current_rung_id_; + } + return editor_service_.firstRungId(logic_id_); +} diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index 5856dc2..c72b3ac 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -1,16 +1,13 @@ #pragma once #include "domain/control_logic_model.h" +#include "services/logic_editor_service.h" #include #include -class LogicEditorService; -struct LogicEditorResult; -class QGraphicsPathItem; class QGraphicsScene; -class QMouseEvent; class QResizeEvent; class LogicEditorWidget final : public QGraphicsView @@ -27,38 +24,34 @@ public: void reloadLogic(); void selectNode(const std::string &node_id); std::string selectedNodeId() const; - LogicEditorResult addNode(const LogicNodeConfig &config); + std::string selectedRungId() const; + std::string selectedStageId() const; + + LogicEditorResult addRung(); + LogicEditorResult appendCondition(const LogicNodeConfig &config); + LogicEditorResult addParallelCondition(const LogicNodeConfig &config); + LogicEditorResult setOutput(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; + class RungItem; 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); + std::string currentRungId() const; LogicEditorService &editor_service_; QGraphicsScene *scene_ = nullptr; std::string logic_id_; + std::string current_rung_id_; bool editing_enabled_ = true; - std::string connecting_from_node_id_; - QGraphicsPathItem *preview_item_ = nullptr; }; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index cdf434b..d7a3896 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -231,6 +231,12 @@ void MainWindow::configureActions() logic_tool_bar_ = addToolBar(tr("控制逻辑节点")); logic_tool_bar_->setObjectName(QStringLiteral("logicToolBar")); logic_tool_bar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + add_rung_action_ = logic_tool_bar_->addAction( + style()->standardIcon(QStyle::SP_FileIcon), tr("新建网络")); + parallel_insert_action_ = logic_tool_bar_->addAction( + style()->standardIcon(QStyle::SP_ArrowDown), tr("并联插入")); + parallel_insert_action_->setCheckable(true); + logic_tool_bar_->addSeparator(); add_normally_open_action_ = logic_tool_bar_->addAction( style()->standardIcon(QStyle::SP_ArrowRight), tr("常开")); add_normally_closed_action_ = logic_tool_bar_->addAction( @@ -245,6 +251,8 @@ void MainWindow::configureActions() style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("D 比较")); delete_logic_action_ = logic_tool_bar_->addAction( style()->standardIcon(QStyle::SP_TrashIcon), tr("删除")); + add_rung_action_->setObjectName(QStringLiteral("addRungAction")); + parallel_insert_action_->setObjectName(QStringLiteral("parallelInsertAction")); add_normally_open_action_->setObjectName(QStringLiteral("addNormallyOpenAction")); add_normally_closed_action_->setObjectName(QStringLiteral("addNormallyClosedAction")); add_normal_coil_action_->setObjectName(QStringLiteral("addNormalCoilAction")); @@ -252,18 +260,22 @@ void MainWindow::configureActions() 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("删除选中的逻辑节点或连接")); + add_rung_action_->setToolTip(tr("在梯形图末尾新增网络")); + parallel_insert_action_->setToolTip(tr("将下一个条件并联到当前选中的条件")); + 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_rung_action_, &QAction::triggered, + this, &MainWindow::addLogicRung); connect(add_normally_open_action_, &QAction::triggered, this, [this] { - addLogicNode(ContactNodeConfig{ + addLogicCondition(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}); }); @@ -271,7 +283,7 @@ void MainWindow::configureActions() this, [this] { - addLogicNode(ContactNodeConfig{ + addLogicCondition(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyClosed}); }); @@ -279,7 +291,7 @@ void MainWindow::configureActions() this, [this] { - addLogicNode(CoilNodeConfig{ + setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Normal}); }); @@ -287,7 +299,7 @@ void MainWindow::configureActions() this, [this] { - addLogicNode(CoilNodeConfig{ + setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Set}); }); @@ -295,7 +307,7 @@ void MainWindow::configureActions() this, [this] { - addLogicNode(CoilNodeConfig{ + setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Reset}); }); @@ -303,7 +315,7 @@ void MainWindow::configureActions() this, [this] { - addLogicNode(CompareNodeConfig{ + addLogicCondition(CompareNodeConfig{ RegisterAddress{RegisterArea::D, 0}, ComparisonOperator::Equal, 0}); @@ -435,13 +447,6 @@ void MainWindow::configureLogicEditor() { 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, @@ -802,18 +807,49 @@ void MainWindow::applySelectedControlProperties() statusBar()->showMessage(tr("控件属性已更新"), 3000); } -void MainWindow::addLogicNode(const LogicNodeConfig &config) +void MainWindow::addLogicCondition(const LogicNodeConfig &config) +{ + const LogicEditorResult result = parallel_insert_action_->isChecked() + ? logic_editor_widget_->addParallelCondition(config) + : logic_editor_widget_->appendCondition(config); + if (!result.succeeded) + { + showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false); + return; + } + parallel_insert_action_->setChecked(false); + selected_logic_node_id_ = result.id; + showLogicNodeProperties(result.id); + refreshProjectUi(); + statusBar()->showMessage(tr("已添加逻辑条件"), 3000); +} + +void MainWindow::setLogicOutput(const LogicNodeConfig &config) { - const LogicEditorResult result = logic_editor_widget_->addNode(config); + const LogicEditorResult result = logic_editor_widget_->setOutput(config); if (!result.succeeded) { - showProjectResult(tr("添加逻辑节点"), fromUtf8(result.message), false); + showProjectResult(tr("设置输出线圈"), fromUtf8(result.message), false); return; } selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); - statusBar()->showMessage(tr("已添加逻辑节点"), 3000); + statusBar()->showMessage(tr("输出线圈已设置"), 3000); +} + +void MainWindow::addLogicRung() +{ + const LogicEditorResult result = logic_editor_widget_->addRung(); + if (!result.succeeded) + { + showProjectResult(tr("新建网络"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_.clear(); + showLogicNodeProperties({}); + refreshProjectUi(); + statusBar()->showMessage(tr("已新建网络"), 3000); } void MainWindow::deleteSelectedLogicObject() diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 5192bab..dc4084f 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -92,9 +92,13 @@ private: void deleteSelectedControl(); // 将属性表单内容应用到当前选中控件 void applySelectedControlProperties(); - // 向当前控制逻辑添加指定配置的节点 - void addLogicNode(const LogicNodeConfig &config); - // 删除当前选中的逻辑节点或连接 + // 向当前网络添加串联条件,或按当前插入模式添加并联条件 + void addLogicCondition(const LogicNodeConfig &config); + // 设置当前网络右侧的输出线圈 + void setLogicOutput(const LogicNodeConfig &config); + // 新增一个梯形图网络 + void addLogicRung(); + // 删除当前选中的逻辑节点或网络 void deleteSelectedLogicObject(); // 将逻辑属性表单内容应用到当前选中节点 void applySelectedLogicNodeProperties(); @@ -150,6 +154,8 @@ private: QAction *add_numeric_display_action_ = nullptr; QAction *add_numeric_input_action_ = nullptr; QAction *delete_control_action_ = nullptr; + QAction *add_rung_action_ = nullptr; + QAction *parallel_insert_action_ = nullptr; QAction *add_normally_open_action_ = nullptr; QAction *add_normally_closed_action_ = nullptr; QAction *add_normal_coil_action_ = nullptr; diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 12936e9..c81dfde 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -56,7 +56,7 @@ void testRegisterRepositorySeparatesAreas() Project makeValidProject() { - // 构造包含 HMI 绑定和逻辑连接的最小合法工程作为测试基线 + // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线 HmiControl start_button; start_button.id = "start-button"; start_button.type = HmiControlType::Button; @@ -83,8 +83,15 @@ Project makeValidProject() ControlLogic logic; logic.id = "start-logic"; logic.name = "Start logic"; - logic.nodes = {contact, coil}; - logic.connections.push_back({contact.id, coil.id}); + LadderStage stage; + stage.id = "stage-1"; + stage.branches.push_back(contact); + LadderRung rung; + rung.id = "rung-1"; + rung.name = "Network 1"; + rung.stages.push_back(stage); + rung.output = coil; + logic.rungs.push_back(rung); Project project; project.metadata = {"sample-project", "Sample project", "1.0"}; @@ -117,8 +124,14 @@ void testLogicNodeConfigurationBoundaries() require(comparison.validate(), "comparison node bound to D address must be valid"); } -void testLogicGraphBoundaries() +void testLadderLogicBoundaries() { + LogicNode stop; + stop.id = "stop"; + stop.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 1}, + ContactMode::NormallyClosed}; + LogicNode start; start.id = "start"; start.config = ContactNodeConfig{ @@ -140,29 +153,34 @@ void testLogicGraphBoundaries() 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"); + LadderRung rung; + rung.id = "rung-1"; + rung.name = "Self hold"; + rung.stages.push_back({"stage-stop", {stop}}); + rung.stages.push_back({"stage-start", {start, run_contact}}); + rung.output = coil; + logic.rungs.push_back(rung); + require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid"); + + logic.rungs.front().stages.front().branches.push_back(coil); + require(!logic.validate(), "a ladder condition stage must reject coils"); + + logic.rungs.front().stages.front().branches.pop_back(); + logic.rungs.front().output = start; + require(!logic.validate(), "a ladder output must be a coil"); + + logic.rungs.front().output.reset(); + require(!logic.validate(), "conditions without an output must fail full validation"); + + LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt}; + require(empty_rung.validate(), "an empty editing network must be valid"); + + empty_rung.output = coil; + require(!empty_rung.validate(), "an output without conditions must be rejected"); + + logic.rungs.front().output = coil; + logic.rungs.front().stages.at(1).branches.at(1).id = start.id; + require(!logic.validate(), "logic node ids must be unique"); } void testModelsValidateBindingsAndIdentifiers() @@ -225,7 +243,7 @@ int main() testRegisterAddressBoundaries(); testRegisterRepositorySeparatesAreas(); testLogicNodeConfigurationBoundaries(); - testLogicGraphBoundaries(); + testLadderLogicBoundaries(); testModelsValidateBindingsAndIdentifiers(); testRuntimeStateBoundaries(); } diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index e48ff54..c4606c5 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -39,33 +39,53 @@ void testEditorOperations() require(logic_result.succeeded, "default logic must be created"); const std::string logic_id = logic_result.id; - const LogicEditorResult start_result = service.addNode( + const std::string rung_id = service.firstRungId(logic_id); + require(!rung_id.empty(), "default logic must contain an editable rung"); + + const LogicEditorResult stop_result = service.appendCondition( logic_id, + rung_id, + ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 1}, + ContactMode::NormallyClosed}); + const LogicEditorResult start_result = service.appendCondition( + logic_id, + rung_id, ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, - ContactMode::NormallyOpen}, - {80, 80}); - const LogicEditorResult coil_result = service.addNode( + ContactMode::NormallyOpen}); + require(stop_result.succeeded && start_result.succeeded, + "series contacts must be appended as ladder stages"); + + const std::string start_stage_id = service.stageIdForNode( + logic_id, start_result.id); + const LogicEditorResult hold_result = service.addParallelCondition( logic_id, + rung_id, + start_stage_id, + ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 2}, + ContactMode::NormallyOpen}); + const LogicEditorResult coil_result = service.setOutput( + logic_id, + rung_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"); + RegisterAddress{RegisterArea::M, 2}, + CoilMode::Normal}); + require(hold_result.succeeded && coil_result.succeeded, + "parallel hold contact and output coil must be added"); + + const LadderRung *rung = service.findRung(logic_id, rung_id); + require(rung != nullptr && rung->stages.size() == 2, + "series conditions must occupy ordered stages"); + require(rung->stages.at(1).branches.size() == 2, + "parallel conditions must share one stage"); + require(rung->output.has_value(), "rung output must be fixed separately"); + require(service.findLogic(logic_id)->validate(), + "configured self-hold ladder must pass full validation"); 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(start != nullptr, "added condition must be discoverable"); require(service.updateNodeConfig( logic_id, start_result.id, @@ -75,12 +95,21 @@ void testEditorOperations() .succeeded, "contact properties must be editable"); + require(service.removeNode(logic_id, hold_result.id).succeeded, + "parallel branch deletion must succeed"); + require(service.findRung(logic_id, rung_id)->stages.at(1).branches.size() == 1, + "deleting one branch must keep the ladder stage"); 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"); + "last branch deletion must succeed"); + require(service.findRung(logic_id, rung_id)->stages.size() == 1, + "deleting the last branch must remove the empty stage"); + + const LogicEditorResult second_rung = service.addRung(logic_id); + require(second_rung.succeeded, "additional ladder rungs must be supported"); + require(service.removeRung(logic_id, second_rung.id).succeeded, + "additional ladder rungs must be removable"); + require(!service.removeRung(logic_id, rung_id).succeeded, + "the only remaining ladder rung must not be removed"); } } // namespace diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 295bc1a..adb7552 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -82,6 +82,8 @@ void testModeActionsControlEditingAvailability() window, "addNormallyOpenAction"); QAction *add_normal_coil_action = requiredChild( window, "addNormalCoilAction"); + QAction *parallel_insert_action = requiredChild( + window, "parallelInsertAction"); QDockWidget *project_dock = requiredChild(window, "projectDock"); QDockWidget *properties_dock = requiredChild(window, "propertiesDock"); QLabel *selection = requiredChild(window, "selectionValueLabel"); @@ -111,12 +113,19 @@ void testModeActionsControlEditingAvailability() require(editor_service.findPage(page_id)->controls.empty(), "deleting a selected control must update the HMI page model"); + add_normally_open_action->trigger(); + parallel_insert_action->trigger(); 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"); + require(logic != nullptr && logic->rungs.size() == 1, + "logic actions must edit the default ladder rung"); + require(logic->rungs.front().stages.size() == 1 + && logic->rungs.front().stages.front().branches.size() == 2, + "parallel insert mode must add a branch to the selected stage"); + require(logic->rungs.front().output.has_value(), + "coil action must set the fixed ladder output"); offline_action->trigger(); require(mode_service.mode() == ApplicationMode::OfflineRunning, diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index 3dcd6c4..c342714 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -22,7 +22,7 @@ void require(bool condition, const std::string &message) Project makeExampleProject() { - // 构造覆盖四种 HMI 控件和三种逻辑节点的完整 JSON 往返样本 + // 构造覆盖 HMI 控件和梯形图串并联结构的完整 JSON 往返样本 HmiControl start_button; start_button.id = "start-button"; start_button.type = HmiControlType::Button; @@ -68,7 +68,6 @@ Project makeExampleProject() contact.config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}; - contact.position = {80, 100}; LogicNode compare; compare.id = "temperature-check"; @@ -76,22 +75,30 @@ Project makeExampleProject() RegisterAddress{RegisterArea::D, 2}, ComparisonOperator::GreaterThanOrEqual, static_cast(100)}; - compare.position = {320, 100}; + + LogicNode hold_contact; + hold_contact.id = "hold-contact"; + hold_contact.config = ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 1}, + ContactMode::NormallyOpen}; LogicNode coil; coil.id = "run-coil"; coil.config = CoilNodeConfig{ RegisterAddress{RegisterArea::M, 1}, CoilMode::Set}; - coil.position = {560, 100}; ControlLogic logic; logic.id = "start-logic"; logic.name = "Start logic"; logic.enabled = false; - logic.nodes = {contact, compare, coil}; - logic.connections.push_back({contact.id, compare.id}); - logic.connections.push_back({compare.id, coil.id}); + LadderRung rung; + rung.id = "rung-1"; + rung.name = "Network 1"; + rung.stages.push_back({"stage-1", {contact, hold_contact}}); + rung.stages.push_back({"stage-2", {compare}}); + rung.output = coil; + logic.rungs.push_back(rung); Project project; project.metadata = {"example-project", "Example project", "1.0"}; @@ -154,6 +161,15 @@ void testExampleProjectRoundTrip() const QString second_path = directory.filePath("example-copy.json"); require(service.saveAs(first_path.toStdString()).succeeded, "example project save must succeed"); + const QByteArray saved_json = readBytes(first_path); + require(saved_json.contains("\"rungs\"") + && saved_json.contains("\"stages\"") + && saved_json.contains("\"branches\"") + && saved_json.contains("\"output\""), + "saved logic must use the ladder hierarchy"); + require(!saved_json.contains("\"position\"") + && !saved_json.contains("\"connections\""), + "saved ladder logic must not contain free-graph fields"); require(service.load(first_path.toStdString()).succeeded, "example project load must succeed"); @@ -177,13 +193,16 @@ void testExampleProjectRoundTrip() "control logic count must survive round trip"); require(!project.controlLogics.front().enabled, "control logic enabled state must survive round trip"); - require(project.controlLogics.front().nodes.size() == 3, - "logic node count must survive round trip"); - require(project.controlLogics.front().nodes.front().position.x == 80, - "logic node position must survive round trip"); + const LadderRung &rung = project.controlLogics.front().rungs.front(); + require(rung.stages.size() == 2, + "ladder stage count must survive round trip"); + require(rung.stages.front().branches.size() == 2, + "parallel ladder branches must survive round trip"); + require(rung.output.has_value(), + "ladder output must survive round trip"); const auto &compare = std::get( - project.controlLogics.front().nodes.at(1).config); + rung.stages.at(1).branches.front().config); require(compare.address.index() == 2 && compare.value == 100, "comparison configuration must survive round trip"); diff --git a/docs/ai/handoff.md b/docs/ai/handoff.md index 92b4cd3..0c3afc7 100644 --- a/docs/ai/handoff.md +++ b/docs/ai/handoff.md @@ -86,12 +86,11 @@ - 新增控件先以未绑定状态进入编辑器,避免自动分配地址造成误绑定。 - 离线运行的按钮采用点击切换 M 位语义,数值输入写入一个带符号 16 位 D 字。 - `HmiRuntimeService` 仅通过 `RegisterRepository` 访问寄存器,不依赖虚拟寄存器、串口或 Modbus。 - - 控制逻辑使用有向无环图;串联为 AND、多个前驱汇合为 OR、一个输出连接多个后继为并行分支。 - - 线圈只能作为终点,保存时要求线圈有输入且每个条件节点最终到达线圈。 - - 状态保持通过触点与线圈共享 M 地址表达,不创建图上的反馈环。 - - `LogicEditorService` 负责节点和连接编辑,删除节点会同时删除关联连接。 - - `LogicEditorWidget` 只保存节点 ID 和连接端点,不访问 HMI、寄存器或 PLC。 - - 节点位置保存在 `1.0` 工程文件中,并作为逻辑节点的必填工程字段。 + - 控制逻辑使用梯形图层级:逻辑包含网络,网络包含串联条件级,条件级内部包含并联条件,输出线圈固定在右侧。 + - 条件级之间执行 AND,同级并联条件执行 OR;状态保持由常闭停止与“常开启动 OR 常开运行反馈”串联后驱动运行线圈。 + - `LogicEditorService` 负责网络、串联条件、并联条件、输出线圈、属性和删除操作。 + - `LogicEditorWidget` 自动布局母线、支路和节点,不允许自由拖动或手动画线,不保存节点坐标。 + - 控制逻辑编辑器不访问 HMI、寄存器仓库或 PLC,工程格式版本保持 `1.0`。 - Validation run and results: - 在 `build/baseline/` 执行 qmake 与 `mingw32-make -j2`,构建成功。 - 已启动 `integrated_platform.exe` 并正常退出。 @@ -115,6 +114,7 @@ - 控制逻辑编辑器完成后,领域、逻辑编辑服务、工程管理和主窗口测试全部通过。 - Debug 应用构建成功。 - 启动 Debug 应用确认主窗口创建成功,窗口标题为“综合平台编程器”。 + - 梯形图结构重构后,领域、逻辑编辑服务、工程管理和主窗口测试重新构建并全部通过。 - Remaining work: 进入开发顺序第 8 步,实现离线软件逻辑执行器和 HMI 闭环。 - Known risks / blockers: 无。 -- Suggested next command: 为控制逻辑建立固定周期的软件执行器并按拓扑顺序求值。 +- Suggested next command: 为控制逻辑建立固定周期的软件执行器,按网络顺序执行并计算级间 AND 与级内 OR。 diff --git a/docs/architecture.md b/docs/architecture.md index 147c884..b2b700b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ main.cpp -> UI + Services + Infrastructure | `register_repository.*` | 定义 M 位和 D 字的统一读写接口,并提供离线内存实现。 | | `project_storage.*` | 定义工程文件保存和加载的领域存储契约,不依赖具体文件格式。 | | `hmi_model.*` | 定义 HMI 页面、控件、位置、扩展属性和寄存器绑定,并校验控件位于页面边界内。 | -| `control_logic_model.*` | 使用独立配置类型定义触点、线圈和数值比较节点,并校验地址区域与连接引用。 | +| `control_logic_model.*` | 定义网络、串联级、并联条件和输出线圈,并校验节点类型及地址区域。 | | `project_model.*` | 聚合工程元数据、HMI 页面和控制逻辑,并校验工程内标识唯一性。 | | `runtime_state.*` | 定义编辑态、离线运行态、真机运行态及合法切换规则。 | @@ -84,15 +84,15 @@ main.cpp -> UI + Services + Infrastructure 常量比较。新增节点时应增加独立配置类型及其校验和执行处理,不得向通用 `LogicNode` 持续 添加只对单一节点有效的可选字段。定时器不是当前原始需求范围,不提前建立模型或执行逻辑。 -控制逻辑使用有向无环图表达。没有前驱的条件节点从隐式左母线取得真值,条件节点输出为 -前驱输入与自身条件的逻辑与;多个前驱在汇合处执行逻辑或,一个输出连接多个后继表示并行 -分支。线圈只能作为终点。状态保持通过触点和线圈绑定同一个 M 地址表达,不建立从线圈返回 -触点的环形连接。上述语义是后续软件逻辑执行器的确定性求值契约。 +控制逻辑按标准梯形图的受限结构表达:一个逻辑包含多个网络,一个网络包含按顺序串联的多个 +条件级,每个条件级包含一个或多个并联条件,输出线圈固定在网络最右侧。条件级之间执行逻辑与, +同级并联条件执行逻辑或。状态保持通过启动触点和运行反馈触点并联,并与停止触点串联后驱动运行 +线圈表达。该结构是后续软件逻辑执行器的确定性求值契约,不需要通用图遍历或连接表。 -`services/LogicEditorService` 负责逻辑节点的添加、移动、属性更新、连接、断开和删除。编辑服务 -只要求每次操作后的结构合法,允许用户暂时保留尚未连接完成的中间状态;工程保存仍调用完整 -领域校验,拒绝无输入线圈和不能到达线圈的条件节点。`ui/LogicEditorWidget` 只投影节点标识和 -连接端点,通过服务提交修改,不访问 HMI、寄存器仓库或 PLC。节点画布位置随工程保存。 +`services/LogicEditorService` 负责新增和删除网络、追加串联条件、增加并联条件、设置输出线圈、 +修改属性和删除节点。空网络可作为编辑占位;非空网络保存时必须同时具备条件和输出线圈。 +`ui/LogicEditorWidget` 自动排列左右母线、串联级、并联支路和右侧线圈,不允许自由拖动或手动画线, +也不保存像素坐标。编辑器通过服务提交修改,不访问 HMI、寄存器仓库或 PLC。 ## 运行模式边界 diff --git a/docs/开发顺序.md b/docs/开发顺序.md index 8ee18d8..8cf4b0f 100644 --- a/docs/开发顺序.md +++ b/docs/开发顺序.md @@ -53,11 +53,12 @@ ## 7. 实现控制逻辑编辑器 -- 建立图形逻辑模型,支持基本连接、删除和属性配置。 +- 建立梯形图逻辑模型,支持网络、串联条件、并联支路、输出线圈、删除和属性配置。 - 初版实现常开、常闭、普通线圈、置位/复位以及 D 值与常量比较。 - 定时器不属于当前原始需求范围,只有在后续需求明确时才新增对应配置和执行逻辑。 - 新增节点类型时使用独立配置类型,不向通用节点结构持续堆叠无关字段。 - 编辑器只生成逻辑模型,不直接修改 HMI 或 PLC。 +- 画布按梯形图结构自动布局,不允许节点自由拖动,也不要求用户手动画线。 完成标准:能够配置一套简单的启动、停止和状态保持逻辑。