Explorar el Código

feat: 优化梯形图网格编辑与范围并联

main
suyu hace 1 mes
padre
commit
7c31462718
Se han modificado 9 ficheros con 585 adiciones y 373 borrados
  1. +193
    -42
      app/src/services/logic_editor_service.cpp
  2. +5
    -11
      app/src/services/logic_editor_service.h
  3. +294
    -259
      app/src/ui/logic_editor_widget.cpp
  4. +4
    -6
      app/src/ui/logic_editor_widget.h
  5. +62
    -30
      app/src/ui/main_window.cpp
  6. +3
    -2
      app/src/ui/main_window.h
  7. +1
    -7
      app/src/ui/main_window.ui
  8. +22
    -13
      app/tests/logic_editor_service_tests.cpp
  9. +1
    -3
      app/tests/main_window_tests.cpp

+ 193
- 42
app/src/services/logic_editor_service.cpp Ver fichero

@@ -3,6 +3,8 @@
#include "project_service.h"

#include <algorithm>
#include <iterator>
#include <unordered_set>
#include <type_traits>
#include <utility>

@@ -44,6 +46,142 @@ ConditionExpression *findParentExpression(
return nullptr;
}

using NodeIdSet = std::unordered_set<std::string>;

NodeIdSet conditionNodeIds(const ConditionExpression &expression)
{
std::vector<const LogicNode *> nodes;
collectConditionNodes(expression, &nodes);
NodeIdSet ids;
for (const LogicNode *node : nodes)
{
ids.insert(node->id);
}
return ids;
}

bool isSubset(const NodeIdSet &subset, const NodeIdSet &values)
{
return std::all_of(
subset.cbegin(), subset.cend(),
[&values](const std::string &value) { return values.count(value) != 0U; });
}

bool sameValues(const NodeIdSet &left, const NodeIdSet &right)
{
return left.size() == right.size() && isSubset(left, right);
}

void addParallelSibling(
ConditionExpression *target,
ConditionExpression branch,
const std::string &container_id)
{
if (target->kind == ConditionExpressionKind::Parallel)
{
target->children.push_back(std::move(branch));
return;
}
ConditionExpression original = std::move(*target);
*target = makeContainer(
container_id,
ConditionExpressionKind::Parallel,
std::move(original),
std::move(branch));
}

bool addParallelForSelection(
ConditionExpression *expression,
const NodeIdSet &selected_ids,
ConditionExpression *branch,
const std::string &parallel_id,
const std::string &series_id)
{
const NodeIdSet expression_ids = conditionNodeIds(*expression);
if (sameValues(expression_ids, selected_ids))
{
addParallelSibling(expression, std::move(*branch), parallel_id);
return true;
}

std::vector<NodeIdSet> child_ids;
child_ids.reserve(expression->children.size());
std::vector<std::size_t> matching_children;
for (std::size_t index = 0; index < expression->children.size(); ++index)
{
child_ids.push_back(conditionNodeIds(expression->children[index]));
const bool intersects = std::any_of(
child_ids.back().cbegin(), child_ids.back().cend(),
[&selected_ids](const std::string &id)
{
return selected_ids.count(id) != 0U;
});
if (intersects)
{
matching_children.push_back(index);
}
}
if (matching_children.empty())
{
return false;
}
if (matching_children.size() == 1U)
{
const std::size_t child_index = matching_children.front();
if (expression->kind == ConditionExpressionKind::Parallel
&& sameValues(child_ids[child_index], selected_ids))
{
expression->children.push_back(std::move(*branch));
return true;
}
return addParallelForSelection(
&expression->children[child_index],
selected_ids,
branch,
parallel_id,
series_id);
}
if (expression->kind != ConditionExpressionKind::Series)
{
return false;
}

const std::size_t first = matching_children.front();
const std::size_t last = matching_children.back();
if (last - first + 1U != matching_children.size())
{
return false;
}
NodeIdSet range_ids;
for (std::size_t index = first; index <= last; ++index)
{
range_ids.insert(child_ids[index].cbegin(), child_ids[index].cend());
}
if (!sameValues(range_ids, selected_ids))
{
return false;
}

ConditionExpression range;
range.id = series_id;
range.kind = ConditionExpressionKind::Series;
auto range_begin = expression->children.begin() + static_cast<std::ptrdiff_t>(first);
auto range_end = expression->children.begin() + static_cast<std::ptrdiff_t>(last + 1U);
range.children.insert(
range.children.end(),
std::make_move_iterator(range_begin),
std::make_move_iterator(range_end));
range_begin = expression->children.erase(range_begin, range_end);

ConditionExpression parallel = makeContainer(
parallel_id,
ConditionExpressionKind::Parallel,
std::move(range),
std::move(*branch));
expression->children.insert(range_begin, std::move(parallel));
return true;
}

