From 7ed3d8b388ef0d629cf4be06dc772d967da2dcc3 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Thu, 13 Aug 2026 09:41:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=8C=96=E6=A2=AF=E5=BD=A2=E5=9B=BE=E7=BC=96=E8=BE=91=E4=B8=8E?= =?UTF-8?q?=E8=BD=A8=E8=BF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/domain/control_logic_model.cpp | 249 +++++- app/src/domain/control_logic_model.h | 35 +- app/src/services/logic_editor_service.cpp | 488 ++++++------ app/src/services/logic_editor_service.h | 28 +- .../services/offline_simulation_service.cpp | 9 +- app/src/services/offline_simulation_service.h | 3 + app/src/services/software_logic_executor.cpp | 89 ++- app/src/services/software_logic_executor.h | 18 +- app/src/ui/logic_editor_widget.cpp | 738 +++++++++++------- app/src/ui/logic_editor_widget.h | 19 +- app/tests/domain_tests.cpp | 42 +- app/tests/logic_editor_service_tests.cpp | 166 ++-- .../offline_simulation_service_tests.cpp | 76 +- 13 files changed, 1273 insertions(+), 687 deletions(-) diff --git a/app/src/domain/control_logic_model.cpp b/app/src/domain/control_logic_model.cpp index 482be5e..6d88892 100644 --- a/app/src/domain/control_logic_model.cpp +++ b/app/src/domain/control_logic_model.cpp @@ -1,6 +1,7 @@ #include "control_logic_model.h" #include +#include namespace { @@ -84,6 +85,53 @@ bool hasDuplicateId(const std::vector &items) return false; } +bool validateUniqueExpressionIds( + const ConditionExpression &expression, std::string *error) +{ + std::vector ids; + collectConditionExpressionIds(expression, &ids); + std::sort(ids.begin(), ids.end()); + if (std::adjacent_find(ids.cbegin(), ids.cend()) != ids.cend()) + { + setError(error, "condition expression ids must be unique within a rung"); + return false; + } + return true; +} + +void normalizeExpression(ConditionExpression *expression) +{ + if (expression == nullptr || expression->kind == ConditionExpressionKind::Node) + { + return; + } + for (ConditionExpression &child : expression->children) + { + normalizeExpression(&child); + } + + std::vector flattened; + for (ConditionExpression &child : expression->children) + { + if (child.kind == expression->kind) + { + for (ConditionExpression &grandchild : child.children) + { + flattened.push_back(std::move(grandchild)); + } + } + else + { + flattened.push_back(std::move(child)); + } + } + expression->children = std::move(flattened); + if (expression->children.size() == 1U) + { + *expression = std::move(expression->children.front()); + } +} + } // namespace bool LogicNode::validate(std::string *error) const @@ -94,10 +142,7 @@ bool LogicNode::validate(std::string *error) const return false; } return std::visit( - [error](const auto &config) - { - return validateConfig(config, error); - }, + [error](const auto &config) { return validateConfig(config, error); }, config); } @@ -116,50 +161,171 @@ bool LogicNode::isOutput() const return std::holds_alternative(config); } -bool LadderStage::validate(std::string *error) const +ConditionExpression ConditionExpression::fromNode(LogicNode logic_node) { - if (id.empty() || branches.empty()) + ConditionExpression expression; + expression.id = logic_node.id; + expression.kind = ConditionExpressionKind::Node; + expression.node = std::move(logic_node); + return expression; +} + +bool ConditionExpression::validate(std::string *error) const +{ + if (id.empty()) { - setError(error, "ladder stage id and branches must not be empty"); + setError(error, "condition expression id must not be empty"); return false; } - if (hasDuplicateId(branches)) + if (kind == ConditionExpressionKind::Node) { - setError(error, "parallel branch node ids must be unique"); + if (!node.has_value() || !children.empty() || !node->isCondition()) + { + setError(error, "condition leaf requires one condition node and no children"); + return false; + } + return node->validate(error); + } + if (node.has_value() || children.size() < 2U) + { + setError(error, "series and parallel expressions require at least two children"); return false; } - for (const LogicNode &node : branches) + for (const ConditionExpression &child : children) { - if (!node.validate(error)) + if (child.kind == kind) { + setError(error, "nested expressions of the same kind must be normalized"); return false; } - if (!node.isCondition()) + if (!child.validate(error)) { - setError(error, "ladder stage may contain condition nodes only"); return false; } } return true; } -bool LadderStage::validateForRunning(std::string *error) const +bool ConditionExpression::validateForRunning(std::string *error) const { if (!validate(error)) { return false; } - for (const LogicNode &node : branches) + if (kind == ConditionExpressionKind::Node) { - if (!node.isConfigured()) + if (!node->isConfigured()) + { + setError(error, "ladder condition " + node->id + " is not configured"); + return false; + } + return true; + } + for (const ConditionExpression &child : children) + { + if (!child.validateForRunning(error)) { - setError(error, "ladder condition " + node.id + " is not configured"); return false; } } return true; } +void normalizeConditionExpression(std::optional *expression) +{ + if (expression == nullptr || !expression->has_value()) + { + return; + } + normalizeExpression(&expression->value()); + if (expression->value().kind != ConditionExpressionKind::Node + && expression->value().children.empty()) + { + expression->reset(); + } +} + +const LogicNode *findConditionNode( + const ConditionExpression &expression, const std::string &node_id) +{ + if (expression.kind == ConditionExpressionKind::Node) + { + return expression.node->id == node_id ? &*expression.node : nullptr; + } + for (const ConditionExpression &child : expression.children) + { + if (const LogicNode *node = findConditionNode(child, node_id)) + { + return node; + } + } + return nullptr; +} + +LogicNode *findConditionNode( + ConditionExpression &expression, const std::string &node_id) +{ + return const_cast(findConditionNode( + static_cast(expression), node_id)); +} + +const ConditionExpression *findConditionExpression( + const ConditionExpression &expression, const std::string &expression_id) +{ + if (expression.id == expression_id) + { + return &expression; + } + for (const ConditionExpression &child : expression.children) + { + if (const ConditionExpression *found = findConditionExpression( + child, expression_id)) + { + return found; + } + } + return nullptr; +} + +ConditionExpression *findConditionExpression( + ConditionExpression &expression, const std::string &expression_id) +{ + return const_cast(findConditionExpression( + static_cast(expression), expression_id)); +} + +void collectConditionNodes( + const ConditionExpression &expression, std::vector *nodes) +{ + if (nodes == nullptr) + { + return; + } + if (expression.kind == ConditionExpressionKind::Node) + { + nodes->push_back(&*expression.node); + return; + } + for (const ConditionExpression &child : expression.children) + { + collectConditionNodes(child, nodes); + } +} + +void collectConditionExpressionIds( + const ConditionExpression &expression, std::vector *ids) +{ + if (ids == nullptr) + { + return; + } + ids->push_back(expression.id); + for (const ConditionExpression &child : expression.children) + { + collectConditionExpressionIds(child, ids); + } +} + bool LadderRung::validate(std::string *error) const { return validateStructure(error); @@ -171,21 +337,18 @@ bool LadderRung::validateForRunning(std::string *error) const { return false; } - if (stages.empty() && !output.has_value()) + if (!condition.has_value() && !output.has_value()) { return true; } - if (stages.empty() || !output.has_value()) + if (!condition.has_value() || !output.has_value()) { setError(error, "incomplete ladder network requires conditions and an output coil"); return false; } - for (const LadderStage &stage : stages) + if (!condition->validateForRunning(error)) { - if (!stage.validateForRunning(error)) - { - return false; - } + return false; } if (!output->isConfigured()) { @@ -202,30 +365,23 @@ bool LadderRung::validateStructure(std::string *error) const 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; - for (const LadderStage &stage : stages) + if (condition.has_value()) { - if (!stage.validate(error)) + if (!condition->validate(error) || !validateUniqueExpressionIds(*condition, error)) { return false; } - for (const LogicNode &node : stage.branches) + std::vector nodes; + collectConditionNodes(*condition, &nodes); + for (const LogicNode *node : nodes) { - node_ids.push_back(node.id); + node_ids.push_back(node->id); } } if (output.has_value()) { - if (!output->validate(error)) - { - return false; - } - if (!output->isOutput()) + if (!output->validate(error) || !output->isOutput()) { setError(error, "ladder rung output must be a coil node"); return false; @@ -270,18 +426,22 @@ bool ControlLogic::validateStructure(std::string *error) const return false; } std::vector node_ids; + std::vector expression_ids; for (const LadderRung &rung : rungs) { if (!rung.validateStructure(error)) { return false; } - for (const LadderStage &stage : rung.stages) + if (rung.condition.has_value()) { - for (const LogicNode &node : stage.branches) + std::vector nodes; + collectConditionNodes(*rung.condition, &nodes); + for (const LogicNode *node : nodes) { - node_ids.push_back(node.id); + node_ids.push_back(node->id); } + collectConditionExpressionIds(*rung.condition, &expression_ids); } if (rung.output.has_value()) { @@ -289,11 +449,18 @@ bool ControlLogic::validateStructure(std::string *error) const } } std::sort(node_ids.begin(), node_ids.end()); + std::sort(expression_ids.begin(), expression_ids.end()); if (std::adjacent_find(node_ids.cbegin(), node_ids.cend()) != node_ids.cend()) { setError(error, "logic node ids must be unique within a logic"); return false; } + if (std::adjacent_find(expression_ids.cbegin(), expression_ids.cend()) + != expression_ids.cend()) + { + setError(error, "condition expression 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 14dbb6d..8c6fedc 100644 --- a/app/src/domain/control_logic_model.h +++ b/app/src/domain/control_logic_model.h @@ -64,22 +64,47 @@ struct LogicNode bool isOutput() const; }; -// 同一个串联级内的条件互为并联关系,任一条件成立即通过该级 -struct LadderStage +enum class ConditionExpressionKind +{ + Node, + Series, + Parallel +}; + +// 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 +struct ConditionExpression { std::string id; - std::vector branches; + ConditionExpressionKind kind = ConditionExpressionKind::Node; + std::optional node; + std::vector children; + static ConditionExpression fromNode(LogicNode node); bool validate(std::string *error = nullptr) const; bool validateForRunning(std::string *error = nullptr) const; }; -// 梯级中的各级按顺序串联,输出线圈固定在最右侧 +// 删除后折叠单子项容器并合并相邻同类容器,保持表达式的规范形态 +void normalizeConditionExpression(std::optional *expression); +const LogicNode *findConditionNode( + const ConditionExpression &expression, const std::string &node_id); +LogicNode *findConditionNode( + ConditionExpression &expression, const std::string &node_id); +const ConditionExpression *findConditionExpression( + const ConditionExpression &expression, const std::string &expression_id); +ConditionExpression *findConditionExpression( + ConditionExpression &expression, const std::string &expression_id); +void collectConditionNodes( + const ConditionExpression &expression, std::vector *nodes); +void collectConditionExpressionIds( + const ConditionExpression &expression, std::vector *ids); + +// 一个网络包含一棵结构化条件表达式,输出线圈固定在最右侧 struct LadderRung { std::string id; std::string name; - std::vector stages; + std::optional condition; std::optional output; bool validate(std::string *error = nullptr) const; diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index 9cb97ab..e2193dc 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -3,7 +3,6 @@ #include "project_service.h" #include -#include #include #include @@ -11,11 +10,85 @@ namespace { LogicNode makeNode(const std::string &id, const LogicNodeConfig &config) { - LogicNode node; - node.id = id; - node.config = config; - node.configured = false; - return node; + return {id, config, false}; +} + +ConditionExpression makeContainer( + const std::string &id, + ConditionExpressionKind kind, + ConditionExpression first, + ConditionExpression second) +{ + ConditionExpression expression; + expression.id = id; + expression.kind = kind; + expression.children.push_back(std::move(first)); + expression.children.push_back(std::move(second)); + return expression; +} + +ConditionExpression *findParentExpression( + ConditionExpression &expression, const std::string &child_id) +{ + for (ConditionExpression &child : expression.children) + { + if (child.id == child_id) + { + return &expression; + } + if (ConditionExpression *parent = findParentExpression(child, child_id)) + { + return parent; + } + } + return nullptr; +} + +bool removeExpressionRecursive( + ConditionExpression *expression, const std::string &expression_id) +{ + if (expression == nullptr || expression->kind == ConditionExpressionKind::Node) + { + return false; + } + const auto removable = std::find_if( + expression->children.begin(), + expression->children.end(), + [&expression_id](const ConditionExpression &child) + { + return child.id == expression_id; + }); + if (removable != expression->children.end()) + { + expression->children.erase(removable); + return true; + } + for (ConditionExpression &child : expression->children) + { + if (removeExpressionRecursive(&child, expression_id)) + { + return true; + } + } + return false; +} + +LadderRung *findEditableRung( + Project &project, + const std::string &logic_id, + const std::string &rung_id) +{ + const auto logic = std::find_if( + project.controlLogics.begin(), project.controlLogics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); + if (logic == project.controlLogics.end()) + { + return nullptr; + } + const auto rung = std::find_if( + logic->rungs.begin(), logic->rungs.end(), + [&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; }); + return rung == logic->rungs.end() ? nullptr : &*rung; } } // namespace @@ -29,12 +102,8 @@ const ControlLogic *LogicEditorService::findLogic(const std::string &logic_id) c { const auto &logics = project_service_.project().controlLogics; const auto logic = std::find_if( - logics.cbegin(), - logics.cend(), - [&logic_id](const ControlLogic &candidate) - { - return candidate.id == logic_id; - }); + logics.cbegin(), logics.cend(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); return logic == logics.cend() ? nullptr : &*logic; } @@ -47,12 +116,8 @@ const LadderRung *LogicEditorService::findRung( return nullptr; } const auto rung = std::find_if( - logic->rungs.cbegin(), - logic->rungs.cend(), - [&rung_id](const LadderRung &candidate) - { - return candidate.id == rung_id; - }); + logic->rungs.cbegin(), logic->rungs.cend(), + [&rung_id](const LadderRung &candidate) { return candidate.id == rung_id; }); return rung == logic->rungs.cend() ? nullptr : &*rung; } @@ -66,28 +131,31 @@ const LogicNode *LogicEditorService::findNode( } for (const LadderRung &rung : logic->rungs) { - for (const LadderStage &stage : rung.stages) - { - 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; } + if (rung.condition.has_value()) + { + if (const LogicNode *node = findConditionNode(*rung.condition, node_id)) + { + return node; + } + } } return nullptr; } +const ConditionExpression *LogicEditorService::findExpression( + const std::string &logic_id, + const std::string &rung_id, + const std::string &expression_id) const +{ + const LadderRung *rung = findRung(logic_id, rung_id); + return rung == nullptr || !rung->condition.has_value() + ? nullptr : findConditionExpression(*rung->condition, expression_id); +} + std::string LogicEditorService::firstLogicId() const { const auto &logics = project_service_.project().controlLogics; @@ -97,8 +165,7 @@ std::string LogicEditorService::firstLogicId() const 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; + return logic == nullptr || logic->rungs.empty() ? std::string{} : logic->rungs.front().id; } std::string LogicEditorService::rungIdForNode( @@ -111,50 +178,12 @@ std::string LogicEditorService::rungIdForNode( } for (const LadderRung &rung : logic->rungs) { - if (rung.output.has_value() && rung.output->id == node_id) + if ((rung.output.has_value() && rung.output->id == node_id) + || (rung.condition.has_value() + && findConditionNode(*rung.condition, node_id) != nullptr)) { 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 {}; } @@ -168,7 +197,7 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() ControlLogic logic; logic.id = "logic-1"; logic.name = "控制逻辑 1"; - logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt}); + logic.rungs.push_back({"rung-1", "网络 1", std::nullopt, std::nullopt}); Project &project = project_service_.editProject(); project.controlLogics.push_back(std::move(logic)); return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; @@ -186,12 +215,8 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id) rung.name = "网络 " + std::to_string(logic->rungs.size() + 1U); 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; - }); + project.controlLogics.begin(), project.controlLogics.end(), + [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); target->rungs.push_back(std::move(rung)); return {true, LogicEditorError::None, {}, target->rungs.back().id}; } @@ -230,124 +255,146 @@ LogicEditorResult LogicEditorService::appendCondition( const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); - const LadderRung *rung = findRung(logic_id, rung_id); - if (logic == nullptr) + if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) { - return failure(LogicEditorError::LogicNotFound, "control logic was not found"); - } - if (rung == nullptr) - { - return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); + return failure( + logic == nullptr ? LogicEditorError::LogicNotFound : LogicEditorError::RungNotFound, + logic == nullptr ? "control logic was not found" : "ladder rung was not found"); } if (!isConditionConfig(config)) { 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 (!node.validate(&error)) + ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); + Project &project = project_service_.editProject(); + LadderRung *rung = findEditableRung(project, logic_id, rung_id); + if (!rung->condition.has_value()) { - return failure(LogicEditorError::InvalidNode, error); + rung->condition = std::move(leaf); + } + else if (rung->condition->kind == ConditionExpressionKind::Series) + { + rung->condition->children.push_back(std::move(leaf)); + } + else + { + rung->condition = makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Series, + std::move(*rung->condition), + std::move(leaf)); } - LadderStage stage; - stage.id = makeUniqueStageId(*rung); - stage.branches.push_back(std::move(node)); - Project &project = project_service_.editProject(); - 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::addParallelCondition( +LogicEditorResult LogicEditorService::insertCondition( const std::string &logic_id, const std::string &rung_id, - const std::string &stage_id, + const std::string &target_expression_id, + SeriesInsertPosition position, const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); - const LadderRung *rung = findRung(logic_id, rung_id); - if (logic == nullptr) + if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) { - return failure(LogicEditorError::LogicNotFound, "control logic was not found"); + return failure(LogicEditorError::RungNotFound, "ladder network was not found"); } - if (rung == nullptr) + if (!isConditionConfig(config) + || findExpression(logic_id, rung_id, target_expression_id) == nullptr) { - return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); - } - 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::StageNotFound, "ladder stage was not found"); - } - if (!isConditionConfig(config)) - { - return failure(LogicEditorError::InvalidNode, "parallel branch requires a condition"); + return failure(LogicEditorError::ExpressionNotFound, "series insertion target was not found"); } const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); - LogicNode node = makeNode(node_id, config); - std::string error; - if (!node.validate(&error)) - { - return failure(LogicEditorError::InvalidNode, error); - } + ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); 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; }); - 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)); + LadderRung *rung = findEditableRung(project, logic_id, rung_id); + ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id); + ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id); + if (parent != nullptr && parent->kind == ConditionExpressionKind::Series) + { + const auto target_iterator = std::find_if( + parent->children.begin(), parent->children.end(), + [&target_expression_id](const ConditionExpression &child) + { + return child.id == target_expression_id; + }); + parent->children.insert( + position == SeriesInsertPosition::Before + ? target_iterator : target_iterator + 1, + std::move(leaf)); + } + else + { + ConditionExpression original = std::move(*target); + *target = position == SeriesInsertPosition::Before + ? makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Series, + std::move(leaf), + std::move(original)) + : makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Series, + std::move(original), + std::move(leaf)); + } return {true, LogicEditorError::None, {}, node_id}; } -LogicEditorResult LogicEditorService::setOutput( +LogicEditorResult LogicEditorService::addParallelCondition( const std::string &logic_id, const std::string &rung_id, + const std::string &target_expression_id, const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); - if (logic == nullptr) + if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) { - return failure(LogicEditorError::LogicNotFound, "control logic was not found"); + return failure(LogicEditorError::RungNotFound, "ladder network was not found"); } - if (findRung(logic_id, rung_id) == nullptr) + if (!isConditionConfig(config) + || findExpression(logic_id, rung_id, target_expression_id) == nullptr) { - return failure(LogicEditorError::RungNotFound, "ladder rung was not found"); + return failure(LogicEditorError::ExpressionNotFound, "parallel target was not found"); } - if (!isOutputConfig(config)) + const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); + ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); + Project &project = project_service_.editProject(); + LadderRung *rung = findEditableRung(project, logic_id, rung_id); + ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id); + ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id); + if (parent != nullptr && parent->kind == ConditionExpressionKind::Parallel) { - return failure(LogicEditorError::InvalidNode, "ladder output must be a coil"); + parent->children.push_back(std::move(leaf)); } - 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)) + else { - return failure(LogicEditorError::InvalidNode, error); + ConditionExpression original = std::move(*target); + *target = makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Parallel, + std::move(original), + std::move(leaf)); } + return {true, LogicEditorError::None, {}, node_id}; +} + +LogicEditorResult LogicEditorService::setOutput( + const std::string &logic_id, + const std::string &rung_id, + const LogicNodeConfig &config) +{ + const ControlLogic *logic = findLogic(logic_id); + const LadderRung *existing_rung = findRung(logic_id, rung_id); + if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config)) + { + return failure(LogicEditorError::InvalidNode, "ladder output requires a valid coil"); + } + const std::string node_id = existing_rung->output.has_value() + ? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); 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); + findEditableRung(project, logic_id, rung_id)->output = makeNode(node_id, config); return {true, LogicEditorError::None, {}, node_id}; } @@ -367,8 +414,7 @@ LogicEditorResult LogicEditorService::updateNodeConfig( LogicEditorError::UnsupportedNodeChange, "node category cannot be changed after creation"); } - LogicNode candidate = makeNode(node_id, config); - candidate.configured = true; + LogicNode candidate{node_id, config, true}; std::string error; if (!candidate.validate(&error)) { @@ -383,24 +429,19 @@ LogicEditorResult LogicEditorService::updateNodeConfig( } for (LadderRung &rung : logic.rungs) { - for (LadderStage &stage : rung.stages) - { - for (LogicNode &editable : stage.branches) - { - if (editable.id == node_id) - { - editable.config = config; - editable.configured = true; - return {true, LogicEditorError::None, {}, node_id}; - } - } - } if (rung.output.has_value() && rung.output->id == node_id) { - rung.output->config = config; - rung.output->configured = true; + *rung.output = candidate; return {true, LogicEditorError::None, {}, node_id}; } + if (rung.condition.has_value()) + { + if (LogicNode *editable = findConditionNode(*rung.condition, node_id)) + { + *editable = candidate; + return {true, LogicEditorError::None, {}, node_id}; + } + } } } return failure(LogicEditorError::NodeNotFound, "logic node was not found"); @@ -409,40 +450,42 @@ LogicEditorResult LogicEditorService::updateNodeConfig( LogicEditorResult LogicEditorService::removeNode( const std::string &logic_id, const std::string &node_id) { - if (findNode(logic_id, node_id) == nullptr) + const std::string rung_id = rungIdForNode(logic_id, node_id); + if (rung_id.empty()) { return failure(LogicEditorError::NodeNotFound, "logic node was not found"); } + const LadderRung *rung = findRung(logic_id, rung_id); + if (rung->output.has_value() && rung->output->id == node_id) + { + Project &project = project_service_.editProject(); + findEditableRung(project, logic_id, rung_id)->output.reset(); + return {true, LogicEditorError::None, {}, node_id}; + } + return removeExpression(logic_id, rung_id, node_id); +} + +LogicEditorResult LogicEditorService::removeExpression( + const std::string &logic_id, + const std::string &rung_id, + const std::string &expression_id) +{ + if (findExpression(logic_id, rung_id, expression_id) == nullptr) + { + return failure(LogicEditorError::ExpressionNotFound, "condition branch was not found"); + } Project &project = project_service_.editProject(); - for (ControlLogic &logic : project.controlLogics) + LadderRung *rung = findEditableRung(project, logic_id, rung_id); + if (rung->condition->id == expression_id) { - if (logic.id != logic_id) - { - continue; - } - for (LadderRung &rung : logic.rungs) - { - if (rung.output.has_value() && rung.output->id == node_id) - { - rung.output.reset(); - return {true, LogicEditorError::None, {}, node_id}; - } - for (LadderStage &stage : rung.stages) - { - 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()); - } + rung->condition.reset(); } - return {true, LogicEditorError::None, {}, node_id}; + else + { + removeExpressionRecursive(&*rung->condition, expression_id); + normalizeConditionExpression(&rung->condition); + } + return {true, LogicEditorError::None, {}, expression_id}; } bool LogicEditorService::isConditionConfig(const LogicNodeConfig &config) @@ -469,10 +512,7 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) { return "coil"; } - else - { - return "compare"; - } + return "compare"; }, config); } @@ -486,13 +526,9 @@ std::string LogicEditorService::makeUniqueNodeId( bool found = false; for (const LadderRung &rung : logic.rungs) { - found = (rung.output.has_value() && rung.output->id == candidate); - for (const LadderStage &stage : rung.stages) - { - found = found || std::any_of( - stage.branches.cbegin(), stage.branches.cend(), - [&candidate](const LogicNode &node) { return node.id == candidate; }); - } + found = found || (rung.output.has_value() && rung.output->id == candidate) + || (rung.condition.has_value() + && findConditionNode(*rung.condition, candidate) != nullptr); } if (!found) { @@ -501,28 +537,32 @@ std::string LogicEditorService::makeUniqueNodeId( } } -std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic) +std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic) { 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; })) + const std::string candidate = "expression-" + std::to_string(index); + bool found = false; + for (const LadderRung &rung : logic.rungs) + { + found = found || (rung.condition.has_value() + && findConditionExpression(*rung.condition, candidate) != nullptr); + } + if (!found) { return candidate; } } } -std::string LogicEditorService::makeUniqueStageId(const LadderRung &rung) +std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic) { for (std::size_t index = 1;; ++index) { - const std::string candidate = "stage-" + std::to_string(index); + const std::string candidate = "rung-" + std::to_string(index); if (std::none_of( - rung.stages.cbegin(), rung.stages.cend(), - [&candidate](const LadderStage &stage) { return stage.id == candidate; })) + logic.rungs.cbegin(), logic.rungs.cend(), + [&candidate](const LadderRung &rung) { return rung.id == candidate; })) { return candidate; } diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index ab11d26..f32429c 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -11,13 +11,19 @@ enum class LogicEditorError None, LogicNotFound, RungNotFound, - StageNotFound, + ExpressionNotFound, NodeNotFound, InvalidNode, InvalidOperation, UnsupportedNodeChange }; +enum class SeriesInsertPosition +{ + Before, + After +}; + struct LogicEditorResult { bool succeeded = false; @@ -36,12 +42,14 @@ public: 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; + const ConditionExpression *findExpression( + const std::string &logic_id, + const std::string &rung_id, + const std::string &expression_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 addRung(const std::string &logic_id); @@ -51,10 +59,16 @@ public: const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config); + LogicEditorResult insertCondition( + const std::string &logic_id, + const std::string &rung_id, + const std::string &target_expression_id, + SeriesInsertPosition position, + const LogicNodeConfig &config); LogicEditorResult addParallelCondition( const std::string &logic_id, const std::string &rung_id, - const std::string &stage_id, + const std::string &target_expression_id, const LogicNodeConfig &config); LogicEditorResult setOutput( const std::string &logic_id, @@ -66,6 +80,10 @@ public: const LogicNodeConfig &config); LogicEditorResult removeNode( const std::string &logic_id, const std::string &node_id); + LogicEditorResult removeExpression( + const std::string &logic_id, + const std::string &rung_id, + const std::string &expression_id); private: static bool isConditionConfig(const LogicNodeConfig &config); @@ -73,8 +91,8 @@ private: static std::string nodePrefix(const LogicNodeConfig &config); static std::string makeUniqueNodeId( const ControlLogic &logic, const std::string &prefix); + static std::string makeUniqueExpressionId(const ControlLogic &logic); 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/services/offline_simulation_service.cpp b/app/src/services/offline_simulation_service.cpp index c40c76d..55d78a5 100644 --- a/app/src/services/offline_simulation_service.cpp +++ b/app/src/services/offline_simulation_service.cpp @@ -35,6 +35,7 @@ SimulationStartResult OfflineSimulationService::start( logic_snapshot_ = std::move(snapshot); successful_scan_count_ = 0; last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; + trace_snapshot_.clear(); state_ = SimulationState::Running; timer_.start(); emit stateChanged(); @@ -66,13 +67,14 @@ LogicScanResult OfflineSimulationService::executeOnce() {}}; } const LogicScanResult result = executor_.executeScan( - logic_snapshot_, repository_); + logic_snapshot_, repository_, &trace_snapshot_); if (!result.succeeded) { enterFault(result); return result; } ++successful_scan_count_; + emit scanCompleted(); return result; } @@ -96,6 +98,11 @@ const LogicScanResult &OfflineSimulationService::lastError() const return last_error_; } +const LogicTraceSnapshot &OfflineSimulationService::traceSnapshot() const +{ + return trace_snapshot_; +} + void OfflineSimulationService::handleTimeout() { executeOnce(); diff --git a/app/src/services/offline_simulation_service.h b/app/src/services/offline_simulation_service.h index 8366564..8c0a048 100644 --- a/app/src/services/offline_simulation_service.h +++ b/app/src/services/offline_simulation_service.h @@ -49,9 +49,11 @@ public: int scanIntervalMs() const; std::uint64_t successfulScanCount() const; const LogicScanResult &lastError() const; + const LogicTraceSnapshot &traceSnapshot() const; signals: void stateChanged(); + void scanCompleted(); private: void handleTimeout(); @@ -64,4 +66,5 @@ private: SimulationState state_ = SimulationState::Stopped; std::uint64_t successful_scan_count_ = 0; LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; + LogicTraceSnapshot trace_snapshot_; }; diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index ebd3209..4ca53e8 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -60,6 +60,13 @@ bool compareWord( } // namespace +void LogicTraceSnapshot::clear() +{ + nodeValues.clear(); + expressionValues.clear(); + rungValues.clear(); +} + LogicScanResult SoftwareLogicExecutor::validate( const std::vector &logics) const { @@ -112,7 +119,8 @@ LogicScanResult SoftwareLogicExecutor::validate( LogicScanResult SoftwareLogicExecutor::executeScan( const std::vector &logics, - RegisterRepository &repository) const + RegisterRepository &repository, + LogicTraceSnapshot *trace) const { const LogicScanResult validation = validate(logics); if (!validation.succeeded) @@ -120,6 +128,10 @@ LogicScanResult SoftwareLogicExecutor::executeScan( return validation; } + if (trace != nullptr) + { + trace->clear(); + } for (const ControlLogic &logic : logics) { if (!logic.enabled) @@ -128,32 +140,26 @@ LogicScanResult SoftwareLogicExecutor::executeScan( } for (const LadderRung &rung : logic.rungs) { - if (rung.stages.empty() && !rung.output.has_value()) + if (!rung.condition.has_value() && !rung.output.has_value()) { continue; } - bool rung_value = true; - for (const LadderStage &stage : rung.stages) + bool rung_value = false; + LogicScanResult result = evaluateExpression( + *rung.condition, repository, trace, &rung_value); + if (!result.succeeded) { - bool stage_value = false; - for (const LogicNode &node : stage.branches) - { - bool condition_value = false; - LogicScanResult result = evaluateCondition( - node, repository, &condition_value); - if (!result.succeeded) - { - result.logicId = logic.id; - result.rungId = rung.id; - return result; - } - stage_value = stage_value || condition_value; - } - rung_value = rung_value && stage_value; + result.logicId = logic.id; + result.rungId = rung.id; + return result; } - - LogicScanResult result = writeOutput( + if (trace != nullptr) + { + trace->rungValues[rung.id] = rung_value; + trace->nodeValues[rung.output->id] = rung_value; + } + result = writeOutput( *rung.output, rung_value, repository); if (!result.succeeded) { @@ -166,6 +172,47 @@ LogicScanResult SoftwareLogicExecutor::executeScan( return success(); } +LogicScanResult SoftwareLogicExecutor::evaluateExpression( + const ConditionExpression &expression, + RegisterRepository &repository, + LogicTraceSnapshot *trace, + bool *value) const +{ + if (value == nullptr) + { + return failure(LogicScanError::InvalidLogic, "expression result target is missing"); + } + if (expression.kind == ConditionExpressionKind::Node) + { + LogicScanResult result = evaluateCondition(*expression.node, repository, value); + if (result.succeeded && trace != nullptr) + { + trace->nodeValues[expression.node->id] = *value; + trace->expressionValues[expression.id] = *value; + } + return result; + } + + bool accumulated = expression.kind == ConditionExpressionKind::Series; + for (const ConditionExpression &child : expression.children) + { + bool child_value = false; + LogicScanResult result = evaluateExpression(child, repository, trace, &child_value); + if (!result.succeeded) + { + return result; + } + accumulated = expression.kind == ConditionExpressionKind::Series + ? accumulated && child_value : accumulated || child_value; + } + *value = accumulated; + if (trace != nullptr) + { + trace->expressionValues[expression.id] = accumulated; + } + return success(); +} + LogicScanResult SoftwareLogicExecutor::evaluateCondition( const LogicNode &node, RegisterRepository &repository, diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index e3bc484..4458e7a 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -4,6 +4,7 @@ #include "domain/register_repository.h" #include +#include #include enum class LogicScanError @@ -25,6 +26,15 @@ struct LogicScanResult std::string nodeId; }; +struct LogicTraceSnapshot +{ + std::unordered_map nodeValues; + std::unordered_map expressionValues; + std::unordered_map rungValues; + + void clear(); +}; + // 按工程顺序执行受限梯形图的一次确定性扫描 class SoftwareLogicExecutor { @@ -32,13 +42,19 @@ public: LogicScanResult validate(const std::vector &logics) const; LogicScanResult executeScan( const std::vector &logics, - RegisterRepository &repository) const; + RegisterRepository &repository, + LogicTraceSnapshot *trace = nullptr) const; private: LogicScanResult evaluateCondition( const LogicNode &node, RegisterRepository &repository, bool *value) const; + LogicScanResult evaluateExpression( + const ConditionExpression &expression, + RegisterRepository &repository, + LogicTraceSnapshot *trace, + bool *value) const; LogicScanResult writeOutput( const LogicNode &node, bool rung_value, diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 0043d9f..2f78b6f 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -9,28 +9,41 @@ #include #include -#include #include namespace { -constexpr qreal kMinimumSceneWidth = 1280.0; -constexpr qreal kLeftRailX = 72.0; -constexpr qreal kStageWidth = 184.0; -constexpr qreal kNodeWidth = 128.0; -constexpr qreal kNodeTerminalX = kNodeWidth / 2.0; -constexpr qreal kBranchSpacing = 96.0; +constexpr qreal kLeftRailX = 64.0; +constexpr qreal kMinimumSceneWidth = 940.0; +constexpr qreal kNodeWidth = 116.0; +constexpr qreal kNodeHeight = 92.0; +constexpr qreal kNodeTerminalX = 54.0; +constexpr qreal kSeriesGap = 26.0; +constexpr qreal kParallelGap = 22.0; +constexpr qreal kExpressionPadding = 18.0; +constexpr qreal kRungHeaderHeight = 38.0; constexpr qreal kRungGap = 18.0; -constexpr qreal kRungBaseHeight = 132.0; -constexpr qreal kMainLineOffset = 78.0; -constexpr qreal kLadderLineWidth = 1.6; -constexpr qreal kPlaceholderWidth = 112.0; -constexpr qreal kPlaceholderHeight = 30.0; +constexpr qreal kOutputWidth = 128.0; +constexpr qreal kOutputGap = 72.0; +constexpr qreal kLadderLineWidth = 1.8; +const QColor kLadderColor(QStringLiteral("#263842")); +const QColor kActiveColor(QStringLiteral("#16854f")); +const QColor kFaultColor(QStringLiteral("#c5362e")); +const QColor kSelectionColor(QStringLiteral("#dfeef5")); +const QColor kSelectionBorderColor(QStringLiteral("#277da1")); +const QColor kGroupBorderColor(QStringLiteral("#8ea4af")); +const QColor kPlaceholderColor(QStringLiteral("#81919b")); + +struct ExpressionSize +{ + qreal width = kNodeWidth; + qreal height = kNodeHeight; +}; -const QColor kLadderColor(QStringLiteral("#26343d")); -const QColor kSelectionColor(QStringLiteral("#dcecf4")); -const QColor kSelectionBorderColor(QStringLiteral("#1677a8")); -const QColor kPlaceholderColor(QStringLiteral("#82919b")); +QString registerAddressText(const RegisterAddress &address) +{ + return QString::fromStdString(address.toString()); +} QString comparisonText(ComparisonOperator comparison) { @@ -42,16 +55,8 @@ QString comparisonText(ComparisonOperator comparison) case ComparisonOperator::LessThanOrEqual: return QStringLiteral("<="); case ComparisonOperator::GreaterThan: return QStringLiteral(">"); case ComparisonOperator::GreaterThanOrEqual: return QStringLiteral(">="); - default: return QStringLiteral("?"); } -} - -QString registerAddressText(const RegisterAddress &address) -{ - return QStringLiteral("%1%2") - .arg(address.area() == RegisterArea::M ? QStringLiteral("M") - : QStringLiteral("D")) - .arg(address.index()); + return QStringLiteral("?"); } QString nodeToolTip(const LogicNodeConfig &config) @@ -62,25 +67,16 @@ QString nodeToolTip(const LogicNodeConfig &config) using Config = std::decay_t; if constexpr (std::is_same_v) { - return value.mode == ContactMode::NormallyOpen - ? LogicEditorWidget::tr("常开触点 %1").arg( - registerAddressText(value.address)) - : LogicEditorWidget::tr("常闭触点 %1").arg( - registerAddressText(value.address)); + return LogicEditorWidget::tr("%1触点:%2") + .arg(value.mode == ContactMode::NormallyOpen + ? LogicEditorWidget::tr("常开") + : LogicEditorWidget::tr("常闭")) + .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { - QString mode = LogicEditorWidget::tr("普通线圈"); - if (value.mode == CoilMode::Set) - { - mode = LogicEditorWidget::tr("置位线圈"); - } - else if (value.mode == CoilMode::Reset) - { - mode = LogicEditorWidget::tr("复位线圈"); - } - return QStringLiteral("%1 %2").arg( - mode, registerAddressText(value.address)); + return LogicEditorWidget::tr("输出线圈:%1") + .arg(registerAddressText(value.address)); } else { @@ -93,43 +89,52 @@ QString nodeToolTip(const LogicNodeConfig &config) config); } -QString nodeAddressText(const RegisterAddress &address, bool configured) +ExpressionSize measureExpression(const ConditionExpression &expression) { - return configured ? registerAddressText(address) - : LogicEditorWidget::tr("< M 地址 >"); + if (expression.kind == ConditionExpressionKind::Node) + { + return {}; + } + ExpressionSize size{0.0, 0.0}; + if (expression.kind == ConditionExpressionKind::Series) + { + for (const ConditionExpression &child : expression.children) + { + const ExpressionSize child_size = measureExpression(child); + size.width += child_size.width; + size.height = std::max(size.height, child_size.height); + } + size.width += kSeriesGap * static_cast(expression.children.size() - 1U); + } + else + { + for (const ConditionExpression &child : expression.children) + { + const ExpressionSize child_size = measureExpression(child); + size.width = std::max(size.width, child_size.width); + size.height += child_size.height; + } + size.height += kParallelGap * static_cast(expression.children.size() - 1U); + } + size.width += kExpressionPadding * 2.0; + size.height += kExpressionPadding * 2.0; + return size; } -qreal rungHeight(const LadderRung &rung) +QPen ladderPen(bool active) { - std::size_t maximum_branches = 1U; - for (const LadderStage &stage : rung.stages) - { - maximum_branches = std::max(maximum_branches, stage.branches.size()); - } - return kRungBaseHeight - + static_cast(maximum_branches - 1U) * kBranchSpacing; + return QPen(active ? kActiveColor : kLadderColor, + active ? 2.6 : kLadderLineWidth); } -void addPlaceholder( - QGraphicsScene &scene, - const QPointF ¢er, - const QString &text, - qreal width = kPlaceholderWidth) -{ - const QRectF bounds( - center.x() - width / 2.0, - center.y() - kPlaceholderHeight / 2.0, - width, - kPlaceholderHeight); - scene.addRect( - bounds, - QPen(kPlaceholderColor, 1.2, Qt::DashLine), - QBrush(Qt::white)); - QGraphicsTextItem *label = scene.addText(text); - label->setDefaultTextColor(kPlaceholderColor); - label->setPos( - center.x() - label->boundingRect().width() / 2.0, - center.y() - label->boundingRect().height() / 2.0); +bool traceValue( + const LogicTraceSnapshot &trace, + const std::unordered_map LogicTraceSnapshot::*member, + const std::string &id) +{ + const auto &values = trace.*member; + const auto found = values.find(id); + return found != values.end() && found->second; } } // namespace @@ -140,31 +145,31 @@ public: NodeItem( const LogicNode &node, const std::string &rung_id, - const std::string &stage_id, - const QPointF ¢er) + const std::string &expression_id, + const QPointF ¢er, + bool active, + bool faulted) : node_id_(node.id), rung_id_(rung_id), - stage_id_(stage_id), + expression_id_(expression_id), config_(node.config), - configured_(node.configured) + configured_(node.configured), + active_(active), + faulted_(faulted) { setPos(center); setFlag(ItemIsSelectable, true); - setZValue(1.0); + setZValue(3.0); setToolTip(configured_ ? nodeToolTip(config_) - : LogicEditorWidget::tr( - "待配置:请在属性区设置地址和参数")); + : LogicEditorWidget::tr("待配置:请在属性区设置地址和参数")); } QRectF boundingRect() const override { - return {-kNodeWidth / 2.0, -46.0, kNodeWidth, 92.0}; + return {-kNodeWidth / 2.0, -kNodeHeight / 2.0, kNodeWidth, kNodeHeight}; } - void paint( - QPainter *painter, - const QStyleOptionGraphicsItem *option, - QWidget *) override + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { painter->setRenderHint(QPainter::Antialiasing, true); const bool selected = (option->state & QStyle::State_Selected) != 0; @@ -172,12 +177,12 @@ public: { painter->fillRect(boundingRect(), kSelectionColor); } - const QColor symbol_color = selected ? kSelectionBorderColor : kLadderColor; - painter->setPen(QPen(symbol_color, selected ? 2.0 : kLadderLineWidth)); - - QFont text_font = painter->font(); - text_font.setPointSizeF(9.5); - painter->setFont(text_font); + const QColor symbol_color = faulted_ ? kFaultColor + : selected ? kSelectionBorderColor : active_ ? kActiveColor : kLadderColor; + painter->setPen(QPen(symbol_color, active_ || faulted_ ? 2.6 : kLadderLineWidth)); + QFont font = painter->font(); + font.setPointSizeF(9.5); + painter->setFont(font); if (const auto *contact = std::get_if(&config_)) { @@ -190,127 +195,149 @@ public: { painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } - painter->setPen(kLadderColor); painter->drawText( QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, - nodeAddressText(contact->address, configured_)); + configured_ ? registerAddressText(contact->address) : tr("< M 地址 >")); } else if (const auto *coil = std::get_if(&config_)) { painter->fillRect(QRectF(-34, -23, 68, 46), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-16, 0)); painter->drawLine(QPointF(16, 0), QPointF(kNodeTerminalX, 0)); - QPainterPath left_coil; - left_coil.moveTo(-2, -20); - left_coil.cubicTo(-24, -16, -24, 16, -2, 20); - painter->drawPath(left_coil); - QPainterPath right_coil; - right_coil.moveTo(2, -20); - right_coil.cubicTo(24, -16, 24, 16, 2, 20); - painter->drawPath(right_coil); - if (coil->mode == CoilMode::Set || coil->mode == CoilMode::Reset) + QPainterPath left; + left.moveTo(-2, -20); + left.cubicTo(-24, -16, -24, 16, -2, 20); + painter->drawPath(left); + QPainterPath right; + right.moveTo(2, -20); + right.cubicTo(24, -16, 24, 16, 2, 20); + painter->drawPath(right); + if (coil->mode != CoilMode::Normal) { painter->drawText( QRectF(-12, -14, 24, 28), Qt::AlignCenter, - coil->mode == CoilMode::Set ? QStringLiteral("S") - : QStringLiteral("R")); + coil->mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R")); } - painter->setPen(kLadderColor); painter->drawText( QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, - nodeAddressText(coil->address, configured_)); + configured_ ? registerAddressText(coil->address) : tr("< M 地址 >")); } else if (const auto *comparison = std::get_if(&config_)) { - constexpr qreal box_half_width = 47.0; - const QRectF instruction_box(-box_half_width, -17, 94, 34); - painter->fillRect(instruction_box.adjusted(-1, -1, 1, 1), Qt::white); - painter->drawLine( - QPointF(-kNodeTerminalX, 0), QPointF(-box_half_width, 0)); - painter->drawLine( - QPointF(box_half_width, 0), QPointF(kNodeTerminalX, 0)); - painter->drawRect(instruction_box); + const QRectF box(-47, -17, 94, 34); + painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); + painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); + painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); + painter->drawRect(box); + painter->drawText(box, Qt::AlignCenter, + QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); painter->drawText( - instruction_box, - Qt::AlignCenter, - QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); - painter->setPen(kLadderColor); - painter->drawText( - QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), - Qt::AlignCenter, - configured_ ? registerAddressText(comparison->address) - : tr("< D 地址 >")); + QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, + configured_ ? registerAddressText(comparison->address) : tr("< D 地址 >")); painter->drawText( - QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), - Qt::AlignCenter, - configured_ ? QString::number(comparison->value) - : LogicEditorWidget::tr("< 常量 >")); + QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), Qt::AlignCenter, + configured_ ? QString::number(comparison->value) : tr("< 常量 >")); } } const std::string &nodeId() const { return node_id_; } const std::string &rungId() const { return rung_id_; } - const std::string &stageId() const { return stage_id_; } + const std::string &expressionId() const { return expression_id_; } private: std::string node_id_; std::string rung_id_; - std::string stage_id_; + std::string expression_id_; LogicNodeConfig config_; bool configured_ = true; + bool active_ = false; + bool faulted_ = false; +}; + +class LogicEditorWidget::ExpressionItem final : public QGraphicsItem +{ +public: + ExpressionItem( + std::string expression_id, + std::string rung_id, + ConditionExpressionKind kind, + const QRectF &bounds) + : expression_id_(std::move(expression_id)), + rung_id_(std::move(rung_id)), + kind_(kind), + bounds_(bounds) + { + setFlag(ItemIsSelectable, true); + setZValue(1.0); + setToolTip(kind_ == ConditionExpressionKind::Parallel + ? LogicEditorWidget::tr("并联支路组") + : LogicEditorWidget::tr("串联条件组")); + } + + QRectF boundingRect() const override { return bounds_; } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override + { + if ((option->state & QStyle::State_Selected) == 0) + { + return; + } + painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); + painter->setBrush(QColor(223, 238, 245, 45)); + painter->drawRect(bounds_.adjusted(2, 2, -2, -2)); + painter->setPen(kGroupBorderColor); + painter->drawText( + bounds_.adjusted(8, 3, -8, -3), + Qt::AlignRight | Qt::AlignTop, + kind_ == ConditionExpressionKind::Parallel + ? LogicEditorWidget::tr("并联组") : LogicEditorWidget::tr("串联组")); + } + + const std::string &expressionId() const { return expression_id_; } + const std::string &rungId() const { return rung_id_; } + +private: + std::string expression_id_; + std::string rung_id_; + ConditionExpressionKind kind_ = ConditionExpressionKind::Series; + QRectF bounds_; }; class LogicEditorWidget::RungItem final : public QGraphicsItem { public: - RungItem( - const LadderRung &rung, - int number, - qreal top, - qreal height, - qreal scene_width) + RungItem(const LadderRung &rung, int number, qreal top, qreal height, qreal width) : rung_id_(rung.id), - name_(QString::fromUtf8(rung.name.data(), static_cast(rung.name.size()))), + name_(QString::fromStdString(rung.name)), number_(number), height_(height), - scene_width_(scene_width) + width_(width) { setPos(0, top); setFlag(ItemIsSelectable, true); setZValue(-2.0); } - QRectF boundingRect() const override - { - return {20, 0, scene_width_ - 40, height_}; - } + QRectF boundingRect() const override { return {20, 0, width_ - 40, height_}; } - void paint( - QPainter *painter, - const QStyleOptionGraphicsItem *option, - QWidget *) override + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { if ((option->state & QStyle::State_Selected) != 0) { - painter->fillRect( - boundingRect(), QColor(QStringLiteral("#f0f7fa"))); + painter->fillRect(boundingRect(), QColor(QStringLiteral("#f0f7fa"))); } painter->setPen(QColor(QStringLiteral("#62717b"))); - QString title = QStringLiteral("网络 %1").arg(number_); - if (!name_.isEmpty() && !name_.startsWith(QStringLiteral("网络 "))) + QString title = tr("网络 %1").arg(number_); + if (!name_.isEmpty() && !name_.startsWith(tr("网络 "))) { title += QStringLiteral(":") + name_; } - painter->drawText( - QRectF(28, 6, 260, 22), - Qt::AlignLeft | Qt::AlignVCenter, - title); + painter->drawText(QRectF(28, 6, 320, 22), Qt::AlignLeft | Qt::AlignVCenter, title); painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); - painter->drawLine( - QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); + painter->drawLine(QPointF(28, height_ - 1), QPointF(width_ - 28, height_ - 1)); } const std::string &rungId() const { return rung_id_; } @@ -319,19 +346,135 @@ private: std::string rung_id_; QString name_; int number_ = 0; - qreal height_ = 0; - qreal scene_width_ = 0; + qreal height_ = 0.0; + qreal width_ = 0.0; +}; + +namespace { + +struct RenderResult +{ + QPointF input; + QPointF output; }; -LogicEditorWidget::LogicEditorWidget( - LogicEditorService &editor_service, - QWidget *parent) +RenderResult renderExpression( + QGraphicsScene &scene, + const ConditionExpression &expression, + const std::string &rung_id, + const QRectF &bounds, + const LogicTraceSnapshot &trace, + bool trace_enabled, + const std::string &fault_node_id) +{ + const bool active = trace_enabled && traceValue( + trace, &LogicTraceSnapshot::expressionValues, expression.id); + if (expression.kind == ConditionExpressionKind::Node) + { + const QPointF center = bounds.center(); + scene.addItem(new LogicEditorWidget::NodeItem( + *expression.node, + rung_id, + expression.id, + center, + trace_enabled && traceValue( + trace, &LogicTraceSnapshot::nodeValues, expression.node->id), + expression.node->id == fault_node_id)); + return {QPointF(bounds.left(), center.y()), QPointF(bounds.right(), center.y())}; + } + + scene.addItem(new LogicEditorWidget::ExpressionItem( + expression.id, rung_id, expression.kind, bounds)); + if (expression.kind == ConditionExpressionKind::Series) + { + qreal x = bounds.left() + kExpressionPadding; + RenderResult first; + RenderResult previous; + for (std::size_t index = 0; index < expression.children.size(); ++index) + { + const ExpressionSize size = measureExpression(expression.children[index]); + const QRectF child_bounds( + x, + bounds.center().y() - size.height / 2.0, + size.width, + size.height); + const RenderResult current = renderExpression( + scene, + expression.children[index], + rung_id, + child_bounds, + trace, + trace_enabled, + fault_node_id); + if (index == 0U) + { + first = current; + } + else + { + scene.addLine(QLineF(previous.output, current.input), ladderPen(active)); + } + previous = current; + x += size.width + kSeriesGap; + } + scene.addLine(QLineF(QPointF(bounds.left(), first.input.y()), first.input), ladderPen(active)); + scene.addLine(QLineF(previous.output, QPointF(bounds.right(), previous.output.y())), ladderPen(active)); + return {QPointF(bounds.left(), first.input.y()), QPointF(bounds.right(), previous.output.y())}; + } + + qreal y = bounds.top() + kExpressionPadding; + std::vector branches; + for (const ConditionExpression &child : expression.children) + { + const ExpressionSize size = measureExpression(child); + const QRectF child_bounds( + bounds.center().x() - size.width / 2.0, + y, + size.width, + size.height); + branches.push_back(renderExpression( + scene, child, rung_id, child_bounds, trace, trace_enabled, fault_node_id)); + y += size.height + kParallelGap; + } + const qreal left_join = bounds.left() + 8.0; + const qreal right_join = bounds.right() - 8.0; + const qreal top_y = branches.front().input.y(); + const qreal bottom_y = branches.back().input.y(); + scene.addLine(QLineF(left_join, top_y, left_join, bottom_y), ladderPen(active)); + scene.addLine(QLineF(right_join, top_y, right_join, bottom_y), ladderPen(active)); + for (std::size_t index = 0; index < branches.size(); ++index) + { + const bool branch_active = trace_enabled && traceValue( + trace, &LogicTraceSnapshot::expressionValues, expression.children[index].id); + scene.addLine( + QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), + ladderPen(branch_active)); + scene.addLine( + QLineF(branches[index].output, QPointF(right_join, branches[index].output.y())), + ladderPen(branch_active)); + } + return {QPointF(bounds.left(), top_y), QPointF(bounds.right(), top_y)}; +} + +void addPlaceholder(QGraphicsScene &scene, const QPointF ¢er, const QString &text) +{ + const QRectF bounds(center.x() - 58, center.y() - 20, 116, 40); + scene.addRect(bounds, QPen(kPlaceholderColor, 1.2, Qt::DashLine), QBrush(Qt::white)); + QGraphicsTextItem *label = scene.addText(text); + label->setDefaultTextColor(kPlaceholderColor); + label->setPos(center.x() - label->boundingRect().width() / 2.0, + center.y() - label->boundingRect().height() / 2.0); +} + +} // namespace + +LogicEditorWidget::LogicEditorWidget(LogicEditorService &editor_service, QWidget *parent) : QGraphicsView(parent), editor_service_(editor_service) { scene_ = new QGraphicsScene(this); setScene(scene_); setRenderHint(QPainter::Antialiasing, true); - setBackgroundBrush(QColor(QStringLiteral("#ffffff"))); + setBackgroundBrush(Qt::white); setDragMode(QGraphicsView::RubberBandDrag); setAlignment(Qt::AlignLeft | Qt::AlignTop); connect(scene_, &QGraphicsScene::selectionChanged, @@ -355,6 +498,23 @@ void LogicEditorWidget::setEditingEnabled(bool enabled) setInteractive(enabled); } +void LogicEditorWidget::setRuntimeTrace( + const LogicTraceSnapshot &trace, const std::string &fault_node_id) +{ + trace_ = trace; + fault_node_id_ = fault_node_id; + runtime_trace_enabled_ = true; + reloadLogic(); +} + +void LogicEditorWidget::clearRuntimeTrace() +{ + trace_.clear(); + fault_node_id_.clear(); + runtime_trace_enabled_ = false; + reloadLogic(); +} + void LogicEditorWidget::reloadLogic() { scene_->clear(); @@ -364,129 +524,100 @@ void LogicEditorWidget::reloadLogic() scene_->setSceneRect(0, 0, kMinimumSceneWidth, 400); return; } + if (current_rung_id_.empty() && !logic->rungs.empty()) + { + current_rung_id_ = logic->rungs.front().id; + } - std::size_t maximum_stage_count = 0U; + qreal maximum_condition_width = 360.0; for (const LadderRung &rung : logic->rungs) { - maximum_stage_count = std::max(maximum_stage_count, rung.stages.size()); + if (rung.condition.has_value()) + { + maximum_condition_width = std::max( + maximum_condition_width, measureExpression(*rung.condition).width); + } } 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()) - { - current_rung_id_ = logic->rungs.front().id; - } + kLeftRailX + maximum_condition_width + kOutputGap + kOutputWidth + 96.0); + const qreal right_rail_x = scene_width - 64.0; + const qreal output_center_x = right_rail_x - kOutputWidth / 2.0; qreal top = 24.0; - int rung_number = 1; + int 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 + kMainLineOffset; - + const ExpressionSize size = rung.condition.has_value() + ? measureExpression(*rung.condition) : ExpressionSize{240.0, kNodeHeight}; + const qreal height = kRungHeaderHeight + size.height + 28.0; + const qreal center_y = top + kRungHeaderHeight + size.height / 2.0; + scene_->addItem(new RungItem(rung, number, top, height, scene_width)); scene_->addLine( - kLeftRailX, top + 38.0, kLeftRailX, top + height - 16.0, + QLineF(kLeftRailX, top + 30, kLeftRailX, top + height - 12), QPen(kLadderColor, 2.4)); scene_->addLine( - right_rail_x, top + 38.0, right_rail_x, top + height - 16.0, + QLineF(right_rail_x, top + 30, right_rail_x, top + height - 12), QPen(kLadderColor, 2.4)); - qreal cursor_x = kLeftRailX; - for (const LadderStage &stage : rung.stages) + QPointF expression_output(kLeftRailX, center_y); + if (rung.condition.has_value()) { - 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(kLadderColor, kLadderLineWidth)); - scene_->addLine( - right_join, main_y, next_x, main_y, - QPen(kLadderColor, kLadderLineWidth)); - if (stage.branches.size() > 1U) - { - const qreal bottom_y = main_y - + static_cast(stage.branches.size() - 1U) * kBranchSpacing; - scene_->addLine( - left_join, main_y, left_join, bottom_y, - QPen(kLadderColor, kLadderLineWidth)); - scene_->addLine( - right_join, main_y, right_join, bottom_y, - QPen(kLadderColor, kLadderLineWidth)); - } - for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) - { - const qreal branch_y = main_y + static_cast(branch) * kBranchSpacing; - scene_->addLine( - left_join, branch_y, right_join, branch_y, - QPen(kLadderColor, kLadderLineWidth)); - 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_center_x = right_rail_x - 68.0; - if (rung.stages.empty()) - { - const qreal condition_center_x = kLeftRailX + kStageWidth / 2.0; - scene_->addLine( - kLeftRailX, - main_y, - condition_center_x - kPlaceholderWidth / 2.0, - main_y, - QPen(kLadderColor, kLadderLineWidth)); - scene_->addLine( - condition_center_x + kPlaceholderWidth / 2.0, - main_y, - output_center_x, - main_y, - QPen(kLadderColor, kLadderLineWidth)); - addPlaceholder( + const QRectF bounds( + kLeftRailX + 24.0, + center_y - size.height / 2.0, + size.width, + size.height); + const RenderResult rendered = renderExpression( *scene_, - QPointF(condition_center_x, main_y), - tr("添加条件")); + *rung.condition, + rung.id, + bounds, + trace_, + runtime_trace_enabled_, + fault_node_id_); + const bool active = runtime_trace_enabled_ && traceValue( + trace_, &LogicTraceSnapshot::rungValues, rung.id); + scene_->addLine( + QLineF(QPointF(kLeftRailX, rendered.input.y()), rendered.input), + ladderPen(active)); + expression_output = rendered.output; } else { - scene_->addLine( - cursor_x, main_y, output_center_x, main_y, - QPen(kLadderColor, kLadderLineWidth)); + addPlaceholder(*scene_, QPointF(kLeftRailX + 120.0, center_y), tr("添加条件")); + scene_->addLine(QLineF(kLeftRailX, center_y, kLeftRailX + 62.0, center_y), + ladderPen(false)); + expression_output = QPointF(kLeftRailX + 178.0, center_y); } + + const bool rung_active = runtime_trace_enabled_ && traceValue( + trace_, &LogicTraceSnapshot::rungValues, rung.id); + scene_->addLine( + QLineF(expression_output, QPointF(output_center_x - kNodeTerminalX, expression_output.y())), + ladderPen(rung_active)); if (rung.output.has_value()) { - scene_->addLine( - output_center_x, main_y, right_rail_x, main_y, - QPen(kLadderColor, kLadderLineWidth)); scene_->addItem(new NodeItem( *rung.output, rung.id, {}, - QPointF(output_center_x, main_y))); + QPointF(output_center_x, expression_output.y()), + rung_active, + rung.output->id == fault_node_id_)); } else { - constexpr qreal output_placeholder_width = 96.0; - scene_->addLine( - output_center_x + output_placeholder_width / 2.0, - main_y, - right_rail_x, - main_y, - QPen(kLadderColor, kLadderLineWidth)); - addPlaceholder( - *scene_, - QPointF(output_center_x, main_y), - tr("输出线圈"), - output_placeholder_width); + addPlaceholder(*scene_, QPointF(output_center_x, expression_output.y()), tr("输出线圈")); } + scene_->addLine( + QLineF(output_center_x + kNodeTerminalX, + expression_output.y(), + right_rail_x, + expression_output.y()), + ladderPen(rung_active)); top += height + kRungGap; - ++rung_number; + ++number; } scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + 20.0)); } @@ -495,8 +626,7 @@ void LogicEditorWidget::selectNode(const std::string &node_id) { for (QGraphicsItem *item : scene_->items()) { - NodeItem *node = dynamic_cast(item); - if (node != nullptr) + if (NodeItem *node = dynamic_cast(item)) { node->setSelected(node->nodeId() == node_id); if (node->nodeId() == node_id) @@ -511,8 +641,7 @@ std::string LogicEditorWidget::selectedNodeId() const { for (QGraphicsItem *item : scene_->selectedItems()) { - const NodeItem *node = dynamic_cast(item); - if (node != nullptr) + if (const NodeItem *node = dynamic_cast(item)) { return node->nodeId(); } @@ -520,32 +649,37 @@ std::string LogicEditorWidget::selectedNodeId() const return {}; } -std::string LogicEditorWidget::selectedRungId() const +std::string LogicEditorWidget::selectedExpressionId() const { for (QGraphicsItem *item : scene_->selectedItems()) { - const NodeItem *node = dynamic_cast(item); - if (node != nullptr) + if (const NodeItem *node = dynamic_cast(item)) { - return node->rungId(); + return node->expressionId(); } - const RungItem *rung = dynamic_cast(item); - if (rung != nullptr) + if (const ExpressionItem *expression = dynamic_cast(item)) { - return rung->rungId(); + return expression->expressionId(); } } return {}; } -std::string LogicEditorWidget::selectedStageId() const +std::string LogicEditorWidget::selectedRungId() const { for (QGraphicsItem *item : scene_->selectedItems()) { - const NodeItem *node = dynamic_cast(item); - if (node != nullptr) + if (const NodeItem *node = dynamic_cast(item)) { - return node->stageId(); + return node->rungId(); + } + if (const ExpressionItem *expression = dynamic_cast(item)) + { + return expression->rungId(); + } + if (const RungItem *rung = dynamic_cast(item)) + { + return rung->rungId(); } } return {}; @@ -584,23 +718,49 @@ LogicEditorResult LogicEditorWidget::appendCondition(const LogicNodeConfig &conf return result; } -LogicEditorResult LogicEditorWidget::addParallelCondition( - const LogicNodeConfig &config) +LogicEditorResult LogicEditorWidget::insertCondition( + const LogicNodeConfig &config, SeriesInsertPosition position) +{ + const std::string rung_id = selectedRungId(); + const std::string expression_id = selectedExpressionId(); + LogicEditorResult result; + if (rung_id.empty() || expression_id.empty()) + { + result = {false, LogicEditorError::InvalidOperation, + "请先选择串联插入目标节点或支路", {}}; + } + else + { + result = editor_service_.insertCondition( + logic_id_, rung_id, expression_id, position, config); + } + if (result.succeeded) + { + reloadLogic(); + selectNode(result.id); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; +} + +LogicEditorResult LogicEditorWidget::addParallelCondition(const LogicNodeConfig &config) { const std::string rung_id = selectedRungId(); - const std::string stage_id = selectedStageId(); + const std::string expression_id = selectedExpressionId(); LogicEditorResult result; - if (rung_id.empty() || stage_id.empty()) + if (rung_id.empty() || expression_id.empty()) { - result = {false, - LogicEditorError::InvalidOperation, - "请先选择要添加并联分支的触点或比较条件", - {}}; + result = {false, LogicEditorError::InvalidOperation, + "请先选择要并联的节点或整条支路", {}}; } else { result = editor_service_.addParallelCondition( - logic_id_, rung_id, stage_id, config); + logic_id_, rung_id, expression_id, config); } if (result.succeeded) { @@ -634,26 +794,31 @@ LogicEditorResult LogicEditorWidget::setOutput(const LogicNodeConfig &config) LogicEditorResult LogicEditorWidget::deleteSelected() { - const std::string node_id = selectedNodeId(); LogicEditorResult result; + const std::string node_id = selectedNodeId(); + const std::string expression_id = selectedExpressionId(); + const std::string rung_id = selectedRungId(); if (!node_id.empty()) { result = editor_service_.removeNode(logic_id_, node_id); } - else + else if (!expression_id.empty()) + { + result = editor_service_.removeExpression(logic_id_, rung_id, expression_id); + } + else if (!rung_id.empty()) { - const std::string rung_id = selectedRungId(); - if (rung_id.empty()) - { - return {false, LogicEditorError::InvalidOperation, - "请先选择要删除的逻辑节点或网络", {}}; - } result = editor_service_.removeRung(logic_id_, rung_id); if (result.succeeded && current_rung_id_ == rung_id) { current_rung_id_ = editor_service_.firstRungId(logic_id_); } } + else + { + return {false, LogicEditorError::InvalidOperation, + "请先选择要删除的逻辑节点、支路或网络", {}}; + } if (result.succeeded) { reloadLogic(); @@ -694,9 +859,6 @@ std::string LogicEditorWidget::currentRungId() const { return selected; } - if (!current_rung_id_.empty()) - { - return current_rung_id_; - } - return editor_service_.firstRungId(logic_id_); + return current_rung_id_.empty() + ? editor_service_.firstRungId(logic_id_) : current_rung_id_; } diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index c72b3ac..d231df4 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -2,6 +2,7 @@ #include "domain/control_logic_model.h" #include "services/logic_editor_service.h" +#include "services/software_logic_executor.h" #include @@ -15,20 +16,30 @@ class LogicEditorWidget final : public QGraphicsView Q_OBJECT public: + class NodeItem; + class ExpressionItem; + class RungItem; + explicit LogicEditorWidget( LogicEditorService &editor_service, QWidget *parent = nullptr); void setLogicId(const std::string &logic_id); void setEditingEnabled(bool enabled); + void setRuntimeTrace( + const LogicTraceSnapshot &trace, + const std::string &fault_node_id = {}); + void clearRuntimeTrace(); void reloadLogic(); void selectNode(const std::string &node_id); std::string selectedNodeId() const; + std::string selectedExpressionId() const; std::string selectedRungId() const; - std::string selectedStageId() const; LogicEditorResult addRung(); LogicEditorResult appendCondition(const LogicNodeConfig &config); + LogicEditorResult insertCondition( + const LogicNodeConfig &config, SeriesInsertPosition position); LogicEditorResult addParallelCondition(const LogicNodeConfig &config); LogicEditorResult setOutput(const LogicNodeConfig &config); LogicEditorResult deleteSelected(); @@ -42,9 +53,6 @@ protected: void resizeEvent(QResizeEvent *event) override; private: - class NodeItem; - class RungItem; - void handleSelectionChanged(); void reportFailure(const LogicEditorResult &result); std::string currentRungId() const; @@ -53,5 +61,8 @@ private: QGraphicsScene *scene_ = nullptr; std::string logic_id_; std::string current_rung_id_; + LogicTraceSnapshot trace_; + std::string fault_node_id_; + bool runtime_trace_enabled_ = false; bool editing_enabled_ = true; }; diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 8da83f8..47cf10c 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -83,13 +83,10 @@ Project makeValidProject() ControlLogic logic; logic.id = "start-logic"; logic.name = "Start logic"; - 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.condition = ConditionExpression::fromNode(contact); rung.output = coil; logic.rungs.push_back(rung); @@ -153,19 +150,30 @@ void testLadderLogicBoundaries() ControlLogic logic; logic.id = "hold-logic"; logic.name = "Hold logic"; + ConditionExpression start_parallel; + start_parallel.id = "parallel-start"; + start_parallel.kind = ConditionExpressionKind::Parallel; + start_parallel.children = { + ConditionExpression::fromNode(start), + ConditionExpression::fromNode(run_contact)}; + ConditionExpression root; + root.id = "series-root"; + root.kind = ConditionExpressionKind::Series; + root.children = { + ConditionExpression::fromNode(stop), + start_parallel}; 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.condition = root; 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().condition->children.front() = + ConditionExpression::fromNode(coil); + require(!logic.validate(), "a ladder condition expression must reject coils"); + logic.rungs.front().condition = root; logic.rungs.front().output = start; require(!logic.validate(), "a ladder output must be a coil"); @@ -174,7 +182,7 @@ void testLadderLogicBoundaries() require(!logic.validateForRunning(), "conditions without an output must block runtime validation"); - LadderRung empty_rung{"rung-empty", "Empty network", {}, std::nullopt}; + LadderRung empty_rung{"rung-empty", "Empty network", std::nullopt, std::nullopt}; require(empty_rung.validate(), "an empty editing network must be valid"); empty_rung.output = coil; @@ -183,8 +191,18 @@ void testLadderLogicBoundaries() "an output without conditions must block runtime validation"); logic.rungs.front().output = coil; - logic.rungs.front().stages.at(1).branches.at(1).id = start.id; + logic.rungs.front().condition = root; + logic.rungs.front().condition->children.at(1).children.at(1).node->id = start.id; require(!logic.validate(), "logic node ids must be unique"); + + ConditionExpression nested_parallel; + nested_parallel.id = "parallel-nested"; + nested_parallel.kind = ConditionExpressionKind::Parallel; + nested_parallel.children = { + ConditionExpression::fromNode(start), + root}; + require(nested_parallel.validate(), + "nested series and parallel expressions must be valid"); } void testModelsValidateBindingsAndIdentifiers() diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index 096b865..e075448 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -2,7 +2,6 @@ #include "services/logic_editor_service.h" #include "services/project_service.h" -#include #include #include @@ -30,99 +29,98 @@ void require(bool condition, const std::string &message) } } -void testEditorOperations() +ContactNodeConfig contact(int address) +{ + return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen}; +} + +void testStructuredEditingAndNormalization() { TestProjectStorage storage; ProjectService project_service(storage); LogicEditorService service(project_service); - const LogicEditorResult logic_result = service.ensureDefaultLogic(); - require(logic_result.succeeded, "default logic must be created"); - const std::string logic_id = logic_result.id; - + const std::string logic_id = service.ensureDefaultLogic().id; 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}); - 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( + const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0)); + const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1)); + require(first.succeeded && second.succeeded, "series append must succeed"); + + const LadderRung *rung = service.findRung(logic_id, rung_id); + require(rung->condition->kind == ConditionExpressionKind::Series + && rung->condition->children.size() == 2U, + "two appended nodes must form a series expression"); + + const std::string second_expression_id = second.id; + const LogicEditorResult parallel = service.addParallelCondition( + logic_id, rung_id, second_expression_id, contact(2)); + require(parallel.succeeded, "parallel insertion must succeed"); + rung = service.findRung(logic_id, rung_id); + const ConditionExpression *parallel_expression = service.findExpression( + logic_id, rung_id, rung->condition->children.at(1).id); + require(parallel_expression != nullptr + && parallel_expression->kind == ConditionExpressionKind::Parallel, + "selected node must become a parallel expression"); + + const LogicEditorResult nested_series = service.insertCondition( logic_id, rung_id, - CoilNodeConfig{ - RegisterAddress{RegisterArea::M, 2}, - CoilMode::Normal}); - require(hold_result.succeeded && coil_result.succeeded, - "parallel hold contact and output coil must be added"); - - require(!service.findNode(logic_id, stop_result.id)->configured, - "new logic nodes must remain unconfigured until properties are applied"); - require(service.updateNodeConfig( + parallel.id, + SeriesInsertPosition::After, + contact(3)); + require(nested_series.succeeded, "a parallel branch must accept a series node"); + rung = service.findRung(logic_id, rung_id); + require(rung->condition->kind == ConditionExpressionKind::Series, + "root must remain a series expression"); + const ConditionExpression &nested_parallel_expression = rung->condition->children.at(1); + require(nested_parallel_expression.kind == ConditionExpressionKind::Parallel + && nested_parallel_expression.children.at(1).kind + == ConditionExpressionKind::Series, + "editor must express A AND (B OR (C AND D))"); + + require(service.removeNode(logic_id, nested_series.id).succeeded, + "nested series node deletion must succeed"); + rung = service.findRung(logic_id, rung_id); + require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel + && rung->condition->children.at(1).children.at(1).kind + == ConditionExpressionKind::Node, + "single-child series container must collapse after deletion"); + + require(service.removeNode(logic_id, parallel.id).succeeded, + "parallel leaf deletion must succeed"); + rung = service.findRung(logic_id, rung_id); + require(rung->condition->kind == ConditionExpressionKind::Series + && rung->condition->children.size() == 2U, + "single-child parallel container must collapse after deletion"); + + require(service.setOutput( logic_id, - stop_result.id, - ContactNodeConfig{ - RegisterAddress{RegisterArea::M, 1}, - ContactMode::NormallyClosed}) + rung_id, + CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}) .succeeded, - "applying logic node properties must succeed"); - require(service.findNode(logic_id, stop_result.id)->configured, - "applying logic node properties must mark the node configured"); - - 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"); + "output coil must be set"); require(service.findLogic(logic_id)->validate(), - "configured self-hold ladder must pass full validation"); + "structured editing result must remain a valid draft"); +} - const LogicNode *start = service.findNode(logic_id, start_result.id); - require(start != nullptr, "added condition must be discoverable"); - require(service.updateNodeConfig( - logic_id, - start_result.id, - ContactNodeConfig{ - RegisterAddress{RegisterArea::M, 2}, - ContactMode::NormallyClosed}) - .succeeded, - "contact properties must be editable"); - - require(service.removeNode(logic_id, 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, - "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"); +void testBranchLevelParallelInsertion() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + LogicEditorService service(project_service); + const std::string logic_id = service.ensureDefaultLogic().id; + const std::string rung_id = service.firstRungId(logic_id); + service.appendCondition(logic_id, rung_id, contact(0)); + service.appendCondition(logic_id, rung_id, contact(1)); + + const std::string series_id = service.findRung(logic_id, rung_id)->condition->id; + const LogicEditorResult branch = service.addParallelCondition( + logic_id, rung_id, series_id, contact(2)); + require(branch.succeeded, "a whole series branch must accept a parallel condition"); + const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition; + require(root.kind == ConditionExpressionKind::Parallel + && root.children.front().kind == ConditionExpressionKind::Series, + "branch-level insertion must express (A AND B) OR C"); } } // namespace @@ -131,14 +129,14 @@ int main() { try { - testEditorOperations(); + testStructuredEditingAndNormalization(); + testBranchLevelParallelInsertion(); } catch (const std::exception &error) { std::cerr << "logic editor service tests failed: " << error.what() << '\n'; return 1; } - std::cout << "logic editor service tests passed\n"; return 0; } diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 8418a0e..930d9e0 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -49,9 +49,38 @@ LadderRung rung(const std::string &id, LadderRung result; result.id = id; result.name = id; + std::vector series_children; for (std::size_t index = 0; index < stages.size(); ++index) { - result.stages.push_back({id + "-stage-" + std::to_string(index), stages[index]}); + std::vector parallel_children; + for (const LogicNode &node : stages[index]) + { + parallel_children.push_back(ConditionExpression::fromNode(node)); + } + if (parallel_children.size() == 1U) + { + series_children.push_back(std::move(parallel_children.front())); + } + else + { + ConditionExpression parallel; + parallel.id = id + "-parallel-" + std::to_string(index); + parallel.kind = ConditionExpressionKind::Parallel; + parallel.children = std::move(parallel_children); + series_children.push_back(std::move(parallel)); + } + } + if (series_children.size() == 1U) + { + result.condition = std::move(series_children.front()); + } + else + { + ConditionExpression series; + series.id = id + "-series"; + series.kind = ConditionExpressionKind::Series; + series.children = std::move(series_children); + result.condition = std::move(series); } result.output = output; return result; @@ -82,6 +111,50 @@ void writeWord(RegisterRepository &repository, int address, std::int16_t value) "test word write must succeed"); } +void testNestedSeriesParallelExpression() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + ConditionExpression nested_series; + nested_series.id = "nested-series"; + nested_series.kind = ConditionExpressionKind::Series; + nested_series.children = { + ConditionExpression::fromNode(contact("b", 1)), + ConditionExpression::fromNode(contact("c", 2))}; + ConditionExpression root; + root.id = "root-parallel"; + root.kind = ConditionExpressionKind::Parallel; + root.children = { + ConditionExpression::fromNode(contact("a", 0)), + nested_series}; + LadderRung nested_rung; + nested_rung.id = "nested-rung"; + nested_rung.name = "nested-rung"; + nested_rung.condition = root; + nested_rung.output = coil("nested-output", 10); + const ControlLogic program = logic({nested_rung}); + + writeBit(repository, 1, true); + writeBit(repository, 2, true); + LogicTraceSnapshot trace; + require(executor.executeScan({program}, repository, &trace).succeeded, + "nested expression scan must succeed"); + require(readBit(repository, 10), "B AND C branch must energize A OR (B AND C)"); + require(trace.expressionValues.at("nested-series") + && trace.expressionValues.at("root-parallel") + && trace.rungValues.at("nested-rung"), + "scan trace must expose active nested expression and rung values"); + + writeBit(repository, 2, false); + require(executor.executeScan({program}, repository, &trace).succeeded, + "nested false scan must succeed"); + require(!readBit(repository, 10), "incomplete B AND C branch must be false"); + writeBit(repository, 0, true); + require(executor.executeScan({program}, repository, &trace).succeeded, + "alternate branch scan must succeed"); + require(readBit(repository, 10), "A branch must independently energize output"); +} + void testSeriesParallelContactsAndSequentialVisibility() { VirtualRegisterRepository repository; @@ -269,6 +342,7 @@ int main(int argc, char *argv[]) try { testSeriesParallelContactsAndSequentialVisibility(); + testNestedSeriesParallelExpression(); testAllComparisons(); testSetResetAndDisabledLogic(); testConflictingCoilsAreRejected();