| @@ -229,7 +229,9 @@ bool validateUniqueExpressionIds( | |||||
| void normalizeExpression(ConditionExpression *expression) | void normalizeExpression(ConditionExpression *expression) | ||||
| { | { | ||||
| if (expression == nullptr || expression->kind == ConditionExpressionKind::Node) | |||||
| if (expression == nullptr | |||||
| || expression->kind == ConditionExpressionKind::Node | |||||
| || expression->kind == ConditionExpressionKind::Wire) | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -479,6 +481,26 @@ ConditionExpression ConditionExpression::fromNode(LogicNode logic_node) | |||||
| return expression; | 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 | bool ConditionExpression::validate(std::string *error) const | ||||
| { | { | ||||
| if (id.empty()) | if (id.empty()) | ||||
| @@ -488,14 +510,30 @@ bool ConditionExpression::validate(std::string *error) const | |||||
| } | } | ||||
| if (kind == ConditionExpressionKind::Node) | 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, "条件叶节点必须包含一个条件节点且不能包含子表达式"); | setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| return node->validate(error); | 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, "串联和并联表达式至少需要两个子表达式"); | setError(error, "串联和并联表达式至少需要两个子表达式"); | ||||
| return false; | return false; | ||||
| @@ -530,6 +568,10 @@ bool ConditionExpression::validateForRunning(std::string *error) const | |||||
| } | } | ||||
| return true; | return true; | ||||
| } | } | ||||
| if (kind == ConditionExpressionKind::Wire) | |||||
| { | |||||
| return true; | |||||
| } | |||||
| for (const ConditionExpression &child : children) | for (const ConditionExpression &child : children) | ||||
| { | { | ||||
| if (!child.validateForRunning(error)) | if (!child.validateForRunning(error)) | ||||
| @@ -548,6 +590,7 @@ void normalizeConditionExpression(std::optional<ConditionExpression> *expression | |||||
| } | } | ||||
| normalizeExpression(&expression->value()); | normalizeExpression(&expression->value()); | ||||
| if (expression->value().kind != ConditionExpressionKind::Node | if (expression->value().kind != ConditionExpressionKind::Node | ||||
| && expression->value().kind != ConditionExpressionKind::Wire | |||||
| && expression->value().children.empty()) | && expression->value().children.empty()) | ||||
| { | { | ||||
| expression->reset(); | expression->reset(); | ||||
| @@ -557,6 +600,10 @@ void normalizeConditionExpression(std::optional<ConditionExpression> *expression | |||||
| const LogicNode *findConditionNode( | const LogicNode *findConditionNode( | ||||
| const ConditionExpression &expression, const std::string &node_id) | const ConditionExpression &expression, const std::string &node_id) | ||||
| { | { | ||||
| if (expression.kind == ConditionExpressionKind::Wire) | |||||
| { | |||||
| return nullptr; | |||||
| } | |||||
| if (expression.kind == ConditionExpressionKind::Node) | if (expression.kind == ConditionExpressionKind::Node) | ||||
| { | { | ||||
| return expression.node->id == node_id ? &*expression.node : nullptr; | return expression.node->id == node_id ? &*expression.node : nullptr; | ||||
| @@ -615,6 +662,10 @@ void collectConditionNodes( | |||||
| nodes->push_back(&*expression.node); | nodes->push_back(&*expression.node); | ||||
| return; | return; | ||||
| } | } | ||||
| if (expression.kind == ConditionExpressionKind::Wire) | |||||
| { | |||||
| return; | |||||
| } | |||||
| for (const ConditionExpression &child : expression.children) | for (const ConditionExpression &child : expression.children) | ||||
| { | { | ||||
| collectConditionNodes(child, nodes); | collectConditionNodes(child, nodes); | ||||
| @@ -204,19 +204,32 @@ struct LogicNode | |||||
| enum class ConditionExpressionKind | enum class ConditionExpressionKind | ||||
| { | { | ||||
| Node, | Node, | ||||
| Wire, | |||||
| Series, | Series, | ||||
| Parallel | 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 | struct ConditionExpression | ||||
| { | { | ||||
| std::string id; | std::string id; | ||||
| ConditionExpressionKind kind = ConditionExpressionKind::Node; | ConditionExpressionKind kind = ConditionExpressionKind::Node; | ||||
| std::optional<LogicNode> node; | std::optional<LogicNode> node; | ||||
| std::optional<WireSegment> wire; | |||||
| std::vector<ConditionExpression> children; | std::vector<ConditionExpression> children; | ||||
| static ConditionExpression fromNode(LogicNode node); | static ConditionExpression fromNode(LogicNode node); | ||||
| static ConditionExpression fromWire(std::string id, int column_span = 1); | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -1468,6 +1468,8 @@ QString expressionKindText(ConditionExpressionKind kind) | |||||
| { | { | ||||
| case ConditionExpressionKind::Node: | case ConditionExpressionKind::Node: | ||||
| return QStringLiteral("node"); | return QStringLiteral("node"); | ||||
| case ConditionExpressionKind::Wire: | |||||
| return QStringLiteral("wire"); | |||||
| case ConditionExpressionKind::Series: | case ConditionExpressionKind::Series: | ||||
| return QStringLiteral("series"); | return QStringLiteral("series"); | ||||
| case ConditionExpressionKind::Parallel: | case ConditionExpressionKind::Parallel: | ||||
| @@ -1485,6 +1487,10 @@ QJsonObject serializeConditionExpression(const ConditionExpression &expression) | |||||
| { | { | ||||
| object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node)); | object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node)); | ||||
| } | } | ||||
| else if (expression.kind == ConditionExpressionKind::Wire) | |||||
| { | |||||
| object.insert(QStringLiteral("columnSpan"), expression.wire->columnSpan); | |||||
| } | |||||
| else | else | ||||
| { | { | ||||
| QJsonArray children; | QJsonArray children; | ||||
| @@ -1525,11 +1531,29 @@ bool parseConditionExpression( | |||||
| expression->node = std::move(parsed_node); | expression->node = std::move(parsed_node); | ||||
| return true; | 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") | if (kind != "series" && kind != "parallel") | ||||
| { | { | ||||
| return state->fail( | return state->fail( | ||||
| ProjectStorageError::InvalidField, | ProjectStorageError::InvalidField, | ||||
| context + ".kind 必须是 node、series 或 parallel"); | |||||
| context + ".kind 必须是 node、wire、series 或 parallel"); | |||||
| } | } | ||||
| QJsonArray children; | QJsonArray children; | ||||
| if (!readArray(object, "children", context, &children, state)) | if (!readArray(object, "children", context, &children, state)) | ||||
| @@ -75,18 +75,43 @@ ConditionExpression *findParentExpression( | |||||
| using NodeIdSet = std::unordered_set<std::string>; | using NodeIdSet = std::unordered_set<std::string>; | ||||
| NodeIdSet conditionNodeIds(const ConditionExpression &expression) | |||||
| NodeIdSet conditionLeafIds(const ConditionExpression &expression) | |||||
| { | { | ||||
| std::vector<const LogicNode *> nodes; | |||||
| collectConditionNodes(expression, &nodes); | |||||
| NodeIdSet ids; | 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; | 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) | bool isSubset(const NodeIdSet &subset, const NodeIdSet &values) | ||||
| { | { | ||||
| return std::all_of( | return std::all_of( | ||||
| @@ -124,7 +149,7 @@ bool addParallelForSelection( | |||||
| const std::string ¶llel_id, | const std::string ¶llel_id, | ||||
| const std::string &series_id) | const std::string &series_id) | ||||
| { | { | ||||
| const NodeIdSet expression_ids = conditionNodeIds(*expression); | |||||
| const NodeIdSet expression_ids = conditionLeafIds(*expression); | |||||
| if (sameValues(expression_ids, selected_ids)) | if (sameValues(expression_ids, selected_ids)) | ||||
| { | { | ||||
| addParallelSibling(expression, std::move(*branch), parallel_id); | addParallelSibling(expression, std::move(*branch), parallel_id); | ||||
| @@ -136,7 +161,7 @@ bool addParallelForSelection( | |||||
| std::vector<std::size_t> matching_children; | std::vector<std::size_t> matching_children; | ||||
| for (std::size_t index = 0; index < expression->children.size(); ++index) | 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( | const bool intersects = std::any_of( | ||||
| child_ids.back().cbegin(), child_ids.back().cend(), | child_ids.back().cbegin(), child_ids.back().cend(), | ||||
| [&selected_ids](const std::string &id) | [&selected_ids](const std::string &id) | ||||
| @@ -212,7 +237,9 @@ bool addParallelForSelection( | |||||
| bool removeExpressionRecursive( | bool removeExpressionRecursive( | ||||
| ConditionExpression *expression, const std::string &expression_id) | 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; | return false; | ||||
| } | } | ||||
| @@ -238,6 +265,65 @@ bool removeExpressionRecursive( | |||||
| return false; | return false; | ||||
| } | } | ||||
| std::optional<int> 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<NodeIdSet> child_ids; | |||||
| std::vector<std::size_t> 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<int>{columns} : std::nullopt; | |||||
| } | |||||
| LadderRung *findEditableRung( | LadderRung *findEditableRung( | ||||
| Project &project, | Project &project, | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| @@ -329,6 +415,7 @@ bool LogicEditorService::expressionsEqual( | |||||
| { | { | ||||
| if (left.id != right.id || left.kind != right.kind | if (left.id != right.id || left.kind != right.kind | ||||
| || left.node.has_value() != right.node.has_value() | || left.node.has_value() != right.node.has_value() | ||||
| || left.wire.has_value() != right.wire.has_value() | |||||
| || left.children.size() != right.children.size()) | || left.children.size() != right.children.size()) | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -337,6 +424,11 @@ bool LogicEditorService::expressionsEqual( | |||||
| { | { | ||||
| return false; | 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) | for (std::size_t index = 0; index < left.children.size(); ++index) | ||||
| { | { | ||||
| if (!expressionsEqual(left.children[index], right.children[index])) | if (!expressionsEqual(left.children[index], right.children[index])) | ||||
| @@ -809,6 +901,50 @@ LogicEditorResult LogicEditorService::appendCondition( | |||||
| return {true, LogicEditorError::None, {}, node_id}; | 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( | LogicEditorResult LogicEditorService::insertConditionAfter( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -858,6 +994,91 @@ LogicEditorResult LogicEditorService::insertConditionAfter( | |||||
| return {true, LogicEditorError::None, {}, node_id}; | 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( | LogicEditorResult LogicEditorService::addParallelBranch( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -885,7 +1106,7 @@ LogicEditorResult LogicEditorService::addParallelBranch( | |||||
| LogicEditorError::InvalidOperation, | LogicEditorError::InvalidOperation, | ||||
| "并联选择中存在重复节点"); | "并联选择中存在重复节点"); | ||||
| } | } | ||||
| const NodeIdSet available_ids = conditionNodeIds(*existing_rung->condition); | |||||
| const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition); | |||||
| if (!isSubset(selected_ids, available_ids)) | if (!isSubset(selected_ids, available_ids)) | ||||
| { | { | ||||
| return failure( | return failure( | ||||
| @@ -918,6 +1139,72 @@ LogicEditorResult LogicEditorService::addParallelBranch( | |||||
| return {true, LogicEditorError::None, {}, node_id}; | return {true, LogicEditorError::None, {}, node_id}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::addParallelWireBranch( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| const std::vector<std::string> &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<int> 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( | LogicEditorResult LogicEditorService::setOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -1100,6 +1387,58 @@ LogicEditorResult LogicEditorService::removeExpression( | |||||
| return {true, LogicEditorError::None, {}, expression_id}; | return {true, LogicEditorError::None, {}, expression_id}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::removeExpressions( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| const std::vector<std::string> &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 | bool LogicEditorService::canUndo() const | ||||
| { | { | ||||
| return history_.canUndo(); | 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) | std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic) | ||||
| { | { | ||||
| for (std::size_t index = 1;; ++index) | for (std::size_t index = 1;; ++index) | ||||
| @@ -68,16 +68,34 @@ public: | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| LogicEditorResult appendWire( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| int column_span = 1); | |||||
| LogicEditorResult insertConditionAfter( | LogicEditorResult insertConditionAfter( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &target_node_id, | const std::string &target_node_id, | ||||
| const LogicNodeConfig &config); | 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( | LogicEditorResult addParallelBranch( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &selected_node_ids, | const std::vector<std::string> &selected_node_ids, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| LogicEditorResult addParallelWireBranch( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| const std::vector<std::string> &selected_expression_ids); | |||||
| LogicEditorResult setOutput( | LogicEditorResult setOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -96,6 +114,10 @@ public: | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &expression_id); | const std::string &expression_id); | ||||
| LogicEditorResult removeExpressions( | |||||
| const std::string &logic_id, | |||||
| const std::string &rung_id, | |||||
| const std::vector<std::string> &expression_ids); | |||||
| bool canUndo() const; | bool canUndo() const; | ||||
| bool canRedo() const; | bool canRedo() const; | ||||
| @@ -129,6 +151,7 @@ private: | |||||
| static std::string nodePrefix(const LogicNodeConfig &config); | static std::string nodePrefix(const LogicNodeConfig &config); | ||||
| static std::string makeUniqueNodeId( | static std::string makeUniqueNodeId( | ||||
| const ControlLogic &logic, const std::string &prefix); | const ControlLogic &logic, const std::string &prefix); | ||||
| static std::string makeUniqueWireId(const ControlLogic &logic); | |||||
| static std::string makeUniqueExpressionId(const ControlLogic &logic); | static std::string makeUniqueExpressionId(const ControlLogic &logic); | ||||
| static std::string makeUniqueRungId(const ControlLogic &logic); | static std::string makeUniqueRungId(const ControlLogic &logic); | ||||
| static LogicEditorResult failure( | static LogicEditorResult failure( | ||||
| @@ -74,7 +74,10 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current) | |||||
| void LogicTraceValues::clear() | void LogicTraceValues::clear() | ||||
| { | { | ||||
| nodeValues.clear(); | nodeValues.clear(); | ||||
| nodePowerValues.clear(); | |||||
| expressionValues.clear(); | expressionValues.clear(); | ||||
| expressionInputValues.clear(); | |||||
| expressionPowerValues.clear(); | |||||
| rungValues.clear(); | rungValues.clear(); | ||||
| tonValues.clear(); | tonValues.clear(); | ||||
| counterValues.clear(); | counterValues.clear(); | ||||
| @@ -95,7 +98,10 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| if (values != logicValues.cend()) | if (values != logicValues.cend()) | ||||
| { | { | ||||
| projection.nodeValues = values->second.nodeValues; | projection.nodeValues = values->second.nodeValues; | ||||
| projection.nodePowerValues = values->second.nodePowerValues; | |||||
| projection.expressionValues = values->second.expressionValues; | projection.expressionValues = values->second.expressionValues; | ||||
| projection.expressionInputValues = values->second.expressionInputValues; | |||||
| projection.expressionPowerValues = values->second.expressionPowerValues; | |||||
| projection.rungValues = values->second.rungValues; | projection.rungValues = values->second.rungValues; | ||||
| projection.tonValues = values->second.tonValues; | projection.tonValues = values->second.tonValues; | ||||
| projection.counterValues = values->second.counterValues; | projection.counterValues = values->second.counterValues; | ||||
| @@ -224,7 +230,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| bool rung_value = false; | bool rung_value = false; | ||||
| LogicScanResult result = evaluateExpression( | 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) | if (!result.succeeded) | ||||
| { | { | ||||
| result.logicId = logic.id; | result.logicId = logic.id; | ||||
| @@ -235,6 +246,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| { | { | ||||
| logic_trace->rungValues[rung.id] = rung_value; | logic_trace->rungValues[rung.id] = rung_value; | ||||
| logic_trace->nodeValues[rung.output->id] = rung_value; | logic_trace->nodeValues[rung.output->id] = rung_value; | ||||
| logic_trace->nodePowerValues[rung.output->id] = rung_value; | |||||
| } | } | ||||
| bool output_value = false; | bool output_value = false; | ||||
| result = executeOutput( | result = executeOutput( | ||||
| @@ -254,6 +266,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| if (logic_trace != nullptr) | if (logic_trace != nullptr) | ||||
| { | { | ||||
| logic_trace->nodeValues[rung.output->id] = output_value; | 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()) | if (values != trace->logicValues.cend()) | ||||
| { | { | ||||
| trace->nodeValues = values->second.nodeValues; | trace->nodeValues = values->second.nodeValues; | ||||
| trace->nodePowerValues = values->second.nodePowerValues; | |||||
| trace->expressionValues = values->second.expressionValues; | trace->expressionValues = values->second.expressionValues; | ||||
| trace->expressionInputValues = values->second.expressionInputValues; | |||||
| trace->expressionPowerValues = values->second.expressionPowerValues; | |||||
| trace->rungValues = values->second.rungValues; | trace->rungValues = values->second.rungValues; | ||||
| trace->tonValues = values->second.tonValues; | trace->tonValues = values->second.tonValues; | ||||
| trace->counterValues = values->second.counterValues; | trace->counterValues = values->second.counterValues; | ||||
| @@ -284,12 +300,27 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool input_power, | |||||
| bool *value) | bool *value) | ||||
| { | { | ||||
| if (value == nullptr) | if (value == nullptr) | ||||
| { | { | ||||
| return failure(LogicScanError::InvalidLogic, "缺少表达式结果接收对象"); | 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) | if (expression.kind == ConditionExpressionKind::Node) | ||||
| { | { | ||||
| LogicScanResult result = evaluateCondition( | LogicScanResult result = evaluateCondition( | ||||
| @@ -297,28 +328,43 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||||
| if (result.succeeded && trace != nullptr) | if (result.succeeded && trace != nullptr) | ||||
| { | { | ||||
| trace->nodeValues[expression.node->id] = *value; | trace->nodeValues[expression.node->id] = *value; | ||||
| trace->nodePowerValues[expression.node->id] = input_power && *value; | |||||
| trace->expressionValues[expression.id] = *value; | trace->expressionValues[expression.id] = *value; | ||||
| trace->expressionPowerValues[expression.id] = input_power && *value; | |||||
| } | } | ||||
| return result; | return result; | ||||
| } | } | ||||
| bool accumulated = expression.kind == ConditionExpressionKind::Series; | bool accumulated = expression.kind == ConditionExpressionKind::Series; | ||||
| bool power = input_power; | |||||
| for (const ConditionExpression &child : expression.children) | for (const ConditionExpression &child : expression.children) | ||||
| { | { | ||||
| bool child_value = false; | bool child_value = false; | ||||
| const bool child_input = expression.kind == ConditionExpressionKind::Series | |||||
| ? power : input_power; | |||||
| LogicScanResult result = evaluateExpression( | LogicScanResult result = evaluateExpression( | ||||
| logic_id, child, repository, trace, &child_value); | |||||
| logic_id, | |||||
| child, | |||||
| repository, | |||||
| trace, | |||||
| child_input, | |||||
| &child_value); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| return result; | return result; | ||||
| } | } | ||||
| accumulated = expression.kind == ConditionExpressionKind::Series | accumulated = expression.kind == ConditionExpressionKind::Series | ||||
| ? accumulated && child_value : accumulated || child_value; | ? accumulated && child_value : accumulated || child_value; | ||||
| if (expression.kind == ConditionExpressionKind::Series) | |||||
| { | |||||
| power = power && child_value; | |||||
| } | |||||
| } | } | ||||
| *value = accumulated; | *value = accumulated; | ||||
| if (trace != nullptr) | if (trace != nullptr) | ||||
| { | { | ||||
| trace->expressionValues[expression.id] = accumulated; | trace->expressionValues[expression.id] = accumulated; | ||||
| trace->expressionPowerValues[expression.id] = input_power && accumulated; | |||||
| } | } | ||||
| return success(); | return success(); | ||||
| } | } | ||||
| @@ -56,7 +56,10 @@ struct WordTraceValue | |||||
| struct LogicTraceValues | struct LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, bool> nodeValues; | std::unordered_map<std::string, bool> nodeValues; | ||||
| std::unordered_map<std::string, bool> nodePowerValues; | |||||
| std::unordered_map<std::string, bool> expressionValues; | std::unordered_map<std::string, bool> expressionValues; | ||||
| std::unordered_map<std::string, bool> expressionInputValues; | |||||
| std::unordered_map<std::string, bool> expressionPowerValues; | |||||
| std::unordered_map<std::string, bool> rungValues; | std::unordered_map<std::string, bool> rungValues; | ||||
| std::unordered_map<std::string, TonTraceValue> tonValues; | std::unordered_map<std::string, TonTraceValue> tonValues; | ||||
| std::unordered_map<std::string, CounterTraceValue> counterValues; | std::unordered_map<std::string, CounterTraceValue> counterValues; | ||||
| @@ -111,6 +114,7 @@ private: | |||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool input_power, | |||||
| bool *value); | bool *value); | ||||
| LogicScanResult executeOutput( | LogicScanResult executeOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| @@ -182,6 +182,10 @@ ExpressionMetrics measureExpression(const ConditionExpression &expression) | |||||
| { | { | ||||
| return {}; | return {}; | ||||
| } | } | ||||
| if (expression.kind == ConditionExpressionKind::Wire) | |||||
| { | |||||
| return {expression.wire->columnSpan, 1}; | |||||
| } | |||||
| ExpressionMetrics metrics{0, 0}; | ExpressionMetrics metrics{0, 0}; | ||||
| if (expression.kind == ConditionExpressionKind::Series) | if (expression.kind == ConditionExpressionKind::Series) | ||||
| @@ -273,14 +277,16 @@ public: | |||||
| const QPointF ¢er, | const QPointF ¢er, | ||||
| const QString ®ister_comment, | const QString ®ister_comment, | ||||
| bool active, | bool active, | ||||
| bool faulted) | |||||
| bool faulted, | |||||
| bool condition) | |||||
| : node_id_(node.id), | : node_id_(node.id), | ||||
| rung_id_(rung_id), | rung_id_(rung_id), | ||||
| config_(node.config), | config_(node.config), | ||||
| register_comment_(register_comment), | register_comment_(register_comment), | ||||
| configured_(node.configured), | configured_(node.configured), | ||||
| active_(active), | active_(active), | ||||
| faulted_(faulted) | |||||
| faulted_(faulted), | |||||
| condition_(condition) | |||||
| { | { | ||||
| setPos(center); | setPos(center); | ||||
| setFlag(ItemIsSelectable, true); | setFlag(ItemIsSelectable, true); | ||||
| @@ -518,6 +524,7 @@ public: | |||||
| const std::string &nodeId() const { return node_id_; } | const std::string &nodeId() const { return node_id_; } | ||||
| const std::string &rungId() const { return rung_id_; } | const std::string &rungId() const { return rung_id_; } | ||||
| bool isConditionNode() const { return condition_; } | |||||
| private: | private: | ||||
| std::string node_id_; | std::string node_id_; | ||||
| @@ -527,6 +534,110 @@ private: | |||||
| bool configured_ = true; | bool configured_ = true; | ||||
| bool active_ = false; | bool active_ = false; | ||||
| bool faulted_ = 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<qreal>(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 | class LogicEditorWidget::RungItem final : public QGraphicsItem | ||||
| @@ -627,15 +738,31 @@ RenderResult renderExpression( | |||||
| bool trace_enabled, | bool trace_enabled, | ||||
| const std::string &fault_node_id) | 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<qreal>(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) | if (expression.kind == ConditionExpressionKind::Node) | ||||
| { | { | ||||
| const QPointF center( | const QPointF center( | ||||
| top_left.x() + kCellWidth / 2.0, | top_left.x() + kCellWidth / 2.0, | ||||
| top_left.y() + kCellHeight / 2.0); | top_left.y() + kCellHeight / 2.0); | ||||
| const bool node_active = trace_enabled && traceValue( | const bool node_active = trace_enabled && traceValue( | ||||
| trace, &LogicTraceSnapshot::nodeValues, expression.node->id); | |||||
| trace, &LogicTraceSnapshot::nodePowerValues, expression.node->id); | |||||
| scene.addLine( | scene.addLine( | ||||
| QLineF( | QLineF( | ||||
| QPointF(top_left.x(), center.y()), | QPointF(top_left.x(), center.y()), | ||||
| @@ -655,7 +782,8 @@ RenderResult renderExpression( | |||||
| : QString{}; | : QString{}; | ||||
| }(), | }(), | ||||
| node_active, | node_active, | ||||
| expression.node->id == fault_node_id)); | |||||
| expression.node->id == fault_node_id, | |||||
| true)); | |||||
| return { | return { | ||||
| QPointF(top_left.x(), center.y()), | QPointF(top_left.x(), center.y()), | ||||
| QPointF(top_left.x() + kCellWidth, center.y())}; | QPointF(top_left.x() + kCellWidth, center.y())}; | ||||
| @@ -685,7 +813,13 @@ RenderResult renderExpression( | |||||
| } | } | ||||
| else | 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; | previous = current; | ||||
| x += static_cast<qreal>(child_metrics.columns) * kCellWidth; | x += static_cast<qreal>(child_metrics.columns) * kCellWidth; | ||||
| @@ -715,14 +849,36 @@ RenderResult renderExpression( | |||||
| const qreal left_join = top_left.x(); | const qreal left_join = top_left.x(); | ||||
| const qreal right_join = top_left.x() + static_cast<qreal>(metrics.columns) * kCellWidth; | const qreal right_join = top_left.x() + static_cast<qreal>(metrics.columns) * kCellWidth; | ||||
| const qreal top_y = branches.front().input.y(); | 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) | for (std::size_t index = 0; index < branches.size(); ++index) | ||||
| { | { | ||||
| const bool branch_active = trace_enabled && traceValue( | const bool branch_active = trace_enabled && traceValue( | ||||
| trace, | trace, | ||||
| &LogicTraceSnapshot::expressionValues, | |||||
| &LogicTraceSnapshot::expressionPowerValues, | |||||
| expression.children[index].id); | expression.children[index].id); | ||||
| scene.addLine( | scene.addLine( | ||||
| QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), | QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), | ||||
| @@ -898,7 +1054,8 @@ void LogicEditorWidget::reloadLogic() | |||||
| rung.output->id) | rung.output->id) | ||||
| : rung_active; | : rung_active; | ||||
| }(), | }(), | ||||
| rung.output->id == fault_node_id_)); | |||||
| rung.output->id == fault_node_id_, | |||||
| false)); | |||||
| } | } | ||||
| else | 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<NodeItem *>(item)) | |||||
| { | |||||
| selected = node->isConditionNode() && node->nodeId() == expression_id; | |||||
| if (selected) | |||||
| { | |||||
| current_rung_id_ = node->rungId(); | |||||
| ensureVisible(node); | |||||
| } | |||||
| } | |||||
| else if (WireItem *wire = dynamic_cast<WireItem *>(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 | std::string LogicEditorWidget::selectedNodeId() const | ||||
| { | { | ||||
| const std::vector<std::string> ids = selectedNodeIds(); | const std::vector<std::string> ids = selectedNodeIds(); | ||||
| @@ -973,6 +1162,73 @@ std::vector<std::string> LogicEditorWidget::selectedNodeIds() const | |||||
| return ids; | return ids; | ||||
| } | } | ||||
| std::vector<std::string> LogicEditorWidget::selectedExpressionIds() const | |||||
| { | |||||
| std::vector<std::pair<QPointF, std::string>> positioned_ids; | |||||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||||
| { | |||||
| if (const NodeItem *node = dynamic_cast<const NodeItem *>(item)) | |||||
| { | |||||
| if (node->isConditionNode()) | |||||
| { | |||||
| positioned_ids.emplace_back(node->scenePos(), node->nodeId()); | |||||
| } | |||||
| } | |||||
| else if (const WireItem *wire = dynamic_cast<const WireItem *>(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<std::string> ids; | |||||
| ids.reserve(positioned_ids.size()); | |||||
| for (const auto &positioned_id : positioned_ids) | |||||
| { | |||||
| ids.push_back(positioned_id.second); | |||||
| } | |||||
| return ids; | |||||
| } | |||||
| std::vector<std::string> LogicEditorWidget::selectedWireIds() const | |||||
| { | |||||
| std::vector<std::string> ids; | |||||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||||
| { | |||||
| if (const WireItem *wire = dynamic_cast<const WireItem *>(item)) | |||||
| { | |||||
| ids.push_back(wire->expressionId()); | |||||
| } | |||||
| } | |||||
| return ids; | |||||
| } | |||||
| std::vector<std::string> LogicEditorWidget::selectedBranchIds() const | |||||
| { | |||||
| std::vector<std::string> ids; | |||||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||||
| { | |||||
| if (const VerticalConnectorItem *connector = | |||||
| dynamic_cast<const VerticalConnectorItem *>(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 LogicEditorWidget::selectedRungId() const | ||||
| { | { | ||||
| std::string rung_id; | std::string rung_id; | ||||
| @@ -986,6 +1242,23 @@ std::string LogicEditorWidget::selectedRungId() const | |||||
| } | } | ||||
| rung_id = node->rungId(); | rung_id = node->rungId(); | ||||
| } | } | ||||
| else if (const WireItem *wire = dynamic_cast<const WireItem *>(item)) | |||||
| { | |||||
| if (!rung_id.empty() && rung_id != wire->rungId()) | |||||
| { | |||||
| return {}; | |||||
| } | |||||
| rung_id = wire->rungId(); | |||||
| } | |||||
| else if (const VerticalConnectorItem *connector = | |||||
| dynamic_cast<const VerticalConnectorItem *>(item)) | |||||
| { | |||||
| if (!rung_id.empty() && rung_id != connector->rungId()) | |||||
| { | |||||
| return {}; | |||||
| } | |||||
| rung_id = connector->rungId(); | |||||
| } | |||||
| else if (const RungItem *rung = dynamic_cast<const RungItem *>(item)) | else if (const RungItem *rung = dynamic_cast<const RungItem *>(item)) | ||||
| { | { | ||||
| if (rung_id.empty()) | if (rung_id.empty()) | ||||
| @@ -1015,12 +1288,19 @@ LogicEditorResult LogicEditorWidget::addRung() | |||||
| LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | ||||
| { | { | ||||
| const std::vector<std::string> selected_expressions = selectedExpressionIds(); | |||||
| const std::vector<std::string> selected_wires = selectedWireIds(); | |||||
| const std::vector<std::string> selected_ids = selectedNodeIds(); | const std::vector<std::string> selected_ids = selectedNodeIds(); | ||||
| LogicEditorResult result; | LogicEditorResult result; | ||||
| if (selected_ids.size() > 1U) | |||||
| if (selected_expressions.size() > 1U || selected_wires.size() > 1U) | |||||
| { | { | ||||
| result = {false, LogicEditorError::InvalidOperation, | 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) | else if (selected_ids.size() == 1U) | ||||
| { | { | ||||
| @@ -1038,7 +1318,7 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | |||||
| if (result.succeeded) | if (result.succeeded) | ||||
| { | { | ||||
| reloadLogic(); | reloadLogic(); | ||||
| selectNode(result.id); | |||||
| selectExpression(result.id); | |||||
| emit graphChanged(); | emit graphChanged(); | ||||
| } | } | ||||
| else | else | ||||
| @@ -1075,7 +1355,133 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co | |||||
| if (result.succeeded) | if (result.succeeded) | ||||
| { | { | ||||
| reloadLogic(); | reloadLogic(); | ||||
| selectNode(result.id); | |||||
| selectExpression(result.id); | |||||
| emit graphChanged(); | |||||
| } | |||||
| else | |||||
| { | |||||
| reportFailure(result); | |||||
| } | |||||
| return result; | |||||
| } | |||||
| LogicEditorResult LogicEditorWidget::addHorizontalWire() | |||||
| { | |||||
| const std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> 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(); | emit graphChanged(); | ||||
| } | } | ||||
| else | else | ||||
| @@ -1105,9 +1511,38 @@ LogicEditorResult LogicEditorWidget::setOutput( | |||||
| LogicEditorResult LogicEditorWidget::deleteSelected() | LogicEditorResult LogicEditorWidget::deleteSelected() | ||||
| { | { | ||||
| const std::vector<std::string> expression_ids = selectedExpressionIds(); | |||||
| const std::vector<std::string> wire_ids = selectedWireIds(); | |||||
| const std::vector<std::string> branch_ids = selectedBranchIds(); | |||||
| const std::vector<std::string> node_ids = selectedNodeIds(); | const std::vector<std::string> node_ids = selectedNodeIds(); | ||||
| LogicEditorResult result; | 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<const NodeItem *>(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); | result = editor_service_.removeNodes(logic_id_, node_ids); | ||||
| } | } | ||||
| @@ -18,6 +18,8 @@ class LogicEditorWidget final : public QGraphicsView | |||||
| public: | public: | ||||
| class NodeItem; | class NodeItem; | ||||
| class WireItem; | |||||
| class VerticalConnectorItem; | |||||
| class RungItem; | class RungItem; | ||||
| explicit LogicEditorWidget( | explicit LogicEditorWidget( | ||||
| @@ -39,6 +41,10 @@ public: | |||||
| LogicEditorResult addRung(); | LogicEditorResult addRung(); | ||||
| LogicEditorResult addCondition(const LogicNodeConfig &config); | LogicEditorResult addCondition(const LogicNodeConfig &config); | ||||
| LogicEditorResult addParallelBranch(const LogicNodeConfig &config); | LogicEditorResult addParallelBranch(const LogicNodeConfig &config); | ||||
| LogicEditorResult addHorizontalWire(); | |||||
| LogicEditorResult addVerticalWire(); | |||||
| LogicEditorResult deleteHorizontalWire(); | |||||
| LogicEditorResult deleteVerticalWire(); | |||||
| LogicEditorResult setOutput( | LogicEditorResult setOutput( | ||||
| const LogicNodeConfig &config, bool configured = false); | const LogicNodeConfig &config, bool configured = false); | ||||
| LogicEditorResult deleteSelected(); | LogicEditorResult deleteSelected(); | ||||
| @@ -55,6 +61,11 @@ private: | |||||
| void handleSelectionChanged(); | void handleSelectionChanged(); | ||||
| void reportFailure(const LogicEditorResult &result); | void reportFailure(const LogicEditorResult &result); | ||||
| std::string currentRungId() const; | std::string currentRungId() const; | ||||
| void selectExpression(const std::string &expression_id); | |||||
| void selectWire(const std::string &wire_id); | |||||
| std::vector<std::string> selectedExpressionIds() const; | |||||
| std::vector<std::string> selectedWireIds() const; | |||||
| std::vector<std::string> selectedBranchIds() const; | |||||
| LogicEditorService &editor_service_; | LogicEditorService &editor_service_; | ||||
| QGraphicsScene *scene_ = nullptr; | QGraphicsScene *scene_ = nullptr; | ||||
| @@ -395,6 +395,14 @@ void MainWindow::configureActions() | |||||
| connect(ui_->addRungAction, &QAction::triggered, | connect(ui_->addRungAction, &QAction::triggered, | ||||
| this, &MainWindow::addLogicRung); | 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, | connect(ui_->parallelInsertAction, &QAction::triggered, | ||||
| this, | this, | ||||
| [this] | [this] | ||||
| @@ -727,6 +735,10 @@ void MainWindow::configureAppearance() | |||||
| ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete)); | ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete)); | ||||
| ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung)); | ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung)); | ||||
| ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch)); | 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_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact)); | ||||
| ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); | ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); | ||||
| ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); | ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); | ||||
| @@ -1030,6 +1042,10 @@ void MainWindow::updateEditActions() | |||||
| || (logic_active && logic_editor_service_.canRedo()))); | || (logic_active && logic_editor_service_.canRedo()))); | ||||
| ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active)); | ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active)); | ||||
| ui_->clearSelectionAction->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() | void MainWindow::clearEditorHistories() | ||||
| @@ -1194,6 +1210,62 @@ void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config) | |||||
| statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000); | 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) | void MainWindow::setLogicOutput(const LogicNodeConfig &config) | ||||
| { | { | ||||
| const LogicEditorResult result = logic_editor_widget_->setOutput(config); | const LogicEditorResult result = logic_editor_widget_->setOutput(config); | ||||
| @@ -136,6 +136,10 @@ private: | |||||
| void addLogicCondition(const LogicNodeConfig &config); | void addLogicCondition(const LogicNodeConfig &config); | ||||
| // 为当前选中的连续逻辑范围建立并联支路 | // 为当前选中的连续逻辑范围建立并联支路 | ||||
| void addLogicParallelBranch(const LogicNodeConfig &config); | void addLogicParallelBranch(const LogicNodeConfig &config); | ||||
| void addLogicHorizontalWire(); | |||||
| void addLogicVerticalWire(); | |||||
| void deleteLogicHorizontalWire(); | |||||
| void deleteLogicVerticalWire(); | |||||
| // 设置当前网络右侧的输出线圈 | // 设置当前网络右侧的输出线圈 | ||||
| void setLogicOutput(const LogicNodeConfig &config); | void setLogicOutput(const LogicNodeConfig &config); | ||||
| void configureAndSetLogicOutput(const LogicNodeConfig &config); | void configureAndSetLogicOutput(const LogicNodeConfig &config); | ||||
| @@ -250,6 +250,8 @@ | |||||
| <addaction name="redoAction"/> | <addaction name="redoAction"/> | ||||
| <addaction name="separator"/> | <addaction name="separator"/> | ||||
| <addaction name="deleteSelectionAction"/> | <addaction name="deleteSelectionAction"/> | ||||
| <addaction name="deleteHorizontalWireAction"/> | |||||
| <addaction name="deleteVerticalWireAction"/> | |||||
| <addaction name="clearSelectionAction"/> | <addaction name="clearSelectionAction"/> | ||||
| </widget> | </widget> | ||||
| <widget class="QMenu" name="runMenu"> | <widget class="QMenu" name="runMenu"> | ||||
| @@ -331,6 +333,8 @@ | |||||
| <bool>false</bool> | <bool>false</bool> | ||||
| </attribute> | </attribute> | ||||
| <addaction name="addRungAction"/> | <addaction name="addRungAction"/> | ||||
| <addaction name="insertHorizontalWireAction"/> | |||||
| <addaction name="insertVerticalWireAction"/> | |||||
| <addaction name="parallelInsertAction"/> | <addaction name="parallelInsertAction"/> | ||||
| <addaction name="deleteLogicAction"/> | <addaction name="deleteLogicAction"/> | ||||
| <addaction name="separator"/> | <addaction name="separator"/> | ||||
| @@ -1059,6 +1063,50 @@ | |||||
| <string>在梯形图末尾新增网络</string> | <string>在梯形图末尾新增网络</string> | ||||
| </property> | </property> | ||||
| </action> | </action> | ||||
| <action name="insertHorizontalWireAction"> | |||||
| <property name="text"> | |||||
| <string>横线</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>在所选条件或横线后插入横线;没有选择时追加到当前网络</string> | |||||
| </property> | |||||
| <property name="shortcut"> | |||||
| <string>F11</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="insertVerticalWireAction"> | |||||
| <property name="text"> | |||||
| <string>竖线</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>为所选连续逻辑范围建立横线旁路和竖线连接</string> | |||||
| </property> | |||||
| <property name="shortcut"> | |||||
| <string>F12</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="deleteHorizontalWireAction"> | |||||
| <property name="text"> | |||||
| <string>删除横线</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>删除画布中选中的横线</string> | |||||
| </property> | |||||
| <property name="shortcut"> | |||||
| <string>Shift+F11</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="deleteVerticalWireAction"> | |||||
| <property name="text"> | |||||
| <string>删除竖线</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>删除画布中选中的竖线及其对应并联支路</string> | |||||
| </property> | |||||
| <property name="shortcut"> | |||||
| <string>Shift+F12</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="parallelInsertAction"> | <action name="parallelInsertAction"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>并联支路</string> | <string>并联支路</string> | ||||
| @@ -359,6 +359,24 @@ QPixmap renderIcon(UiIcon icon, int size) | |||||
| painter.drawLine(QPointF(7, 12), QPointF(17, 12)); | painter.drawLine(QPointF(7, 12), QPointF(17, 12)); | ||||
| break; | 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: | case UiIcon::ParallelBranch: | ||||
| { | { | ||||
| painter.drawLine(QPointF(2, 12), QPointF(6, 12)); | painter.drawLine(QPointF(2, 12), QPointF(6, 12)); | ||||
| @@ -28,6 +28,8 @@ enum class UiIcon | |||||
| AlarmSettings, | AlarmSettings, | ||||
| Delete, | Delete, | ||||
| AddRung, | AddRung, | ||||
| HorizontalWire, | |||||
| VerticalWire, | |||||
| ParallelBranch, | ParallelBranch, | ||||
| NormallyOpenContact, | NormallyOpenContact, | ||||
| NormallyClosedContact, | NormallyClosedContact, | ||||
| @@ -603,6 +603,26 @@ void testLadderLogicBoundaries() | |||||
| logic.rungs.push_back(rung); | logic.rungs.push_back(rung); | ||||
| require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid"); | 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() = | logic.rungs.front().condition->children.front() = | ||||
| ConditionExpression::fromNode(coil); | ConditionExpression::fromNode(coil); | ||||
| require(!logic.validate(), "a ladder condition expression must reject coils"); | require(!logic.validate(), "a ladder condition expression must reject coils"); | ||||
| @@ -132,6 +132,66 @@ void testRangeParallelInsertion() | |||||
| "a non-contiguous selection must be rejected"); | "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() | void testLogicLifecycleAndOrdering() | ||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| @@ -305,6 +365,7 @@ int main() | |||||
| { | { | ||||
| testStructuredEditingAndNormalization(); | testStructuredEditingAndNormalization(); | ||||
| testRangeParallelInsertion(); | testRangeParallelInsertion(); | ||||
| testStructuredWireEditing(); | |||||
| testLogicLifecycleAndOrdering(); | testLogicLifecycleAndOrdering(); | ||||
| testEdgeTimerNodesAndRungComments(); | testEdgeTimerNodesAndRungComments(); | ||||
| testHistoryAndAtomicBatchDelete(); | testHistoryAndAtomicBatchDelete(); | ||||
| @@ -670,6 +670,14 @@ void testModeActionsControlEditingAvailability() | |||||
| window, "addNormalCoilAction"); | window, "addNormalCoilAction"); | ||||
| QAction *parallel_insert_action = requiredChild<QAction>( | QAction *parallel_insert_action = requiredChild<QAction>( | ||||
| window, "parallelInsertAction"); | window, "parallelInsertAction"); | ||||
| QAction *insert_horizontal_wire_action = requiredChild<QAction>( | |||||
| window, "insertHorizontalWireAction"); | |||||
| QAction *insert_vertical_wire_action = requiredChild<QAction>( | |||||
| window, "insertVerticalWireAction"); | |||||
| QAction *delete_horizontal_wire_action = requiredChild<QAction>( | |||||
| window, "deleteHorizontalWireAction"); | |||||
| QAction *delete_vertical_wire_action = requiredChild<QAction>( | |||||
| window, "deleteVerticalWireAction"); | |||||
| QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | ||||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | ||||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | ||||
| @@ -725,6 +733,10 @@ void testModeActionsControlEditingAvailability() | |||||
| "configureAlarmsAction", | "configureAlarmsAction", | ||||
| "deleteControlAction", | "deleteControlAction", | ||||
| "addRungAction", | "addRungAction", | ||||
| "insertHorizontalWireAction", | |||||
| "insertVerticalWireAction", | |||||
| "deleteHorizontalWireAction", | |||||
| "deleteVerticalWireAction", | |||||
| "parallelInsertAction", | "parallelInsertAction", | ||||
| "addNormallyOpenAction", | "addNormallyOpenAction", | ||||
| "addNormallyClosedAction", | "addNormallyClosedAction", | ||||
| @@ -799,7 +811,15 @@ void testModeActionsControlEditingAvailability() | |||||
| require(editing_action->isChecked(), "editing action must be selected initially"); | require(editing_action->isChecked(), "editing action must be selected initially"); | ||||
| require(undo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Z) | require(undo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Z) | ||||
| && redo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Y) | && 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"); | "core editor actions must expose the agreed keyboard shortcuts"); | ||||
| require(project_dock->isEnabled(), "project dock must be enabled while editing"); | require(project_dock->isEnabled(), "project dock must be enabled while editing"); | ||||
| require(properties_dock->isEnabled(), "properties 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); | collectConditionNodes(*restored_rung.condition, &restored_nodes); | ||||
| require(restored_nodes.size() == 1U, | require(restored_nodes.size() == 1U, | ||||
| "logic redo must restore the original condition node"); | "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(); | parallel_insert_action->trigger(); | ||||
| add_normal_coil_action->trigger(); | add_normal_coil_action->trigger(); | ||||
| const ControlLogic *logic = logic_editor_service.findLogic( | const ControlLogic *logic = logic_editor_service.findLogic( | ||||
| @@ -257,6 +257,44 @@ void testNestedSeriesParallelExpression() | |||||
| require(readBit(repository, 10), "A branch must independently energize output"); | 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() | void testSeriesParallelContactsAndSequentialVisibility() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -862,6 +900,7 @@ int main(int argc, char *argv[]) | |||||
| { | { | ||||
| testSeriesParallelContactsAndSequentialVisibility(); | testSeriesParallelContactsAndSequentialVisibility(); | ||||
| testNestedSeriesParallelExpression(); | testNestedSeriesParallelExpression(); | ||||
| testWirePassThroughAndPowerTrace(); | |||||
| testAllComparisons(); | testAllComparisons(); | ||||
| testSetResetAndDisabledLogic(); | testSetResetAndDisabledLogic(); | ||||
| testMultipleLogicScanOrderAndTraceIsolation(); | testMultipleLogicScanOrderAndTraceIsolation(); | ||||
| @@ -148,6 +148,7 @@ Project makeExampleProject() | |||||
| series.kind = ConditionExpressionKind::Series; | series.kind = ConditionExpressionKind::Series; | ||||
| series.children = { | series.children = { | ||||
| std::move(parallel), | std::move(parallel), | ||||
| ConditionExpression::fromWire("start-wire", 2), | |||||
| ConditionExpression::fromNode(compare)}; | ConditionExpression::fromNode(compare)}; | ||||
| rung.condition = std::move(series); | rung.condition = std::move(series); | ||||
| rung.output = coil; | rung.output = coil; | ||||
| @@ -496,6 +497,9 @@ void testExampleProjectRoundTrip() | |||||
| == ConditionExpressionKind::Parallel | == ConditionExpressionKind::Parallel | ||||
| && rung.condition->children.front().children.size() == 2U, | && rung.condition->children.front().children.size() == 2U, | ||||
| "parallel expression branches must survive round trip"); | "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(), | require(rung.output.has_value(), | ||||
| "ladder output must survive round trip"); | "ladder output must survive round trip"); | ||||
| @@ -553,7 +557,7 @@ void testExampleProjectRoundTrip() | |||||
| "ADD and SUB operations must survive round trip"); | "ADD and SUB operations must survive round trip"); | ||||
| const auto &compare = std::get<CompareNodeConfig>( | const auto &compare = std::get<CompareNodeConfig>( | ||||
| rung.condition->children.at(1).node->config); | |||||
| rung.condition->children.at(2).node->config); | |||||
| require(compare.address.index() == 2 && compare.value == 100, | require(compare.address.index() == 2 && compare.value == 100, | ||||
| "comparison configuration must survive round trip"); | "comparison configuration must survive round trip"); | ||||
| require(service.saveAs(second_path.toStdString()).succeeded, | require(service.saveAs(second_path.toStdString()).succeeded, | ||||