bool removeExpressionRecursive(
ConditionExpression *expression, const std::string &expression_id)
{
@@ -288,11 +426,10 @@ LogicEditorResult LogicEditorService::appendCondition(
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::insertCondition(
LogicEditorResult LogicEditorService::insertConditionAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
SeriesInsertPosition position,
const std::string &target_node_id,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
@@ -300,52 +437,46 @@ LogicEditorResult LogicEditorService::insertCondition(
{
return failure(LogicEditorError::RungNotFound, "ladder network was not found");
}
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (!isConditionConfig(config)
|| findExpression(logic_id, rung_id, target_expression_id) == nullptr)
|| existing_rung == nullptr
|| !existing_rung->condition.has_value()
|| findConditionNode(*existing_rung->condition, target_node_id) == nullptr)
{
return failure(LogicEditorError::ExpressionNotFound, "series insertion target was not found");
return failure(LogicEditorError::NodeNotFound, "series insertion target was not found");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
Project &project = project_service_.editProject();
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id);
ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id);
ConditionExpression *target = findConditionExpression(*rung->condition, target_node_id);
ConditionExpression *parent = findParentExpression(*rung->condition, target_node_id);
if (parent != nullptr && parent->kind == ConditionExpressionKind::Series)
{
const auto target_iterator = std::find_if(
parent->children.begin(), parent->children.end(),
[&target_expression_id](const ConditionExpression &child)
[&target_node_id](const ConditionExpression &child)
{
return child.id == target_expression_id;
return child.id == target_node_id;
});
parent->children.insert(
position == SeriesInsertPosition::Before
? target_iterator : target_iterator + 1,
std::move(leaf));
parent->children.insert(target_iterator + 1, std::move(leaf));
}
else
{
ConditionExpression original = std::move(*target);
*target = position == SeriesInsertPosition::Before
? makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(leaf),
std::move(original))
: makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(original),
std::move(leaf));
*target = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(original),
std::move(leaf));
}
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelCondition(
LogicEditorResult LogicEditorService::addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
@@ -353,29 +484,49 @@ LogicEditorResult LogicEditorService::addParallelCondition(
{
return failure(LogicEditorError::RungNotFound, "ladder network was not found");
}
if (!isConditionConfig(config)
|| findExpression(logic_id, rung_id, target_expression_id) == nullptr)
if (!isConditionConfig(config) || selected_node_ids.empty())
{
return failure(LogicEditorError::ExpressionNotFound, "parallel target was not found");
return failure(LogicEditorError::InvalidOperation, "parallel branch requires a selection");
}
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (!existing_rung->condition.has_value())
{
return failure(LogicEditorError::InvalidOperation, "ladder network has no condition");
}
NodeIdSet selected_ids(selected_node_ids.cbegin(), selected_node_ids.cend());
if (selected_ids.size() != selected_node_ids.size())
{
return failure(
LogicEditorError::InvalidOperation,
"parallel selection contains duplicates");
}
const NodeIdSet available_ids = conditionNodeIds(*existing_rung->condition);
if (!isSubset(selected_ids, available_ids))
{
return failure(
LogicEditorError::NodeNotFound,
"parallel selection contains an unknown node");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
Project &project = project_service_.editProject();
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
ConditionExpression *target = findConditionExpression(*rung->condition, target_expression_id);
ConditionExpression *parent = findParentExpression(*rung->condition, target_expression_id);
if (parent != nullptr && parent->kind == ConditionExpressionKind::Parallel)
const std::string parallel_id = makeUniqueExpressionId(*logic);
std::string series_id = parallel_id + "-range";
while (findExpression(logic_id, rung_id, series_id) != nullptr)
{
parent->children.push_back(std::move(leaf));
series_id += "-range";
}
else
Project &project = project_service_.editProject();
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
if (!addParallelForSelection(
&*rung->condition,
selected_ids,
&leaf,
parallel_id,
series_id))
{
ConditionExpression original = std::move(*target);
*target = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Parallel,
std::move(original),
std::move(leaf));
return failure(
LogicEditorError::InvalidOperation,
"parallel selection must be one continuous logic range");
}
return {true, LogicEditorError::None, {}, node_id};
}


+ 5
- 11
app/src/services/logic_editor_service.h Ver fichero

@@ -3,6 +3,7 @@
#include "domain/control_logic_model.h"

#include <string>
#include <vector>

class ProjectService;

@@ -18,12 +19,6 @@ enum class LogicEditorError
UnsupportedNodeChange
};

enum class SeriesInsertPosition
{
Before,
After
};

struct LogicEditorResult
{
bool succeeded = false;
@@ -59,16 +54,15 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config);
LogicEditorResult insertCondition(
LogicEditorResult insertConditionAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
SeriesInsertPosition position,
const std::string &target_node_id,
const LogicNodeConfig &config);
LogicEditorResult addParallelCondition(
LogicEditorResult addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config);
LogicEditorResult setOutput(
const std::string &logic_id,


+ 294
- 259
app/src/ui/logic_editor_widget.cpp Ver fichero

@@ -13,31 +13,34 @@

namespace {

constexpr qreal kLeftRailX = 64.0;
constexpr qreal kMinimumSceneWidth = 940.0;
constexpr qreal kNodeWidth = 116.0;
constexpr qreal kNodeHeight = 92.0;
constexpr qreal kNodeTerminalX = 54.0;
constexpr qreal kSeriesGap = 26.0;
constexpr qreal kParallelGap = 22.0;
constexpr qreal kExpressionPadding = 18.0;
constexpr qreal kRungHeaderHeight = 38.0;
constexpr qreal kSceneMargin = 28.0;
constexpr qreal kRailInset = 40.0;
constexpr qreal kMinimumSceneWidth = 980.0;
constexpr qreal kCellWidth = 128.0;
constexpr qreal kCellHeight = 86.0;
constexpr qreal kNodeTerminalX = 50.0;
constexpr qreal kRungHeaderHeight = 34.0;
constexpr qreal kRungGap = 18.0;
constexpr qreal kOutputWidth = 128.0;
constexpr qreal kOutputGap = 72.0;
constexpr qreal kLadderLineWidth = 1.8;
constexpr int kMinimumLogicColumns = 7;
const QColor kLadderColor(QStringLiteral("#263842"));
const QColor kActiveColor(QStringLiteral("#16854f"));
const QColor kFaultColor(QStringLiteral("#c5362e"));
const QColor kSelectionColor(QStringLiteral("#dfeef5"));
const QColor kSelectionBorderColor(QStringLiteral("#277da1"));
const QColor kGroupBorderColor(QStringLiteral("#8ea4af"));
const QColor kGridColor(QStringLiteral("#e8edf0"));
const QColor kPlaceholderColor(QStringLiteral("#81919b"));

struct ExpressionSize
struct ExpressionMetrics
{
qreal width = kNodeWidth;
qreal height = kNodeHeight;
int columns = 1;
int rows = 1;
};

struct RenderResult
{
QPointF input;
QPointF output;
};

QString registerAddressText(const RegisterAddress &address)
@@ -89,36 +92,33 @@ QString nodeToolTip(const LogicNodeConfig &config)
config);
}

ExpressionSize measureExpression(const ConditionExpression &expression)
ExpressionMetrics measureExpression(const ConditionExpression &expression)
{
if (expression.kind == ConditionExpressionKind::Node)
{
return {};
}
ExpressionSize size{0.0, 0.0};

ExpressionMetrics metrics{0, 0};
if (expression.kind == ConditionExpressionKind::Series)
{
for (const ConditionExpression &child : expression.children)
{
const ExpressionSize child_size = measureExpression(child);
size.width += child_size.width;
size.height = std::max(size.height, child_size.height);
const ExpressionMetrics child_metrics = measureExpression(child);
metrics.columns += child_metrics.columns;
metrics.rows = std::max(metrics.rows, child_metrics.rows);
}
size.width += kSeriesGap * static_cast<qreal>(expression.children.size() - 1U);
}
else
{
for (const ConditionExpression &child : expression.children)
{
const ExpressionSize child_size = measureExpression(child);
size.width = std::max(size.width, child_size.width);
size.height += child_size.height;
const ExpressionMetrics child_metrics = measureExpression(child);
metrics.columns = std::max(metrics.columns, child_metrics.columns);
metrics.rows += child_metrics.rows;
}
size.height += kParallelGap * static_cast<qreal>(expression.children.size() - 1U);
}
size.width += kExpressionPadding * 2.0;
size.height += kExpressionPadding * 2.0;
return size;
return metrics;
}

