#include "logic_editor_widget.h" #include #include #include #include #include #include #include #include #include namespace { constexpr qreal kSceneMargin = 28.0; constexpr qreal kRailInset = 40.0; constexpr qreal kMinimumSceneWidth = 980.0; constexpr qreal kCellWidth = 128.0; constexpr qreal kCellHeight = 120.0; constexpr qreal kNodeTerminalX = 50.0; constexpr qreal kRungHeaderHeight = 52.0; constexpr qreal kRungGap = 18.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 kGridColor(QStringLiteral("#e8edf0")); const QColor kPlaceholderColor(QStringLiteral("#81919b")); struct ExpressionMetrics { int columns = 1; int rows = 1; }; struct RenderResult { QPointF input; QPointF output; }; QString registerAddressText(const RegisterAddress &address) { return QString::fromStdString(address.toString()); } QString timerAddressText(const TimerAddress &address) { return QString::fromStdString(address.toString()); } QString counterAddressText(const CounterAddress &address) { return QString::fromStdString(address.toString()); } QString wordOperandText(const WordOperand &operand) { return operand.kind == WordOperandKind::Register ? registerAddressText(operand.address) : QString::number(operand.constant); } void drawRegisterComment( QPainter *painter, const QString &comment, qreal top = 22.0) { if (comment.isEmpty()) { return; } QFont font = painter->font(); font.setPointSizeF(8.0); painter->setFont(font); const QString visible_comment = painter->fontMetrics().elidedText( comment, Qt::ElideRight, static_cast(kCellWidth - 8.0)); painter->drawText( QRectF(-kCellWidth / 2.0, top, kCellWidth, 18), Qt::AlignCenter, visible_comment); } 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 nodeToolTip(const LogicNodeConfig &config) { return std::visit( [](const auto &value) -> QString { using Config = std::decay_t; if constexpr (std::is_same_v) { return LogicEditorWidget::tr("%1触点:%2") .arg(value.mode == ContactMode::NormallyOpen ? LogicEditorWidget::tr("常开") : LogicEditorWidget::tr("常闭")) .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("%1沿触点:%2") .arg(value.mode == EdgeMode::Rising ? LogicEditorWidget::tr("上升") : LogicEditorWidget::tr("下降")) .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("T 触点:%1") .arg(timerAddressText(value.address)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("C 触点:%1") .arg(counterAddressText(value.address)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("输出线圈:%1") .arg(registerAddressText(value.address)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("比较条件:%1 %2 %3") .arg(registerAddressText(value.address)) .arg(comparisonText(value.comparison)) .arg(value.value); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("TON:%1,预设 %2 ms") .arg(timerAddressText(value.address)) .arg(value.presetMs); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("%1:%2,CV %3,PV %4,复位 %5") .arg(value.mode == CounterMode::Up ? QStringLiteral("CTU") : QStringLiteral("CTD")) .arg(counterAddressText(value.address)) .arg(registerAddressText(value.currentValueAddress)) .arg(wordOperandText(value.preset)) .arg(registerAddressText(value.resetAddress)); } else if constexpr (std::is_same_v) { return LogicEditorWidget::tr("MOVE:%1 -> %2") .arg(wordOperandText(value.source)) .arg(registerAddressText(value.destination)); } else { return LogicEditorWidget::tr("%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); } ExpressionMetrics measureExpression(const ConditionExpression &expression) { if (expression.kind == ConditionExpressionKind::Node) { return {}; } if (expression.kind == ConditionExpressionKind::Wire) { return {expression.wire->columnSpan, 1}; } ExpressionMetrics metrics{0, 0}; if (expression.kind == ConditionExpressionKind::Series) { for (const ConditionExpression &child : expression.children) { const ExpressionMetrics child_metrics = measureExpression(child); metrics.columns += child_metrics.columns; metrics.rows = std::max(metrics.rows, child_metrics.rows); } } else { for (const ConditionExpression &child : expression.children) { const ExpressionMetrics child_metrics = measureExpression(child); metrics.columns = std::max(metrics.columns, child_metrics.columns); metrics.rows += child_metrics.rows; } } return metrics; } QPen ladderPen(bool active) { return QPen(active ? kActiveColor : kLadderColor, active ? 2.6 : kLadderLineWidth); } bool traceValue( const LogicTraceSnapshot &trace, const std::unordered_map LogicTraceSnapshot::*member, const std::string &id) { const auto &values = trace.*member; const auto found = values.find(id); return found != values.end() && found->second; } 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 { public: NodeItem( const LogicNode &node, const std::string &rung_id, const QPointF ¢er, const QString ®ister_comment, bool active, bool faulted, bool condition) : node_id_(node.id), rung_id_(rung_id), config_(node.config), register_comment_(register_comment), configured_(node.configured), active_(active), faulted_(faulted), condition_(condition) { setPos(center); setFlag(ItemIsSelectable, true); setZValue(3.0); QString tool_tip = configured_ ? nodeToolTip(config_) : LogicEditorWidget::tr("待配置:请在属性区设置地址和参数"); if (!register_comment_.isEmpty()) { tool_tip += LogicEditorWidget::tr("\n注释:%1").arg(register_comment_); } setToolTip(tool_tip); } QRectF boundingRect() const override { return {-kCellWidth / 2.0, -kCellHeight / 2.0, kCellWidth, kCellHeight}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { painter->setRenderHint(QPainter::Antialiasing, true); const bool selected = (option->state & QStyle::State_Selected) != 0; if (selected) { painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor); painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); painter->drawRect(boundingRect().adjusted(3, 3, -3, -3)); } const QColor symbol_color = faulted_ ? kFaultColor : selected ? kSelectionBorderColor : active_ ? kActiveColor : kLadderColor; painter->setPen(QPen(symbol_color, active_ || faulted_ ? 2.6 : kLadderLineWidth)); QFont font = painter->font(); font.setPointSizeF(9.5); painter->setFont(font); if (const auto *contact = std::get_if(&config_)) { 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)); painter->drawLine(QPointF(18, -15), QPointF(18, 15)); if (contact->mode == ContactMode::NormallyClosed) { painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(contact->address) : tr("< M 地址 >")); drawRegisterComment(painter, register_comment_); } else if (const auto *edge = std::get_if(&config_)) { 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)); painter->drawLine(QPointF(18, -15), QPointF(18, 15)); painter->drawText( QRectF(-14, -12, 28, 24), Qt::AlignCenter, edge->mode == EdgeMode::Rising ? QStringLiteral("P") : QStringLiteral("N")); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(edge->address) : tr("< M 地址 >")); drawRegisterComment(painter, register_comment_); } else if (const auto *timer_contact = std::get_if(&config_)) { 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)); painter->drawLine(QPointF(18, -15), QPointF(18, 15)); if (timer_contact->mode == ContactMode::NormallyClosed) { painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? timerAddressText(timer_contact->address) : tr("< T 地址 >")); } else if (const auto *counter_contact = std::get_if(&config_)) { 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)); painter->drawLine(QPointF(18, -15), QPointF(18, 15)); if (counter_contact->mode == ContactMode::NormallyClosed) { painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); } painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? counterAddressText(counter_contact->address) : tr("< C 地址 >")); } else if (const auto *coil = std::get_if(&config_)) { 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; left.moveTo(-2, -20); left.cubicTo(-24, -16, -24, 16, -2, 20); painter->drawPath(left); QPainterPath right; right.moveTo(2, -20); right.cubicTo(24, -16, 24, 16, 2, 20); painter->drawPath(right); if (coil->mode != CoilMode::Normal) { painter->drawText( QRectF(-12, -14, 24, 28), Qt::AlignCenter, coil->mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R")); } painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(coil->address) : tr("< M 地址 >")); drawRegisterComment(painter, register_comment_); } else if (const auto *comparison = std::get_if(&config_)) { const QRectF box(-47, -17, 94, 34); painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); painter->drawText( box, Qt::AlignCenter, QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? registerAddressText(comparison->address) : tr("< D 地址 >")); painter->drawText( QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), Qt::AlignCenter, configured_ ? QString::number(comparison->value) : tr("< 常量 >")); drawRegisterComment(painter, register_comment_, 40.0); } else if (const auto *ton = std::get_if(&config_)) { const QRectF box(-47, -18, 94, 36); painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); painter->drawText(box, Qt::AlignCenter, QStringLiteral("TON")); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? timerAddressText(ton->address) : tr("< T 地址 >")); painter->drawText( QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), Qt::AlignCenter, configured_ ? QStringLiteral("PT %1 ms").arg(ton->presetMs) : tr("< 预设时间 >")); } else if (const auto *counter = std::get_if(&config_)) { const QRectF box(-47, -18, 94, 36); painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); painter->drawText( box, Qt::AlignCenter, counter->mode == CounterMode::Up ? QStringLiteral("CTU") : QStringLiteral("CTD")); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? counterAddressText(counter->address) : tr("< C 地址 >")); painter->drawText( QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), Qt::AlignCenter, configured_ ? QStringLiteral("CV %1 / PV %2") .arg(registerAddressText(counter->currentValueAddress)) .arg(wordOperandText(counter->preset)) : tr("< CV / PV >")); } else if (const auto *move = std::get_if(&config_)) { const QRectF box(-47, -18, 94, 36); painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); painter->drawText(box, Qt::AlignCenter, QStringLiteral("MOVE")); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? QStringLiteral("%1 -> %2") .arg(wordOperandText(move->source)) .arg(registerAddressText(move->destination)) : tr("< 源 -> 目标 >")); } else if (const auto *arithmetic = std::get_if(&config_)) { const QRectF box(-47, -18, 94, 36); painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); painter->drawRect(box); painter->drawText( box, Qt::AlignCenter, arithmetic->operation == ArithmeticOperation::Add ? QStringLiteral("ADD") : QStringLiteral("SUB")); painter->drawText( QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), Qt::AlignCenter, configured_ ? QStringLiteral("%1,%2 -> %3") .arg(wordOperandText(arithmetic->left)) .arg(wordOperandText(arithmetic->right)) .arg(registerAddressText(arithmetic->destination)) : tr("< 操作数 -> 目标 >")); } } const std::string &nodeId() const { return node_id_; } const std::string &rungId() const { return rung_id_; } bool isConditionNode() const { return condition_; } private: std::string node_id_; std::string rung_id_; LogicNodeConfig config_; QString register_comment_; bool configured_ = true; bool active_ = false; bool faulted_ = false; bool condition_ = true; }; class LogicEditorWidget::WireItem final : public QGraphicsItem { public: WireItem( const std::string &expression_id, const std::string &rung_id, const QPointF ¢er, int column_span, bool active) : expression_id_(expression_id), rung_id_(rung_id), width_(static_cast(column_span) * kCellWidth), active_(active) { setPos(center); setFlag(ItemIsSelectable, true); setZValue(2.0); setToolTip(LogicEditorWidget::tr("横线:%1 列,可用触点直接替换") .arg(column_span)); } QRectF boundingRect() const override { return {-width_ / 2.0, -kCellHeight / 2.0, width_, kCellHeight}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { const bool selected = (option->state & QStyle::State_Selected) != 0; if (selected) { painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor); painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); painter->drawRect(boundingRect().adjusted(3, 3, -3, -3)); } painter->setPen(ladderPen(active_)); painter->drawLine(QPointF(-width_ / 2.0, 0), QPointF(width_ / 2.0, 0)); } const std::string &expressionId() const { return expression_id_; } const std::string &rungId() const { return rung_id_; } private: std::string expression_id_; std::string rung_id_; qreal width_ = kCellWidth; bool active_ = false; }; class LogicEditorWidget::VerticalConnectorItem final : public QGraphicsItem { public: VerticalConnectorItem( const std::string &branch_expression_id, const std::string &rung_id, qreal x, qreal top, qreal bottom, bool active) : branch_expression_id_(branch_expression_id), rung_id_(rung_id), height_(bottom - top), active_(active) { setPos(x, top); setFlag(ItemIsSelectable, true); setZValue(2.5); setToolTip(LogicEditorWidget::tr("竖线连接:删除将移除对应并联支路")); } QRectF boundingRect() const override { return {-8.0, 0.0, 16.0, height_}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { const bool selected = (option->state & QStyle::State_Selected) != 0; painter->setPen(QPen( selected ? kSelectionBorderColor : active_ ? kActiveColor : kLadderColor, selected || active_ ? 3.0 : kLadderLineWidth)); painter->drawLine(QPointF(0, 0), QPointF(0, height_)); if (selected) { painter->setPen(QPen(kSelectionBorderColor, 1.0, Qt::DashLine)); painter->drawRect(boundingRect().adjusted(1, 1, -1, -1)); } } const std::string &branchExpressionId() const { return branch_expression_id_; } const std::string &rungId() const { return rung_id_; } private: std::string branch_expression_id_; std::string rung_id_; qreal height_ = 0.0; bool active_ = false; }; class LogicEditorWidget::RungItem final : public QGraphicsItem { public: RungItem( const LadderRung &rung, int number, qreal top, qreal height, qreal width, qreal cursor_left) : rung_id_(rung.id), name_(QString::fromStdString(rung.name)), comment_(QString::fromStdString(rung.comment)), number_(number), height_(height), width_(width), cursor_left_(cursor_left), show_cursor_(!rung.condition.has_value()) { setPos(0, top); setFlag(ItemIsSelectable, true); setZValue(-2.0); if (!comment_.isEmpty()) { setToolTip(comment_); } } QRectF boundingRect() const override { return {kSceneMargin, 0, width_, height_}; } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { const bool selected = (option->state & QStyle::State_Selected) != 0; if (selected) { 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_); if (!name_.isEmpty() && !name_.startsWith(tr("网络 "))) { title += QStringLiteral(":") + name_; } painter->drawText( QRectF(kSceneMargin + 8, 5, 320, 22), Qt::AlignLeft | Qt::AlignVCenter, title); if (!comment_.isEmpty()) { painter->setPen(QColor(QStringLiteral("#7b8790"))); const QString visible_comment = painter->fontMetrics().elidedText( comment_, Qt::ElideRight, static_cast(width_ - 16.0)); painter->drawText( QRectF(kSceneMargin + 8, 24, width_ - 16, 22), Qt::AlignLeft | Qt::AlignVCenter, visible_comment); } painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); painter->drawLine( QPointF(kSceneMargin + 8, height_ - 1), QPointF(kSceneMargin + width_ - 8, height_ - 1)); } const std::string &rungId() const { return rung_id_; } private: std::string rung_id_; QString name_; QString comment_; int number_ = 0; qreal height_ = 0.0; qreal width_ = 0.0; qreal cursor_left_ = 0.0; bool show_cursor_ = false; }; namespace { RenderResult renderExpression( QGraphicsScene &scene, const LogicEditorService &editor_service, const ConditionExpression &expression, const std::string &rung_id, const QPointF &top_left, const ExpressionMetrics &metrics, const LogicTraceSnapshot &trace, bool trace_enabled, const std::string &fault_node_id) { if (expression.kind == ConditionExpressionKind::Wire) { const bool active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::expressionPowerValues, expression.id); const qreal width = static_cast(expression.wire->columnSpan) * kCellWidth; const QPointF center( top_left.x() + width / 2.0, top_left.y() + kCellHeight / 2.0); scene.addItem(new LogicEditorWidget::WireItem( expression.id, rung_id, center, expression.wire->columnSpan, active)); return { QPointF(top_left.x(), center.y()), QPointF(top_left.x() + width, center.y())}; } if (expression.kind == ConditionExpressionKind::Node) { const QPointF center( top_left.x() + kCellWidth / 2.0, top_left.y() + kCellHeight / 2.0); const bool node_active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::nodePowerValues, 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, center, [&editor_service, &expression] { const std::optional address = registerAddressForLogicNode(expression.node->config); return address.has_value() ? QString::fromStdString( editor_service.registerCommentFor(*address)) : QString{}; }(), node_active, expression.node->id == fault_node_id, true)); return { QPointF(top_left.x(), center.y()), QPointF(top_left.x() + kCellWidth, center.y())}; } if (expression.kind == ConditionExpressionKind::Series) { qreal x = top_left.x(); RenderResult first; RenderResult previous; for (std::size_t index = 0; index < expression.children.size(); ++index) { const ExpressionMetrics child_metrics = measureExpression(expression.children[index]); const RenderResult current = renderExpression( scene, editor_service, expression.children[index], rung_id, QPointF(x, top_left.y()), child_metrics, trace, trace_enabled, fault_node_id); if (index == 0U) { first = current; } else { const bool previous_active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::expressionPowerValues, expression.children[index - 1U].id); scene.addLine( QLineF(previous.output, current.input), ladderPen(previous_active)); } previous = current; x += static_cast(child_metrics.columns) * kCellWidth; } return {first.input, previous.output}; } qreal y = top_left.y(); std::vector branches; branches.reserve(expression.children.size()); for (const ConditionExpression &child : expression.children) { const ExpressionMetrics child_metrics = measureExpression(child); branches.push_back(renderExpression( scene, editor_service, 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 = top_left.x(); const qreal right_join = top_left.x() + static_cast(metrics.columns) * kCellWidth; const qreal top_y = branches.front().input.y(); const bool parallel_input_active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::expressionInputValues, expression.id); for (std::size_t index = 1; index < branches.size(); ++index) { const qreal segment_top = branches[index - 1U].input.y(); const qreal segment_bottom = branches[index].input.y(); const bool branch_active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::expressionPowerValues, expression.children[index].id); scene.addItem(new LogicEditorWidget::VerticalConnectorItem( expression.children[index].id, rung_id, left_join, segment_top, segment_bottom, parallel_input_active)); scene.addItem(new LogicEditorWidget::VerticalConnectorItem( expression.children[index].id, rung_id, right_join, segment_top, segment_bottom, branch_active)); } for (std::size_t index = 0; index < branches.size(); ++index) { const bool branch_active = trace_enabled && traceValue( trace, &LogicTraceSnapshot::expressionPowerValues, expression.children[index].id); scene.addLine( QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), ladderPen(branch_active)); scene.addLine( QLineF(branches[index].output, QPointF(right_join, branches[index].output.y())), ladderPen(branch_active)); } return {QPointF(left_join, top_y), QPointF(right_join, top_y)}; } } // namespace LogicEditorWidget::LogicEditorWidget(LogicEditorService &editor_service, QWidget *parent) : QGraphicsView(parent), editor_service_(editor_service) { scene_ = new QGraphicsScene(this); setScene(scene_); setRenderHint(QPainter::Antialiasing, true); setBackgroundBrush(Qt::white); setDragMode(QGraphicsView::RubberBandDrag); setAlignment(Qt::AlignLeft | Qt::AlignTop); connect(scene_, &QGraphicsScene::selectionChanged, this, &LogicEditorWidget::handleSelectionChanged); } void LogicEditorWidget::setLogicId(const std::string &logic_id) { if (logic_id_ == logic_id) { return; } logic_id_ = logic_id; current_rung_id_.clear(); reloadLogic(); } void LogicEditorWidget::setEditingEnabled(bool enabled) { editing_enabled_ = enabled; setInteractive(enabled); } void LogicEditorWidget::setRuntimeTrace( const LogicTraceSnapshot &trace, const std::string &fault_node_id) { trace_ = trace; fault_node_id_ = fault_node_id; runtime_trace_enabled_ = true; reloadLogic(); } void LogicEditorWidget::clearRuntimeTrace() { trace_.clear(); fault_node_id_.clear(); runtime_trace_enabled_ = false; reloadLogic(); } void LogicEditorWidget::reloadLogic() { scene_->clear(); const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) { scene_->setSceneRect(0, 0, kMinimumSceneWidth, 400); return; } if (current_rung_id_.empty() && !logic->rungs.empty()) { current_rung_id_ = logic->rungs.front().id; } int condition_columns = 1; for (const LadderRung &rung : logic->rungs) { if (rung.condition.has_value()) { condition_columns = std::max( condition_columns, measureExpression(*rung.condition).columns); } } const int grid_columns = std::max(kMinimumLogicColumns, condition_columns + 1); 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 = kSceneMargin; int number = 1; for (const LadderRung &rung : logic->rungs) { 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(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, grid_top, right_rail_x, grid_top + static_cast(grid_rows) * kCellHeight), QPen(kLadderColor, 2.4)); const qreal main_y = grid_top + kCellHeight / 2.0; QPointF expression_output(left_rail_x, main_y); if (rung.condition.has_value()) { const RenderResult rendered = renderExpression( *scene_, editor_service_, *rung.condition, rung.id, QPointF(left_rail_x, grid_top), metrics, trace_, runtime_trace_enabled_, fault_node_id_); expression_output = rendered.output; } else { addPlaceholder( *scene_, QRectF(left_rail_x, grid_top, kCellWidth, kCellHeight), rung.output.has_value() ? tr("无条件") : 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_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_left + kCellWidth / 2.0, main_y), [&rung, this] { const std::optional address = registerAddressForLogicNode(rung.output->config); return address.has_value() ? QString::fromStdString( editor_service_.registerCommentFor(*address)) : QString{}; }(), [&rung, this, rung_active] { return (std::holds_alternative(rung.output->config) || std::holds_alternative( rung.output->config)) ? traceValue( trace_, &LogicTraceSnapshot::nodeValues, rung.output->id) : rung_active; }(), rung.output->id == fault_node_id_, false)); } else { 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)); } top += rung_height + kRungGap; ++number; } scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + kSceneMargin)); } void LogicEditorWidget::selectNode(const std::string &node_id) { for (QGraphicsItem *item : scene_->items()) { if (NodeItem *node = dynamic_cast(item)) { node->setSelected(node->nodeId() == node_id); if (node->nodeId() == node_id) { current_rung_id_ = node->rungId(); ensureVisible(node); } } else { item->setSelected(false); } } } void LogicEditorWidget::selectExpression(const std::string &expression_id) { for (QGraphicsItem *item : scene_->items()) { bool selected = false; if (NodeItem *node = dynamic_cast(item)) { selected = node->isConditionNode() && node->nodeId() == expression_id; if (selected) { current_rung_id_ = node->rungId(); ensureVisible(node); } } else if (WireItem *wire = dynamic_cast(item)) { selected = wire->expressionId() == expression_id; if (selected) { current_rung_id_ = wire->rungId(); ensureVisible(wire); } } item->setSelected(selected); } } void LogicEditorWidget::selectWire(const std::string &wire_id) { selectExpression(wire_id); } std::string LogicEditorWidget::selectedNodeId() const { const std::vector ids = selectedNodeIds(); return ids.size() == 1U ? ids.front() : std::string{}; } std::vector LogicEditorWidget::selectedNodeIds() const { std::vector> positioned_ids; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { positioned_ids.emplace_back(node->scenePos(), node->nodeId()); } } std::sort( positioned_ids.begin(), positioned_ids.end(), [](const auto &left, const auto &right) { if (!qFuzzyCompare(left.first.y(), right.first.y())) { return left.first.y() < right.first.y(); } return left.first.x() < right.first.x(); }); std::vector ids; ids.reserve(positioned_ids.size()); for (const auto &positioned_id : positioned_ids) { ids.push_back(positioned_id.second); } return ids; } std::vector LogicEditorWidget::selectedExpressionIds() const { std::vector> positioned_ids; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { if (node->isConditionNode()) { positioned_ids.emplace_back(node->scenePos(), node->nodeId()); } } else if (const WireItem *wire = dynamic_cast(item)) { positioned_ids.emplace_back(wire->scenePos(), wire->expressionId()); } } std::sort( positioned_ids.begin(), positioned_ids.end(), [](const auto &left, const auto &right) { if (!qFuzzyCompare(left.first.y(), right.first.y())) { return left.first.y() < right.first.y(); } return left.first.x() < right.first.x(); }); std::vector ids; ids.reserve(positioned_ids.size()); for (const auto &positioned_id : positioned_ids) { ids.push_back(positioned_id.second); } return ids; } std::vector LogicEditorWidget::selectedWireIds() const { std::vector ids; for (QGraphicsItem *item : scene_->selectedItems()) { if (const WireItem *wire = dynamic_cast(item)) { ids.push_back(wire->expressionId()); } } return ids; } std::vector LogicEditorWidget::selectedBranchIds() const { std::vector ids; for (QGraphicsItem *item : scene_->selectedItems()) { if (const VerticalConnectorItem *connector = dynamic_cast(item)) { if (std::find(ids.cbegin(), ids.cend(), connector->branchExpressionId()) == ids.cend()) { ids.push_back(connector->branchExpressionId()); } } } return ids; } std::string LogicEditorWidget::selectedRungId() const { std::string rung_id; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { if (!rung_id.empty() && rung_id != node->rungId()) { return {}; } rung_id = node->rungId(); } else if (const WireItem *wire = dynamic_cast(item)) { if (!rung_id.empty() && rung_id != wire->rungId()) { return {}; } rung_id = wire->rungId(); } else if (const VerticalConnectorItem *connector = dynamic_cast(item)) { if (!rung_id.empty() && rung_id != connector->rungId()) { return {}; } rung_id = connector->rungId(); } else if (const RungItem *rung = dynamic_cast(item)) { if (rung_id.empty()) { rung_id = rung->rungId(); } } } return rung_id; } LogicEditorResult LogicEditorWidget::addRung() { const LogicEditorResult result = editor_service_.addRung(logic_id_); if (result.succeeded) { current_rung_id_ = result.id; reloadLogic(); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) { const std::vector selected_expressions = selectedExpressionIds(); const std::vector selected_wires = selectedWireIds(); const std::vector selected_ids = selectedNodeIds(); LogicEditorResult result; if (selected_expressions.size() > 1U || selected_wires.size() > 1U) { result = {false, LogicEditorError::InvalidOperation, "串联插入或替换横线时只能选择一个条件对象", {}}; } else if (selected_wires.size() == 1U) { result = editor_service_.replaceWireWithCondition( logic_id_, currentRungId(), selected_wires.front(), config); } else if (selected_ids.size() == 1U) { 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_.appendCondition(logic_id_, currentRungId(), config); } if (result.succeeded) { reloadLogic(); selectExpression(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config) { const std::string rung_id = selectedRungId(); 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() || condition_ids.empty()) { result = {false, LogicEditorError::InvalidOperation, "请在同一网络中选择要并联的连续节点", {}}; } else { result = editor_service_.addParallelBranch( logic_id_, rung_id, condition_ids, config); } if (result.succeeded) { reloadLogic(); selectExpression(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addHorizontalWire() { const std::vector selected_ids = selectedExpressionIds(); LogicEditorResult result; if (selected_ids.size() > 1U) { result = {false, LogicEditorError::InvalidOperation, "插入横线时只能选择一个条件对象", {}}; } else if (selected_ids.size() == 1U) { result = editor_service_.insertWireAfter( logic_id_, currentRungId(), selected_ids.front()); } else { result = editor_service_.appendWire(logic_id_, currentRungId()); } if (result.succeeded) { reloadLogic(); selectWire(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::addVerticalWire() { const std::string rung_id = selectedRungId(); const std::vector selected_ids = selectedExpressionIds(); LogicEditorResult result; if (rung_id.empty() || selected_ids.empty()) { result = {false, LogicEditorError::InvalidOperation, "请在同一网络中选择要连接的连续条件或横线", {}}; } else { result = editor_service_.addParallelWireBranch( logic_id_, rung_id, selected_ids); } if (result.succeeded) { reloadLogic(); selectWire(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteHorizontalWire() { const std::vector wire_ids = selectedWireIds(); const std::string rung_id = selectedRungId(); if (wire_ids.empty() || rung_id.empty()) { const LogicEditorResult result = { false, LogicEditorError::InvalidOperation, "请先选择要删除的横线", {}}; reportFailure(result); return result; } const LogicEditorResult result = editor_service_.removeExpressions( logic_id_, rung_id, wire_ids); if (result.succeeded) { reloadLogic(); emit nodeSelected({}); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteVerticalWire() { const std::vector branch_ids = selectedBranchIds(); if (branch_ids.empty()) { const LogicEditorResult result = { false, LogicEditorError::InvalidOperation, "请先选择要删除的竖线连接", {}}; reportFailure(result); return result; } const std::string rung_id = selectedRungId(); LogicEditorResult result; if (rung_id.empty()) { result = {false, LogicEditorError::InvalidOperation, "竖线连接必须位于同一网络", {}}; } else { result = editor_service_.removeExpressions( logic_id_, rung_id, branch_ids); } if (result.succeeded) { reloadLogic(); emit nodeSelected({}); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::setOutput( const LogicNodeConfig &config, bool configured) { const LogicEditorResult result = editor_service_.setOutput( logic_id_, currentRungId(), config, configured); if (result.succeeded) { reloadLogic(); selectNode(result.id); emit graphChanged(); } else { reportFailure(result); } return result; } LogicEditorResult LogicEditorWidget::deleteSelected() { const std::vector expression_ids = selectedExpressionIds(); const std::vector wire_ids = selectedWireIds(); const std::vector branch_ids = selectedBranchIds(); const std::vector node_ids = selectedNodeIds(); LogicEditorResult result; if (!branch_ids.empty()) { result = editor_service_.removeExpressions( logic_id_, selectedRungId(), branch_ids); } else if (!wire_ids.empty()) { bool output_selected = false; for (QGraphicsItem *item : scene_->selectedItems()) { if (const NodeItem *node = dynamic_cast(item)) { output_selected = output_selected || !node->isConditionNode(); } } if (output_selected) { result = {false, LogicEditorError::InvalidOperation, "不能同时删除横线和输出节点", {}}; } else { result = editor_service_.removeExpressions( logic_id_, selectedRungId(), expression_ids); } } else if (!node_ids.empty()) { result = editor_service_.removeNodes(logic_id_, node_ids); } 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_); } } if (result.succeeded) { reloadLogic(); emit nodeSelected({}); emit graphChanged(); } else { reportFailure(result); } return result; } void LogicEditorWidget::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); } void LogicEditorWidget::handleSelectionChanged() { const std::string rung_id = selectedRungId(); if (!rung_id.empty()) { current_rung_id_ = rung_id; } emit nodeSelected(QString::fromStdString(selectedNodeId())); } void LogicEditorWidget::reportFailure(const LogicEditorResult &result) { emit editorError(QString::fromStdString(result.message)); } std::string LogicEditorWidget::currentRungId() const { const std::string selected = selectedRungId(); if (!selected.empty()) { return selected; } return current_rung_id_.empty() ? editor_service_.firstRungId(logic_id_) : current_rung_id_; }