From 7c314627182d4c6eb26d2f8d5064d7cb59853271 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Thu, 13 Aug 2026 10:52:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A2=AF=E5=BD=A2?= =?UTF-8?q?=E5=9B=BE=E7=BD=91=E6=A0=BC=E7=BC=96=E8=BE=91=E4=B8=8E=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E5=B9=B6=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/services/logic_editor_service.cpp | 235 +++++++-- app/src/services/logic_editor_service.h | 16 +- app/src/ui/logic_editor_widget.cpp | 553 ++++++++++++---------- app/src/ui/logic_editor_widget.h | 10 +- app/src/ui/main_window.cpp | 92 ++-- app/src/ui/main_window.h | 5 +- app/src/ui/main_window.ui | 8 +- app/tests/logic_editor_service_tests.cpp | 35 +- app/tests/main_window_tests.cpp | 4 +- 9 files changed, 585 insertions(+), 373 deletions(-) diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index e2193dc..d9beceb 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -3,6 +3,8 @@ #include "project_service.h" #include +#include +#include #include #include @@ -44,6 +46,142 @@ ConditionExpression *findParentExpression( return nullptr; } +using NodeIdSet = std::unordered_set; + +NodeIdSet conditionNodeIds(const ConditionExpression &expression) +{ + std::vector nodes; + collectConditionNodes(expression, &nodes); + NodeIdSet ids; + for (const LogicNode *node : nodes) + { + ids.insert(node->id); + } + return ids; +} + +bool isSubset(const NodeIdSet &subset, const NodeIdSet &values) +{ + return std::all_of( + subset.cbegin(), subset.cend(), + [&values](const std::string &value) { return values.count(value) != 0U; }); +} + +bool sameValues(const NodeIdSet &left, const NodeIdSet &right) +{ + return left.size() == right.size() && isSubset(left, right); +} + +void addParallelSibling( + ConditionExpression *target, + ConditionExpression branch, + const std::string &container_id) +{ + if (target->kind == ConditionExpressionKind::Parallel) + { + target->children.push_back(std::move(branch)); + return; + } + ConditionExpression original = std::move(*target); + *target = makeContainer( + container_id, + ConditionExpressionKind::Parallel, + std::move(original), + std::move(branch)); +} + +bool addParallelForSelection( + ConditionExpression *expression, + const NodeIdSet &selected_ids, + ConditionExpression *branch, + const std::string ¶llel_id, + const std::string &series_id) +{ + const NodeIdSet expression_ids = conditionNodeIds(*expression); + if (sameValues(expression_ids, selected_ids)) + { + addParallelSibling(expression, std::move(*branch), parallel_id); + return true; + } + + std::vector child_ids; + child_ids.reserve(expression->children.size()); + std::vector matching_children; + for (std::size_t index = 0; index < expression->children.size(); ++index) + { + child_ids.push_back(conditionNodeIds(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.empty()) + { + return false; + } + if (matching_children.size() == 1U) + { + const std::size_t child_index = matching_children.front(); + if (expression->kind == ConditionExpressionKind::Parallel + && sameValues(child_ids[child_index], selected_ids)) + { + expression->children.push_back(std::move(*branch)); + return true; + } + return addParallelForSelection( + &expression->children[child_index], + selected_ids, + branch, + parallel_id, + series_id); + } + if (expression->kind != ConditionExpressionKind::Series) + { + return false; + } + + const std::size_t first = matching_children.front(); + const std::size_t last = matching_children.back(); + if (last - first + 1U != matching_children.size()) + { + return false; + } + NodeIdSet range_ids; + for (std::size_t index = first; index <= last; ++index) + { + range_ids.insert(child_ids[index].cbegin(), child_ids[index].cend()); + } + if (!sameValues(range_ids, selected_ids)) + { + return false; + } + + ConditionExpression range; + range.id = series_id; + range.kind = ConditionExpressionKind::Series; + auto range_begin = expression->children.begin() + static_cast(first); + auto range_end = expression->children.begin() + static_cast(last + 1U); + range.children.insert( + range.children.end(), + std::make_move_iterator(range_begin), + std::make_move_iterator(range_end)); + range_begin = expression->children.erase(range_begin, range_end); + + ConditionExpression parallel = makeContainer( + parallel_id, + ConditionExpressionKind::Parallel, + std::move(range), + std::move(*branch)); + expression->children.insert(range_begin, std::move(parallel)); + return true; +} + bool removeExpressionRecursive( ConditionExpression *expression, const std::string &expression_id) { @@ -288,11 +426,10 @@ LogicEditorResult LogicEditorService::appendCondition( return {true, LogicEditorError::None, {}, node_id}; } -LogicEditorResult LogicEditorService::insertCondition( +LogicEditorResult LogicEditorService::insertConditionAfter( const std::string &logic_id, const std::string &rung_id, - const std::string &target_expression_id, - SeriesInsertPosition position, + const std::string &target_node_id, const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); @@ -300,52 +437,46 @@ LogicEditorResult LogicEditorService::insertCondition( { return failure(LogicEditorError::RungNotFound, "ladder network was not found"); } + const LadderRung *existing_rung = findRung(logic_id, rung_id); if (!isConditionConfig(config) - || findExpression(logic_id, rung_id, target_expression_id) == nullptr) + || existing_rung == nullptr + || !existing_rung->condition.has_value() + || findConditionNode(*existing_rung->condition, target_node_id) == nullptr) { - return failure(LogicEditorError::ExpressionNotFound, "series insertion target was not found"); + return failure(LogicEditorError::NodeNotFound, "series insertion target was not found"); } const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); Project &project = project_service_.editProject(); LadderRung *rung = findEditableRung(project, logic_id, rung_id); - ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id); - ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id); + ConditionExpression *target = findConditionExpression(*rung->condition, target_node_id); + ConditionExpression *parent = findParentExpression(*rung->condition, target_node_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) + [&target_node_id](const ConditionExpression &child) { - return child.id == target_expression_id; + return child.id == target_node_id; }); - parent->children.insert( - position == SeriesInsertPosition::Before - ? target_iterator : target_iterator + 1, - std::move(leaf)); + parent->children.insert(target_iterator + 1, std::move(leaf)); } else { ConditionExpression original = std::move(*target); - *target = position == SeriesInsertPosition::Before - ? makeContainer( - makeUniqueExpressionId(*logic), - ConditionExpressionKind::Series, - std::move(leaf), - std::move(original)) - : makeContainer( - makeUniqueExpressionId(*logic), - ConditionExpressionKind::Series, - std::move(original), - std::move(leaf)); + *target = makeContainer( + makeUniqueExpressionId(*logic), + ConditionExpressionKind::Series, + std::move(original), + std::move(leaf)); } return {true, LogicEditorError::None, {}, node_id}; } -LogicEditorResult LogicEditorService::addParallelCondition( +LogicEditorResult LogicEditorService::addParallelBranch( const std::string &logic_id, const std::string &rung_id, - const std::string &target_expression_id, + const std::vector &selected_node_ids, const LogicNodeConfig &config) { const ControlLogic *logic = findLogic(logic_id); @@ -353,29 +484,49 @@ LogicEditorResult LogicEditorService::addParallelCondition( { return failure(LogicEditorError::RungNotFound, "ladder network was not found"); } - if (!isConditionConfig(config) - || findExpression(logic_id, rung_id, target_expression_id) == nullptr) + if (!isConditionConfig(config) || selected_node_ids.empty()) { - return failure(LogicEditorError::ExpressionNotFound, "parallel target was not found"); + return failure(LogicEditorError::InvalidOperation, "parallel branch requires a selection"); + } + const LadderRung *existing_rung = findRung(logic_id, rung_id); + if (!existing_rung->condition.has_value()) + { + return failure(LogicEditorError::InvalidOperation, "ladder network has no condition"); + } + NodeIdSet selected_ids(selected_node_ids.cbegin(), selected_node_ids.cend()); + if (selected_ids.size() != selected_node_ids.size()) + { + return failure( + LogicEditorError::InvalidOperation, + "parallel selection contains duplicates"); + } + const NodeIdSet available_ids = conditionNodeIds(*existing_rung->condition); + if (!isSubset(selected_ids, available_ids)) + { + return failure( + LogicEditorError::NodeNotFound, + "parallel selection contains an unknown node"); } const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); - Project &project = project_service_.editProject(); - LadderRung *rung = findEditableRung(project, logic_id, rung_id); - ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id); - ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id); - if (parent != nullptr && parent->kind == ConditionExpressionKind::Parallel) + const std::string parallel_id = makeUniqueExpressionId(*logic); + std::string series_id = parallel_id + "-range"; + while (findExpression(logic_id, rung_id, series_id) != nullptr) { - parent->children.push_back(std::move(leaf)); + series_id += "-range"; } - else + Project &project = project_service_.editProject(); + LadderRung *rung = findEditableRung(project, logic_id, rung_id); + if (!addParallelForSelection( + &*rung->condition, + selected_ids, + &leaf, + parallel_id, + series_id)) { - ConditionExpression original = std::move(*target); - *target = makeContainer( - makeUniqueExpressionId(*logic), - ConditionExpressionKind::Parallel, - std::move(original), - std::move(leaf)); + return failure( + LogicEditorError::InvalidOperation, + "parallel selection must be one continuous logic range"); } return {true, LogicEditorError::None, {}, node_id}; } diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index f32429c..3b4fe00 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -3,6 +3,7 @@ #include "domain/control_logic_model.h" #include +#include class ProjectService; @@ -18,12 +19,6 @@ enum class LogicEditorError UnsupportedNodeChange }; -enum class SeriesInsertPosition -{ - Before, - After -}; - struct LogicEditorResult { bool succeeded = false; @@ -59,16 +54,15 @@ public: const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config); - LogicEditorResult insertCondition( + LogicEditorResult insertConditionAfter( const std::string &logic_id, const std::string &rung_id, - const std::string &target_expression_id, - SeriesInsertPosition position, + const std::string &target_node_id, const LogicNodeConfig &config); - LogicEditorResult addParallelCondition( + LogicEditorResult addParallelBranch( const std::string &logic_id, const std::string &rung_id, - const std::string &target_expression_id, + const std::vector &selected_node_ids, const LogicNodeConfig &config); LogicEditorResult setOutput( const std::string &logic_id, diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 2f78b6f..fa5b56b 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -13,31 +13,34 @@ namespace { -constexpr qreal kLeftRailX = 64.0; -constexpr qreal kMinimumSceneWidth = 940.0; -constexpr qreal kNodeWidth = 116.0; -constexpr qreal kNodeHeight = 92.0; -constexpr qreal kNodeTerminalX = 54.0; -constexpr qreal kSeriesGap = 26.0; -constexpr qreal kParallelGap = 22.0; -constexpr qreal kExpressionPadding = 18.0; -constexpr qreal kRungHeaderHeight = 38.0; +constexpr qreal kSceneMargin = 28.0; +constexpr qreal kRailInset = 40.0; +constexpr qreal kMinimumSceneWidth = 980.0; +constexpr qreal kCellWidth = 128.0; +constexpr qreal kCellHeight = 86.0; +constexpr qreal kNodeTerminalX = 50.0; +constexpr qreal kRungHeaderHeight = 34.0; constexpr qreal kRungGap = 18.0; -constexpr qreal kOutputWidth = 128.0; -constexpr qreal kOutputGap = 72.0; constexpr qreal kLadderLineWidth = 1.8; +constexpr int kMinimumLogicColumns = 7; const QColor kLadderColor(QStringLiteral("#263842")); const QColor kActiveColor(QStringLiteral("#16854f")); const QColor kFaultColor(QStringLiteral("#c5362e")); const QColor kSelectionColor(QStringLiteral("#dfeef5")); const QColor kSelectionBorderColor(QStringLiteral("#277da1")); -const QColor kGroupBorderColor(QStringLiteral("#8ea4af")); +const QColor kGridColor(QStringLiteral("#e8edf0")); const QColor kPlaceholderColor(QStringLiteral("#81919b")); -struct ExpressionSize +struct ExpressionMetrics { - qreal width = kNodeWidth; - qreal height = kNodeHeight; + int columns = 1; + int rows = 1; +}; + +struct RenderResult +{ + QPointF input; + QPointF output; }; QString registerAddressText(const RegisterAddress &address) @@ -89,36 +92,33 @@ QString nodeToolTip(const LogicNodeConfig &config) config); } -ExpressionSize measureExpression(const ConditionExpression &expression) +ExpressionMetrics measureExpression(const ConditionExpression &expression) { if (expression.kind == ConditionExpressionKind::Node) { return {}; } - ExpressionSize size{0.0, 0.0}; + + ExpressionMetrics metrics{0, 0}; if (expression.kind == ConditionExpressionKind::Series) { for (const ConditionExpression &child : expression.children) { - const ExpressionSize child_size = measureExpression(child); - size.width += child_size.width; - size.height = std::max(size.height, child_size.height); + const ExpressionMetrics child_metrics = measureExpression(child); + metrics.columns += child_metrics.columns; + metrics.rows = std::max(metrics.rows, child_metrics.rows); } - size.width += kSeriesGap * static_cast(expression.children.size() - 1U); } else { for (const ConditionExpression &child : expression.children) { - const ExpressionSize child_size = measureExpression(child); - size.width = std::max(size.width, child_size.width); - size.height += child_size.height; + const ExpressionMetrics child_metrics = measureExpression(child); + metrics.columns = std::max(metrics.columns, child_metrics.columns); + metrics.rows += child_metrics.rows; } - size.height += kParallelGap * static_cast(expression.children.size() - 1U); } - size.width += kExpressionPadding * 2.0; - size.height += kExpressionPadding * 2.0; - return size; + return metrics; } QPen ladderPen(bool active) @@ -137,6 +137,47 @@ bool traceValue( return found != values.end() && found->second; } +void addGrid( + QGraphicsScene &scene, + qreal left, + qreal top, + int columns, + int rows) +{ + QPen pen(kGridColor, 1.0); + pen.setCosmetic(true); + for (int column = 0; column <= columns; ++column) + { + const qreal x = left + static_cast(column) * kCellWidth; + QGraphicsLineItem *line = scene.addLine( + QLineF(x, top, x, top + static_cast(rows) * kCellHeight), pen); + line->setZValue(-10.0); + } + for (int row = 0; row <= rows; ++row) + { + const qreal y = top + static_cast(row) * kCellHeight; + QGraphicsLineItem *line = scene.addLine( + QLineF(left, y, left + static_cast(columns) * kCellWidth, y), pen); + line->setZValue(-10.0); + } +} + +void addPlaceholder(QGraphicsScene &scene, const QRectF &cell, const QString &text) +{ + const QRectF bounds = cell.adjusted(12, 18, -12, -18); + QGraphicsRectItem *item = scene.addRect( + bounds, + QPen(kPlaceholderColor, 1.2, Qt::DashLine), + QBrush(QColor(255, 255, 255, 220))); + item->setZValue(1.0); + QGraphicsTextItem *label = scene.addText(text); + label->setDefaultTextColor(kPlaceholderColor); + label->setPos( + bounds.center().x() - label->boundingRect().width() / 2.0, + bounds.center().y() - label->boundingRect().height() / 2.0); + label->setZValue(2.0); +} + } // namespace class LogicEditorWidget::NodeItem final : public QGraphicsItem @@ -145,13 +186,11 @@ public: NodeItem( const LogicNode &node, const std::string &rung_id, - const std::string &expression_id, const QPointF ¢er, bool active, bool faulted) : node_id_(node.id), rung_id_(rung_id), - expression_id_(expression_id), config_(node.config), configured_(node.configured), active_(active), @@ -166,7 +205,7 @@ public: QRectF boundingRect() const override { - return {-kNodeWidth / 2.0, -kNodeHeight / 2.0, kNodeWidth, kNodeHeight}; + return {-kCellWidth / 2.0, -kCellHeight / 2.0, kCellWidth, kCellHeight}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override @@ -175,8 +214,11 @@ public: const bool selected = (option->state & QStyle::State_Selected) != 0; if (selected) { - painter->fillRect(boundingRect(), kSelectionColor); + 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)); } + const QColor symbol_color = faulted_ ? kFaultColor : selected ? kSelectionBorderColor : active_ ? kActiveColor : kLadderColor; painter->setPen(QPen(symbol_color, active_ || faulted_ ? 2.6 : kLadderLineWidth)); @@ -186,7 +228,7 @@ public: if (const auto *contact = std::get_if(&config_)) { - painter->fillRect(QRectF(-24, -21, 48, 42), Qt::white); + painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0)); painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0)); painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); @@ -196,13 +238,13 @@ public: painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } painter->drawText( - QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), + QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(contact->address) : tr("< M 地址 >")); } else if (const auto *coil = std::get_if(&config_)) { - painter->fillRect(QRectF(-34, -23, 68, 46), Qt::white); + painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-16, 0)); painter->drawLine(QPointF(16, 0), QPointF(kNodeTerminalX, 0)); QPainterPath left; @@ -221,7 +263,7 @@ public: coil->mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R")); } painter->drawText( - QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), + QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(coil->address) : tr("< M 地址 >")); } @@ -232,102 +274,72 @@ public: painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); - painter->drawText(box, Qt::AlignCenter, - QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); painter->drawText( - QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter, + box, Qt::AlignCenter, + QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); + painter->drawText( + QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), + Qt::AlignCenter, configured_ ? registerAddressText(comparison->address) : tr("< D 地址 >")); painter->drawText( - QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), Qt::AlignCenter, + QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), + Qt::AlignCenter, configured_ ? QString::number(comparison->value) : tr("< 常量 >")); } } const std::string &nodeId() const { return node_id_; } const std::string &rungId() const { return rung_id_; } - const std::string &expressionId() const { return expression_id_; } private: std::string node_id_; std::string rung_id_; - std::string expression_id_; LogicNodeConfig config_; bool configured_ = true; bool active_ = false; bool faulted_ = false; }; -class LogicEditorWidget::ExpressionItem final : public QGraphicsItem -{ -public: - ExpressionItem( - std::string expression_id, - std::string rung_id, - ConditionExpressionKind kind, - const QRectF &bounds) - : expression_id_(std::move(expression_id)), - rung_id_(std::move(rung_id)), - kind_(kind), - bounds_(bounds) - { - setFlag(ItemIsSelectable, true); - setZValue(1.0); - setToolTip(kind_ == ConditionExpressionKind::Parallel - ? LogicEditorWidget::tr("并联支路组") - : LogicEditorWidget::tr("串联条件组")); - } - - QRectF boundingRect() const override { return bounds_; } - - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override - { - if ((option->state & QStyle::State_Selected) == 0) - { - return; - } - painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); - painter->setBrush(QColor(223, 238, 245, 45)); - painter->drawRect(bounds_.adjusted(2, 2, -2, -2)); - painter->setPen(kGroupBorderColor); - painter->drawText( - bounds_.adjusted(8, 3, -8, -3), - Qt::AlignRight | Qt::AlignTop, - kind_ == ConditionExpressionKind::Parallel - ? LogicEditorWidget::tr("并联组") : LogicEditorWidget::tr("串联组")); - } - - const std::string &expressionId() const { return expression_id_; } - const std::string &rungId() const { return rung_id_; } - -private: - std::string expression_id_; - std::string rung_id_; - ConditionExpressionKind kind_ = ConditionExpressionKind::Series; - QRectF bounds_; -}; - class LogicEditorWidget::RungItem final : public QGraphicsItem { public: - RungItem(const LadderRung &rung, int number, qreal top, qreal height, qreal width) + RungItem( + const LadderRung &rung, + int number, + qreal top, + qreal height, + qreal width, + qreal cursor_left) : rung_id_(rung.id), name_(QString::fromStdString(rung.name)), number_(number), height_(height), - width_(width) + width_(width), + cursor_left_(cursor_left), + show_cursor_(!rung.condition.has_value()) { setPos(0, top); setFlag(ItemIsSelectable, true); setZValue(-2.0); } - QRectF boundingRect() const override { return {20, 0, width_ - 40, height_}; } + QRectF boundingRect() const override { return {kSceneMargin, 0, width_, height_}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { - if ((option->state & QStyle::State_Selected) != 0) + const bool selected = (option->state & QStyle::State_Selected) != 0; + if (selected) { - painter->fillRect(boundingRect(), QColor(QStringLiteral("#f0f7fa"))); + painter->fillRect(boundingRect(), QColor(240, 247, 250, 90)); + if (show_cursor_) + { + painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); + painter->drawRect(QRectF( + cursor_left_ + 3, + kRungHeaderHeight + 3, + kCellWidth - 6, + kCellHeight - 6)); + } } painter->setPen(QColor(QStringLiteral("#62717b"))); QString title = tr("网络 %1").arg(number_); @@ -335,9 +347,14 @@ public: { title += QStringLiteral(":") + name_; } - painter->drawText(QRectF(28, 6, 320, 22), Qt::AlignLeft | Qt::AlignVCenter, title); + painter->drawText( + QRectF(kSceneMargin + 8, 5, 320, 22), + Qt::AlignLeft | Qt::AlignVCenter, + title); painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); - painter->drawLine(QPointF(28, height_ - 1), QPointF(width_ - 28, height_ - 1)); + painter->drawLine( + QPointF(kSceneMargin + 8, height_ - 1), + QPointF(kSceneMargin + width_ - 8, height_ - 1)); } const std::string &rungId() const { return rung_id_; } @@ -348,21 +365,18 @@ private: int number_ = 0; qreal height_ = 0.0; qreal width_ = 0.0; + qreal cursor_left_ = 0.0; + bool show_cursor_ = false; }; namespace { -struct RenderResult -{ - QPointF input; - QPointF output; -}; - RenderResult renderExpression( QGraphicsScene &scene, const ConditionExpression &expression, const std::string &rung_id, - const QRectF &bounds, + const QPointF &top_left, + const ExpressionMetrics &metrics, const LogicTraceSnapshot &trace, bool trace_enabled, const std::string &fault_node_id) @@ -371,38 +385,41 @@ RenderResult renderExpression( trace, &LogicTraceSnapshot::expressionValues, expression.id); if (expression.kind == ConditionExpressionKind::Node) { - const QPointF center = bounds.center(); + const QPointF center( + top_left.x() + kCellWidth / 2.0, + top_left.y() + kCellHeight / 2.0); + const bool node_active = trace_enabled && traceValue( + trace, &LogicTraceSnapshot::nodeValues, expression.node->id); + scene.addLine( + QLineF( + QPointF(top_left.x(), center.y()), + QPointF(top_left.x() + kCellWidth, center.y())), + ladderPen(node_active)); scene.addItem(new LogicEditorWidget::NodeItem( *expression.node, rung_id, - expression.id, center, - trace_enabled && traceValue( - trace, &LogicTraceSnapshot::nodeValues, expression.node->id), + node_active, expression.node->id == fault_node_id)); - return {QPointF(bounds.left(), center.y()), QPointF(bounds.right(), center.y())}; + return { + QPointF(top_left.x(), center.y()), + QPointF(top_left.x() + kCellWidth, center.y())}; } - scene.addItem(new LogicEditorWidget::ExpressionItem( - expression.id, rung_id, expression.kind, bounds)); if (expression.kind == ConditionExpressionKind::Series) { - qreal x = bounds.left() + kExpressionPadding; + qreal x = top_left.x(); RenderResult first; RenderResult previous; for (std::size_t index = 0; index < expression.children.size(); ++index) { - const ExpressionSize size = measureExpression(expression.children[index]); - const QRectF child_bounds( - x, - bounds.center().y() - size.height / 2.0, - size.width, - size.height); + const ExpressionMetrics child_metrics = measureExpression(expression.children[index]); const RenderResult current = renderExpression( scene, expression.children[index], rung_id, - child_bounds, + QPointF(x, top_left.y()), + child_metrics, trace, trace_enabled, fault_node_id); @@ -415,29 +432,31 @@ RenderResult renderExpression( scene.addLine(QLineF(previous.output, current.input), ladderPen(active)); } previous = current; - x += size.width + kSeriesGap; + x += static_cast(child_metrics.columns) * kCellWidth; } - scene.addLine(QLineF(QPointF(bounds.left(), first.input.y()), first.input), ladderPen(active)); - scene.addLine(QLineF(previous.output, QPointF(bounds.right(), previous.output.y())), ladderPen(active)); - return {QPointF(bounds.left(), first.input.y()), QPointF(bounds.right(), previous.output.y())}; + return {first.input, previous.output}; } - qreal y = bounds.top() + kExpressionPadding; + qreal y = top_left.y(); std::vector branches; + branches.reserve(expression.children.size()); for (const ConditionExpression &child : expression.children) { - const ExpressionSize size = measureExpression(child); - const QRectF child_bounds( - bounds.center().x() - size.width / 2.0, - y, - size.width, - size.height); + const ExpressionMetrics child_metrics = measureExpression(child); branches.push_back(renderExpression( - scene, child, rung_id, child_bounds, trace, trace_enabled, fault_node_id)); - y += size.height + kParallelGap; + scene, + child, + rung_id, + QPointF(top_left.x(), y), + child_metrics, + trace, + trace_enabled, + fault_node_id)); + y += static_cast(child_metrics.rows) * kCellHeight; } - const qreal left_join = bounds.left() + 8.0; - const qreal right_join = bounds.right() - 8.0; + + const qreal left_join = top_left.x(); + const qreal right_join = top_left.x() + static_cast(metrics.columns) * kCellWidth; const qreal top_y = branches.front().input.y(); const qreal bottom_y = branches.back().input.y(); scene.addLine(QLineF(left_join, top_y, left_join, bottom_y), ladderPen(active)); @@ -445,7 +464,9 @@ RenderResult renderExpression( for (std::size_t index = 0; index < branches.size(); ++index) { const bool branch_active = trace_enabled && traceValue( - trace, &LogicTraceSnapshot::expressionValues, expression.children[index].id); + trace, + &LogicTraceSnapshot::expressionValues, + expression.children[index].id); scene.addLine( QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), ladderPen(branch_active)); @@ -453,17 +474,7 @@ RenderResult renderExpression( QLineF(branches[index].output, QPointF(right_join, branches[index].output.y())), ladderPen(branch_active)); } - return {QPointF(bounds.left(), top_y), QPointF(bounds.right(), top_y)}; -} - -void addPlaceholder(QGraphicsScene &scene, const QPointF ¢er, const QString &text) -{ - const QRectF bounds(center.x() - 58, center.y() - 20, 116, 40); - scene.addRect(bounds, QPen(kPlaceholderColor, 1.2, Qt::DashLine), QBrush(Qt::white)); - QGraphicsTextItem *label = scene.addText(text); - label->setDefaultTextColor(kPlaceholderColor); - label->setPos(center.x() - label->boundingRect().width() / 2.0, - center.y() - label->boundingRect().height() / 2.0); + return {QPointF(left_join, top_y), QPointF(right_join, top_y)}; } } // namespace @@ -529,97 +540,105 @@ void LogicEditorWidget::reloadLogic() current_rung_id_ = logic->rungs.front().id; } - qreal maximum_condition_width = 360.0; + int condition_columns = 1; for (const LadderRung &rung : logic->rungs) { if (rung.condition.has_value()) { - maximum_condition_width = std::max( - maximum_condition_width, measureExpression(*rung.condition).width); + condition_columns = std::max( + condition_columns, + measureExpression(*rung.condition).columns); } } - const qreal scene_width = std::max( - kMinimumSceneWidth, - kLeftRailX + maximum_condition_width + kOutputGap + kOutputWidth + 96.0); - const qreal right_rail_x = scene_width - 64.0; - const qreal output_center_x = right_rail_x - kOutputWidth / 2.0; + const int grid_columns = std::max(kMinimumLogicColumns, condition_columns + 2); + const qreal left_rail_x = kSceneMargin + kRailInset; + const qreal right_rail_x = left_rail_x + static_cast(grid_columns) * kCellWidth; + const qreal scene_width = std::max(kMinimumSceneWidth, right_rail_x + kSceneMargin); - qreal top = 24.0; + qreal top = kSceneMargin; int number = 1; for (const LadderRung &rung : logic->rungs) { - const ExpressionSize size = rung.condition.has_value() - ? measureExpression(*rung.condition) : ExpressionSize{240.0, kNodeHeight}; - const qreal height = kRungHeaderHeight + size.height + 28.0; - const qreal center_y = top + kRungHeaderHeight + size.height / 2.0; - scene_->addItem(new RungItem(rung, number, top, height, scene_width)); + const ExpressionMetrics metrics = rung.condition.has_value() + ? measureExpression(*rung.condition) : ExpressionMetrics{}; + const int grid_rows = std::max(1, metrics.rows); + const qreal grid_top = top + kRungHeaderHeight; + const qreal rung_height = kRungHeaderHeight + + static_cast(grid_rows) * kCellHeight + 12.0; + scene_->addItem(new RungItem( + rung, + number, + top, + rung_height, + scene_width - 2.0 * kSceneMargin, + left_rail_x)); + addGrid(*scene_, left_rail_x, grid_top, grid_columns, grid_rows); scene_->addLine( - QLineF(kLeftRailX, top + 30, kLeftRailX, top + height - 12), + QLineF(left_rail_x, grid_top, left_rail_x, + grid_top + static_cast(grid_rows) * kCellHeight), QPen(kLadderColor, 2.4)); scene_->addLine( - QLineF(right_rail_x, top + 30, right_rail_x, top + height - 12), + QLineF(right_rail_x, grid_top, right_rail_x, + grid_top + static_cast(grid_rows) * kCellHeight), QPen(kLadderColor, 2.4)); - QPointF expression_output(kLeftRailX, center_y); + const qreal main_y = grid_top + kCellHeight / 2.0; + QPointF expression_output(left_rail_x, main_y); if (rung.condition.has_value()) { - const QRectF bounds( - kLeftRailX + 24.0, - center_y - size.height / 2.0, - size.width, - size.height); const RenderResult rendered = renderExpression( *scene_, *rung.condition, rung.id, - bounds, + QPointF(left_rail_x, grid_top), + metrics, trace_, runtime_trace_enabled_, fault_node_id_); - const bool active = runtime_trace_enabled_ && traceValue( - trace_, &LogicTraceSnapshot::rungValues, rung.id); - scene_->addLine( - QLineF(QPointF(kLeftRailX, rendered.input.y()), rendered.input), - ladderPen(active)); expression_output = rendered.output; } else { - addPlaceholder(*scene_, QPointF(kLeftRailX + 120.0, center_y), tr("添加条件")); - scene_->addLine(QLineF(kLeftRailX, center_y, kLeftRailX + 62.0, center_y), - ladderPen(false)); - expression_output = QPointF(kLeftRailX + 178.0, center_y); + addPlaceholder( + *scene_, + QRectF(left_rail_x, grid_top, kCellWidth, kCellHeight), + tr("添加条件")); } const bool rung_active = runtime_trace_enabled_ && traceValue( trace_, &LogicTraceSnapshot::rungValues, rung.id); + const qreal output_left = right_rail_x - kCellWidth; scene_->addLine( - QLineF(expression_output, QPointF(output_center_x - kNodeTerminalX, expression_output.y())), + QLineF(expression_output, QPointF(output_left, main_y)), ladderPen(rung_active)); if (rung.output.has_value()) { + scene_->addLine( + QLineF(QPointF(output_left, main_y), QPointF(right_rail_x, main_y)), + ladderPen(rung_active)); scene_->addItem(new NodeItem( *rung.output, rung.id, - {}, - QPointF(output_center_x, expression_output.y()), + QPointF(output_left + kCellWidth / 2.0, main_y), rung_active, rung.output->id == fault_node_id_)); } else { - addPlaceholder(*scene_, QPointF(output_center_x, expression_output.y()), tr("输出线圈")); + addPlaceholder( + *scene_, + QRectF(output_left, grid_top, kCellWidth, kCellHeight), + tr("输出线圈")); + scene_->addLine( + QLineF( + QPointF(output_left + kCellWidth, main_y), + QPointF(right_rail_x, main_y)), + ladderPen(rung_active)); } - scene_->addLine( - QLineF(output_center_x + kNodeTerminalX, - expression_output.y(), - right_rail_x, - expression_output.y()), - ladderPen(rung_active)); - top += height + kRungGap; + top += rung_height + kRungGap; ++number; } - scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + 20.0)); + scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + kSceneMargin)); } void LogicEditorWidget::selectNode(const std::string &node_id) @@ -631,58 +650,74 @@ void LogicEditorWidget::selectNode(const std::string &node_id) node->setSelected(node->nodeId() == node_id); if (node->nodeId() == node_id) { + current_rung_id_ = node->rungId(); ensureVisible(node); } } + else + { + item->setSelected(false); + } } } std::string LogicEditorWidget::selectedNodeId() const { - for (QGraphicsItem *item : scene_->selectedItems()) - { - if (const NodeItem *node = dynamic_cast(item)) - { - return node->nodeId(); - } - } - return {}; + const std::vector ids = selectedNodeIds(); + return ids.size() == 1U ? ids.front() : std::string{}; } -std::string LogicEditorWidget::selectedExpressionId() const +std::vector LogicEditorWidget::selectedNodeIds() const { + std::vector> positioned_ids; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { - return node->expressionId(); + positioned_ids.emplace_back(node->scenePos(), node->nodeId()); } - if (const ExpressionItem *expression = dynamic_cast(item)) + } + std::sort( + positioned_ids.begin(), positioned_ids.end(), + [](const auto &left, const auto &right) { - return expression->expressionId(); - } + if (!qFuzzyCompare(left.first.y(), right.first.y())) + { + return left.first.y() < right.first.y(); + } + return left.first.x() < right.first.x(); + }); + std::vector ids; + ids.reserve(positioned_ids.size()); + for (const auto &positioned_id : positioned_ids) + { + ids.push_back(positioned_id.second); } - return {}; + return ids; } std::string LogicEditorWidget::selectedRungId() const { + std::string rung_id; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { - return node->rungId(); - } - if (const ExpressionItem *expression = dynamic_cast(item)) - { - return expression->rungId(); + if (!rung_id.empty() && rung_id != node->rungId()) + { + return {}; + } + rung_id = node->rungId(); } - if (const RungItem *rung = dynamic_cast(item)) + else if (const RungItem *rung = dynamic_cast(item)) { - return rung->rungId(); + if (rung_id.empty()) + { + rung_id = rung->rungId(); + } } } - return {}; + return rung_id; } LogicEditorResult LogicEditorWidget::addRung() @@ -701,38 +736,27 @@ LogicEditorResult LogicEditorWidget::addRung() return result; } -LogicEditorResult LogicEditorWidget::appendCondition(const LogicNodeConfig &config) -{ - const LogicEditorResult result = editor_service_.appendCondition( - logic_id_, currentRungId(), config); - if (result.succeeded) - { - reloadLogic(); - selectNode(result.id); - emit graphChanged(); - } - else - { - reportFailure(result); - } - return result; -} - -LogicEditorResult LogicEditorWidget::insertCondition( - const LogicNodeConfig &config, SeriesInsertPosition position) +LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) { - const std::string rung_id = selectedRungId(); - const std::string expression_id = selectedExpressionId(); + const std::vector selected_ids = selectedNodeIds(); LogicEditorResult result; - if (rung_id.empty() || expression_id.empty()) + if (selected_ids.size() > 1U) { result = {false, LogicEditorError::InvalidOperation, - "请先选择串联插入目标节点或支路", {}}; + "串联插入时只能选择一个节点", {}}; + } + else if (selected_ids.size() == 1U) + { + const LogicNode *selected_node = editor_service_.findNode( + logic_id_, selected_ids.front()); + result = selected_node != nullptr && selected_node->isCondition() + ? editor_service_.insertConditionAfter( + logic_id_, currentRungId(), selected_ids.front(), config) + : editor_service_.appendCondition(logic_id_, currentRungId(), config); } else { - result = editor_service_.insertCondition( - logic_id_, rung_id, expression_id, position, config); + result = editor_service_.appendCondition(logic_id_, currentRungId(), config); } if (result.succeeded) { @@ -747,20 +771,29 @@ LogicEditorResult LogicEditorWidget::insertCondition( return result; } -LogicEditorResult LogicEditorWidget::addParallelCondition(const LogicNodeConfig &config) +LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config) { const std::string rung_id = selectedRungId(); - const std::string expression_id = selectedExpressionId(); + const std::vector selected_ids = selectedNodeIds(); + std::vector condition_ids; + for (const std::string &node_id : selected_ids) + { + const LogicNode *node = editor_service_.findNode(logic_id_, node_id); + if (node != nullptr && node->isCondition()) + { + condition_ids.push_back(node_id); + } + } LogicEditorResult result; - if (rung_id.empty() || expression_id.empty()) + if (rung_id.empty() || condition_ids.empty()) { result = {false, LogicEditorError::InvalidOperation, - "请先选择要并联的节点或整条支路", {}}; + "请在同一网络中选择要并联的连续节点", {}}; } else { - result = editor_service_.addParallelCondition( - logic_id_, rung_id, expression_id, config); + result = editor_service_.addParallelBranch( + logic_id_, rung_id, condition_ids, config); } if (result.succeeded) { @@ -794,31 +827,33 @@ LogicEditorResult LogicEditorWidget::setOutput(const LogicNodeConfig &config) LogicEditorResult LogicEditorWidget::deleteSelected() { + const std::vector node_ids = selectedNodeIds(); LogicEditorResult result; - const std::string node_id = selectedNodeId(); - const std::string expression_id = selectedExpressionId(); - const std::string rung_id = selectedRungId(); - if (!node_id.empty()) - { - result = editor_service_.removeNode(logic_id_, node_id); - } - else if (!expression_id.empty()) + if (!node_ids.empty()) { - result = editor_service_.removeExpression(logic_id_, rung_id, expression_id); + for (const std::string &node_id : node_ids) + { + result = editor_service_.removeNode(logic_id_, node_id); + if (!result.succeeded) + { + break; + } + } } - else if (!rung_id.empty()) + else { + const std::string rung_id = selectedRungId(); + if (rung_id.empty()) + { + return {false, LogicEditorError::InvalidOperation, + "请先选择要删除的逻辑节点或网络", {}}; + } result = editor_service_.removeRung(logic_id_, rung_id); if (result.succeeded && current_rung_id_ == rung_id) { current_rung_id_ = editor_service_.firstRungId(logic_id_); } } - else - { - return {false, LogicEditorError::InvalidOperation, - "请先选择要删除的逻辑节点、支路或网络", {}}; - } if (result.succeeded) { reloadLogic(); diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index d231df4..e975dcc 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -7,6 +7,7 @@ #include #include +#include class QGraphicsScene; class QResizeEvent; @@ -17,7 +18,6 @@ class LogicEditorWidget final : public QGraphicsView public: class NodeItem; - class ExpressionItem; class RungItem; explicit LogicEditorWidget( @@ -33,14 +33,12 @@ public: void reloadLogic(); void selectNode(const std::string &node_id); std::string selectedNodeId() const; - std::string selectedExpressionId() const; + std::vector selectedNodeIds() const; std::string selectedRungId() const; LogicEditorResult addRung(); - LogicEditorResult appendCondition(const LogicNodeConfig &config); - LogicEditorResult insertCondition( - const LogicNodeConfig &config, SeriesInsertPosition position); - LogicEditorResult addParallelCondition(const LogicNodeConfig &config); + LogicEditorResult addCondition(const LogicNodeConfig &config); + LogicEditorResult addParallelBranch(const LogicNodeConfig &config); LogicEditorResult setOutput(const LogicNodeConfig &config); LogicEditorResult deleteSelected(); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 6936d05..496caf9 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -206,14 +208,51 @@ void MainWindow::configureActions() connect(ui_->deleteControlAction, &QAction::triggered, this, &MainWindow::deleteSelectedControl); - logic_insert_action_group_ = new QActionGroup(this); - logic_insert_action_group_->setExclusive(true); - logic_insert_action_group_->addAction(ui_->appendInsertAction); - logic_insert_action_group_->addAction(ui_->beforeInsertAction); - logic_insert_action_group_->addAction(ui_->afterInsertAction); - logic_insert_action_group_->addAction(ui_->parallelInsertAction); connect(ui_->addRungAction, &QAction::triggered, this, &MainWindow::addLogicRung); + connect(ui_->parallelInsertAction, &QAction::triggered, + this, + [this] + { + addLogicParallelBranch(ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 0}, + ContactMode::NormallyOpen}); + }); + QMenu *parallel_menu = new QMenu(this); + QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点")); + QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点")); + QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件")); + connect(parallel_open, &QAction::triggered, + this, + [this] + { + addLogicParallelBranch(ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 0}, + ContactMode::NormallyOpen}); + }); + connect(parallel_closed, &QAction::triggered, + this, + [this] + { + addLogicParallelBranch(ContactNodeConfig{ + RegisterAddress{RegisterArea::M, 0}, + ContactMode::NormallyClosed}); + }); + connect(parallel_compare, &QAction::triggered, + this, + [this] + { + addLogicParallelBranch(CompareNodeConfig{ + RegisterAddress{RegisterArea::D, 0}, + ComparisonOperator::Equal, + 0}); + }); + ui_->parallelInsertAction->setMenu(parallel_menu); + if (QToolButton *parallel_button = qobject_cast( + ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction))) + { + parallel_button->setPopupMode(QToolButton::MenuButtonPopup); + } connect(ui_->addNormallyOpenAction, &QAction::triggered, this, [this] @@ -315,9 +354,6 @@ void MainWindow::configureAppearance() style()->standardIcon(QStyle::SP_FileDialogContentsView)); ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); - ui_->appendInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); - ui_->beforeInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft)); - ui_->afterInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); @@ -352,7 +388,8 @@ void MainWindow::configureAppearance() "QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }" "QToolButton:checked { background: #dcece3; border-color: #75a58a; }" "QDockWidget { color: #24313b; font-weight: 600; }" - "QDockWidget::title { background: #e9edf0; padding: 6px; border-bottom: 1px solid #cbd2d9; }" + "QDockWidget::title { background: #e9edf0; padding: 6px;" + " border-bottom: 1px solid #cbd2d9; }" "QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }" "QTabWidget::pane { border: 0; background: #f3f5f7; }" "QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }" @@ -924,37 +961,32 @@ void MainWindow::applySelectedControlProperties() void MainWindow::addLogicCondition(const LogicNodeConfig &config) { - LogicEditorResult result; - if (ui_->beforeInsertAction->isChecked()) - { - result = logic_editor_widget_->insertCondition( - config, SeriesInsertPosition::Before); - } - else if (ui_->afterInsertAction->isChecked()) - { - result = logic_editor_widget_->insertCondition( - config, SeriesInsertPosition::After); - } - else if (ui_->parallelInsertAction->isChecked()) - { - result = logic_editor_widget_->addParallelCondition(config); - } - else - { - result = logic_editor_widget_->appendCondition(config); - } + const LogicEditorResult result = logic_editor_widget_->addCondition(config); if (!result.succeeded) { showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false); return; } - ui_->appendInsertAction->setChecked(true); selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); statusBar()->showMessage(tr("已添加逻辑条件"), 3000); } +void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config) +{ + const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config); + if (!result.succeeded) + { + showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false); + return; + } + selected_logic_node_id_ = result.id; + showLogicNodeProperties(result.id); + refreshProjectUi(); + statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000); +} + void MainWindow::setLogicOutput(const LogicNodeConfig &config) { const LogicEditorResult result = logic_editor_widget_->setOutput(config); diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index d1a4992..c00781a 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -90,8 +90,10 @@ private: void deleteSelectedControl(); // 将属性表单内容应用到当前选中控件 void applySelectedControlProperties(); - // 向当前网络添加串联条件,或按当前插入模式添加并联条件 + // 在当前光标节点后添加串联条件,没有选择时追加到网络末尾 void addLogicCondition(const LogicNodeConfig &config); + // 为当前选中的连续逻辑范围建立并联支路 + void addLogicParallelBranch(const LogicNodeConfig &config); // 设置当前网络右侧的输出线圈 void setLogicOutput(const LogicNodeConfig &config); // 新增一个梯形图网络 @@ -152,7 +154,6 @@ private: LogicEditorService &logic_editor_service_; HmiRuntimeService &hmi_runtime_service_; QActionGroup *mode_action_group_ = nullptr; - QActionGroup *logic_insert_action_group_ = nullptr; HmiEditorWidget *hmi_editor_widget_ = nullptr; LogicEditorWidget *logic_editor_widget_ = nullptr; QTimer *runtime_refresh_timer_ = nullptr; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index da52ee7..25bc75b 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -288,9 +288,6 @@ TopToolBarArea false - - - @@ -572,10 +569,7 @@ 数值输入添加数值输入控件 删除删除当前控件 新建网络在梯形图末尾新增网络 - truetrue末尾追加将下一个条件追加到当前网络末尾 - true前插将下一个条件串联到选中节点或支路之前 - true后插将下一个条件串联到选中节点或支路之后 - true并联插入将下一个条件并联到选中节点或整条支路 + 并联支路为画布中选中的连续节点建立并联支路 常开向当前网络添加常开触点 常闭向当前网络添加常闭触点 线圈设置当前网络的普通线圈 diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index e075448..6fb4ea0 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -52,8 +52,8 @@ void testStructuredEditingAndNormalization() "two appended nodes must form a series expression"); const std::string second_expression_id = second.id; - const LogicEditorResult parallel = service.addParallelCondition( - logic_id, rung_id, second_expression_id, contact(2)); + const LogicEditorResult parallel = service.addParallelBranch( + logic_id, rung_id, {second_expression_id}, contact(2)); require(parallel.succeeded, "parallel insertion must succeed"); rung = service.findRung(logic_id, rung_id); const ConditionExpression *parallel_expression = service.findExpression( @@ -62,11 +62,10 @@ void testStructuredEditingAndNormalization() && parallel_expression->kind == ConditionExpressionKind::Parallel, "selected node must become a parallel expression"); - const LogicEditorResult nested_series = service.insertCondition( + const LogicEditorResult nested_series = service.insertConditionAfter( logic_id, rung_id, parallel.id, - SeriesInsertPosition::After, contact(3)); require(nested_series.succeeded, "a parallel branch must accept a series node"); rung = service.findRung(logic_id, rung_id); @@ -103,7 +102,7 @@ void testStructuredEditingAndNormalization() "structured editing result must remain a valid draft"); } -void testBranchLevelParallelInsertion() +void testRangeParallelInsertion() { TestProjectStorage storage; ProjectService project_service(storage); @@ -113,14 +112,24 @@ void testBranchLevelParallelInsertion() service.appendCondition(logic_id, rung_id, contact(0)); service.appendCondition(logic_id, rung_id, contact(1)); - const std::string series_id = service.findRung(logic_id, rung_id)->condition->id; - const LogicEditorResult branch = service.addParallelCondition( - logic_id, rung_id, series_id, contact(2)); - require(branch.succeeded, "a whole series branch must accept a parallel condition"); + service.appendCondition(logic_id, rung_id, contact(2)); + const LogicEditorResult branch = service.addParallelBranch( + logic_id, rung_id, {"contact-2", "contact-3"}, contact(3)); + require(branch.succeeded, "a continuous series range must accept a parallel branch"); const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition; - require(root.kind == ConditionExpressionKind::Parallel - && root.children.front().kind == ConditionExpressionKind::Series, - "branch-level insertion must express (A AND B) OR C"); + require(root.kind == ConditionExpressionKind::Series + && root.children.size() == 2U + && root.children.at(1).kind == ConditionExpressionKind::Parallel + && root.children.at(1).children.front().kind + == ConditionExpressionKind::Series, + "range insertion must express A AND ((B AND C) OR D)"); + require(root.validate(), "range insertion must preserve normalized topology"); + + const LogicEditorResult invalid = service.addParallelBranch( + logic_id, rung_id, {"contact-1", "contact-3"}, contact(4)); + require(!invalid.succeeded + && invalid.error == LogicEditorError::InvalidOperation, + "a non-contiguous selection must be rejected"); } } // namespace @@ -130,7 +139,7 @@ int main() try { testStructuredEditingAndNormalization(); - testBranchLevelParallelInsertion(); + testRangeParallelInsertion(); } catch (const std::exception &error) { diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 1baf611..8e7b78d 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -126,7 +126,6 @@ void testModeActionsControlEditingAvailability() add_normally_open_action->trigger(); parallel_insert_action->trigger(); - add_normally_open_action->trigger(); add_normal_coil_action->trigger(); const ControlLogic *logic = logic_editor_service.findLogic( logic_editor_service.firstLogicId()); @@ -136,10 +135,9 @@ void testModeActionsControlEditingAvailability() && logic->rungs.front().condition->kind == ConditionExpressionKind::Parallel && logic->rungs.front().condition->children.size() == 2U, - "parallel insert mode must create a parallel expression"); + "parallel branch action must create a parallel expression"); require(logic->rungs.front().output.has_value(), "coil action must set the fixed ladder output"); - offline_action->trigger(); require(mode_service.mode() == ApplicationMode::Editing, "unconfigured ladder nodes must block offline running");