QPen ladderPen(bool active)
@@ -137,6 +137,47 @@ bool traceValue(
return found != values.end() && found->second;
}

void addGrid(
QGraphicsScene &scene,
qreal left,
qreal top,
int columns,
int rows)
{
QPen pen(kGridColor, 1.0);
pen.setCosmetic(true);
for (int column = 0; column <= columns; ++column)
{
const qreal x = left + static_cast<qreal>(column) * kCellWidth;
QGraphicsLineItem *line = scene.addLine(
QLineF(x, top, x, top + static_cast<qreal>(rows) * kCellHeight), pen);
line->setZValue(-10.0);
}
for (int row = 0; row <= rows; ++row)
{
const qreal y = top + static_cast<qreal>(row) * kCellHeight;
QGraphicsLineItem *line = scene.addLine(
QLineF(left, y, left + static_cast<qreal>(columns) * kCellWidth, y), pen);
line->setZValue(-10.0);
}
}

void addPlaceholder(QGraphicsScene &scene, const QRectF &cell, const QString &text)
{
const QRectF bounds = cell.adjusted(12, 18, -12, -18);
QGraphicsRectItem *item = scene.addRect(
bounds,
QPen(kPlaceholderColor, 1.2, Qt::DashLine),
QBrush(QColor(255, 255, 255, 220)));
item->setZValue(1.0);
QGraphicsTextItem *label = scene.addText(text);
label->setDefaultTextColor(kPlaceholderColor);
label->setPos(
bounds.center().x() - label->boundingRect().width() / 2.0,
bounds.center().y() - label->boundingRect().height() / 2.0);
label->setZValue(2.0);
}

} // namespace

class LogicEditorWidget::NodeItem final : public QGraphicsItem
@@ -145,13 +186,11 @@ public:
NodeItem(
const LogicNode &node,
const std::string &rung_id,
const std::string &expression_id,
const QPointF &center,
bool active,
bool faulted)
: node_id_(node.id),
rung_id_(rung_id),
expression_id_(expression_id),
config_(node.config),
configured_(node.configured),
active_(active),
@@ -166,7 +205,7 @@ public:

QRectF boundingRect() const override
{
return {-kNodeWidth / 2.0, -kNodeHeight / 2.0, kNodeWidth, kNodeHeight};
return {-kCellWidth / 2.0, -kCellHeight / 2.0, kCellWidth, kCellHeight};
}

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override
@@ -175,8 +214,11 @@ public:
const bool selected = (option->state & QStyle::State_Selected) != 0;
if (selected)
{
painter->fillRect(boundingRect(), kSelectionColor);
painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor);
painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine));
painter->drawRect(boundingRect().adjusted(3, 3, -3, -3));
}

const QColor symbol_color = faulted_ ? kFaultColor
: selected ? kSelectionBorderColor : active_ ? kActiveColor : kLadderColor;
painter->setPen(QPen(symbol_color, active_ || faulted_ ? 2.6 : kLadderLineWidth));
@@ -186,7 +228,7 @@ public:

if (const auto *contact = std::get_if<ContactNodeConfig>(&config_))
{
painter->fillRect(QRectF(-24, -21, 48, 42), Qt::white);
painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0));
painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0));
painter->drawLine(QPointF(-18, -15), QPointF(-18, 15));
@@ -196,13 +238,13 @@ public:
painter->drawLine(QPointF(-23, 18), QPointF(23, -18));
}
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18),
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? registerAddressText(contact->address) : tr("< M 地址 >"));
}
else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_))
{
painter->fillRect(QRectF(-34, -23, 68, 46), Qt::white);
painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white);
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-16, 0));
painter->drawLine(QPointF(16, 0), QPointF(kNodeTerminalX, 0));
QPainterPath left;
@@ -221,7 +263,7 @@ public:
coil->mode == CoilMode::Set ? QStringLiteral("S") : QStringLiteral("R"));
}
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18),
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? registerAddressText(coil->address) : tr("< M 地址 >"));
}
@@ -232,102 +274,72 @@ public:
painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0));
painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0));
painter->drawRect(box);
painter->drawText(box, Qt::AlignCenter,
QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison)));
painter->drawText(
QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), Qt::AlignCenter,
box, Qt::AlignCenter,
QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison)));
painter->drawText(
QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? registerAddressText(comparison->address) : tr("< D 地址 >"));
painter->drawText(
QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), Qt::AlignCenter,
QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18),
Qt::AlignCenter,
configured_ ? QString::number(comparison->value) : tr("< 常量 >"));
}
}

const std::string &nodeId() const { return node_id_; }
const std::string &rungId() const { return rung_id_; }
const std::string &expressionId() const { return expression_id_; }

private:
std::string node_id_;
std::string rung_id_;
std::string expression_id_;
LogicNodeConfig config_;
bool configured_ = true;
bool active_ = false;
bool faulted_ = false;
};

class LogicEditorWidget::ExpressionItem final : public QGraphicsItem
{
public:
ExpressionItem(
std::string expression_id,
std::string rung_id,
ConditionExpressionKind kind,
const QRectF &bounds)
: expression_id_(std::move(expression_id)),
rung_id_(std::move(rung_id)),
kind_(kind),
bounds_(bounds)
{
setFlag(ItemIsSelectable, true);
setZValue(1.0);
setToolTip(kind_ == ConditionExpressionKind::Parallel
? LogicEditorWidget::tr("并联支路组")
: LogicEditorWidget::tr("串联条件组"));
}

QRectF boundingRect() const override { return bounds_; }

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override
{
if ((option->state & QStyle::State_Selected) == 0)
{
return;
}
painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine));
painter->setBrush(QColor(223, 238, 245, 45));
painter->drawRect(bounds_.adjusted(2, 2, -2, -2));
painter->setPen(kGroupBorderColor);
painter->drawText(
bounds_.adjusted(8, 3, -8, -3),
Qt::AlignRight | Qt::AlignTop,
kind_ == ConditionExpressionKind::Parallel
? LogicEditorWidget::tr("并联组") : LogicEditorWidget::tr("串联组"));
}

const std::string &expressionId() const { return expression_id_; }
const std::string &rungId() const { return rung_id_; }

private:
std::string expression_id_;
std::string rung_id_;
ConditionExpressionKind kind_ = ConditionExpressionKind::Series;
QRectF bounds_;
};

