#include "logic_editor_widget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr qreal kLabelWidth = 60.0; constexpr qreal kCellWidth = 96.0; constexpr qreal kOutputWidth = 224.0; constexpr qreal kRowHeight = 78.0; constexpr qreal kCommentBandHeight = 28.0; constexpr qreal kTop = 18.0; constexpr qreal kLeftBus = kLabelWidth; constexpr qreal kConditionRight = kLabelWidth + ProjectLimits::kMaximumConditionColumns * kCellWidth; constexpr qreal kRightBus = kConditionRight + kOutputWidth; constexpr qreal kSceneRightMargin = 24.0; const QColor kCanvasBackground(QStringLiteral("#ffffff")); const QColor kGridColor(QStringLiteral("#d8e0e4")); const QColor kLadderColor(QStringLiteral("#263842")); const QColor kActiveColor(QStringLiteral("#16854f")); const QColor kFaultColor(QStringLiteral("#c5362e")); const QColor kCommentColor(QStringLiteral("#16854f")); const QColor kSelectionFill(QStringLiteral("#dfeef5")); const QColor kSelectionBorder(QStringLiteral("#277da1")); QString registerAddressText(const RegisterAddress &address) { return QString::fromStdString(address.toString()); } QString wordOperandText(const WordOperand &operand) { return operand.kind == WordOperandKind::Register ? registerAddressText(operand.address) : QString::number(operand.constant); } QString comparisonText(ComparisonOperator comparison) { switch (comparison) { case ComparisonOperator::Equal: return QStringLiteral("="); case ComparisonOperator::NotEqual: return QStringLiteral("<>"); case ComparisonOperator::LessThan: return QStringLiteral("<"); case ComparisonOperator::LessThanOrEqual: return QStringLiteral("<="); case ComparisonOperator::GreaterThan: return QStringLiteral(">"); case ComparisonOperator::GreaterThanOrEqual: return QStringLiteral(">="); } return QStringLiteral("?"); } QString logicCommandText(const LogicNodeConfig &config) { return std::visit( [](const auto &value) -> QString { using Config = std::decay_t; if constexpr (std::is_same_v) { return QStringLiteral("%1 %2") .arg(value.mode == ContactMode::NormallyOpen ? QStringLiteral("LD") : QStringLiteral("LDI")) .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { return QStringLiteral("%1 %2") .arg(value.mode == EdgeMode::Rising ? QStringLiteral("LDP") : QStringLiteral("LDF")) .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { const QString mnemonic = value.mode == CoilMode::Set ? QStringLiteral("SET") : value.mode == CoilMode::Reset ? QStringLiteral("RST") : QStringLiteral("OUT"); return QStringLiteral("%1 %2") .arg(mnemonic) .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { return QStringLiteral("LD%1 %2 %3") .arg(comparisonText(value.comparison)) .arg(registerAddressText(value.address)) .arg(value.value); } else if constexpr (std::is_same_v) { return QStringLiteral("MOV %1 %2") .arg(wordOperandText(value.source)) .arg(registerAddressText(value.destination)); } else { return QStringLiteral("%1 %2 %3 %4") .arg(value.operation == ArithmeticOperation::Add ? QStringLiteral("ADD") : QStringLiteral("SUB")) .arg(wordOperandText(value.left)) .arg(wordOperandText(value.right)) .arg(registerAddressText(value.destination)); } }, config); } QString commandCompletionPrefix(const QString &text) { return text.section(QRegularExpression(QStringLiteral("\\s+")), 0, 0) .toUpper(); } QPen ladderPen(bool active, bool faulted = false) { QPen pen(faulted ? kFaultColor : active ? kActiveColor : kLadderColor); pen.setWidthF(active || faulted ? 2.5 : 1.8); pen.setJoinStyle(Qt::MiterJoin); pen.setCapStyle(Qt::SquareCap); return pen; } bool containsId( const std::vector &ids, const std::string &candidate) { return std::find(ids.cbegin(), ids.cend(), candidate) != ids.cend(); } bool containsCell( const std::vector> &cells, const std::pair &candidate) { return std::find(cells.cbegin(), cells.cend(), candidate) != cells.cend(); } struct GridRow { qreal top = 0.0; }; class GridLayerItem final : public QGraphicsItem { public: explicit GridLayerItem(std::vector rows) : rows_(std::move(rows)) { setZValue(0.0); } QRectF boundingRect() const override { if (rows_.empty()) { return {}; } return { kLeftBus, rows_.front().top, kRightBus - kLeftBus, rows_.back().top + kRowHeight - rows_.front().top}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { painter->setRenderHint(QPainter::Antialiasing, false); QPen grid_pen(kGridColor); grid_pen.setWidthF(1.0); grid_pen.setCosmetic(true); painter->setPen(grid_pen); for (const GridRow &row : rows_) { painter->fillRect( QRectF( kLeftBus, row.top, kRightBus - kLeftBus, kRowHeight), kCanvasBackground); painter->drawRect( QRectF( kLeftBus, row.top, kRightBus - kLeftBus, kRowHeight)); for (int boundary = 1; boundary <= ProjectLimits::kMaximumConditionColumns; ++boundary) { const qreal x = kLeftBus + static_cast(boundary) * kCellWidth; painter->drawLine( QPointF(x, row.top), QPointF(x, row.top + kRowHeight)); } } } private: std::vector rows_; }; class CellContentItem final : public QGraphicsItem { public: CellContentItem( const LadderCell &cell, const std::string &rung_id, int column, const QPointF &top_left, bool input_active, bool output_active, bool faulted) : cell_(cell), input_active_(input_active), output_active_(output_active), faulted_(faulted) { setPos(top_left); setZValue(10.0); setData(0, QStringLiteral("cell")); setData(1, QString::fromStdString(rung_id)); setData(2, column); setData(3, QString::fromStdString( cell.node.has_value() ? cell.node->id : cell.id)); setData(4, static_cast(cell.kind)); setData(5, QString::fromStdString(cell.id)); setData(6, input_active_); setData(7, output_active_); } QRectF boundingRect() const override { return {0.0, 0.0, kCellWidth, kRowHeight}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { if (cell_.kind == LadderCellKind::Gap) { return; } painter->setRenderHint(QPainter::Antialiasing, true); const auto use_pen = [this, painter](bool active) { painter->setPen(ladderPen(active, faulted_)); }; const qreal center_x = kCellWidth / 2.0; const qreal center_y = kRowHeight / 2.0 + 6.0; if (cell_.kind == LadderCellKind::Wire || !cell_.node.has_value()) { use_pen(input_active_); painter->drawLine( QPointF(0.0, center_y), QPointF(center_x, center_y)); use_pen(output_active_); painter->drawLine( QPointF(center_x, center_y), QPointF(kCellWidth, center_y)); return; } const LogicNode &node = *cell_.node; QFont font = painter->font(); font.setPointSizeF(8.5); painter->setFont(font); std::visit( [&](const auto &config) { using Config = std::decay_t; if constexpr (std::is_same_v || std::is_same_v) { const qreal left = center_x - 15.0; const qreal right = center_x + 15.0; use_pen(input_active_); painter->drawLine(QPointF(0.0, center_y), QPointF(left, center_y)); use_pen(output_active_); painter->drawLine(QPointF(right, center_y), QPointF(kCellWidth, center_y)); painter->drawLine(QPointF(left, center_y - 14.0), QPointF(left, center_y + 14.0)); painter->drawLine(QPointF(right, center_y - 14.0), QPointF(right, center_y + 14.0)); if constexpr (std::is_same_v) { if (config.mode == ContactMode::NormallyClosed) { painter->drawLine( QPointF(left - 4.0, center_y + 17.0), QPointF(right + 4.0, center_y - 17.0)); } } else { painter->drawText( QRectF(center_x - 13.0, center_y - 12.0, 26.0, 24.0), Qt::AlignCenter, config.mode == EdgeMode::Rising ? QStringLiteral("P") : QStringLiteral("N")); } painter->drawText( QRectF(2.0, 3.0, kCellWidth - 4.0, 20.0), Qt::AlignCenter, node.configured ? registerAddressText(config.address) : QStringLiteral("")); } else if constexpr (std::is_same_v) { const QRectF box(7.0, center_y - 15.0, kCellWidth - 14.0, 30.0); use_pen(input_active_); painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y)); use_pen(output_active_); painter->drawLine(QPointF(box.right(), center_y), QPointF(kCellWidth, center_y)); painter->drawRect(box); QFont compare_font = painter->font(); compare_font.setPointSizeF(7.5); painter->setFont(compare_font); painter->drawText( box.adjusted(2.0, 0.0, -2.0, 0.0), Qt::AlignCenter, node.configured ? QStringLiteral("%1 %2 %3") .arg(registerAddressText(config.address)) .arg(comparisonText(config.comparison)) .arg(config.value) : QStringLiteral("<比较>")); } else { use_pen(input_active_); painter->drawLine( QPointF(0.0, center_y), QPointF(center_x, center_y)); use_pen(output_active_); painter->drawLine( QPointF(center_x, center_y), QPointF(kCellWidth, center_y)); } }, node.config); } private: LadderCell cell_; bool input_active_ = false; bool output_active_ = false; bool faulted_ = false; }; class OutputContentItem final : public QGraphicsItem { public: OutputContentItem( const LadderRung &rung, const QPointF &top_left, bool input_active, bool symbol_active, bool faulted) : rung_id_(rung.id), output_(rung.output), input_active_(input_active), symbol_active_(symbol_active), faulted_(faulted) { setPos(top_left); setZValue(10.0); setData(0, QStringLiteral("output")); setData(1, QString::fromStdString(rung.id)); setData(2, QString::fromStdString( rung.output.has_value() ? rung.output->id : std::string{})); setData(3, input_active_); setData(4, symbol_active_); } QRectF boundingRect() const override { return {0.0, 0.0, kOutputWidth, kRowHeight}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { if (!output_.has_value()) { return; } painter->setRenderHint(QPainter::Antialiasing, true); const auto use_pen = [this, painter](bool active) { painter->setPen(ladderPen(active, faulted_)); }; const qreal center_y = kRowHeight / 2.0 + 6.0; const LogicNode &node = *output_; QFont font = painter->font(); font.setPointSizeF(8.5); painter->setFont(font); std::visit( [&](const auto &config) { using Config = std::decay_t; if constexpr (std::is_same_v) { const qreal center_x = kOutputWidth / 2.0; use_pen(input_active_); painter->drawLine(QPointF(0.0, center_y), QPointF(center_x - 22.0, center_y)); painter->drawLine(QPointF(center_x + 22.0, center_y), QPointF(kOutputWidth, center_y)); use_pen(symbol_active_); QPainterPath left; left.moveTo(center_x - 3.0, center_y - 18.0); left.cubicTo( center_x - 25.0, center_y - 14.0, center_x - 25.0, center_y + 14.0, center_x - 3.0, center_y + 18.0); painter->drawPath(left); QPainterPath right; right.moveTo(center_x + 3.0, center_y - 18.0); right.cubicTo( center_x + 25.0, center_y - 14.0, center_x + 25.0, center_y + 14.0, center_x + 3.0, center_y + 18.0); painter->drawPath(right); if (config.mode != CoilMode::Normal) { painter->drawText( QRectF(center_x - 13.0, center_y - 13.0, 26.0, 26.0), Qt::AlignCenter, config.mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R")); } painter->drawText( QRectF(2.0, 3.0, kOutputWidth - 4.0, 20.0), Qt::AlignCenter, node.configured ? registerAddressText(config.address) : QStringLiteral("")); } else if constexpr (std::is_same_v) { const QRectF box(26.0, 11.0, kOutputWidth - 52.0, kRowHeight - 22.0); use_pen(input_active_); painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y)); painter->drawLine(QPointF(box.right(), center_y), QPointF(kOutputWidth, center_y)); use_pen(symbol_active_); painter->drawRect(box); QFont mnemonic_font = painter->font(); mnemonic_font.setBold(true); painter->setFont(mnemonic_font); painter->drawText( QRectF(box.left(), box.top() + 2.0, box.width(), 20.0), Qt::AlignCenter, QStringLiteral("MOV")); mnemonic_font.setBold(false); mnemonic_font.setPointSizeF(8.0); painter->setFont(mnemonic_font); painter->drawText( QRectF(box.left() + 4.0, box.top() + 23.0, box.width() - 8.0, 20.0), Qt::AlignCenter, node.configured ? QStringLiteral("%1 -> %2") .arg(wordOperandText(config.source)) .arg(registerAddressText(config.destination)) : QStringLiteral("<源> -> <目标>")); } else if constexpr (std::is_same_v) { const QRectF box(18.0, 11.0, kOutputWidth - 36.0, kRowHeight - 22.0); use_pen(input_active_); painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y)); painter->drawLine(QPointF(box.right(), center_y), QPointF(kOutputWidth, center_y)); use_pen(symbol_active_); painter->drawRect(box); QFont mnemonic_font = painter->font(); mnemonic_font.setBold(true); painter->setFont(mnemonic_font); painter->drawText( QRectF(box.left(), box.top() + 2.0, box.width(), 20.0), Qt::AlignCenter, config.operation == ArithmeticOperation::Add ? QStringLiteral("ADD") : QStringLiteral("SUB")); mnemonic_font.setBold(false); mnemonic_font.setPointSizeF(8.0); painter->setFont(mnemonic_font); painter->drawText( QRectF(box.left() + 4.0, box.top() + 23.0, box.width() - 8.0, 20.0), Qt::AlignCenter, node.configured ? QStringLiteral("%1, %2 -> %3") .arg(wordOperandText(config.left)) .arg(wordOperandText(config.right)) .arg(registerAddressText(config.destination)) : QStringLiteral("<左>, <右> -> <目标>")); } else { use_pen(input_active_); painter->drawLine( QPointF(0.0, center_y), QPointF(kOutputWidth, center_y)); } }, node.config); } private: std::string rung_id_; std::optional output_; bool input_active_ = false; bool symbol_active_ = false; bool faulted_ = false; }; class VerticalConnectionItem final : public QGraphicsItem { public: VerticalConnectionItem( const VerticalConnection &connection, qreal x, qreal top, qreal bottom, bool active) : height_(bottom - top), active_(active) { setPos(x, top); setZValue(20.0); setData(0, QStringLiteral("vertical")); setData(1, QString::fromStdString(connection.id)); setData(2, QString::fromStdString(connection.upperRungId)); setData(3, connection.columnBoundary); setData(4, QString::fromStdString(connection.lowerRungId)); setData(5, active_); } QRectF boundingRect() const override { return {-7.0, 0.0, 14.0, height_}; } QPainterPath shape() const override { QPainterPath path; path.moveTo(0.0, 0.0); path.lineTo(0.0, height_); QPainterPathStroker stroker; stroker.setWidth(12.0); return stroker.createStroke(path); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { painter->setPen(ladderPen(active_)); painter->drawLine(QPointF(0.0, 0.0), QPointF(0.0, height_)); } private: qreal height_ = 0.0; bool active_ = false; }; class SelectionOverlayItem final : public QGraphicsItem { public: explicit SelectionOverlayItem(std::vector rectangles) : rectangles_(std::move(rectangles)) { for (const QRectF &rectangle : rectangles_) { bounds_ = bounds_.isNull() ? rectangle : bounds_.united(rectangle); } bounds_.adjust(-2.0, -2.0, 2.0, 2.0); setZValue(100.0); } QRectF boundingRect() const override { return bounds_; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { painter->setRenderHint(QPainter::Antialiasing, false); QPen pen(kSelectionBorder, 1.4, Qt::DashLine); pen.setCosmetic(true); painter->setPen(pen); painter->setBrush(QColor( kSelectionFill.red(), kSelectionFill.green(), kSelectionFill.blue(), 90)); for (const QRectF &rectangle : rectangles_) { painter->drawRect(rectangle.adjusted(2.5, 2.5, -2.5, -2.5)); } } private: std::vector rectangles_; QRectF bounds_; }; } // namespace LogicEditorWidget::LogicEditorWidget( LogicEditorService &editor_service, QWidget *parent) : QGraphicsView(parent), editor_service_(editor_service), command_service_(editor_service), scene_(new QGraphicsScene(this)) { setScene(scene_); setRenderHint(QPainter::Antialiasing, true); setBackgroundBrush(kCanvasBackground); setDragMode(QGraphicsView::NoDrag); setAlignment(Qt::AlignLeft | Qt::AlignTop); selection_band_ = new QRubberBand(QRubberBand::Rectangle, viewport()); selection_band_->hide(); command_editor_ = new QLineEdit(viewport()); command_editor_->setObjectName(QStringLiteral("logicCommandInput")); command_editor_->setPlaceholderText( tr("输入 PLC 指令,例如 LD M0、AND M1、OUT M10")); command_editor_->setClearButtonEnabled(true); command_editor_->setVisible(false); command_editor_->setToolTip( tr("触点使用 M 地址,比较和数据指令使用 D 地址")); command_completer_ = new QCompleter(command_editor_); auto *command_model = new QStandardItemModel(0, 3, command_completer_); command_model->setHeaderData(0, Qt::Horizontal, tr("指令")); command_model->setHeaderData(1, Qt::Horizontal, tr("操作数")); command_model->setHeaderData(2, Qt::Horizontal, tr("说明")); for (const LogicCommandSuggestion &suggestion : LogicCommandService::suggestions()) { QList row{ new QStandardItem(QString::fromStdString(suggestion.mnemonic)), new QStandardItem(QString::fromStdString(suggestion.operand_hint)), new QStandardItem(QString::fromStdString(suggestion.description))}; for (QStandardItem *item : row) { item->setEnabled(suggestion.supported); item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); } command_model->appendRow(row); } command_completer_->setModel(command_model); command_completer_->setCompletionColumn(0); command_completer_->setCaseSensitivity(Qt::CaseInsensitive); command_completer_->setCompletionMode(QCompleter::PopupCompletion); auto *command_popup = new QTreeView; command_popup->setRootIsDecorated(false); command_popup->setItemsExpandable(false); command_popup->setHeaderHidden(false); command_popup->setUniformRowHeights(true); command_popup->setAlternatingRowColors(true); command_popup->setMinimumSize(620, 300); command_completer_->setPopup(command_popup); command_popup->header()->setStretchLastSection(true); command_popup->setColumnWidth(0, 96); command_popup->setColumnWidth(1, 190); command_editor_->setCompleter(command_completer_); connect( command_editor_, &QLineEdit::textEdited, this, [this](const QString &text) { command_completer_->setCompletionPrefix( commandCompletionPrefix(text)); command_completer_->complete(command_editor_->rect()); }); connect( command_editor_, &QLineEdit::returnPressed, this, &LogicEditorWidget::commitCommandInput); command_editor_->installEventFilter(this); } void LogicEditorWidget::setLogicId(const std::string &logic_id) { const bool logic_changed = logic_id_ != logic_id; clearGesture(); logic_id_ = logic_id; if (logic_changed) { cancelCommandInput(); selected_rung_id_.clear(); selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; selected_vertical_connection_id_.clear(); } else if (!selected_rung_id_.empty() && editor_service_.findRung(logic_id_, selected_rung_id_) == nullptr) { selected_rung_id_.clear(); selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; selected_vertical_connection_id_.clear(); } else { if (!selected_vertical_connection_id_.empty() && editor_service_.findConnection( logic_id_, selected_vertical_connection_id_) == nullptr) { selected_vertical_connection_id_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; } selected_cells_.erase( std::remove_if( selected_cells_.begin(), selected_cells_.end(), [this](const auto &position) { const LadderCell *cell = editor_service_.findCell( logic_id_, position.first, position.second); return cell == nullptr || cell->kind == LadderCellKind::Gap; }), selected_cells_.end()); selected_output_rung_ids_.erase( std::remove_if( selected_output_rung_ids_.begin(), selected_output_rung_ids_.end(), [this](const std::string &rung_id) { const LadderRung *rung = editor_service_.findRung( logic_id_, rung_id); return rung == nullptr || !rung->output.has_value(); }), selected_output_rung_ids_.end()); selected_vertical_connection_ids_.erase( std::remove_if( selected_vertical_connection_ids_.begin(), selected_vertical_connection_ids_.end(), [this](const std::string &connection_id) { return editor_service_.findConnection( logic_id_, connection_id) == nullptr; }), selected_vertical_connection_ids_.end()); selected_row_ids_.erase( std::remove_if( selected_row_ids_.begin(), selected_row_ids_.end(), [this](const std::string &rung_id) { return editor_service_.findRung(logic_id_, rung_id) == nullptr; }), selected_row_ids_.end()); synchronizeSelectedNodes(); selected_vertical_connection_id_ = selected_vertical_connection_ids_.empty() ? std::string{} : selected_vertical_connection_ids_.back(); if (selected_cell_ && editor_service_.findCell( logic_id_, selected_rung_id_, selected_column_) == nullptr) { selected_column_ = -1; selected_cell_ = false; } } rebuildScene(); } void LogicEditorWidget::setEditingEnabled(bool enabled) { clearGesture(); editing_enabled_ = enabled; if (!enabled) { cancelCommandInput(); mouse_wire_mode_ = MouseWireMode::Select; selection_pressed_ = false; selection_dragging_ = false; selection_band_->hide(); } rebuildScene(); } void LogicEditorWidget::setMouseWireMode(MouseWireMode mode) { clearGesture(); mouse_wire_mode_ = editing_enabled_ ? mode : MouseWireMode::Select; selection_pressed_ = false; selection_dragging_ = false; selection_band_->hide(); viewport()->setCursor(mouse_wire_mode_ == MouseWireMode::Select ? Qt::ArrowCursor : Qt::CrossCursor); } LogicEditorWidget::MouseWireMode LogicEditorWidget::mouseWireMode() const { return mouse_wire_mode_; } void LogicEditorWidget::setRuntimeTrace( const LogicTraceSnapshot &trace, const std::string &fault_node_id) { trace_ = trace.forLogic(logic_id_); fault_node_id_ = fault_node_id; runtime_trace_enabled_ = true; rebuildScene(); } void LogicEditorWidget::clearRuntimeTrace() { trace_.clear(); fault_node_id_.clear(); runtime_trace_enabled_ = false; rebuildScene(); } void LogicEditorWidget::reloadLogic() { setLogicId(logic_id_); } void LogicEditorWidget::clearSelection() { selected_rung_id_.clear(); selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; selected_vertical_connection_id_.clear(); rebuildScene(); emit nodeSelected(QString{}); } void LogicEditorWidget::selectNode(const std::string &node_id) { selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; selected_vertical_connection_id_.clear(); if (!node_id.empty()) { selected_node_ids_.push_back(node_id); const std::string rung_id = editor_service_.rungIdForNode( logic_id_, node_id); const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id); if (rung != nullptr) { selected_rung_id_ = rung_id; for (std::size_t index = 0U; index < rung->cells.size(); ++index) { if (rung->cells[index].node.has_value() && rung->cells[index].node->id == node_id) { selected_column_ = static_cast(index); selected_cell_ = true; selected_cells_.push_back({ rung_id, static_cast(index)}); break; } } if (!selected_cell_ && rung->output.has_value() && rung->output->id == node_id) { selected_output_rung_ids_.push_back(rung_id); } } } rebuildScene(); emit nodeSelected(QString::fromStdString(node_id)); } void LogicEditorWidget::focusSyntaxLocation( const std::string &rung_id, int column) { const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id); if (rung == nullptr) { return; } cancelCommandInput(); selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_vertical_connection_id_.clear(); selected_rung_id_ = rung_id; selected_boundary_ = false; const bool output = column >= ProjectLimits::kMaximumLadderColumns; selected_output_ = output; selected_cell_ = !output; selected_column_ = output ? ProjectLimits::kMaximumConditionColumns : std::max(0, std::min( column - 1, ProjectLimits::kMaximumConditionColumns - 1)); if (output) { selected_output_rung_ids_.push_back(rung_id); if (rung->output.has_value()) { selected_node_ids_.push_back(rung->output->id); } } else { const LadderCell &cell = rung->cells[ static_cast(selected_column_)]; if (cell.kind != LadderCellKind::Gap) { selected_cells_.push_back({rung_id, selected_column_}); } if (cell.node.has_value()) { selected_node_ids_.push_back(cell.node->id); } } rebuildScene(); notifySelectionChanged(); const RowLayout *layout = layoutForRung(rung_id); if (layout != nullptr) { const qreal x = output ? kConditionRight : kLeftBus + static_cast(selected_column_) * kCellWidth; ensureVisible( QRectF( x, layout->gridTop, output ? kOutputWidth : kCellWidth, kRowHeight), 24, 24); } setFocus(Qt::OtherFocusReason); } std::string LogicEditorWidget::selectedNodeId() const { return selected_node_ids_.empty() ? std::string{} : selected_node_ids_.front(); } std::vector LogicEditorWidget::selectedNodeIds() const { return selected_node_ids_; } std::string LogicEditorWidget::selectedRungId() const { return !selected_rung_id_.empty() && editor_service_.findRung(logic_id_, selected_rung_id_) != nullptr ? selected_rung_id_ : std::string{}; } bool LogicEditorWidget::hasCopyableSelection() const { return !selected_cells_.empty() || !selected_output_rung_ids_.empty() || !selected_vertical_connection_ids_.empty() || !selected_row_ids_.empty(); } LogicClipboardCopyResult LogicEditorWidget::copySelection() const { LogicSelectionCopyRequest selection; selection.cells = selected_cells_; selection.outputRungIds = selected_output_rung_ids_; selection.verticalConnectionIds = selected_vertical_connection_ids_; selection.wholeRungIds = selected_row_ids_; return editor_service_.copySelection(logic_id_, selection); } LogicPasteTarget LogicEditorWidget::pasteTarget() const { LogicPasteTarget target; target.rungId = selected_rung_id_.empty() ? editor_service_.firstRungId(logic_id_) : selected_rung_id_; target.output = selected_output_; target.boundary = selected_boundary_; target.column = selected_output_ ? ProjectLimits::kMaximumConditionColumns : std::max(selected_column_, 0); return target; } std::string LogicEditorWidget::currentRungId() const { if (!selected_rung_id_.empty() && editor_service_.findRung(logic_id_, selected_rung_id_) != nullptr) { return selected_rung_id_; } return editor_service_.firstRungId(logic_id_); } int LogicEditorWidget::rowAt(const std::string &rung_id) const { const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { return -1; } for (std::size_t index = 0U; index < logic->rungs.size(); ++index) { if (logic->rungs[index].id == rung_id) { return static_cast(index); } } return -1; } void LogicEditorWidget::rebuildRowLayout(const ControlLogic &logic) { row_layouts_.clear(); qreal next_top = kTop; for (std::size_t row = 0U; row < logic.rungs.size(); ++row) { const bool network_head = logic.networkHeadIndex(row) == row; if (network_head) { next_top += kCommentBandHeight; } RowLayout layout; layout.rungId = logic.rungs[row].id; layout.row = static_cast(row); layout.gridTop = next_top; layout.centerY = next_top + kRowHeight / 2.0 + 6.0; layout.bottom = next_top + kRowHeight; layout.networkHead = network_head; row_layouts_.push_back(std::move(layout)); next_top += kRowHeight; } } const LogicEditorWidget::RowLayout *LogicEditorWidget::layoutForRung( const std::string &rung_id) const { const auto found = std::find_if( row_layouts_.cbegin(), row_layouts_.cend(), [&rung_id](const RowLayout &layout) { return layout.rungId == rung_id; }); return found == row_layouts_.cend() ? nullptr : &*found; } LogicEditorWidget::Hit LogicEditorWidget::hitAt( const QPointF &scene_position) const { const QList items = scene_->items(scene_position); const auto hit_for_type = [&items](const QString &wanted) -> Hit { for (QGraphicsItem *item : items) { if (item->data(0).toString() != wanted) { continue; } Hit hit; if (wanted == QStringLiteral("vertical")) { hit.rungId = item->data(2).toString().toStdString(); hit.column = item->data(3).toInt(); hit.vertical = true; hit.lowerRungId = item->data(4).toString().toStdString(); hit.objectId = item->data(1).toString().toStdString(); } else if (wanted == QStringLiteral("boundary")) { hit.rungId = item->data(1).toString().toStdString(); hit.column = item->data(2).toInt(); hit.boundary = true; } else if (wanted == QStringLiteral("cell")) { hit.rungId = item->data(1).toString().toStdString(); hit.column = item->data(2).toInt(); hit.objectId = item->data(3).toString().toStdString(); hit.cellKind = static_cast(item->data(4).toInt()); } else if (wanted == QStringLiteral("output")) { hit.rungId = item->data(1).toString().toStdString(); hit.column = ProjectLimits::kMaximumConditionColumns; hit.output = true; hit.objectId = item->data(2).toString().toStdString(); } else if (wanted == QStringLiteral("rowHeader")) { hit.rungId = item->data(1).toString().toStdString(); hit.rowHeader = true; } return hit; } return {}; }; Hit hit = hit_for_type(QStringLiteral("rowHeader")); if (!hit.rungId.empty()) { return hit; } hit = hit_for_type(QStringLiteral("vertical")); if (!hit.rungId.empty()) { return hit; } hit = hit_for_type(QStringLiteral("boundary")); if (!hit.rungId.empty()) { return hit; } hit = hit_for_type(QStringLiteral("cell")); if (!hit.rungId.empty()) { return hit; } hit = hit_for_type(QStringLiteral("output")); if (!hit.rungId.empty()) { return hit; } return {}; } void LogicEditorWidget::rebuildScene() { scene_->clear(); row_layouts_.clear(); const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { scene_->setSceneRect(0.0, 0.0, kRightBus + kSceneRightMargin, 120.0); return; } rebuildRowLayout(*logic); const qreal height = row_layouts_.empty() ? 120.0 : row_layouts_.back().bottom + 24.0; scene_->setSceneRect( 0.0, 0.0, kRightBus + kSceneRightMargin, height); if (row_layouts_.empty()) { return; } std::vector grid_rows; grid_rows.reserve(logic->rungs.size()); for (std::size_t row = 0U; row < logic->rungs.size(); ++row) { const RowLayout &layout = row_layouts_[row]; GridRow grid_row; grid_row.top = layout.gridTop; grid_rows.push_back(std::move(grid_row)); } scene_->addItem(new GridLayerItem(std::move(grid_rows))); QPen bus_pen(kLadderColor); bus_pen.setWidthF(2.2); bus_pen.setCosmetic(true); QGraphicsLineItem *left_bus = scene_->addLine( kLeftBus, row_layouts_.front().gridTop, kLeftBus, row_layouts_.back().bottom, bus_pen); left_bus->setZValue(5.0); QGraphicsLineItem *right_bus = scene_->addLine( kRightBus, row_layouts_.front().gridTop, kRightBus, row_layouts_.back().bottom, bus_pen); right_bus->setZValue(5.0); for (std::size_t row = 0U; row < logic->rungs.size(); ++row) { const LadderRung &rung = logic->rungs[row]; const RowLayout &layout = row_layouts_[row]; QGraphicsRectItem *row_header = scene_->addRect( QRectF(0.0, layout.gridTop, kLeftBus - 4.0, kRowHeight), QPen(Qt::NoPen), QBrush(Qt::transparent)); row_header->setData(0, QStringLiteral("rowHeader")); row_header->setData(1, QString::fromStdString(rung.id)); row_header->setZValue(29.0); QGraphicsSimpleTextItem *label = scene_->addSimpleText( QStringLiteral("%1") .arg(static_cast(row), 3, 10, QLatin1Char('0'))); label->setBrush(QColor(QStringLiteral("#66757d"))); label->setPos(10.0, layout.centerY - 10.0); label->setZValue(6.0); if (layout.networkHead && !rung.comment.empty()) { QGraphicsSimpleTextItem *comment = scene_->addSimpleText( QString::fromStdString(rung.comment)); QFont comment_font = comment->font(); comment_font.setPointSizeF(9.0); comment->setFont(comment_font); comment->setBrush(kCommentColor); comment->setPos( kLeftBus + 7.0, layout.gridTop - kCommentBandHeight + 4.0); comment->setZValue(6.0); } for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column) { const LadderCell &cell = rung.cells[static_cast(column)]; const qreal x = kLeftBus + static_cast(column) * kCellWidth; const auto input_power = trace_.cellInputPowerValues.find(cell.id); const bool input_active = runtime_trace_enabled_ && input_power != trace_.cellInputPowerValues.end() && input_power->second; const auto output_power = trace_.cellPowerValues.find(cell.id); const bool output_active = runtime_trace_enabled_ && output_power != trace_.cellPowerValues.end() && output_power->second; const bool faulted = cell.node.has_value() && cell.node->id == fault_node_id_; scene_->addItem(new CellContentItem( cell, rung.id, column, QPointF(x, layout.gridTop), input_active, output_active, faulted)); } for (int boundary = 0; boundary <= ProjectLimits::kMaximumConditionColumns; ++boundary) { const qreal x = kLeftBus + static_cast(boundary) * kCellWidth; QPen boundary_pen(Qt::transparent); boundary_pen.setWidthF(12.0); QGraphicsLineItem *hit_line = scene_->addLine( x, layout.gridTop + 8.0, x, layout.bottom - 8.0, boundary_pen); hit_line->setData(0, QStringLiteral("boundary")); hit_line->setData(1, QString::fromStdString(rung.id)); hit_line->setData(2, boundary); hit_line->setZValue(30.0); } const auto rung_power = trace_.rungValues.find(rung.id); const bool input_active = runtime_trace_enabled_ && rung_power != trace_.rungValues.end() && rung_power->second; bool symbol_active = false; if (rung.output.has_value()) { const auto output_value = trace_.nodeValues.find(rung.output->id); symbol_active = runtime_trace_enabled_ && output_value != trace_.nodeValues.end() && output_value->second; } const bool faulted = rung.output.has_value() && rung.output->id == fault_node_id_; scene_->addItem(new OutputContentItem( rung, QPointF(kConditionRight, layout.gridTop), input_active, symbol_active, faulted)); } for (const VerticalConnection &connection : logic->verticalConnections) { const RowLayout *upper = layoutForRung(connection.upperRungId); const RowLayout *lower = layoutForRung(connection.lowerRungId); if (upper == nullptr || lower == nullptr) { continue; } const qreal x = kLeftBus + static_cast(connection.columnBoundary) * kCellWidth; const auto power = trace_.verticalConnectionValues.find(connection.id); const bool active = runtime_trace_enabled_ && power != trace_.verticalConnectionValues.end() && power->second; scene_->addItem(new VerticalConnectionItem( connection, x, upper->centerY, lower->centerY, active)); } std::vector selection_rectangles; for (const std::string &rung_id : selected_row_ids_) { const RowLayout *layout = layoutForRung(rung_id); if (layout != nullptr) { selection_rectangles.emplace_back( 0.0, layout->gridTop, kRightBus, kRowHeight); } } for (const auto &position : selected_cells_) { const RowLayout *layout = layoutForRung(position.first); if (layout != nullptr && position.second >= 0 && position.second < ProjectLimits::kMaximumConditionColumns) { selection_rectangles.emplace_back( kLeftBus + static_cast(position.second) * kCellWidth, layout->gridTop, kCellWidth, kRowHeight); } } for (const std::string &rung_id : selected_output_rung_ids_) { const RowLayout *layout = layoutForRung(rung_id); if (layout != nullptr) { selection_rectangles.emplace_back( kConditionRight, layout->gridTop, kOutputWidth, kRowHeight); } } for (const std::string &connection_id : selected_vertical_connection_ids_) { const VerticalConnection *connection = editor_service_.findConnection( logic_id_, connection_id); if (connection == nullptr) { continue; } const RowLayout *upper = layoutForRung(connection->upperRungId); const RowLayout *lower = layoutForRung(connection->lowerRungId); if (upper == nullptr || lower == nullptr) { continue; } const qreal x = kLeftBus + static_cast(connection->columnBoundary) * kCellWidth; selection_rectangles.emplace_back( x - 7.0, upper->centerY, 14.0, lower->centerY - upper->centerY); } if (selected_cell_) { const std::pair cursor{ selected_rung_id_, selected_column_}; const LadderCell *cell = editor_service_.findCell( logic_id_, cursor.first, cursor.second); const bool no_object_selection = selected_cells_.empty() && selected_output_rung_ids_.empty() && selected_vertical_connection_ids_.empty(); if (!containsCell(selected_cells_, cursor) && cell != nullptr && (cell->kind == LadderCellKind::Gap || no_object_selection)) { const RowLayout *layout = layoutForRung(cursor.first); if (layout != nullptr) { selection_rectangles.emplace_back( kLeftBus + static_cast(cursor.second) * kCellWidth, layout->gridTop, kCellWidth, kRowHeight); } } } if (selected_output_ && !containsId(selected_output_rung_ids_, selected_rung_id_)) { const RowLayout *layout = layoutForRung(selected_rung_id_); if (layout != nullptr) { selection_rectangles.emplace_back( kConditionRight, layout->gridTop, kOutputWidth, kRowHeight); } } if (!selection_rectangles.empty()) { scene_->addItem(new SelectionOverlayItem( std::move(selection_rectangles))); } } void LogicEditorWidget::clearObjectSelection() { selected_node_ids_.clear(); selected_cells_.clear(); selected_output_rung_ids_.clear(); selected_vertical_connection_ids_.clear(); selected_row_ids_.clear(); selected_vertical_connection_id_.clear(); } void LogicEditorWidget::synchronizeSelectedNodes() { selected_node_ids_.clear(); for (const auto &position : selected_cells_) { const LadderCell *cell = editor_service_.findCell( logic_id_, position.first, position.second); if (cell != nullptr && cell->node.has_value()) { selected_node_ids_.push_back(cell->node->id); } } for (const std::string &rung_id : selected_output_rung_ids_) { const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id); if (rung != nullptr && rung->output.has_value()) { selected_node_ids_.push_back(rung->output->id); } } } void LogicEditorWidget::notifySelectionChanged() { emit nodeSelected(selected_node_ids_.empty() ? QString{} : QString::fromStdString(selected_node_ids_.front())); } void LogicEditorWidget::selectObject( const Hit &hit, bool extend_node_selection) { if (hit.rowHeader) { if (!extend_node_selection) { clearObjectSelection(); selected_row_ids_.clear(); } else if (!selected_cells_.empty() || !selected_output_rung_ids_.empty() || !selected_vertical_connection_ids_.empty()) { clearObjectSelection(); } const auto selected = std::find( selected_row_ids_.begin(), selected_row_ids_.end(), hit.rungId); if (extend_node_selection && selected != selected_row_ids_.end()) { selected_row_ids_.erase(selected); } else if (selected == selected_row_ids_.end()) { selected_row_ids_.push_back(hit.rungId); } std::sort( selected_row_ids_.begin(), selected_row_ids_.end(), [this](const std::string &left, const std::string &right) { return rowAt(left) < rowAt(right); }); selected_rung_id_ = hit.rungId; selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; synchronizeSelectedNodes(); rebuildScene(); notifySelectionChanged(); return; } selected_row_ids_.clear(); if (!extend_node_selection) { clearObjectSelection(); } if (hit.rungId.empty()) { if (!extend_node_selection) { selected_rung_id_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; } rebuildScene(); notifySelectionChanged(); return; } selected_rung_id_ = hit.rungId; selected_column_ = hit.column; selected_cell_ = hit.column >= 0 && !hit.boundary && !hit.output && !hit.vertical; selected_output_ = hit.output; selected_boundary_ = hit.boundary || hit.vertical; if (hit.vertical) { const auto selected = std::find( selected_vertical_connection_ids_.begin(), selected_vertical_connection_ids_.end(), hit.objectId); if (extend_node_selection && selected != selected_vertical_connection_ids_.end()) { selected_vertical_connection_ids_.erase(selected); } else if (selected == selected_vertical_connection_ids_.end()) { selected_vertical_connection_ids_.push_back(hit.objectId); } } else if (hit.output && !hit.objectId.empty()) { const auto selected = std::find( selected_output_rung_ids_.begin(), selected_output_rung_ids_.end(), hit.rungId); if (extend_node_selection && selected != selected_output_rung_ids_.end()) { selected_output_rung_ids_.erase(selected); } else if (selected == selected_output_rung_ids_.end()) { selected_output_rung_ids_.push_back(hit.rungId); } } else if (!hit.boundary && !hit.vertical && hit.cellKind != LadderCellKind::Gap) { const std::pair position{hit.rungId, hit.column}; const auto selected = std::find( selected_cells_.begin(), selected_cells_.end(), position); if (extend_node_selection && selected != selected_cells_.end()) { selected_cells_.erase(selected); } else if (selected == selected_cells_.end()) { selected_cells_.push_back(position); } } selected_vertical_connection_id_ = selected_vertical_connection_ids_.empty() ? std::string{} : selected_vertical_connection_ids_.back(); synchronizeSelectedNodes(); rebuildScene(); notifySelectionChanged(); } void LogicEditorWidget::selectObjectsInBand( const QRect &viewport_rect, bool extend_selection) { if (!selected_row_ids_.empty()) { selected_row_ids_.clear(); clearObjectSelection(); } if (!extend_selection) { clearObjectSelection(); } const QPolygonF scene_polygon = mapToScene(viewport_rect.normalized()); QPainterPath selection_path; selection_path.addPolygon(scene_polygon); selection_path.closeSubpath(); const QList items = scene_->items( selection_path, Qt::IntersectsItemShape, Qt::DescendingOrder); for (QGraphicsItem *item : items) { const QString type = item->data(0).toString(); if (type == QStringLiteral("cell")) { const LadderCellKind kind = static_cast( item->data(4).toInt()); if (kind == LadderCellKind::Gap) { continue; } const std::pair position{ item->data(1).toString().toStdString(), item->data(2).toInt()}; if (!containsCell(selected_cells_, position)) { selected_cells_.push_back(position); } } else if (type == QStringLiteral("output") && !item->data(2).toString().isEmpty()) { const std::string rung_id = item->data(1).toString().toStdString(); if (!containsId(selected_output_rung_ids_, rung_id)) { selected_output_rung_ids_.push_back(rung_id); } } else if (type == QStringLiteral("vertical")) { const std::string connection_id = item->data(1).toString().toStdString(); if (!containsId(selected_vertical_connection_ids_, connection_id)) { selected_vertical_connection_ids_.push_back(connection_id); } } } std::sort( selected_cells_.begin(), selected_cells_.end(), [this](const auto &left, const auto &right) { const int left_row = rowAt(left.first); const int right_row = rowAt(right.first); return left_row != right_row ? left_row < right_row : left.second < right.second; }); std::sort( selected_output_rung_ids_.begin(), selected_output_rung_ids_.end(), [this](const std::string &left, const std::string &right) { return rowAt(left) < rowAt(right); }); const Hit center_hit = hitAt(mapToScene(viewport_rect.center())); if (!center_hit.rungId.empty()) { selected_rung_id_ = center_hit.rungId; selected_column_ = center_hit.column; selected_cell_ = center_hit.column >= 0 && !center_hit.output && !center_hit.vertical && !center_hit.boundary && !center_hit.rowHeader; selected_output_ = center_hit.output; selected_boundary_ = center_hit.boundary || center_hit.vertical; } selected_vertical_connection_id_ = selected_vertical_connection_ids_.empty() ? std::string{} : selected_vertical_connection_ids_.back(); synchronizeSelectedNodes(); rebuildScene(); notifySelectionChanged(); } void LogicEditorWidget::beginGesture(const Hit &hit) { if (!editing_enabled_ || hit.rungId.empty() || hit.output) { return; } gesture_active_ = true; gesture_origin_ = hit; gesture_current_ = hit; gesture_scene_position_valid_ = false; viewport()->update(); } void LogicEditorWidget::updateGesture( const Hit &hit, const QPointF &scene_position) { if (!gesture_active_) { return; } gesture_current_ = hit; gesture_scene_position_ = scene_position; gesture_scene_position_valid_ = true; viewport()->update(); } void LogicEditorWidget::finishGesture(const Hit &hit) { if (!gesture_active_) { return; } const Hit origin = gesture_origin_; gesture_active_ = false; gesture_origin_ = {}; gesture_current_ = {}; gesture_scene_position_valid_ = false; viewport()->update(); if (origin.rungId.empty() || hit.rungId.empty()) { return; } LogicEditorResult result; const bool connected = mouse_wire_mode_ == MouseWireMode::Draw; if ((origin.vertical || origin.boundary) && (hit.vertical || hit.boundary)) { if (!connected && origin.vertical && origin.objectId == hit.objectId) { result = editor_service_.removeVerticalConnections( logic_id_, {origin.objectId}); } else if (origin.column != hit.column) { result = {false, LogicEditorError::InvalidOperation, "竖线拖动必须保持在同一列边界", {}}; } else { std::string first_rung = origin.rungId; std::string last_rung = hit.rungId; if (last_rung.empty() && !origin.lowerRungId.empty()) { last_rung = origin.lowerRungId; } result = editor_service_.setVerticalConnectionRange( logic_id_, first_rung, last_rung, origin.column, connected); } } else if (origin.vertical || hit.vertical) { result = {false, LogicEditorError::InvalidOperation, "请从网格边界开始竖向拖动", {}}; } else if (origin.column == hit.column && origin.rungId != hit.rungId) { result = editor_service_.setVerticalConnectionRange( logic_id_, origin.rungId, hit.rungId, origin.column, connected); } else if (origin.rungId == hit.rungId) { result = editor_service_.setHorizontalWireRange( logic_id_, origin.rungId, origin.column, hit.column, connected); } else { result = {false, LogicEditorError::InvalidOperation, "画线必须沿同一行或同一列边界进行", {}}; } if (!result.succeeded) { reportFailure(result); } else { clearObjectSelection(); selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } } void LogicEditorWidget::clearGesture() { if (!gesture_active_ && !gesture_scene_position_valid_) { return; } gesture_active_ = false; gesture_origin_ = {}; gesture_current_ = {}; gesture_scene_position_ = {}; gesture_scene_position_valid_ = false; viewport()->update(); } void LogicEditorWidget::drawGesturePreview(QPainter *painter) const { if (!gesture_active_ || painter == nullptr || gesture_origin_.rungId.empty()) { return; } enum class PreviewKind { Invalid, Horizontal, Vertical }; PreviewKind kind = PreviewKind::Invalid; bool valid = false; const Hit &origin = gesture_origin_; const Hit ¤t = gesture_current_; const bool connected = mouse_wire_mode_ == MouseWireMode::Draw; if (!current.rungId.empty()) { if ((origin.vertical || origin.boundary) && (current.vertical || current.boundary)) { kind = PreviewKind::Vertical; valid = origin.column == current.column && (origin.rungId != current.rungId || (!connected && origin.vertical && origin.objectId == current.objectId)); } else if (origin.vertical || current.vertical) { kind = PreviewKind::Invalid; } else if (origin.column == current.column && origin.rungId != current.rungId) { kind = PreviewKind::Vertical; valid = origin.column >= 0 && origin.column <= ProjectLimits::kMaximumConditionColumns; } else if (origin.rungId == current.rungId) { kind = PreviewKind::Horizontal; valid = origin.column >= 0 && current.column >= 0 && origin.column < ProjectLimits::kMaximumConditionColumns && current.column < ProjectLimits::kMaximumConditionColumns; } } QColor preview_color = valid ? connected ? QColor(QStringLiteral("#277da1")) : QColor(QStringLiteral("#c56a1a")) : kFaultColor; preview_color.setAlpha(220); QPen preview_pen(preview_color, valid ? 2.8 : 2.4, Qt::DashLine); preview_pen.setCosmetic(true); preview_pen.setCapStyle(Qt::SquareCap); painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(preview_pen); painter->setBrush(Qt::NoBrush); if (valid && kind == PreviewKind::Horizontal) { const RowLayout *layout = layoutForRung(origin.rungId); if (layout != nullptr) { const int first = std::min(origin.column, current.column); const int last = std::max(origin.column, current.column); for (int column = first; column <= last; ++column) { const LadderCell *cell = editor_service_.findCell( logic_id_, origin.rungId, column); if (cell == nullptr || cell->kind == LadderCellKind::Node || (mouse_wire_mode_ == MouseWireMode::Erase && cell->kind != LadderCellKind::Wire)) { continue; } const qreal left = kLeftBus + static_cast(column) * kCellWidth; painter->drawLine( QPointF(left, layout->centerY), QPointF(left + kCellWidth, layout->centerY)); } } } else if (valid && kind == PreviewKind::Vertical) { const RowLayout *first = layoutForRung(origin.rungId); const RowLayout *last = layoutForRung(current.rungId); if (first != nullptr && last != nullptr) { const qreal x = kLeftBus + static_cast(origin.column) * kCellWidth; painter->drawLine( QPointF(x, first->centerY), QPointF(x, last->centerY)); } } else { const RowLayout *layout = layoutForRung(origin.rungId); if (layout != nullptr && gesture_scene_position_valid_) { const qreal origin_x = kLeftBus + (origin.vertical || origin.boundary ? static_cast(origin.column) * kCellWidth : (static_cast(origin.column) + 0.5) * kCellWidth); painter->drawLine( QPointF(origin_x, layout->centerY), gesture_scene_position_); } } painter->restore(); } void LogicEditorWidget::showCommandEditor(const Hit &hit) { if (!editing_enabled_ || hit.rungId.empty() || hit.boundary || hit.vertical) { return; } const std::vector parallel_node_ids = selectedNodeIds(); cancelCommandInput(); command_target_.rungId = hit.rungId; command_target_.column = hit.column; const LogicNode *existing = hit.objectId.empty() ? nullptr : editor_service_.findNode(logic_id_, hit.objectId); if (existing != nullptr) { command_target_.kind = LogicCommandTargetKind::ExistingNode; } else if (hit.output) { command_target_.kind = LogicCommandTargetKind::Output; } else { const LadderCell *cell = editor_service_.findCell( logic_id_, hit.rungId, hit.column); command_target_.kind = cell != nullptr && cell->kind == LadderCellKind::Wire ? LogicCommandTargetKind::WireColumn : LogicCommandTargetKind::GapColumn; } command_target_.expressionId = hit.objectId; command_parallel_node_ids_ = parallel_node_ids; QString initial_text; if (existing != nullptr && existing->isConfigured()) { initial_text = logicCommandText(existing->config); } command_editor_->setStyleSheet(QString{}); command_editor_->setToolTip( tr("触点使用 M 地址,比较和数据指令使用 D 地址")); command_editor_->setText(initial_text); command_editor_->setVisible(true); QPointF center; if (!findCommandTargetCenter(command_target_, ¢er)) { cancelCommandInput(); return; } positionCommandInput(center); command_editor_->raise(); command_editor_->setFocus(Qt::MouseFocusReason); command_editor_->selectAll(); } void LogicEditorWidget::commitCommandInput() { if (command_editor_ == nullptr || !command_editor_->isVisible()) { return; } LogicCommandRequest request; request.logicId = logic_id_; request.text = command_editor_->text().trimmed().toStdString(); request.target = command_target_; request.parallelNodeIds = command_parallel_node_ids_; const LogicCommandResult result = command_service_.execute(request); if (!result.succeeded) { command_editor_->setStyleSheet( QStringLiteral("QLineEdit { border: 2px solid #c5362e; }")); command_editor_->setToolTip(QString::fromStdString(result.message)); command_editor_->setFocus(Qt::OtherFocusReason); command_editor_->selectAll(); emit editorError(QString::fromStdString(result.message)); return; } command_editor_->setStyleSheet(QString{}); command_editor_->setToolTip( tr("触点使用 M 地址,比较和数据指令使用 D 地址")); command_parallel_node_ids_.clear(); if (!result.hasNextCursor) { selectNode(result.id); emit graphChanged(); cancelCommandInput(); return; } moveToCursor(result.nextCursor); emit graphChanged(); command_target_ = commandTargetForCursor(result.nextCursor); QPointF center; if (command_target_.rungId.empty() || !findCommandTargetCenter(command_target_, ¢er)) { cancelCommandInput(); return; } command_editor_->clear(); positionCommandInput(center); command_editor_->raise(); command_editor_->setFocus(Qt::OtherFocusReason); } void LogicEditorWidget::cancelCommandInput() { if (command_editor_ != nullptr) { command_editor_->clear(); command_editor_->setStyleSheet(QString{}); command_editor_->setVisible(false); } command_target_ = {}; command_parallel_node_ids_.clear(); } void LogicEditorWidget::positionCommandInput(const QPointF &scene_center) { if (command_editor_ == nullptr) { return; } const QPoint view_center = mapFromScene(scene_center); const int width = 360; const int height = 38; const QRect bounds = viewport()->rect().adjusted(4, 4, -4, -4); const int left = std::clamp( view_center.x() - width / 2, bounds.left(), std::max(bounds.left(), bounds.right() - width + 1)); const int top = std::clamp( view_center.y() - height / 2, bounds.top(), std::max(bounds.top(), bounds.bottom() - height + 1)); command_editor_->setGeometry(left, top, width, height); } bool LogicEditorWidget::findCommandTargetCenter( const LogicCommandTarget &target, QPointF *scene_center) const { if (scene_center == nullptr) { return false; } const RowLayout *layout = layoutForRung(target.rungId); if (layout == nullptr) { return false; } if (target.kind == LogicCommandTargetKind::Output) { *scene_center = QPointF( kConditionRight + kOutputWidth / 2.0, layout->centerY); return true; } int column = target.column; if (target.kind == LogicCommandTargetKind::ExistingNode) { const LadderRung *rung = editor_service_.findRung( logic_id_, target.rungId); if (rung == nullptr) { return false; } if (rung->output.has_value() && rung->output->id == target.expressionId) { *scene_center = QPointF( kConditionRight + kOutputWidth / 2.0, layout->centerY); return true; } const auto found = std::find_if( rung->cells.cbegin(), rung->cells.cend(), [&target](const LadderCell &cell) { return cell.node.has_value() && cell.node->id == target.expressionId; }); if (found == rung->cells.cend()) { return false; } column = static_cast(std::distance(rung->cells.cbegin(), found)); } if (column < 0 || column >= ProjectLimits::kMaximumConditionColumns) { return false; } *scene_center = QPointF( kLeftBus + (static_cast(column) + 0.5) * kCellWidth, layout->centerY); return true; } LogicCommandTarget LogicEditorWidget::commandTargetForCursor( const LogicEditCursor &cursor) const { LogicCommandTarget target; target.rungId = cursor.rungId; target.column = cursor.column; if (cursor.output) { target.kind = LogicCommandTargetKind::Output; return target; } const LadderCell *cell = editor_service_.findCell( logic_id_, cursor.rungId, cursor.column); if (cell != nullptr && cell->node.has_value()) { target.kind = LogicCommandTargetKind::ExistingNode; target.expressionId = cell->node->id; } else { target.kind = cell != nullptr && cell->kind == LadderCellKind::Wire ? LogicCommandTargetKind::WireColumn : LogicCommandTargetKind::GapColumn; } return target; } LogicEditCursor LogicEditorWidget::conditionInsertionCursor() const { const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr || logic->rungs.empty()) { return {}; } const std::string rung_id = currentRungId(); if (selected_output_) { return {rung_id, ProjectLimits::kMaximumConditionColumns, true}; } if (selected_cell_ && selected_column_ >= 0) { int target_column = selected_column_; const LadderCell *cell = editor_service_.findCell( logic_id_, rung_id, selected_column_); if (cell != nullptr && cell->kind == LadderCellKind::Node) { ++target_column; } return { rung_id, std::min(target_column, ProjectLimits::kMaximumConditionColumns), target_column >= ProjectLimits::kMaximumConditionColumns}; } const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id); if (rung != nullptr) { const auto empty = std::find_if( rung->cells.cbegin(), rung->cells.cend(), [](const LadderCell &cell) { return cell.kind == LadderCellKind::Gap; }); if (empty != rung->cells.cend()) { return { rung_id, static_cast(std::distance(rung->cells.cbegin(), empty)), false}; } } return {rung_id, ProjectLimits::kMaximumConditionColumns, true}; } void LogicEditorWidget::moveToCursor(const LogicEditCursor &cursor) { clearObjectSelection(); selected_rung_id_ = cursor.rungId; selected_column_ = cursor.column; selected_cell_ = !cursor.output; selected_output_ = cursor.output; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); const LogicCommandTarget target = commandTargetForCursor(cursor); QPointF center; if (findCommandTargetCenter(target, ¢er)) { ensureVisible( QRectF(center.x() - kCellWidth / 2.0, center.y() - kRowHeight / 2.0, cursor.output ? kOutputWidth : kCellWidth, kRowHeight), 24, 24); } } void LogicEditorWidget::moveToVerticalTarget( const LogicVerticalEditResult &result) { const bool keep_cell = selected_cell_; const bool keep_output = selected_output_; const bool keep_boundary = selected_boundary_; clearObjectSelection(); selected_rung_id_ = result.nextRungId; selected_column_ = result.columnBoundary; selected_cell_ = keep_cell; selected_output_ = keep_output; selected_boundary_ = keep_boundary; rebuildScene(); notifySelectionChanged(); const RowLayout *layout = layoutForRung(result.nextRungId); if (layout == nullptr) { return; } qreal center_x = kLeftBus + (static_cast(result.columnBoundary) + 0.5) * kCellWidth; qreal target_width = kCellWidth; if (keep_boundary) { center_x = kLeftBus + static_cast(result.columnBoundary) * kCellWidth; } else if (keep_output) { center_x = kConditionRight + kOutputWidth / 2.0; target_width = kOutputWidth; } ensureVisible( QRectF( center_x - target_width / 2.0, layout->centerY - kRowHeight / 2.0, target_width, kRowHeight), 24, 24); } LogicEditorResult LogicEditorWidget::finishCursorEdit( const LogicEditResult &result) { if (!result.edit.succeeded) { reportFailure(result.edit); return result.edit; } moveToCursor(result.nextCursor); emit graphChanged(); return result.edit; } void LogicEditorWidget::reportFailure(const LogicEditorResult &result) { emit editorError(QString::fromStdString(result.message)); } void LogicEditorWidget::mousePressEvent(QMouseEvent *event) { const Hit hit = hitAt(mapToScene(event->pos())); setFocus(Qt::MouseFocusReason); if (event->button() == Qt::LeftButton && mouse_wire_mode_ != MouseWireMode::Select && editing_enabled_) { beginGesture(hit); } else if (event->button() == Qt::LeftButton && mouse_wire_mode_ == MouseWireMode::Select) { selection_pressed_ = true; selection_dragging_ = false; selection_origin_ = event->pos(); selection_modifiers_ = event->modifiers(); selection_band_->setGeometry(QRect(selection_origin_, QSize{})); selection_band_->hide(); } else { QGraphicsView::mousePressEvent(event); return; } event->accept(); } void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event) { if (mouse_wire_mode_ == MouseWireMode::Select && selection_pressed_) { if (!selection_dragging_ && (event->pos() - selection_origin_).manhattanLength() >= QApplication::startDragDistance()) { selection_dragging_ = true; selection_band_->show(); } if (selection_dragging_) { selection_band_->setGeometry( QRect(selection_origin_, event->pos()).normalized()); } } else { const QPointF scene_position = mapToScene(event->pos()); updateGesture(hitAt(scene_position), scene_position); } event->accept(); } void LogicEditorWidget::mouseReleaseEvent(QMouseEvent *event) { const Hit hit = hitAt(mapToScene(event->pos())); if (event->button() == Qt::LeftButton && mouse_wire_mode_ == MouseWireMode::Select && selection_pressed_) { const bool extend = selection_modifiers_.testFlag(Qt::ControlModifier); selection_pressed_ = false; selection_band_->hide(); if (selection_dragging_) { selectObjectsInBand( QRect(selection_origin_, event->pos()).normalized(), extend); } else { selectObject(hit, extend); } selection_dragging_ = false; } else if (mouse_wire_mode_ != MouseWireMode::Select) { finishGesture(hit); } else { QGraphicsView::mouseReleaseEvent(event); return; } event->accept(); } void LogicEditorWidget::mouseDoubleClickEvent(QMouseEvent *event) { showCommandEditor(hitAt(mapToScene(event->pos()))); event->accept(); } void LogicEditorWidget::keyPressEvent(QKeyEvent *event) { if (event != nullptr && event->key() == Qt::Key_Escape && event->modifiers() == Qt::NoModifier && mouse_wire_mode_ != MouseWireMode::Select) { setMouseWireMode(MouseWireMode::Select); event->accept(); return; } QGraphicsView::keyPressEvent(event); } void LogicEditorWidget::drawForeground( QPainter *painter, const QRectF &rect) { QGraphicsView::drawForeground(painter, rect); drawGesturePreview(painter); } void LogicEditorWidget::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); if (command_editor_ != nullptr && command_editor_->isVisible()) { QPointF center; if (findCommandTargetCenter(command_target_, ¢er)) { positionCommandInput(center); } } } void LogicEditorWidget::scrollContentsBy(int dx, int dy) { QGraphicsView::scrollContentsBy(dx, dy); if (command_editor_ != nullptr && command_editor_->isVisible()) { QPointF center; if (findCommandTargetCenter(command_target_, ¢er)) { positionCommandInput(center); } } } bool LogicEditorWidget::eventFilter(QObject *watched, QEvent *event) { if (watched == command_editor_ && event != nullptr) { if (event->type() == QEvent::KeyPress) { const auto *key_event = static_cast(event); if (key_event->key() == Qt::Key_Escape) { cancelCommandInput(); return true; } } else if (event->type() == QEvent::FocusOut) { QTimer::singleShot( 0, this, [this] { if (command_editor_ == nullptr || !command_editor_->isVisible() || command_editor_->hasFocus()) { return; } QWidget *focus = QApplication::focusWidget(); QWidget *popup = command_completer_ == nullptr ? nullptr : command_completer_->popup(); if (popup != nullptr && popup->isVisible() && focus != nullptr && (focus == popup || popup->isAncestorOf(focus))) { return; } cancelCommandInput(); }); } } return QGraphicsView::eventFilter(watched, event); } LogicEditorResult LogicEditorWidget::addRung() { const LogicEditorResult result = editor_service_.addRung(logic_id_); if (result.succeeded) { clearObjectSelection(); selected_rung_id_ = result.id; selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::insertRung(bool after) { const std::string reference = selected_rung_id_; const LogicEditorResult result = reference.empty() ? editor_service_.addRung(logic_id_) : editor_service_.insertRung(logic_id_, reference, after); if (result.succeeded) { clearObjectSelection(); selected_rung_id_ = result.id; selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteRung() { const std::string rung_id = selected_rung_id_; if (rung_id.empty()) { return {false, LogicEditorError::RungNotFound, "请先选择要删除的行", {}}; } const LogicEditorResult result = editor_service_.removeRung(logic_id_, rung_id); if (result.succeeded) { clearObjectSelection(); selected_rung_id_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) { return finishCursorEdit(editor_service_.applyConditionAndAdvance( logic_id_, conditionInsertionCursor(), config, false)); } LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config) { if (selected_node_ids_.empty() || selectedRungId().empty()) { const LogicEditorResult result{ false, LogicEditorError::InvalidOperation, "请先选择同一行中要并联的连续条件节点", {}}; reportFailure(result); return result; } const LogicEditorResult result = editor_service_.addParallelBranch( logic_id_, selectedRungId(), selected_node_ids_, config, false); if (result.succeeded) { selectNode(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addHorizontalWire() { const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { const LogicEditorResult result{ false, LogicEditorError::LogicNotFound, "未找到控制逻辑", {}}; reportFailure(result); return result; } if (!logic->rungs.empty() && (selectedRungId().empty() || !selected_cell_)) { const LogicEditorResult result{ false, LogicEditorError::InvalidOperation, "请先选择一个条件网格,再插入横线", {}}; reportFailure(result); return result; } LogicEditCursor cursor = logic->rungs.empty() ? LogicEditCursor{} : conditionInsertionCursor(); if (cursor.output) { const LogicEditorResult result{ false, LogicEditorError::InvalidOperation, "条件区已经填满,请在输出槽配置输出指令", {}}; reportFailure(result); return result; } return finishCursorEdit(editor_service_.applyWireAndAdvance( logic_id_, cursor)); } LogicEditorResult LogicEditorWidget::addVerticalWire() { const std::string upper = selectedRungId(); if (upper.empty() || selected_column_ < 0 || selected_column_ > ProjectLimits::kMaximumConditionColumns || (!selected_cell_ && !selected_output_ && !selected_boundary_)) { return {false, LogicEditorError::InvalidOperation, "请先选择一个列边界或网格,再插入竖线", {}}; } const LogicVerticalEditResult result = editor_service_.applyVerticalConnectionAndAdvance( logic_id_, upper, selected_column_); if (result.edit.succeeded) { moveToVerticalTarget(result); if (result.changed) { emit graphChanged(); } } else { reportFailure(result.edit); } return result.edit; } LogicEditorResult LogicEditorWidget::deleteHorizontalWire() { const std::string rung_id = selectedRungId(); if (rung_id.empty() || !selected_cell_ || selected_column_ < 0 || selected_column_ >= ProjectLimits::kMaximumConditionColumns) { return {false, LogicEditorError::InvalidOperation, "请先选择要删除的横线网格", {}}; } const LadderCell *cell = editor_service_.findCell( logic_id_, rung_id, selected_column_); if (cell == nullptr || cell->kind != LadderCellKind::Wire) { return {false, LogicEditorError::InvalidOperation, "请选择一格横线后再删除", {}}; } const LogicEditorResult result = editor_service_.setHorizontalWireRange( logic_id_, rung_id, selected_column_, selected_column_, false); if (result.succeeded) { selected_cells_.erase( std::remove( selected_cells_.begin(), selected_cells_.end(), std::make_pair(rung_id, selected_column_)), selected_cells_.end()); synchronizeSelectedNodes(); rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteVerticalWire() { if (selected_vertical_connection_id_.empty()) { return {false, LogicEditorError::ConnectionNotFound, "请选择要删除的竖线", {}}; } const LogicEditorResult result = editor_service_.removeVerticalConnections( logic_id_, {selected_vertical_connection_id_}); if (result.succeeded) { selected_vertical_connection_ids_.erase( std::remove( selected_vertical_connection_ids_.begin(), selected_vertical_connection_ids_.end(), selected_vertical_connection_id_), selected_vertical_connection_ids_.end()); selected_vertical_connection_id_ = selected_vertical_connection_ids_.empty() ? std::string{} : selected_vertical_connection_ids_.back(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::setOutput( const LogicNodeConfig &config, bool configured) { return finishCursorEdit(editor_service_.applyOutputAndAdvance( logic_id_, {currentRungId(), ProjectLimits::kMaximumConditionColumns, true}, config, configured)); } LogicClipboardPasteResult LogicEditorWidget::pasteClipboard( const LogicClipboardFragment &fragment) { LogicClipboardPasteResult result = editor_service_.pasteClipboard( logic_id_, fragment, pasteTarget()); if (result.edit.succeeded) { clearObjectSelection(); selected_cells_ = result.selection.cells; selected_output_rung_ids_ = result.selection.outputRungIds; selected_vertical_connection_ids_ = result.selection.verticalConnectionIds; selected_row_ids_ = result.wholeRungIds; if (!selected_row_ids_.empty()) { selected_rung_id_ = selected_row_ids_.back(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; } else if (!selected_cells_.empty()) { selected_rung_id_ = selected_cells_.front().first; selected_column_ = selected_cells_.front().second; selected_cell_ = true; selected_output_ = false; selected_boundary_ = false; } else if (!selected_output_rung_ids_.empty()) { selected_rung_id_ = selected_output_rung_ids_.front(); selected_column_ = ProjectLimits::kMaximumConditionColumns; selected_cell_ = false; selected_output_ = true; selected_boundary_ = false; } else if (!selected_vertical_connection_ids_.empty()) { const VerticalConnection *connection = editor_service_.findConnection( logic_id_, selected_vertical_connection_ids_.front()); if (connection != nullptr) { selected_rung_id_ = connection->upperRungId; selected_column_ = connection->columnBoundary; selected_cell_ = false; selected_output_ = false; selected_boundary_ = true; } } selected_vertical_connection_id_ = selected_vertical_connection_ids_.empty() ? std::string{} : selected_vertical_connection_ids_.back(); synchronizeSelectedNodes(); rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result.edit); } return result; } LogicEditorResult LogicEditorWidget::deleteSelected() { LogicSelectionDeleteRequest selection; selection.cells = selected_cells_; selection.outputRungIds = selected_output_rung_ids_; selection.verticalConnectionIds = selected_vertical_connection_ids_; if (selection.cells.empty() && selection.outputRungIds.empty() && selection.verticalConnectionIds.empty() && selected_cell_ && !selected_rung_id_.empty() && selected_column_ >= 0 && selected_column_ < ProjectLimits::kMaximumConditionColumns) { const LadderCell *cell = editor_service_.findCell( logic_id_, selected_rung_id_, selected_column_); if (cell != nullptr && cell->kind != LadderCellKind::Gap) { selection.cells.push_back({ selected_rung_id_, selected_column_}); } } if (selection.cells.empty() && selection.outputRungIds.empty() && selection.verticalConnectionIds.empty()) { const LogicEditorResult result{ false, LogicEditorError::InvalidOperation, "请先选择逻辑指令、横线、输出块或竖线;整行请使用 Shift+Delete", {}}; reportFailure(result); return result; } const LogicEditorResult result = editor_service_.deleteSelection( logic_id_, selection); if (result.succeeded) { clearObjectSelection(); selected_rung_id_.clear(); selected_column_ = -1; selected_cell_ = false; selected_output_ = false; selected_boundary_ = false; rebuildScene(); notifySelectionChanged(); emit graphChanged(); } else { reportFailure(result); } return result; }