diff --git a/app/src/domain/control_logic_model.cpp b/app/src/domain/control_logic_model.cpp index af7b78a..5ebb5df 100644 --- a/app/src/domain/control_logic_model.cpp +++ b/app/src/domain/control_logic_model.cpp @@ -229,7 +229,9 @@ bool validateUniqueExpressionIds( void normalizeExpression(ConditionExpression *expression) { - if (expression == nullptr || expression->kind == ConditionExpressionKind::Node) + if (expression == nullptr + || expression->kind == ConditionExpressionKind::Node + || expression->kind == ConditionExpressionKind::Wire) { return; } @@ -479,6 +481,26 @@ ConditionExpression ConditionExpression::fromNode(LogicNode logic_node) return expression; } +bool WireSegment::validate(std::string *error) const +{ + if (columnSpan < kMinimumColumnSpan || columnSpan > kMaximumColumnSpan) + { + setError(error, "横线跨度必须在 1~256 列范围内"); + return false; + } + return true; +} + +ConditionExpression ConditionExpression::fromWire( + std::string expression_id, int column_span) +{ + ConditionExpression expression; + expression.id = std::move(expression_id); + expression.kind = ConditionExpressionKind::Wire; + expression.wire = WireSegment{column_span}; + return expression; +} + bool ConditionExpression::validate(std::string *error) const { if (id.empty()) @@ -488,14 +510,30 @@ bool ConditionExpression::validate(std::string *error) const } if (kind == ConditionExpressionKind::Node) { - if (!node.has_value() || !children.empty() || !node->isCondition()) + if (!node.has_value() || wire.has_value() + || !children.empty() || !node->isCondition()) { setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式"); return false; } return node->validate(error); } - if (node.has_value() || children.size() < 2U) + if (kind == ConditionExpressionKind::Wire) + { + if (node.has_value() || !wire.has_value() || !children.empty()) + { + setError(error, "横线叶节点必须包含横线配置且不能包含逻辑节点或子表达式"); + return false; + } + return wire->validate(error); + } + if (kind != ConditionExpressionKind::Series + && kind != ConditionExpressionKind::Parallel) + { + setError(error, "条件表达式使用了不支持的类型"); + return false; + } + if (node.has_value() || wire.has_value() || children.size() < 2U) { setError(error, "串联和并联表达式至少需要两个子表达式"); return false; @@ -530,6 +568,10 @@ bool ConditionExpression::validateForRunning(std::string *error) const } return true; } + if (kind == ConditionExpressionKind::Wire) + { + return true; + } for (const ConditionExpression &child : children) { if (!child.validateForRunning(error)) @@ -548,6 +590,7 @@ void normalizeConditionExpression(std::optional *expression } normalizeExpression(&expression->value()); if (expression->value().kind != ConditionExpressionKind::Node + && expression->value().kind != ConditionExpressionKind::Wire && expression->value().children.empty()) { expression->reset(); @@ -557,6 +600,10 @@ void normalizeConditionExpression(std::optional *expression const LogicNode *findConditionNode( const ConditionExpression &expression, const std::string &node_id) { + if (expression.kind == ConditionExpressionKind::Wire) + { + return nullptr; + } if (expression.kind == ConditionExpressionKind::Node) { return expression.node->id == node_id ? &*expression.node : nullptr; @@ -615,6 +662,10 @@ void collectConditionNodes( nodes->push_back(&*expression.node); return; } + if (expression.kind == ConditionExpressionKind::Wire) + { + return; + } for (const ConditionExpression &child : expression.children) { collectConditionNodes(child, nodes); diff --git a/app/src/domain/control_logic_model.h b/app/src/domain/control_logic_model.h index fde5952..092c9f2 100644 --- a/app/src/domain/control_logic_model.h +++ b/app/src/domain/control_logic_model.h @@ -204,19 +204,32 @@ struct LogicNode enum class ConditionExpressionKind { Node, + Wire, Series, Parallel }; +struct WireSegment +{ + static constexpr int kMinimumColumnSpan = 1; + static constexpr int kMaximumColumnSpan = 256; + + int columnSpan = 1; + + bool validate(std::string *error = nullptr) const; +}; + // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 struct ConditionExpression { std::string id; ConditionExpressionKind kind = ConditionExpressionKind::Node; std::optional node; + std::optional wire; std::vector children; static ConditionExpression fromNode(LogicNode node); + static ConditionExpression fromWire(std::string id, int column_span = 1); bool validate(std::string *error = nullptr) const; bool validateForRunning(std::string *error = nullptr) const; }; diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index 34938d7..907c7b2 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -1468,6 +1468,8 @@ QString expressionKindText(ConditionExpressionKind kind) { case ConditionExpressionKind::Node: return QStringLiteral("node"); + case ConditionExpressionKind::Wire: + return QStringLiteral("wire"); case ConditionExpressionKind::Series: return QStringLiteral("series"); case ConditionExpressionKind::Parallel: @@ -1485,6 +1487,10 @@ QJsonObject serializeConditionExpression(const ConditionExpression &expression) { object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node)); } + else if (expression.kind == ConditionExpressionKind::Wire) + { + object.insert(QStringLiteral("columnSpan"), expression.wire->columnSpan); + } else { QJsonArray children; @@ -1525,11 +1531,29 @@ bool parseConditionExpression( expression->node = std::move(parsed_node); return true; } + if (kind == "wire") + { + int column_span = 0; + if (!readInt( + object, + "columnSpan", + context, + WireSegment::kMinimumColumnSpan, + WireSegment::kMaximumColumnSpan, + &column_span, + state)) + { + return false; + } + expression->kind = ConditionExpressionKind::Wire; + expression->wire = WireSegment{column_span}; + return true; + } if (kind != "series" && kind != "parallel") { return state->fail( ProjectStorageError::InvalidField, - context + ".kind 必须是 node、series 或 parallel"); + context + ".kind 必须是 node、wire、series 或 parallel"); } QJsonArray children; if (!readArray(object, "children", context, &children, state)) diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index e5441fb..1d629d8 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -75,18 +75,43 @@ ConditionExpression *findParentExpression( using NodeIdSet = std::unordered_set; -NodeIdSet conditionNodeIds(const ConditionExpression &expression) +NodeIdSet conditionLeafIds(const ConditionExpression &expression) { - std::vector nodes; - collectConditionNodes(expression, &nodes); NodeIdSet ids; - for (const LogicNode *node : nodes) + if (expression.kind == ConditionExpressionKind::Node + || expression.kind == ConditionExpressionKind::Wire) { - ids.insert(node->id); + ids.insert(expression.id); + return ids; + } + for (const ConditionExpression &child : expression.children) + { + NodeIdSet child_ids = conditionLeafIds(child); + ids.insert(child_ids.cbegin(), child_ids.cend()); } return ids; } +int expressionColumns(const ConditionExpression &expression) +{ + if (expression.kind == ConditionExpressionKind::Node) + { + return 1; + } + if (expression.kind == ConditionExpressionKind::Wire) + { + return expression.wire->columnSpan; + } + int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; + for (const ConditionExpression &child : expression.children) + { + const int child_columns = expressionColumns(child); + columns = expression.kind == ConditionExpressionKind::Series + ? columns + child_columns : std::max(columns, child_columns); + } + return columns; +} + bool isSubset(const NodeIdSet &subset, const NodeIdSet &values) { return std::all_of( @@ -124,7 +149,7 @@ bool addParallelForSelection( const std::string ¶llel_id, const std::string &series_id) { - const NodeIdSet expression_ids = conditionNodeIds(*expression); + const NodeIdSet expression_ids = conditionLeafIds(*expression); if (sameValues(expression_ids, selected_ids)) { addParallelSibling(expression, std::move(*branch), parallel_id); @@ -136,7 +161,7 @@ bool addParallelForSelection( std::vector matching_children; for (std::size_t index = 0; index < expression->children.size(); ++index) { - child_ids.push_back(conditionNodeIds(expression->children[index])); + child_ids.push_back(conditionLeafIds(expression->children[index])); const bool intersects = std::any_of( child_ids.back().cbegin(), child_ids.back().cend(), [&selected_ids](const std::string &id) @@ -212,7 +237,9 @@ bool addParallelForSelection( bool removeExpressionRecursive( ConditionExpression *expression, const std::string &expression_id) { - if (expression == nullptr || expression->kind == ConditionExpressionKind::Node) + if (expression == nullptr + || expression->kind == ConditionExpressionKind::Node + || expression->kind == ConditionExpressionKind::Wire) { return false; } @@ -238,6 +265,65 @@ bool removeExpressionRecursive( return false; } +std::optional selectedExpressionColumns( + const ConditionExpression &expression, + const NodeIdSet &selected_ids) +{ + const NodeIdSet expression_ids = conditionLeafIds(expression); + if (sameValues(expression_ids, selected_ids)) + { + return expressionColumns(expression); + } + if (expression.kind == ConditionExpressionKind::Node + || expression.kind == ConditionExpressionKind::Wire) + { + return std::nullopt; + } + + std::vector child_ids; + std::vector matching_children; + child_ids.reserve(expression.children.size()); + for (std::size_t index = 0; index < expression.children.size(); ++index) + { + child_ids.push_back(conditionLeafIds(expression.children[index])); + const bool intersects = std::any_of( + child_ids.back().cbegin(), child_ids.back().cend(), + [&selected_ids](const std::string &id) + { + return selected_ids.count(id) != 0U; + }); + if (intersects) + { + matching_children.push_back(index); + } + } + if (matching_children.size() == 1U) + { + return selectedExpressionColumns( + expression.children[matching_children.front()], selected_ids); + } + if (matching_children.empty() + || expression.kind != ConditionExpressionKind::Series) + { + return std::nullopt; + } + const std::size_t first = matching_children.front(); + const std::size_t last = matching_children.back(); + if (last - first + 1U != matching_children.size()) + { + return std::nullopt; + } + NodeIdSet range_ids; + int columns = 0; + for (std::size_t index = first; index <= last; ++index) + { + range_ids.insert(child_ids[index].cbegin(), child_ids[index].cend()); + columns += expressionColumns(expression.children[index]); + } + return sameValues(range_ids, selected_ids) + ? std::optional{columns} : std::nullopt; +} + LadderRung *findEditableRung( Project &project, const std::string &logic_id, @@ -329,6 +415,7 @@ bool LogicEditorService::expressionsEqual( { if (left.id != right.id || left.kind != right.kind || left.node.has_value() != right.node.has_value() + || left.wire.has_value() != right.wire.has_value() || left.children.size() != right.children.size()) { return false; @@ -337,6 +424,11 @@ bool LogicEditorService::expressionsEqual( { return false; } + if (left.wire.has_value() + && left.wire->columnSpan != right.wire->columnSpan) + { + return false; + } for (std::size_t index = 0; index < left.children.size(); ++index) { if (!expressionsEqual(left.children[index], right.children[index])) @@ -809,6 +901,50 @@ LogicEditorResult LogicEditorService::appendCondition( return {true, LogicEditorError::None, {}, node_id}; } +LogicEditorResult LogicEditorService::appendWire( + const std::string &logic_id, + const std::string &rung_id, + int column_span) +{ + const ControlLogic *logic = findLogic(logic_id); + if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) + { + return failure( + logic == nullptr ? LogicEditorError::LogicNotFound + : LogicEditorError::RungNotFound, + logic == nullptr ? "未找到控制逻辑" : "未找到梯形图网络"); + } + WireSegment wire{column_span}; + std::string error; + if (!wire.validate(&error)) + { + return failure(LogicEditorError::InvalidOperation, error); + } + const std::string wire_id = makeUniqueWireId(*logic); + ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span); + HistoryState before = captureState(); + LadderRung *rung = findEditableRung( + project_service_.editProject(), logic_id, rung_id); + if (!rung->condition.has_value()) + { + 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)); + } + recordHistory(std::move(before)); + return {true, LogicEditorError::None, {}, wire_id}; +} + LogicEditorResult LogicEditorService::insertConditionAfter( const std::string &logic_id, const std::string &rung_id, @@ -858,6 +994,91 @@ LogicEditorResult LogicEditorService::insertConditionAfter( return {true, LogicEditorError::None, {}, node_id}; } +LogicEditorResult LogicEditorService::insertWireAfter( + const std::string &logic_id, + const std::string &rung_id, + const std::string &target_expression_id, + int column_span) +{ + const ControlLogic *logic = findLogic(logic_id); + const LadderRung *existing_rung = findRung(logic_id, rung_id); + const ConditionExpression *target = findExpression( + logic_id, rung_id, target_expression_id); + WireSegment wire{column_span}; + std::string error; + if (logic == nullptr || existing_rung == nullptr || target == nullptr) + { + return failure(LogicEditorError::ExpressionNotFound, "未找到横线插入位置"); + } + if ((target->kind != ConditionExpressionKind::Node + && target->kind != ConditionExpressionKind::Wire) + || !wire.validate(&error)) + { + return failure( + LogicEditorError::InvalidOperation, + error.empty() ? "只能在触点或横线后插入横线" : error); + } + const std::string wire_id = makeUniqueWireId(*logic); + ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span); + HistoryState before = captureState(); + LadderRung *rung = findEditableRung( + project_service_.editProject(), logic_id, rung_id); + ConditionExpression *editable_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(target_iterator + 1, std::move(leaf)); + } + else + { + ConditionExpression original = std::move(*editable_target); + *editable_target = makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Series, + std::move(original), + std::move(leaf)); + } + recordHistory(std::move(before)); + return {true, LogicEditorError::None, {}, wire_id}; +} + +LogicEditorResult LogicEditorService::replaceWireWithCondition( + const std::string &logic_id, + const std::string &rung_id, + const std::string &wire_expression_id, + const LogicNodeConfig &config) +{ + const ControlLogic *logic = findLogic(logic_id); + const ConditionExpression *wire = findExpression( + logic_id, rung_id, wire_expression_id); + if (logic == nullptr || wire == nullptr + || wire->kind != ConditionExpressionKind::Wire) + { + return failure(LogicEditorError::ExpressionNotFound, "未找到要替换的横线"); + } + if (!isConditionConfig(config)) + { + return failure(LogicEditorError::InvalidNode, "横线只能替换为条件节点"); + } + const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); + HistoryState before = captureState(); + LadderRung *rung = findEditableRung( + project_service_.editProject(), logic_id, rung_id); + ConditionExpression *editable = findConditionExpression( + *rung->condition, wire_expression_id); + *editable = ConditionExpression::fromNode(makeNode(node_id, config)); + recordHistory(std::move(before)); + return {true, LogicEditorError::None, {}, node_id}; +} + LogicEditorResult LogicEditorService::addParallelBranch( const std::string &logic_id, const std::string &rung_id, @@ -885,7 +1106,7 @@ LogicEditorResult LogicEditorService::addParallelBranch( LogicEditorError::InvalidOperation, "并联选择中存在重复节点"); } - const NodeIdSet available_ids = conditionNodeIds(*existing_rung->condition); + const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition); if (!isSubset(selected_ids, available_ids)) { return failure( @@ -918,6 +1139,72 @@ LogicEditorResult LogicEditorService::addParallelBranch( return {true, LogicEditorError::None, {}, node_id}; } +LogicEditorResult LogicEditorService::addParallelWireBranch( + const std::string &logic_id, + const std::string &rung_id, + const std::vector &selected_expression_ids) +{ + const ControlLogic *logic = findLogic(logic_id); + const LadderRung *existing_rung = findRung(logic_id, rung_id); + if (logic == nullptr || existing_rung == nullptr + || !existing_rung->condition.has_value()) + { + return failure(LogicEditorError::RungNotFound, "未找到梯形图条件网络"); + } + if (selected_expression_ids.empty()) + { + return failure( + LogicEditorError::InvalidOperation, + "建立竖线连接前必须选择连续的触点或横线"); + } + NodeIdSet selected_ids( + selected_expression_ids.cbegin(), selected_expression_ids.cend()); + if (selected_ids.size() != selected_expression_ids.size()) + { + return failure(LogicEditorError::InvalidOperation, "并联选择中存在重复对象"); + } + const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition); + if (!isSubset(selected_ids, available_ids)) + { + return failure(LogicEditorError::ExpressionNotFound, "并联选择中包含未知对象"); + } + const std::optional column_span = selectedExpressionColumns( + *existing_rung->condition, selected_ids); + if (!column_span.has_value() + || *column_span > WireSegment::kMaximumColumnSpan) + { + return failure( + LogicEditorError::InvalidOperation, + "竖线连接只能围绕同一支路中的连续逻辑范围"); + } + + const std::string wire_id = makeUniqueWireId(*logic); + ConditionExpression wire = ConditionExpression::fromWire( + wire_id, *column_span); + const std::string parallel_id = makeUniqueExpressionId(*logic); + std::string series_id = parallel_id + "-range"; + while (findExpression(logic_id, rung_id, series_id) != nullptr) + { + series_id += "-range"; + } + HistoryState before = captureState(); + LadderRung *rung = findEditableRung( + project_service_.editProject(), logic_id, rung_id); + if (!addParallelForSelection( + &*rung->condition, + selected_ids, + &wire, + parallel_id, + series_id)) + { + return failure( + LogicEditorError::InvalidOperation, + "竖线连接只能围绕同一支路中的连续逻辑范围"); + } + recordHistory(std::move(before)); + return {true, LogicEditorError::None, {}, wire_id}; +} + LogicEditorResult LogicEditorService::setOutput( const std::string &logic_id, const std::string &rung_id, @@ -1100,6 +1387,58 @@ LogicEditorResult LogicEditorService::removeExpression( return {true, LogicEditorError::None, {}, expression_id}; } +LogicEditorResult LogicEditorService::removeExpressions( + const std::string &logic_id, + const std::string &rung_id, + const std::vector &expression_ids) +{ + const LadderRung *existing_rung = findRung(logic_id, rung_id); + if (existing_rung == nullptr || !existing_rung->condition.has_value()) + { + return failure(LogicEditorError::RungNotFound, "未找到梯形图条件网络"); + } + if (expression_ids.empty()) + { + return failure(LogicEditorError::InvalidOperation, "请先选择要删除的条件或横线"); + } + NodeIdSet unique_ids; + for (const std::string &expression_id : expression_ids) + { + const ConditionExpression *expression = findExpression( + logic_id, rung_id, expression_id); + if (!unique_ids.insert(expression_id).second) + { + return failure(LogicEditorError::InvalidOperation, "删除列表中存在重复对象"); + } + if (expression == nullptr) + { + return failure( + LogicEditorError::ExpressionNotFound, + "删除列表中包含未知条件表达式"); + } + } + + HistoryState before = captureState(); + LadderRung *rung = findEditableRung( + project_service_.editProject(), logic_id, rung_id); + for (const std::string &expression_id : expression_ids) + { + if (!rung->condition.has_value()) + { + break; + } + if (rung->condition->id == expression_id) + { + rung->condition.reset(); + break; + } + removeExpressionRecursive(&*rung->condition, expression_id); + } + normalizeConditionExpression(&rung->condition); + recordHistory(std::move(before)); + return {true, LogicEditorError::None, {}, expression_ids.front()}; +} + bool LogicEditorService::canUndo() const { return history_.canUndo(); @@ -1225,6 +1564,25 @@ std::string LogicEditorService::makeUniqueNodeId( } } +std::string LogicEditorService::makeUniqueWireId(const ControlLogic &logic) +{ + for (std::size_t index = 1;; ++index) + { + const std::string candidate = "wire-" + std::to_string(index); + const bool found = std::any_of( + logic.rungs.cbegin(), logic.rungs.cend(), + [&candidate](const LadderRung &rung) + { + return rung.condition.has_value() + && findConditionExpression(*rung.condition, candidate) != nullptr; + }); + if (!found) + { + return candidate; + } + } +} + std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic) { for (std::size_t index = 1;; ++index) diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index c2045ae..82c01e3 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -68,16 +68,34 @@ public: const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config); + LogicEditorResult appendWire( + const std::string &logic_id, + const std::string &rung_id, + int column_span = 1); LogicEditorResult insertConditionAfter( const std::string &logic_id, const std::string &rung_id, const std::string &target_node_id, const LogicNodeConfig &config); + LogicEditorResult insertWireAfter( + const std::string &logic_id, + const std::string &rung_id, + const std::string &target_expression_id, + int column_span = 1); + LogicEditorResult replaceWireWithCondition( + const std::string &logic_id, + const std::string &rung_id, + const std::string &wire_expression_id, + const LogicNodeConfig &config); LogicEditorResult addParallelBranch( const std::string &logic_id, const std::string &rung_id, const std::vector &selected_node_ids, const LogicNodeConfig &config); + LogicEditorResult addParallelWireBranch( + const std::string &logic_id, + const std::string &rung_id, + const std::vector &selected_expression_ids); LogicEditorResult setOutput( const std::string &logic_id, const std::string &rung_id, @@ -96,6 +114,10 @@ public: const std::string &logic_id, const std::string &rung_id, const std::string &expression_id); + LogicEditorResult removeExpressions( + const std::string &logic_id, + const std::string &rung_id, + const std::vector &expression_ids); bool canUndo() const; bool canRedo() const; @@ -129,6 +151,7 @@ private: static std::string nodePrefix(const LogicNodeConfig &config); static std::string makeUniqueNodeId( const ControlLogic &logic, const std::string &prefix); + static std::string makeUniqueWireId(const ControlLogic &logic); static std::string makeUniqueExpressionId(const ControlLogic &logic); static std::string makeUniqueRungId(const ControlLogic &logic); static LogicEditorResult failure( diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index 90670b8..122d2b5 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -74,7 +74,10 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current) void LogicTraceValues::clear() { nodeValues.clear(); + nodePowerValues.clear(); expressionValues.clear(); + expressionInputValues.clear(); + expressionPowerValues.clear(); rungValues.clear(); tonValues.clear(); counterValues.clear(); @@ -95,7 +98,10 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( if (values != logicValues.cend()) { projection.nodeValues = values->second.nodeValues; + projection.nodePowerValues = values->second.nodePowerValues; projection.expressionValues = values->second.expressionValues; + projection.expressionInputValues = values->second.expressionInputValues; + projection.expressionPowerValues = values->second.expressionPowerValues; projection.rungValues = values->second.rungValues; projection.tonValues = values->second.tonValues; projection.counterValues = values->second.counterValues; @@ -224,7 +230,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( bool rung_value = false; LogicScanResult result = evaluateExpression( - logic.id, *rung.condition, repository, logic_trace, &rung_value); + logic.id, + *rung.condition, + repository, + logic_trace, + true, + &rung_value); if (!result.succeeded) { result.logicId = logic.id; @@ -235,6 +246,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( { logic_trace->rungValues[rung.id] = rung_value; logic_trace->nodeValues[rung.output->id] = rung_value; + logic_trace->nodePowerValues[rung.output->id] = rung_value; } bool output_value = false; result = executeOutput( @@ -254,6 +266,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( if (logic_trace != nullptr) { logic_trace->nodeValues[rung.output->id] = output_value; + logic_trace->nodePowerValues[rung.output->id] = output_value; } } } @@ -268,7 +281,10 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( if (values != trace->logicValues.cend()) { trace->nodeValues = values->second.nodeValues; + trace->nodePowerValues = values->second.nodePowerValues; trace->expressionValues = values->second.expressionValues; + trace->expressionInputValues = values->second.expressionInputValues; + trace->expressionPowerValues = values->second.expressionPowerValues; trace->rungValues = values->second.rungValues; trace->tonValues = values->second.tonValues; trace->counterValues = values->second.counterValues; @@ -284,12 +300,27 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( const ConditionExpression &expression, RegisterRepository &repository, LogicTraceValues *trace, + bool input_power, bool *value) { if (value == nullptr) { return failure(LogicScanError::InvalidLogic, "缺少表达式结果接收对象"); } + if (trace != nullptr) + { + trace->expressionInputValues[expression.id] = input_power; + } + if (expression.kind == ConditionExpressionKind::Wire) + { + *value = true; + if (trace != nullptr) + { + trace->expressionValues[expression.id] = true; + trace->expressionPowerValues[expression.id] = input_power; + } + return success(); + } if (expression.kind == ConditionExpressionKind::Node) { LogicScanResult result = evaluateCondition( @@ -297,28 +328,43 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( if (result.succeeded && trace != nullptr) { trace->nodeValues[expression.node->id] = *value; + trace->nodePowerValues[expression.node->id] = input_power && *value; trace->expressionValues[expression.id] = *value; + trace->expressionPowerValues[expression.id] = input_power && *value; } return result; } bool accumulated = expression.kind == ConditionExpressionKind::Series; + bool power = input_power; for (const ConditionExpression &child : expression.children) { bool child_value = false; + const bool child_input = expression.kind == ConditionExpressionKind::Series + ? power : input_power; LogicScanResult result = evaluateExpression( - logic_id, child, repository, trace, &child_value); + logic_id, + child, + repository, + trace, + child_input, + &child_value); if (!result.succeeded) { return result; } accumulated = expression.kind == ConditionExpressionKind::Series ? accumulated && child_value : accumulated || child_value; + if (expression.kind == ConditionExpressionKind::Series) + { + power = power && child_value; + } } *value = accumulated; if (trace != nullptr) { trace->expressionValues[expression.id] = accumulated; + trace->expressionPowerValues[expression.id] = input_power && accumulated; } return success(); } diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index fd59a31..209025b 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -56,7 +56,10 @@ struct WordTraceValue struct LogicTraceValues { std::unordered_map nodeValues; + std::unordered_map nodePowerValues; std::unordered_map expressionValues; + std::unordered_map expressionInputValues; + std::unordered_map expressionPowerValues; std::unordered_map rungValues; std::unordered_map tonValues; std::unordered_map counterValues; @@ -111,6 +114,7 @@ private: const ConditionExpression &expression, RegisterRepository &repository, LogicTraceValues *trace, + bool input_power, bool *value); LogicScanResult executeOutput( const std::string &logic_id, diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 6d0b5c7..91d58b9 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -182,6 +182,10 @@ ExpressionMetrics measureExpression(const ConditionExpression &expression) { return {}; } + if (expression.kind == ConditionExpressionKind::Wire) + { + return {expression.wire->columnSpan, 1}; + } ExpressionMetrics metrics{0, 0}; if (expression.kind == ConditionExpressionKind::Series) @@ -273,14 +277,16 @@ public: const QPointF ¢er, const QString ®ister_comment, bool active, - bool faulted) + bool faulted, + bool condition) : node_id_(node.id), rung_id_(rung_id), config_(node.config), register_comment_(register_comment), configured_(node.configured), active_(active), - faulted_(faulted) + faulted_(faulted), + condition_(condition) { setPos(center); setFlag(ItemIsSelectable, true); @@ -518,6 +524,7 @@ public: const std::string &nodeId() const { return node_id_; } const std::string &rungId() const { return rung_id_; } + bool isConditionNode() const { return condition_; } private: std::string node_id_; @@ -527,6 +534,110 @@ private: bool configured_ = true; bool active_ = false; bool faulted_ = false; + bool condition_ = true; +}; + +class LogicEditorWidget::WireItem final : public QGraphicsItem +{ +public: + WireItem( + const std::string &expression_id, + const std::string &rung_id, + const QPointF ¢er, + int column_span, + bool active) + : expression_id_(expression_id), + rung_id_(rung_id), + width_(static_cast(column_span) * kCellWidth), + active_(active) + { + setPos(center); + setFlag(ItemIsSelectable, true); + setZValue(2.0); + setToolTip(LogicEditorWidget::tr("横线:%1 列,可用触点直接替换") + .arg(column_span)); + } + + QRectF boundingRect() const override + { + return {-width_ / 2.0, -kCellHeight / 2.0, width_, kCellHeight}; + } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override + { + const bool selected = (option->state & QStyle::State_Selected) != 0; + if (selected) + { + painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor); + painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); + painter->drawRect(boundingRect().adjusted(3, 3, -3, -3)); + } + painter->setPen(ladderPen(active_)); + painter->drawLine(QPointF(-width_ / 2.0, 0), QPointF(width_ / 2.0, 0)); + } + + 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_; + qreal width_ = kCellWidth; + bool active_ = false; +}; + +class LogicEditorWidget::VerticalConnectorItem final : public QGraphicsItem +{ +public: + VerticalConnectorItem( + const std::string &branch_expression_id, + const std::string &rung_id, + qreal x, + qreal top, + qreal bottom, + bool active) + : branch_expression_id_(branch_expression_id), + rung_id_(rung_id), + height_(bottom - top), + active_(active) + { + setPos(x, top); + setFlag(ItemIsSelectable, true); + setZValue(2.5); + setToolTip(LogicEditorWidget::tr("竖线连接:删除将移除对应并联支路")); + } + + QRectF boundingRect() const override + { + return {-8.0, 0.0, 16.0, height_}; + } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override + { + const bool selected = (option->state & QStyle::State_Selected) != 0; + painter->setPen(QPen( + selected ? kSelectionBorderColor + : active_ ? kActiveColor : kLadderColor, + selected || active_ ? 3.0 : kLadderLineWidth)); + painter->drawLine(QPointF(0, 0), QPointF(0, height_)); + if (selected) + { + painter->setPen(QPen(kSelectionBorderColor, 1.0, Qt::DashLine)); + painter->drawRect(boundingRect().adjusted(1, 1, -1, -1)); + } + } + + const std::string &branchExpressionId() const + { + return branch_expression_id_; + } + const std::string &rungId() const { return rung_id_; } + +private: + std::string branch_expression_id_; + std::string rung_id_; + qreal height_ = 0.0; + bool active_ = false; }; class LogicEditorWidget::RungItem final : public QGraphicsItem @@ -627,15 +738,31 @@ RenderResult renderExpression( bool trace_enabled, const std::string &fault_node_id) { - const bool active = trace_enabled && traceValue( - trace, &LogicTraceSnapshot::expressionValues, expression.id); + if (expression.kind == ConditionExpressionKind::Wire) + { + const bool active = trace_enabled && traceValue( + trace, &LogicTraceSnapshot::expressionPowerValues, expression.id); + const qreal width = static_cast(expression.wire->columnSpan) * kCellWidth; + const QPointF center( + top_left.x() + width / 2.0, + top_left.y() + kCellHeight / 2.0); + scene.addItem(new LogicEditorWidget::WireItem( + expression.id, + rung_id, + center, + expression.wire->columnSpan, + active)); + return { + QPointF(top_left.x(), center.y()), + QPointF(top_left.x() + width, center.y())}; + } if (expression.kind == ConditionExpressionKind::Node) { const QPointF center( top_left.x() + kCellWidth / 2.0, top_left.y() + kCellHeight / 2.0); const bool node_active = trace_enabled && traceValue( - trace, &LogicTraceSnapshot::nodeValues, expression.node->id); + trace, &LogicTraceSnapshot::nodePowerValues, expression.node->id); scene.addLine( QLineF( QPointF(top_left.x(), center.y()), @@ -655,7 +782,8 @@ RenderResult renderExpression( : QString{}; }(), node_active, - expression.node->id == fault_node_id)); + expression.node->id == fault_node_id, + true)); return { QPointF(top_left.x(), center.y()), QPointF(top_left.x() + kCellWidth, center.y())}; @@ -685,7 +813,13 @@ RenderResult renderExpression( } else { - scene.addLine(QLineF(previous.output, current.input), ladderPen(active)); + const bool previous_active = trace_enabled && traceValue( + trace, + &LogicTraceSnapshot::expressionPowerValues, + expression.children[index - 1U].id); + scene.addLine( + QLineF(previous.output, current.input), + ladderPen(previous_active)); } previous = current; x += static_cast(child_metrics.columns) * kCellWidth; @@ -715,14 +849,36 @@ RenderResult renderExpression( const qreal left_join = top_left.x(); const qreal right_join = top_left.x() + static_cast(metrics.columns) * kCellWidth; 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)); + const bool parallel_input_active = trace_enabled && traceValue( + trace, &LogicTraceSnapshot::expressionInputValues, expression.id); + for (std::size_t index = 1; index < branches.size(); ++index) + { + const qreal segment_top = branches[index - 1U].input.y(); + const qreal segment_bottom = branches[index].input.y(); + const bool branch_active = trace_enabled && traceValue( + trace, + &LogicTraceSnapshot::expressionPowerValues, + expression.children[index].id); + scene.addItem(new LogicEditorWidget::VerticalConnectorItem( + expression.children[index].id, + rung_id, + left_join, + segment_top, + segment_bottom, + parallel_input_active)); + scene.addItem(new LogicEditorWidget::VerticalConnectorItem( + expression.children[index].id, + rung_id, + right_join, + segment_top, + segment_bottom, + branch_active)); + } for (std::size_t index = 0; index < branches.size(); ++index) { const bool branch_active = trace_enabled && traceValue( trace, - &LogicTraceSnapshot::expressionValues, + &LogicTraceSnapshot::expressionPowerValues, expression.children[index].id); scene.addLine( QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), @@ -898,7 +1054,8 @@ void LogicEditorWidget::reloadLogic() rung.output->id) : rung_active; }(), - rung.output->id == fault_node_id_)); + rung.output->id == fault_node_id_, + false)); } else { @@ -938,6 +1095,38 @@ void LogicEditorWidget::selectNode(const std::string &node_id) } } +void LogicEditorWidget::selectExpression(const std::string &expression_id) +{ + for (QGraphicsItem *item : scene_->items()) + { + bool selected = false; + if (NodeItem *node = dynamic_cast(item)) + { + selected = node->isConditionNode() && node->nodeId() == expression_id; + if (selected) + { + current_rung_id_ = node->rungId(); + ensureVisible(node); + } + } + else if (WireItem *wire = dynamic_cast(item)) + { + selected = wire->expressionId() == expression_id; + if (selected) + { + current_rung_id_ = wire->rungId(); + ensureVisible(wire); + } + } + item->setSelected(selected); + } +} + +void LogicEditorWidget::selectWire(const std::string &wire_id) +{ + selectExpression(wire_id); +} + std::string LogicEditorWidget::selectedNodeId() const { const std::vector ids = selectedNodeIds(); @@ -973,6 +1162,73 @@ std::vector LogicEditorWidget::selectedNodeIds() const return ids; } +std::vector LogicEditorWidget::selectedExpressionIds() const +{ + std::vector> positioned_ids; + for (QGraphicsItem *item : scene_->selectedItems()) + { + if (const NodeItem *node = dynamic_cast(item)) + { + if (node->isConditionNode()) + { + positioned_ids.emplace_back(node->scenePos(), node->nodeId()); + } + } + else if (const WireItem *wire = dynamic_cast(item)) + { + positioned_ids.emplace_back(wire->scenePos(), wire->expressionId()); + } + } + std::sort( + positioned_ids.begin(), positioned_ids.end(), + [](const auto &left, const auto &right) + { + if (!qFuzzyCompare(left.first.y(), right.first.y())) + { + return left.first.y() < right.first.y(); + } + return left.first.x() < right.first.x(); + }); + std::vector ids; + ids.reserve(positioned_ids.size()); + for (const auto &positioned_id : positioned_ids) + { + ids.push_back(positioned_id.second); + } + return ids; +} + +std::vector LogicEditorWidget::selectedWireIds() const +{ + std::vector ids; + for (QGraphicsItem *item : scene_->selectedItems()) + { + if (const WireItem *wire = dynamic_cast(item)) + { + ids.push_back(wire->expressionId()); + } + } + return ids; +} + +std::vector LogicEditorWidget::selectedBranchIds() const +{ + std::vector ids; + for (QGraphicsItem *item : scene_->selectedItems()) + { + if (const VerticalConnectorItem *connector = + dynamic_cast(item)) + { + if (std::find(ids.cbegin(), ids.cend(), connector->branchExpressionId()) + == ids.cend()) + { + ids.push_back(connector->branchExpressionId()); + } + } + } + return ids; +} + std::string LogicEditorWidget::selectedRungId() const { std::string rung_id; @@ -986,6 +1242,23 @@ std::string LogicEditorWidget::selectedRungId() const } rung_id = node->rungId(); } + else if (const WireItem *wire = dynamic_cast(item)) + { + if (!rung_id.empty() && rung_id != wire->rungId()) + { + return {}; + } + rung_id = wire->rungId(); + } + else if (const VerticalConnectorItem *connector = + dynamic_cast(item)) + { + if (!rung_id.empty() && rung_id != connector->rungId()) + { + return {}; + } + rung_id = connector->rungId(); + } else if (const RungItem *rung = dynamic_cast(item)) { if (rung_id.empty()) @@ -1015,12 +1288,19 @@ LogicEditorResult LogicEditorWidget::addRung() LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) { + const std::vector selected_expressions = selectedExpressionIds(); + const std::vector selected_wires = selectedWireIds(); const std::vector selected_ids = selectedNodeIds(); LogicEditorResult result; - if (selected_ids.size() > 1U) + if (selected_expressions.size() > 1U || selected_wires.size() > 1U) { result = {false, LogicEditorError::InvalidOperation, - "串联插入时只能选择一个节点", {}}; + "串联插入或替换横线时只能选择一个条件对象", {}}; + } + else if (selected_wires.size() == 1U) + { + result = editor_service_.replaceWireWithCondition( + logic_id_, currentRungId(), selected_wires.front(), config); } else if (selected_ids.size() == 1U) { @@ -1038,7 +1318,7 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) if (result.succeeded) { reloadLogic(); - selectNode(result.id); + selectExpression(result.id); emit graphChanged(); } else @@ -1075,7 +1355,133 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co if (result.succeeded) { reloadLogic(); - selectNode(result.id); + selectExpression(result.id); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; +} + +LogicEditorResult LogicEditorWidget::addHorizontalWire() +{ + const std::vector selected_ids = selectedExpressionIds(); + LogicEditorResult result; + if (selected_ids.size() > 1U) + { + result = {false, LogicEditorError::InvalidOperation, + "插入横线时只能选择一个条件对象", {}}; + } + else if (selected_ids.size() == 1U) + { + result = editor_service_.insertWireAfter( + logic_id_, currentRungId(), selected_ids.front()); + } + else + { + result = editor_service_.appendWire(logic_id_, currentRungId()); + } + if (result.succeeded) + { + reloadLogic(); + selectWire(result.id); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; +} + +LogicEditorResult LogicEditorWidget::addVerticalWire() +{ + const std::string rung_id = selectedRungId(); + const std::vector selected_ids = selectedExpressionIds(); + LogicEditorResult result; + if (rung_id.empty() || selected_ids.empty()) + { + result = {false, LogicEditorError::InvalidOperation, + "请在同一网络中选择要连接的连续条件或横线", {}}; + } + else + { + result = editor_service_.addParallelWireBranch( + logic_id_, rung_id, selected_ids); + } + if (result.succeeded) + { + reloadLogic(); + selectWire(result.id); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; +} + +LogicEditorResult LogicEditorWidget::deleteHorizontalWire() +{ + const std::vector wire_ids = selectedWireIds(); + const std::string rung_id = selectedRungId(); + if (wire_ids.empty() || rung_id.empty()) + { + const LogicEditorResult result = { + false, + LogicEditorError::InvalidOperation, + "请先选择要删除的横线", + {}}; + reportFailure(result); + return result; + } + const LogicEditorResult result = editor_service_.removeExpressions( + logic_id_, rung_id, wire_ids); + if (result.succeeded) + { + reloadLogic(); + emit nodeSelected({}); + emit graphChanged(); + } + else + { + reportFailure(result); + } + return result; +} + +LogicEditorResult LogicEditorWidget::deleteVerticalWire() +{ + const std::vector branch_ids = selectedBranchIds(); + if (branch_ids.empty()) + { + const LogicEditorResult result = { + false, + LogicEditorError::InvalidOperation, + "请先选择要删除的竖线连接", + {}}; + reportFailure(result); + return result; + } + const std::string rung_id = selectedRungId(); + LogicEditorResult result; + if (rung_id.empty()) + { + result = {false, LogicEditorError::InvalidOperation, + "竖线连接必须位于同一网络", {}}; + } + else + { + result = editor_service_.removeExpressions( + logic_id_, rung_id, branch_ids); + } + if (result.succeeded) + { + reloadLogic(); + emit nodeSelected({}); emit graphChanged(); } else @@ -1105,9 +1511,38 @@ LogicEditorResult LogicEditorWidget::setOutput( LogicEditorResult LogicEditorWidget::deleteSelected() { + const std::vector expression_ids = selectedExpressionIds(); + const std::vector wire_ids = selectedWireIds(); + const std::vector branch_ids = selectedBranchIds(); const std::vector node_ids = selectedNodeIds(); LogicEditorResult result; - if (!node_ids.empty()) + if (!branch_ids.empty()) + { + result = editor_service_.removeExpressions( + logic_id_, selectedRungId(), branch_ids); + } + else if (!wire_ids.empty()) + { + bool output_selected = false; + for (QGraphicsItem *item : scene_->selectedItems()) + { + if (const NodeItem *node = dynamic_cast(item)) + { + output_selected = output_selected || !node->isConditionNode(); + } + } + if (output_selected) + { + result = {false, LogicEditorError::InvalidOperation, + "不能同时删除横线和输出节点", {}}; + } + else + { + result = editor_service_.removeExpressions( + logic_id_, selectedRungId(), expression_ids); + } + } + else if (!node_ids.empty()) { result = editor_service_.removeNodes(logic_id_, node_ids); } diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index 1ea7505..60774cb 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -18,6 +18,8 @@ class LogicEditorWidget final : public QGraphicsView public: class NodeItem; + class WireItem; + class VerticalConnectorItem; class RungItem; explicit LogicEditorWidget( @@ -39,6 +41,10 @@ public: LogicEditorResult addRung(); LogicEditorResult addCondition(const LogicNodeConfig &config); LogicEditorResult addParallelBranch(const LogicNodeConfig &config); + LogicEditorResult addHorizontalWire(); + LogicEditorResult addVerticalWire(); + LogicEditorResult deleteHorizontalWire(); + LogicEditorResult deleteVerticalWire(); LogicEditorResult setOutput( const LogicNodeConfig &config, bool configured = false); LogicEditorResult deleteSelected(); @@ -55,6 +61,11 @@ private: void handleSelectionChanged(); void reportFailure(const LogicEditorResult &result); std::string currentRungId() const; + void selectExpression(const std::string &expression_id); + void selectWire(const std::string &wire_id); + std::vector selectedExpressionIds() const; + std::vector selectedWireIds() const; + std::vector selectedBranchIds() const; LogicEditorService &editor_service_; QGraphicsScene *scene_ = nullptr; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 4a668b7..30590c8 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -395,6 +395,14 @@ void MainWindow::configureActions() connect(ui_->addRungAction, &QAction::triggered, this, &MainWindow::addLogicRung); + connect(ui_->insertHorizontalWireAction, &QAction::triggered, + this, &MainWindow::addLogicHorizontalWire); + connect(ui_->insertVerticalWireAction, &QAction::triggered, + this, &MainWindow::addLogicVerticalWire); + connect(ui_->deleteHorizontalWireAction, &QAction::triggered, + this, &MainWindow::deleteLogicHorizontalWire); + connect(ui_->deleteVerticalWireAction, &QAction::triggered, + this, &MainWindow::deleteLogicVerticalWire); connect(ui_->parallelInsertAction, &QAction::triggered, this, [this] @@ -727,6 +735,10 @@ void MainWindow::configureAppearance() ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung)); ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch)); + ui_->insertHorizontalWireAction->setIcon(makeUiIcon(UiIcon::HorizontalWire)); + ui_->insertVerticalWireAction->setIcon(makeUiIcon(UiIcon::VerticalWire)); + ui_->deleteHorizontalWireAction->setIcon(makeUiIcon(UiIcon::Delete)); + ui_->deleteVerticalWireAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact)); ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); @@ -1030,6 +1042,10 @@ void MainWindow::updateEditActions() || (logic_active && logic_editor_service_.canRedo()))); ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active)); ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active)); + ui_->insertHorizontalWireAction->setEnabled(editable && logic_active); + ui_->insertVerticalWireAction->setEnabled(editable && logic_active); + ui_->deleteHorizontalWireAction->setEnabled(editable && logic_active); + ui_->deleteVerticalWireAction->setEnabled(editable && logic_active); } void MainWindow::clearEditorHistories() @@ -1194,6 +1210,62 @@ void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config) statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000); } +void MainWindow::addLogicHorizontalWire() +{ + const LogicEditorResult result = logic_editor_widget_->addHorizontalWire(); + if (!result.succeeded) + { + showProjectResult(tr("插入横线"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_.clear(); + showLogicNodeProperties({}); + refreshProjectUi(); + statusBar()->showMessage(tr("横线已插入,可直接用触点替换"), 3000); +} + +void MainWindow::addLogicVerticalWire() +{ + const LogicEditorResult result = logic_editor_widget_->addVerticalWire(); + if (!result.succeeded) + { + showProjectResult(tr("插入竖线"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_.clear(); + showLogicNodeProperties({}); + refreshProjectUi(); + statusBar()->showMessage(tr("已建立横线旁路和竖线连接"), 3000); +} + +void MainWindow::deleteLogicHorizontalWire() +{ + const LogicEditorResult result = logic_editor_widget_->deleteHorizontalWire(); + if (!result.succeeded) + { + showProjectResult(tr("删除横线"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_.clear(); + showLogicNodeProperties({}); + refreshProjectUi(); + statusBar()->showMessage(tr("横线已删除"), 3000); +} + +void MainWindow::deleteLogicVerticalWire() +{ + const LogicEditorResult result = logic_editor_widget_->deleteVerticalWire(); + if (!result.succeeded) + { + showProjectResult(tr("删除竖线"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_.clear(); + showLogicNodeProperties({}); + refreshProjectUi(); + statusBar()->showMessage(tr("竖线及对应并联支路已删除"), 3000); +} + void MainWindow::setLogicOutput(const LogicNodeConfig &config) { const LogicEditorResult result = logic_editor_widget_->setOutput(config); diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 0a457e9..a44c608 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -136,6 +136,10 @@ private: void addLogicCondition(const LogicNodeConfig &config); // 为当前选中的连续逻辑范围建立并联支路 void addLogicParallelBranch(const LogicNodeConfig &config); + void addLogicHorizontalWire(); + void addLogicVerticalWire(); + void deleteLogicHorizontalWire(); + void deleteLogicVerticalWire(); // 设置当前网络右侧的输出线圈 void setLogicOutput(const LogicNodeConfig &config); void configureAndSetLogicOutput(const LogicNodeConfig &config); diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 2e5f7f3..4e92146 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -250,6 +250,8 @@ + + @@ -331,6 +333,8 @@ false + + @@ -1059,6 +1063,50 @@ 在梯形图末尾新增网络 + + + 横线 + + + 在所选条件或横线后插入横线;没有选择时追加到当前网络 + + + F11 + + + + + 竖线 + + + 为所选连续逻辑范围建立横线旁路和竖线连接 + + + F12 + + + + + 删除横线 + + + 删除画布中选中的横线 + + + Shift+F11 + + + + + 删除竖线 + + + 删除画布中选中的竖线及其对应并联支路 + + + Shift+F12 + + 并联支路 diff --git a/app/src/ui/toolbar_icon_factory.cpp b/app/src/ui/toolbar_icon_factory.cpp index 7fb3b06..c19f81e 100644 --- a/app/src/ui/toolbar_icon_factory.cpp +++ b/app/src/ui/toolbar_icon_factory.cpp @@ -359,6 +359,24 @@ QPixmap renderIcon(UiIcon icon, int size) painter.drawLine(QPointF(7, 12), QPointF(17, 12)); break; } + case UiIcon::HorizontalWire: + { + painter.drawLine(QPointF(3, 12), QPointF(21, 12)); + painter.setPen(iconPen(kAccent, 2.0)); + painter.drawLine(QPointF(9, 7), QPointF(15, 7)); + painter.drawLine(QPointF(12, 4), QPointF(12, 10)); + break; + } + case UiIcon::VerticalWire: + { + painter.drawLine(QPointF(5, 7), QPointF(19, 7)); + painter.drawLine(QPointF(5, 17), QPointF(19, 17)); + painter.drawLine(QPointF(12, 7), QPointF(12, 17)); + painter.setPen(iconPen(kAccent, 2.0)); + painter.drawLine(QPointF(17, 9), QPointF(17, 15)); + painter.drawLine(QPointF(14, 12), QPointF(20, 12)); + break; + } case UiIcon::ParallelBranch: { painter.drawLine(QPointF(2, 12), QPointF(6, 12)); diff --git a/app/src/ui/toolbar_icon_factory.h b/app/src/ui/toolbar_icon_factory.h index 04f8fe6..1c1d2b5 100644 --- a/app/src/ui/toolbar_icon_factory.h +++ b/app/src/ui/toolbar_icon_factory.h @@ -28,6 +28,8 @@ enum class UiIcon AlarmSettings, Delete, AddRung, + HorizontalWire, + VerticalWire, ParallelBranch, NormallyOpenContact, NormallyClosedContact, diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index c3f410d..a6f5fca 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -603,6 +603,26 @@ void testLadderLogicBoundaries() logic.rungs.push_back(rung); require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid"); + ConditionExpression wire = ConditionExpression::fromWire("wire-1", 2); + require(wire.validate() && wire.validateForRunning() + && wire.wire->columnSpan == 2, + "a configured horizontal wire must be a valid runnable expression leaf"); + ConditionExpression invalid_wire = ConditionExpression::fromWire("wire-invalid", 0); + require(!invalid_wire.validate(), "a zero-column horizontal wire must be rejected"); + invalid_wire = ConditionExpression::fromWire( + "wire-too-wide", WireSegment::kMaximumColumnSpan + 1); + require(!invalid_wire.validate(), "an oversized horizontal wire must be rejected"); + + ConditionExpression wired_series; + wired_series.id = "wired-series"; + wired_series.kind = ConditionExpressionKind::Series; + wired_series.children = { + ConditionExpression::fromNode(stop), + ConditionExpression::fromWire("wire-series"), + start_parallel}; + require(wired_series.validateForRunning(), + "a wire must preserve a valid structured series expression"); + logic.rungs.front().condition->children.front() = ConditionExpression::fromNode(coil); require(!logic.validate(), "a ladder condition expression must reject coils"); diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index a11823b..a2d71b8 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -132,6 +132,66 @@ void testRangeParallelInsertion() "a non-contiguous selection must be rejected"); } +void testStructuredWireEditing() +{ + 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)); + service.appendCondition(logic_id, rung_id, contact(2)); + + const LogicEditorResult branch = service.addParallelWireBranch( + logic_id, rung_id, {"contact-2", "contact-3"}); + require(branch.succeeded && branch.id == "wire-1", + "a continuous range must accept a structured horizontal bypass"); + const LadderRung *rung = service.findRung(logic_id, rung_id); + require(rung->condition->kind == ConditionExpressionKind::Series + && rung->condition->children.at(1).kind + == ConditionExpressionKind::Parallel, + "a vertical connection must produce a parallel expression"); + const ConditionExpression &wire = + rung->condition->children.at(1).children.at(1); + require(wire.kind == ConditionExpressionKind::Wire + && wire.wire->columnSpan == 2, + "the bypass wire span must match the selected two-column range"); + + const LogicEditorResult replacement = service.replaceWireWithCondition( + logic_id, rung_id, branch.id, contact(3)); + require(replacement.succeeded && replacement.id == "contact-4", + "a selected wire must be replaceable by a configured node type"); + const LogicEditorResult extension = service.insertWireAfter( + logic_id, rung_id, replacement.id); + require(extension.succeeded && extension.id == "wire-1", + "a wire id must become reusable after its expression is replaced"); + + rung = service.findRung(logic_id, rung_id); + const ConditionExpression *extended_branch = service.findExpression( + logic_id, rung_id, rung->condition->children.at(1).children.at(1).id); + require(extended_branch != nullptr + && extended_branch->kind == ConditionExpressionKind::Series + && extended_branch->children.at(1).kind + == ConditionExpressionKind::Wire, + "inserting a horizontal wire after a branch node must preserve structure"); + const std::string extended_branch_id = extended_branch->id; + require(!service.addParallelWireBranch( + logic_id, rung_id, {"contact-1", "contact-3"}).succeeded, + "non-contiguous wire connection targets must be rejected"); + + require(service.removeExpressions( + logic_id, rung_id, {extended_branch_id}).succeeded, + "deleting a selected vertical connection branch must succeed atomically"); + rung = service.findRung(logic_id, rung_id); + require(rung->condition->kind == ConditionExpressionKind::Series + && rung->condition->children.size() == 3U, + "removing a bypass branch must normalize back to the original series"); + require(service.undo().succeeded + && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr, + "wire branch deletion must participate in ladder undo history"); +} + void testLogicLifecycleAndOrdering() { TestProjectStorage storage; @@ -305,6 +365,7 @@ int main() { testStructuredEditingAndNormalization(); testRangeParallelInsertion(); + testStructuredWireEditing(); testLogicLifecycleAndOrdering(); testEdgeTimerNodesAndRungComments(); testHistoryAndAtomicBatchDelete(); diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 23c3014..6ef6201 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -670,6 +670,14 @@ void testModeActionsControlEditingAvailability() window, "addNormalCoilAction"); QAction *parallel_insert_action = requiredChild( window, "parallelInsertAction"); + QAction *insert_horizontal_wire_action = requiredChild( + window, "insertHorizontalWireAction"); + QAction *insert_vertical_wire_action = requiredChild( + window, "insertVerticalWireAction"); + QAction *delete_horizontal_wire_action = requiredChild( + window, "deleteHorizontalWireAction"); + QAction *delete_vertical_wire_action = requiredChild( + window, "deleteVerticalWireAction"); QDockWidget *project_dock = requiredChild(window, "projectDock"); QDockWidget *properties_dock = requiredChild(window, "propertiesDock"); QLabel *selection = requiredChild(window, "selectionValueLabel"); @@ -725,6 +733,10 @@ void testModeActionsControlEditingAvailability() "configureAlarmsAction", "deleteControlAction", "addRungAction", + "insertHorizontalWireAction", + "insertVerticalWireAction", + "deleteHorizontalWireAction", + "deleteVerticalWireAction", "parallelInsertAction", "addNormallyOpenAction", "addNormallyClosedAction", @@ -799,7 +811,15 @@ void testModeActionsControlEditingAvailability() require(editing_action->isChecked(), "editing action must be selected initially"); require(undo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Z) && redo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Y) - && delete_selection_action->shortcut() == QKeySequence(Qt::Key_Delete), + && delete_selection_action->shortcut() == QKeySequence(Qt::Key_Delete) + && insert_horizontal_wire_action->shortcut() + == QKeySequence(Qt::Key_F11) + && insert_vertical_wire_action->shortcut() + == QKeySequence(Qt::Key_F12) + && delete_horizontal_wire_action->shortcut() + == QKeySequence(Qt::SHIFT | Qt::Key_F11) + && delete_vertical_wire_action->shortcut() + == QKeySequence(Qt::SHIFT | Qt::Key_F12), "core editor actions must expose the agreed keyboard shortcuts"); require(project_dock->isEnabled(), "project dock must be enabled while editing"); require(properties_dock->isEnabled(), "properties dock must be enabled while editing"); @@ -941,7 +961,54 @@ void testModeActionsControlEditingAvailability() collectConditionNodes(*restored_rung.condition, &restored_nodes); require(restored_nodes.size() == 1U, "logic redo must restore the original condition node"); - logic_editor->selectNode(restored_nodes.front()->id); + const std::string restored_node_id = restored_nodes.front()->id; + logic_editor->selectNode(restored_node_id); + + insert_horizontal_wire_action->trigger(); + const LadderRung &wired_rung = logic_editor_service.findLogic( + logic_editor_service.firstLogicId())->rungs.front(); + require(wired_rung.condition->kind == ConditionExpressionKind::Series + && wired_rung.condition->children.at(1).kind + == ConditionExpressionKind::Wire, + "F11 must insert a selectable horizontal wire after the current condition"); + delete_horizontal_wire_action->trigger(); + require(logic_editor_service.findLogic(logic_editor_service.firstLogicId()) + ->rungs.front().condition->kind == ConditionExpressionKind::Node, + "Shift+F11 must delete the selected horizontal wire"); + logic_editor->selectNode(restored_node_id); + insert_horizontal_wire_action->trigger(); + add_normally_open_action->trigger(); + const LadderRung &replaced_wire_rung = logic_editor_service.findLogic( + logic_editor_service.firstLogicId())->rungs.front(); + require(replaced_wire_rung.condition->kind == ConditionExpressionKind::Series + && replaced_wire_rung.condition->children.at(1).kind + == ConditionExpressionKind::Node, + "adding a contact on a selected wire must replace the wire in place"); + undo_action->trigger(); + undo_action->trigger(); + + logic_editor->selectNode(restored_node_id); + insert_vertical_wire_action->trigger(); + const LadderRung &vertical_rung = logic_editor_service.findLogic( + logic_editor_service.firstLogicId())->rungs.front(); + require(vertical_rung.condition->kind == ConditionExpressionKind::Parallel + && vertical_rung.condition->children.at(1).kind + == ConditionExpressionKind::Wire, + "F12 must create a structured wire bypass with vertical connectors"); + logic_editor->scene()->clearSelection(); + for (QGraphicsItem *item : logic_editor->scene()->items()) + { + if (qFuzzyCompare(item->zValue(), 2.5)) + { + item->setSelected(true); + } + } + delete_vertical_wire_action->trigger(); + require(logic_editor_service.findLogic(logic_editor_service.firstLogicId()) + ->rungs.front().condition->kind == ConditionExpressionKind::Node, + "Shift+F12 must remove the selected vertical connection branch"); + + logic_editor->selectNode(restored_node_id); parallel_insert_action->trigger(); add_normal_coil_action->trigger(); const ControlLogic *logic = logic_editor_service.findLogic( diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 01177e3..4b51014 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -257,6 +257,44 @@ void testNestedSeriesParallelExpression() require(readBit(repository, 10), "A branch must independently energize output"); } +void testWirePassThroughAndPowerTrace() +{ + VirtualRegisterRepository repository; + SoftwareLogicExecutor executor; + ConditionExpression root; + root.id = "wire-series"; + root.kind = ConditionExpressionKind::Series; + root.children = { + ConditionExpression::fromNode(contact("wire-input", 0)), + ConditionExpression::fromWire("wire-segment", 2), + ConditionExpression::fromNode(contact("wire-output", 1))}; + LadderRung wired_rung; + wired_rung.id = "wired-rung"; + wired_rung.name = "wired-rung"; + wired_rung.condition = root; + wired_rung.output = coil("wired-coil", 10); + const ControlLogic program = logic({wired_rung}); + + writeBit(repository, 1, true); + LogicTraceSnapshot trace; + require(executor.executeScan({program}, repository, &trace).succeeded, + "wire expression scan must succeed"); + require(!readBit(repository, 10), + "a horizontal wire must not bypass a false upstream series contact"); + require(trace.expressionValues.at("wire-segment") + && !trace.expressionInputValues.at("wire-segment") + && !trace.expressionPowerValues.at("wire-segment"), + "a wire must remain logically true without showing false upstream power"); + + writeBit(repository, 0, true); + require(executor.executeScan({program}, repository, &trace).succeeded, + "powered wire expression scan must succeed"); + require(readBit(repository, 10) + && trace.expressionInputValues.at("wire-segment") + && trace.expressionPowerValues.at("wire-segment"), + "a powered horizontal wire must pass current to the downstream contact"); +} + void testSeriesParallelContactsAndSequentialVisibility() { VirtualRegisterRepository repository; @@ -862,6 +900,7 @@ int main(int argc, char *argv[]) { testSeriesParallelContactsAndSequentialVisibility(); testNestedSeriesParallelExpression(); + testWirePassThroughAndPowerTrace(); testAllComparisons(); testSetResetAndDisabledLogic(); testMultipleLogicScanOrderAndTraceIsolation(); diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index eda5681..ec9b4ec 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -148,6 +148,7 @@ Project makeExampleProject() series.kind = ConditionExpressionKind::Series; series.children = { std::move(parallel), + ConditionExpression::fromWire("start-wire", 2), ConditionExpression::fromNode(compare)}; rung.condition = std::move(series); rung.output = coil; @@ -496,6 +497,9 @@ void testExampleProjectRoundTrip() == ConditionExpressionKind::Parallel && rung.condition->children.front().children.size() == 2U, "parallel expression branches must survive round trip"); + require(rung.condition->children.at(1).kind == ConditionExpressionKind::Wire + && rung.condition->children.at(1).wire->columnSpan == 2, + "horizontal wire span must survive JSON round trip"); require(rung.output.has_value(), "ladder output must survive round trip"); @@ -553,7 +557,7 @@ void testExampleProjectRoundTrip() "ADD and SUB operations must survive round trip"); const auto &compare = std::get( - rung.condition->children.at(1).node->config); + rung.condition->children.at(2).node->config); require(compare.address.index() == 2 && compare.value == 100, "comparison configuration must survive round trip"); require(service.saveAs(second_path.toStdString()).succeeded,