class LogicEditorWidget::RungItem final : public QGraphicsItem
{
public:
RungItem(const LadderRung &rung, int number, qreal top, qreal height, qreal width)
RungItem(
const LadderRung &rung,
int number,
qreal top,
qreal height,
qreal width,
qreal cursor_left)
: rung_id_(rung.id),
name_(QString::fromStdString(rung.name)),
number_(number),
height_(height),
width_(width)
width_(width),
cursor_left_(cursor_left),
show_cursor_(!rung.condition.has_value())
{
setPos(0, top);
setFlag(ItemIsSelectable, true);
setZValue(-2.0);
}

QRectF boundingRect() const override { return {20, 0, width_ - 40, height_}; }
QRectF boundingRect() const override { return {kSceneMargin, 0, width_, height_}; }

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override
{
if ((option->state & QStyle::State_Selected) != 0)
const bool selected = (option->state & QStyle::State_Selected) != 0;
if (selected)
{
painter->fillRect(boundingRect(), QColor(QStringLiteral("#f0f7fa")));
painter->fillRect(boundingRect(), QColor(240, 247, 250, 90));
if (show_cursor_)
{
painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine));
painter->drawRect(QRectF(
cursor_left_ + 3,
kRungHeaderHeight + 3,
kCellWidth - 6,
kCellHeight - 6));
}
}
painter->setPen(QColor(QStringLiteral("#62717b")));
QString title = tr("网络 %1").arg(number_);
@@ -335,9 +347,14 @@ public:
{
title += QStringLiteral(":") + name_;
}
painter->drawText(QRectF(28, 6, 320, 22), Qt::AlignLeft | Qt::AlignVCenter, title);
painter->drawText(
QRectF(kSceneMargin + 8, 5, 320, 22),
Qt::AlignLeft | Qt::AlignVCenter,
title);
painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1));
painter->drawLine(QPointF(28, height_ - 1), QPointF(width_ - 28, height_ - 1));
painter->drawLine(
QPointF(kSceneMargin + 8, height_ - 1),
QPointF(kSceneMargin + width_ - 8, height_ - 1));
}

const std::string &rungId() const { return rung_id_; }
@@ -348,21 +365,18 @@ private:
int number_ = 0;
qreal height_ = 0.0;
qreal width_ = 0.0;
qreal cursor_left_ = 0.0;
bool show_cursor_ = false;
};

namespace {

struct RenderResult
{
QPointF input;
QPointF output;
};

RenderResult renderExpression(
QGraphicsScene &scene,
const ConditionExpression &expression,
const std::string &rung_id,
const QRectF &bounds,
const QPointF &top_left,
const ExpressionMetrics &metrics,
const LogicTraceSnapshot &trace,
bool trace_enabled,
const std::string &fault_node_id)
@@ -371,38 +385,41 @@ RenderResult renderExpression(
trace, &LogicTraceSnapshot::expressionValues, expression.id);
if (expression.kind == ConditionExpressionKind::Node)
{
const QPointF center = bounds.center();
const QPointF center(
top_left.x() + kCellWidth / 2.0,
top_left.y() + kCellHeight / 2.0);
const bool node_active = trace_enabled && traceValue(
trace, &LogicTraceSnapshot::nodeValues, expression.node->id);
scene.addLine(
QLineF(
QPointF(top_left.x(), center.y()),
QPointF(top_left.x() + kCellWidth, center.y())),
ladderPen(node_active));
scene.addItem(new LogicEditorWidget::NodeItem(
*expression.node,
rung_id,
expression.id,
center,
trace_enabled && traceValue(
trace, &LogicTraceSnapshot::nodeValues, expression.node->id),
node_active,
expression.node->id == fault_node_id));
return {QPointF(bounds.left(), center.y()), QPointF(bounds.right(), center.y())};
return {
QPointF(top_left.x(), center.y()),
QPointF(top_left.x() + kCellWidth, center.y())};
}

scene.addItem(new LogicEditorWidget::ExpressionItem(
expression.id, rung_id, expression.kind, bounds));
if (expression.kind == ConditionExpressionKind::Series)
{
qreal x = bounds.left() + kExpressionPadding;
qreal x = top_left.x();
RenderResult first;
RenderResult previous;
for (std::size_t index = 0; index < expression.children.size(); ++index)
{
const ExpressionSize size = measureExpression(expression.children[index]);
const QRectF child_bounds(
x,
bounds.center().y() - size.height / 2.0,
size.width,
size.height);
const ExpressionMetrics child_metrics = measureExpression(expression.children[index]);
const RenderResult current = renderExpression(
scene,
expression.children[index],
rung_id,
child_bounds,
QPointF(x, top_left.y()),
child_metrics,
trace,
trace_enabled,
fault_node_id);
@@ -415,29 +432,31 @@ RenderResult renderExpression(
scene.addLine(QLineF(previous.output, current.input), ladderPen(active));
}
previous = current;
x += size.width + kSeriesGap;
x += static_cast<qreal>(child_metrics.columns) * kCellWidth;
}
scene.addLine(QLineF(QPointF(bounds.left(), first.input.y()), first.input), ladderPen(active));
scene.addLine(QLineF(previous.output, QPointF(bounds.right(), previous.output.y())), ladderPen(active));
return {QPointF(bounds.left(), first.input.y()), QPointF(bounds.right(), previous.output.y())};
return {first.input, previous.output};
}

qreal y = bounds.top() + kExpressionPadding;
qreal y = top_left.y();
std::vector<RenderResult> branches;
branches.reserve(expression.children.size());
for (const ConditionExpression &child : expression.children)
{
const ExpressionSize size = measureExpression(child);
const QRectF child_bounds(
bounds.center().x() - size.width / 2.0,
y,
size.width,
size.height);
const ExpressionMetrics child_metrics = measureExpression(child);
branches.push_back(renderExpression(
scene, child, rung_id, child_bounds, trace, trace_enabled, fault_node_id));
y += size.height + kParallelGap;
scene,
child,
rung_id,
QPointF(top_left.x(), y),
child_metrics,
trace,
trace_enabled,
fault_node_id));
y += static_cast<qreal>(child_metrics.rows) * kCellHeight;
}
const qreal left_join = bounds.left() + 8.0;
const qreal right_join = bounds.right() - 8.0;

const qreal left_join = top_left.x();
const qreal right_join = top_left.x() + static_cast<qreal>(metrics.columns) * kCellWidth;
const qreal top_y = branches.front().input.y();
const qreal bottom_y = branches.back().input.y();
scene.addLine(QLineF(left_join, top_y, left_join, bottom_y), ladderPen(active));
@@ -445,7 +464,9 @@ RenderResult renderExpression(
for (std::size_t index = 0; index < branches.size(); ++index)
{
const bool branch_active = trace_enabled && traceValue(
trace, &LogicTraceSnapshot::expressionValues, expression.children[index].id);
trace,
&LogicTraceSnapshot::expressionValues,
expression.children[index].id);
scene.addLine(
QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input),
ladderPen(branch_active));
@@ -453,17 +474,7 @@ RenderResult renderExpression(
QLineF(branches[index].output, QPointF(right_join, branches[index].output.y())),
ladderPen(branch_active));
}
return {QPointF(bounds.left(), top_y), QPointF(bounds.right(), top_y)};
}

void addPlaceholder(QGraphicsScene &scene, const QPointF &center, const QString &text)
{
const QRectF bounds(center.x() - 58, center.y() - 20, 116, 40);
scene.addRect(bounds, QPen(kPlaceholderColor, 1.2, Qt::DashLine), QBrush(Qt::white));
QGraphicsTextItem *label = scene.addText(text);
label->setDefaultTextColor(kPlaceholderColor);
label->setPos(center.x() - label->boundingRect().width() / 2.0,
center.y() - label->boundingRect().height() / 2.0);
return {QPointF(left_join, top_y), QPointF(right_join, top_y)};
}

} // namespace
@@ -529,97 +540,105 @@ void LogicEditorWidget::reloadLogic()
current_rung_id_ = logic->rungs.front().id;
}

qreal maximum_condition_width = 360.0;
int condition_columns = 1;
for (const LadderRung &rung : logic->rungs)
{
if (rung.condition.has_value())
{
maximum_condition_width = std::max(
maximum_condition_width, measureExpression(*rung.condition).width);
condition_columns = std::max(
condition_columns,
measureExpression(*rung.condition).columns);
}
}
const qreal scene_width = std::max(
kMinimumSceneWidth,
kLeftRailX + maximum_condition_width + kOutputGap + kOutputWidth + 96.0);
const qreal right_rail_x = scene_width - 64.0;
const qreal output_center_x = right_rail_x - kOutputWidth / 2.0;
const int grid_columns = std::max(kMinimumLogicColumns, condition_columns + 2);
const qreal left_rail_x = kSceneMargin + kRailInset;
const qreal right_rail_x = left_rail_x + static_cast<qreal>(grid_columns) * kCellWidth;
const qreal scene_width = std::max(kMinimumSceneWidth, right_rail_x + kSceneMargin);

qreal top = 24.0;
qreal top = kSceneMargin;
int number = 1;
for (const LadderRung &rung : logic->rungs)
{
const ExpressionSize size = rung.condition.has_value()
? measureExpression(*rung.condition) : ExpressionSize{240.0, kNodeHeight};
const qreal height = kRungHeaderHeight + size.height + 28.0;
const qreal center_y = top + kRungHeaderHeight + size.height / 2.0;
scene_->addItem(new RungItem(rung, number, top, height, scene_width));
const ExpressionMetrics metrics = rung.condition.has_value()
? measureExpression(*rung.condition) : ExpressionMetrics{};
const int grid_rows = std::max(1, metrics.rows);
const qreal grid_top = top + kRungHeaderHeight;
const qreal rung_height = kRungHeaderHeight
+ static_cast<qreal>(grid_rows) * kCellHeight + 12.0;
scene_->addItem(new RungItem(
rung,
number,
top,
rung_height,
scene_width - 2.0 * kSceneMargin,
left_rail_x));
addGrid(*scene_, left_rail_x, grid_top, grid_columns, grid_rows);
scene_->addLine(
QLineF(kLeftRailX, top + 30, kLeftRailX, top + height - 12),
QLineF(left_rail_x, grid_top, left_rail_x,
grid_top + static_cast<qreal>(grid_rows) * kCellHeight),
QPen(kLadderColor, 2.4));
scene_->addLine(
QLineF(right_rail_x, top + 30, right_rail_x, top + height - 12),
QLineF(right_rail_x, grid_top, right_rail_x,
grid_top + static_cast<qreal>(grid_rows) * kCellHeight),
QPen(kLadderColor, 2.4));

QPointF expression_output(kLeftRailX, center_y);
const qreal main_y = grid_top + kCellHeight / 2.0;
QPointF expression_output(left_rail_x, main_y);
if (rung.condition.has_value())
{
const QRectF bounds(
kLeftRailX + 24.0,
center_y - size.height / 2.0,
size.width,
size.height);
const RenderResult rendered = renderExpression(
*scene_,
*rung.condition,
rung.id,
bounds,
QPointF(left_rail_x, grid_top),
metrics,
trace_,
runtime_trace_enabled_,
fault_node_id_);
const bool active = runtime_trace_enabled_ && traceValue(
trace_, &LogicTraceSnapshot::rungValues, rung.id);
scene_->addLine(
QLineF(QPointF(kLeftRailX, rendered.input.y()), rendered.input),
ladderPen(active));
expression_output = rendered.output;
}
else
{
addPlaceholder(*scene_, QPointF(kLeftRailX + 120.0, center_y), tr("添加条件"));
scene_->addLine(QLineF(kLeftRailX, center_y, kLeftRailX + 62.0, center_y),
ladderPen(false));
expression_output = QPointF(kLeftRailX + 178.0, center_y);
addPlaceholder(
*scene_,
QRectF(left_rail_x, grid_top, kCellWidth, kCellHeight),
tr("添加条件"));
}

const bool rung_active = runtime_trace_enabled_ && traceValue(
trace_, &LogicTraceSnapshot::rungValues, rung.id);
const qreal output_left = right_rail_x - kCellWidth;
scene_->addLine(
QLineF(expression_output, QPointF(output_center_x - kNodeTerminalX, expression_output.y())),
QLineF(expression_output, QPointF(output_left, main_y)),
ladderPen(rung_active));
if (rung.output.has_value())
{
scene_->addLine(
QLineF(QPointF(output_left, main_y), QPointF(right_rail_x, main_y)),
ladderPen(rung_active));
scene_->addItem(new NodeItem(
*rung.output,
rung.id,
{},
QPointF(output_center_x, expression_output.y()),
QPointF(output_left + kCellWidth / 2.0, main_y),
rung_active,
rung.output->id == fault_node_id_));
}
else
{
addPlaceholder(*scene_, QPointF(output_center_x, expression_output.y()), tr("输出线圈"));
addPlaceholder(
*scene_,
QRectF(output_left, grid_top, kCellWidth, kCellHeight),
tr("输出线圈"));
scene_->addLine(
QLineF(
QPointF(output_left + kCellWidth, main_y),
QPointF(right_rail_x, main_y)),
ladderPen(rung_active));
}
scene_->addLine(
QLineF(output_center_x + kNodeTerminalX,
expression_output.y(),
right_rail_x,
expression_output.y()),
ladderPen(rung_active));
top += height + kRungGap;
top += rung_height + kRungGap;
++number;
}
scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + 20.0));
scene_->setSceneRect(0, 0, scene_width, std::max(400.0, top + kSceneMargin));
}

void LogicEditorWidget::selectNode(const std::string &node_id)
@@ -631,58 +650,74 @@ void LogicEditorWidget::selectNode(const std::string &node_id)
node->setSelected(node->nodeId() == node_id);
if (node->nodeId() == node_id)
{
current_rung_id_ = node->rungId();
ensureVisible(node);
}
}
else
{
item->setSelected(false);
}
}
}

std::string LogicEditorWidget::selectedNodeId() const
{
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const NodeItem *node = dynamic_cast<const NodeItem *>(item))
{
return node->nodeId();
}
}
return {};
const std::vector<std::string> ids = selectedNodeIds();
return ids.size() == 1U ? ids.front() : std::string{};
}

std::string LogicEditorWidget::selectedExpressionId() const
std::vector<std::string> LogicEditorWidget::selectedNodeIds() const
{
std::vector<std::pair<QPointF, std::string>> positioned_ids;
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const NodeItem *node = dynamic_cast<const NodeItem *>(item))
{
return node->expressionId();
positioned_ids.emplace_back(node->scenePos(), node->nodeId());
}
if (const ExpressionItem *expression = dynamic_cast<const ExpressionItem *>(item))
}
std::sort(
positioned_ids.begin(), positioned_ids.end(),
[](const auto &left, const auto &right)
{
return expression->expressionId();
}
if (!qFuzzyCompare(left.first.y(), right.first.y()))
{
return left.first.y() < right.first.y();
}
return left.first.x() < right.first.x();
});
std::vector<std::string> ids;
ids.reserve(positioned_ids.size());
for (const auto &positioned_id : positioned_ids)
{
ids.push_back(positioned_id.second);
}
return {};
return ids;
}

std::string LogicEditorWidget::selectedRungId() const
{
std::string rung_id;
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const NodeItem *node = dynamic_cast<const NodeItem *>(item))
{
return node->rungId();
}
if (const ExpressionItem *expression = dynamic_cast<const ExpressionItem *>(item))
{
return expression->rungId();
if (!rung_id.empty() && rung_id != node->rungId())
{
return {};
}
rung_id = node->rungId();
}
if (const RungItem *rung = dynamic_cast<const RungItem *>(item))
else if (const RungItem *rung = dynamic_cast<const RungItem *>(item))
{
return rung->rungId();
if (rung_id.empty())
{
rung_id = rung->rungId();
}
}
}
return {};
return rung_id;
}

LogicEditorResult LogicEditorWidget::addRung()
@@ -701,38 +736,27 @@ LogicEditorResult LogicEditorWidget::addRung()
return result;
}

LogicEditorResult LogicEditorWidget::appendCondition(const LogicNodeConfig &config)
{
const LogicEditorResult result = editor_service_.appendCondition(
logic_id_, currentRungId(), config);
if (result.succeeded)
{
reloadLogic();
selectNode(result.id);
emit graphChanged();
}
else
{
reportFailure(result);
}
return result;
}

LogicEditorResult LogicEditorWidget::insertCondition(
const LogicNodeConfig &config, SeriesInsertPosition position)
LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config)
{
const std::string rung_id = selectedRungId();
const std::string expression_id = selectedExpressionId();
const std::vector<std::string> selected_ids = selectedNodeIds();
LogicEditorResult result;
if (rung_id.empty() || expression_id.empty())
if (selected_ids.size() > 1U)
{
result = {false, LogicEditorError::InvalidOperation,
"请先选择串联插入目标节点或支路", {}};
"串联插入时只能选择一个节点", {}};
}
else if (selected_ids.size() == 1U)
{
const LogicNode *selected_node = editor_service_.findNode(
logic_id_, selected_ids.front());
result = selected_node != nullptr && selected_node->isCondition()
? editor_service_.insertConditionAfter(
logic_id_, currentRungId(), selected_ids.front(), config)
: editor_service_.appendCondition(logic_id_, currentRungId(), config);
}
else
{
result = editor_service_.insertCondition(
logic_id_, rung_id, expression_id, position, config);
result = editor_service_.appendCondition(logic_id_, currentRungId(), config);
}
if (result.succeeded)
{
@@ -747,20 +771,29 @@ LogicEditorResult LogicEditorWidget::insertCondition(
return result;
}

LogicEditorResult LogicEditorWidget::addParallelCondition(const LogicNodeConfig &config)
LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config)
{
const std::string rung_id = selectedRungId();
const std::string expression_id = selectedExpressionId();
const std::vector<std::string> selected_ids = selectedNodeIds();
std::vector<std::string> condition_ids;
for (const std::string &node_id : selected_ids)
{
const LogicNode *node = editor_service_.findNode(logic_id_, node_id);
if (node != nullptr && node->isCondition())
{
condition_ids.push_back(node_id);
}
}
LogicEditorResult result;
if (rung_id.empty() || expression_id.empty())
if (rung_id.empty() || condition_ids.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"请先选择要并联的节点或整条支路", {}};
"请在同一网络中选择要并联的连续节点", {}};
}
else
{
result = editor_service_.addParallelCondition(
logic_id_, rung_id, expression_id, config);
result = editor_service_.addParallelBranch(
logic_id_, rung_id, condition_ids, config);
}
if (result.succeeded)
{
@@ -794,31 +827,33 @@ LogicEditorResult LogicEditorWidget::setOutput(const LogicNodeConfig &config)

LogicEditorResult LogicEditorWidget::deleteSelected()
{
const std::vector<std::string> node_ids = selectedNodeIds();
LogicEditorResult result;
const std::string node_id = selectedNodeId();
const std::string expression_id = selectedExpressionId();
const std::string rung_id = selectedRungId();
if (!node_id.empty())
{
result = editor_service_.removeNode(logic_id_, node_id);
}
else if (!expression_id.empty())
if (!node_ids.empty())
{
result = editor_service_.removeExpression(logic_id_, rung_id, expression_id);
for (const std::string &node_id : node_ids)
{
result = editor_service_.removeNode(logic_id_, node_id);
if (!result.succeeded)
{
break;
}
}
}
else if (!rung_id.empty())
else
{
const std::string rung_id = selectedRungId();
if (rung_id.empty())
{
return {false, LogicEditorError::InvalidOperation,
"请先选择要删除的逻辑节点或网络", {}};
}
result = editor_service_.removeRung(logic_id_, rung_id);
if (result.succeeded && current_rung_id_ == rung_id)
{
current_rung_id_ = editor_service_.firstRungId(logic_id_);
}
}
else
{
return {false, LogicEditorError::InvalidOperation,
"请先选择要删除的逻辑节点、支路或网络", {}};
}
if (result.succeeded)
{
reloadLogic();


+ 4
- 6
app/src/ui/logic_editor_widget.h Ver fichero

@@ -7,6 +7,7 @@
#include <QGraphicsView>

#include <string>
#include <vector>

class QGraphicsScene;
class QResizeEvent;
@@ -17,7 +18,6 @@ class LogicEditorWidget final : public QGraphicsView

public:
class NodeItem;
class ExpressionItem;
class RungItem;

explicit LogicEditorWidget(
@@ -33,14 +33,12 @@ public:
void reloadLogic();
void selectNode(const std::string &node_id);
std::string selectedNodeId() const;
std::string selectedExpressionId() const;
std::vector<std::string> selectedNodeIds() const;
std::string selectedRungId() const;

LogicEditorResult addRung();
LogicEditorResult appendCondition(const LogicNodeConfig &config);
LogicEditorResult insertCondition(
const LogicNodeConfig &config, SeriesInsertPosition position);
LogicEditorResult addParallelCondition(const LogicNodeConfig &config);
LogicEditorResult addCondition(const LogicNodeConfig &config);
LogicEditorResult addParallelBranch(const LogicNodeConfig &config);
LogicEditorResult setOutput(const LogicNodeConfig &config);
LogicEditorResult deleteSelected();



+ 62
- 30
app/src/ui/main_window.cpp Ver fichero

@@ -20,6 +20,7 @@
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QMenu>
#include <QPushButton>
#include <QSpinBox>
#include <QStatusBar>
@@ -27,6 +28,7 @@
#include <QTimer>
#include <QTabWidget>
#include <QTableWidget>
#include <QToolButton>
#include <QTreeWidget>
#include <QTreeWidgetItem>

@@ -206,14 +208,51 @@ void MainWindow::configureActions()
connect(ui_->deleteControlAction, &QAction::triggered,
this, &MainWindow::deleteSelectedControl);

logic_insert_action_group_ = new QActionGroup(this);
logic_insert_action_group_->setExclusive(true);
logic_insert_action_group_->addAction(ui_->appendInsertAction);
logic_insert_action_group_->addAction(ui_->beforeInsertAction);
logic_insert_action_group_->addAction(ui_->afterInsertAction);
logic_insert_action_group_->addAction(ui_->parallelInsertAction);
connect(ui_->addRungAction, &QAction::triggered,
this, &MainWindow::addLogicRung);
connect(ui_->parallelInsertAction, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen});
});
QMenu *parallel_menu = new QMenu(this);
QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点"));
QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点"));
QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件"));
connect(parallel_open, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyOpen});
});
connect(parallel_closed, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0},
ContactMode::NormallyClosed});
});
connect(parallel_compare, &QAction::triggered,
this,
[this]
{
addLogicParallelBranch(CompareNodeConfig{
RegisterAddress{RegisterArea::D, 0},
ComparisonOperator::Equal,
0});
});
ui_->parallelInsertAction->setMenu(parallel_menu);
if (QToolButton *parallel_button = qobject_cast<QToolButton *>(
ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction)))
{
parallel_button->setPopupMode(QToolButton::MenuButtonPopup);
}
connect(ui_->addNormallyOpenAction, &QAction::triggered,
this,
[this]
@@ -315,9 +354,6 @@ void MainWindow::configureAppearance()
style()->standardIcon(QStyle::SP_FileDialogContentsView));
ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
ui_->appendInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
ui_->beforeInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft));
ui_->afterInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown));
ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
@@ -352,7 +388,8 @@ void MainWindow::configureAppearance()
"QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }"
"QToolButton:checked { background: #dcece3; border-color: #75a58a; }"
"QDockWidget { color: #24313b; font-weight: 600; }"
"QDockWidget::title { background: #e9edf0; padding: 6px; border-bottom: 1px solid #cbd2d9; }"
"QDockWidget::title { background: #e9edf0; padding: 6px;"
" border-bottom: 1px solid #cbd2d9; }"
"QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }"
"QTabWidget::pane { border: 0; background: #f3f5f7; }"
"QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }"
@@ -924,37 +961,32 @@ void MainWindow::applySelectedControlProperties()

void MainWindow::addLogicCondition(const LogicNodeConfig &config)
{
LogicEditorResult result;
if (ui_->beforeInsertAction->isChecked())
{
result = logic_editor_widget_->insertCondition(
config, SeriesInsertPosition::Before);
}
else if (ui_->afterInsertAction->isChecked())
{
result = logic_editor_widget_->insertCondition(
config, SeriesInsertPosition::After);
}
else if (ui_->parallelInsertAction->isChecked())
{
result = logic_editor_widget_->addParallelCondition(config);
}
else
{
result = logic_editor_widget_->appendCondition(config);
}
const LogicEditorResult result = logic_editor_widget_->addCondition(config);
if (!result.succeeded)
{
showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false);
return;
}
ui_->appendInsertAction->setChecked(true);
selected_logic_node_id_ = result.id;
showLogicNodeProperties(result.id);
refreshProjectUi();
statusBar()->showMessage(tr("已添加逻辑条件"), 3000);
}

void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config)
{
const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config);
if (!result.succeeded)
{
showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false);
return;
}
selected_logic_node_id_ = result.id;
showLogicNodeProperties(result.id);
refreshProjectUi();
statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000);
}

void MainWindow::setLogicOutput(const LogicNodeConfig &config)
{
const LogicEditorResult result = logic_editor_widget_->setOutput(config);


+ 3
- 2
app/src/ui/main_window.h Ver fichero

@@ -90,8 +90,10 @@ private:
void deleteSelectedControl();
// 将属性表单内容应用到当前选中控件
void applySelectedControlProperties();
// 向当前网络添加串联条件,或按当前插入模式添加并联条件
// 在当前光标节点后添加串联条件,没有选择时追加到网络末尾
void addLogicCondition(const LogicNodeConfig &config);
// 为当前选中的连续逻辑范围建立并联支路
void addLogicParallelBranch(const LogicNodeConfig &config);
// 设置当前网络右侧的输出线圈
void setLogicOutput(const LogicNodeConfig &config);
// 新增一个梯形图网络
@@ -152,7 +154,6 @@ private:
LogicEditorService &logic_editor_service_;
HmiRuntimeService &hmi_runtime_service_;
QActionGroup *mode_action_group_ = nullptr;
QActionGroup *logic_insert_action_group_ = nullptr;
HmiEditorWidget *hmi_editor_widget_ = nullptr;
LogicEditorWidget *logic_editor_widget_ = nullptr;
QTimer *runtime_refresh_timer_ = nullptr;


+ 1
- 7
app/src/ui/main_window.ui Ver fichero

@@ -288,9 +288,6 @@
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
<attribute name="toolBarBreak"><bool>false</bool></attribute>
<addaction name="addRungAction"/>
<addaction name="appendInsertAction"/>
<addaction name="beforeInsertAction"/>
<addaction name="afterInsertAction"/>
<addaction name="parallelInsertAction"/>
<addaction name="separator"/>
<addaction name="addNormallyOpenAction"/>
@@ -572,10 +569,7 @@
<action name="addNumericInputAction"><property name="text"><string>数值输入</string></property><property name="toolTip"><string>添加数值输入控件</string></property></action>
<action name="deleteControlAction"><property name="text"><string>删除</string></property><property name="toolTip"><string>删除当前控件</string></property></action>
<action name="addRungAction"><property name="text"><string>新建网络</string></property><property name="toolTip"><string>在梯形图末尾新增网络</string></property></action>
<action name="appendInsertAction"><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><property name="text"><string>末尾追加</string></property><property name="toolTip"><string>将下一个条件追加到当前网络末尾</string></property></action>
<action name="beforeInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>前插</string></property><property name="toolTip"><string>将下一个条件串联到选中节点或支路之前</string></property></action>
<action name="afterInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>后插</string></property><property name="toolTip"><string>将下一个条件串联到选中节点或支路之后</string></property></action>
<action name="parallelInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>并联插入</string></property><property name="toolTip"><string>将下一个条件并联到选中节点或整条支路</string></property></action>
<action name="parallelInsertAction"><property name="text"><string>并联支路</string></property><property name="toolTip"><string>为画布中选中的连续节点建立并联支路</string></property></action>
<action name="addNormallyOpenAction"><property name="text"><string>常开</string></property><property name="toolTip"><string>向当前网络添加常开触点</string></property></action>
<action name="addNormallyClosedAction"><property name="text"><string>常闭</string></property><property name="toolTip"><string>向当前网络添加常闭触点</string></property></action>
<action name="addNormalCoilAction"><property name="text"><string>线圈</string></property><property name="toolTip"><string>设置当前网络的普通线圈</string></property></action>


+ 22
- 13
app/tests/logic_editor_service_tests.cpp Ver fichero

@@ -52,8 +52,8 @@ void testStructuredEditingAndNormalization()
"two appended nodes must form a series expression");

const std::string second_expression_id = second.id;
const LogicEditorResult parallel = service.addParallelCondition(
logic_id, rung_id, second_expression_id, contact(2));
const LogicEditorResult parallel = service.addParallelBranch(
logic_id, rung_id, {second_expression_id}, contact(2));
require(parallel.succeeded, "parallel insertion must succeed");
rung = service.findRung(logic_id, rung_id);
const ConditionExpression *parallel_expression = service.findExpression(
@@ -62,11 +62,10 @@ void testStructuredEditingAndNormalization()
&& parallel_expression->kind == ConditionExpressionKind::Parallel,
"selected node must become a parallel expression");

const LogicEditorResult nested_series = service.insertCondition(
const LogicEditorResult nested_series = service.insertConditionAfter(
logic_id,
rung_id,
parallel.id,
SeriesInsertPosition::After,
contact(3));
require(nested_series.succeeded, "a parallel branch must accept a series node");
rung = service.findRung(logic_id, rung_id);
@@ -103,7 +102,7 @@ void testStructuredEditingAndNormalization()
"structured editing result must remain a valid draft");
}

void testBranchLevelParallelInsertion()
void testRangeParallelInsertion()
{
TestProjectStorage storage;
ProjectService project_service(storage);
@@ -113,14 +112,24 @@ void testBranchLevelParallelInsertion()
service.appendCondition(logic_id, rung_id, contact(0));
service.appendCondition(logic_id, rung_id, contact(1));

const std::string series_id = service.findRung(logic_id, rung_id)->condition->id;
const LogicEditorResult branch = service.addParallelCondition(
logic_id, rung_id, series_id, contact(2));
require(branch.succeeded, "a whole series branch must accept a parallel condition");
service.appendCondition(logic_id, rung_id, contact(2));
const LogicEditorResult branch = service.addParallelBranch(
logic_id, rung_id, {"contact-2", "contact-3"}, contact(3));
require(branch.succeeded, "a continuous series range must accept a parallel branch");
const ConditionExpression &root = *service.findRung(logic_id, rung_id)->condition;
require(root.kind == ConditionExpressionKind::Parallel
&& root.children.front().kind == ConditionExpressionKind::Series,
"branch-level insertion must express (A AND B) OR C");
require(root.kind == ConditionExpressionKind::Series
&& root.children.size() == 2U
&& root.children.at(1).kind == ConditionExpressionKind::Parallel
&& root.children.at(1).children.front().kind
== ConditionExpressionKind::Series,
"range insertion must express A AND ((B AND C) OR D)");
require(root.validate(), "range insertion must preserve normalized topology");

const LogicEditorResult invalid = service.addParallelBranch(
logic_id, rung_id, {"contact-1", "contact-3"}, contact(4));
require(!invalid.succeeded
&& invalid.error == LogicEditorError::InvalidOperation,
"a non-contiguous selection must be rejected");
}

} // namespace
@@ -130,7 +139,7 @@ int main()
try
{
testStructuredEditingAndNormalization();
testBranchLevelParallelInsertion();
testRangeParallelInsertion();
}
catch (const std::exception &error)
{


+ 1
- 3
app/tests/main_window_tests.cpp Ver fichero

@@ -126,7 +126,6 @@ void testModeActionsControlEditingAvailability()

add_normally_open_action->trigger();
parallel_insert_action->trigger();
add_normally_open_action->trigger();
add_normal_coil_action->trigger();
const ControlLogic *logic = logic_editor_service.findLogic(
logic_editor_service.firstLogicId());
@@ -136,10 +135,9 @@ void testModeActionsControlEditingAvailability()
&& logic->rungs.front().condition->kind
== ConditionExpressionKind::Parallel
&& logic->rungs.front().condition->children.size() == 2U,
"parallel insert mode must create a parallel expression");
"parallel branch action must create a parallel expression");
require(logic->rungs.front().output.has_value(),
"coil action must set the fixed ladder output");

offline_action->trigger();
require(mode_service.mode() == ApplicationMode::Editing,
"unconfigured ladder nodes must block offline running");


Cargando…
Cancelar
Guardar