Procházet zdrojové kódy

feat: 支持梯形图结构化横线与竖线编辑

main
suyu před 1 měsícem
rodič
revize
2fa8c1f226
19 změnil soubory, kde provedl 1335 přidání a 35 odebrání
  1. +54
    -3
      app/src/domain/control_logic_model.cpp
  2. +13
    -0
      app/src/domain/control_logic_model.h
  3. +25
    -1
      app/src/infrastructure/json_project_storage.cpp
  4. +367
    -9
      app/src/services/logic_editor_service.cpp
  5. +23
    -0
      app/src/services/logic_editor_service.h
  6. +48
    -2
      app/src/services/software_logic_executor.cpp
  7. +4
    -0
      app/src/services/software_logic_executor.h
  8. +452
    -17
      app/src/ui/logic_editor_widget.cpp
  9. +11
    -0
      app/src/ui/logic_editor_widget.h
  10. +72
    -0
      app/src/ui/main_window.cpp
  11. +4
    -0
      app/src/ui/main_window.h
  12. +48
    -0
      app/src/ui/main_window.ui
  13. +18
    -0
      app/src/ui/toolbar_icon_factory.cpp
  14. +2
    -0
      app/src/ui/toolbar_icon_factory.h
  15. +20
    -0
      app/tests/domain_tests.cpp
  16. +61
    -0
      app/tests/logic_editor_service_tests.cpp
  17. +69
    -2
      app/tests/main_window_tests.cpp
  18. +39
    -0
      app/tests/offline_simulation_service_tests.cpp
  19. +5
    -1
      app/tests/project_management_tests.cpp

+ 54
- 3
app/src/domain/control_logic_model.cpp Zobrazit soubor

@@ -229,7 +229,9 @@ bool validateUniqueExpressionIds(

void normalizeExpression(ConditionExpression *expression)
{
if (expression == nullptr || expression->kind == ConditionExpressionKind::Node)
if (expression == nullptr
|| expression->kind == ConditionExpressionKind::Node
|| expression->kind == ConditionExpressionKind::Wire)
{
return;
}
@@ -479,6 +481,26 @@ ConditionExpression ConditionExpression::fromNode(LogicNode logic_node)
return expression;
}

bool WireSegment::validate(std::string *error) const
{
if (columnSpan < kMinimumColumnSpan || columnSpan > kMaximumColumnSpan)
{
setError(error, "横线跨度必须在 1~256 列范围内");
return false;
}
return true;
}

ConditionExpression ConditionExpression::fromWire(
std::string expression_id, int column_span)
{
ConditionExpression expression;
expression.id = std::move(expression_id);
expression.kind = ConditionExpressionKind::Wire;
expression.wire = WireSegment{column_span};
return expression;
}

bool ConditionExpression::validate(std::string *error) const
{
if (id.empty())
@@ -488,14 +510,30 @@ bool ConditionExpression::validate(std::string *error) const
}
if (kind == ConditionExpressionKind::Node)
{
if (!node.has_value() || !children.empty() || !node->isCondition())
if (!node.has_value() || wire.has_value()
|| !children.empty() || !node->isCondition())
{
setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式");
return false;
}
return node->validate(error);
}
if (node.has_value() || children.size() < 2U)
if (kind == ConditionExpressionKind::Wire)
{
if (node.has_value() || !wire.has_value() || !children.empty())
{
setError(error, "横线叶节点必须包含横线配置且不能包含逻辑节点或子表达式");
return false;
}
return wire->validate(error);
}
if (kind != ConditionExpressionKind::Series
&& kind != ConditionExpressionKind::Parallel)
{
setError(error, "条件表达式使用了不支持的类型");
return false;
}
if (node.has_value() || wire.has_value() || children.size() < 2U)
{
setError(error, "串联和并联表达式至少需要两个子表达式");
return false;
@@ -530,6 +568,10 @@ bool ConditionExpression::validateForRunning(std::string *error) const
}
return true;
}
if (kind == ConditionExpressionKind::Wire)
{
return true;
}
for (const ConditionExpression &child : children)
{
if (!child.validateForRunning(error))
@@ -548,6 +590,7 @@ void normalizeConditionExpression(std::optional<ConditionExpression> *expression
}
normalizeExpression(&expression->value());
if (expression->value().kind != ConditionExpressionKind::Node
&& expression->value().kind != ConditionExpressionKind::Wire
&& expression->value().children.empty())
{
expression->reset();
@@ -557,6 +600,10 @@ void normalizeConditionExpression(std::optional<ConditionExpression> *expression
const LogicNode *findConditionNode(
const ConditionExpression &expression, const std::string &node_id)
{
if (expression.kind == ConditionExpressionKind::Wire)
{
return nullptr;
}
if (expression.kind == ConditionExpressionKind::Node)
{
return expression.node->id == node_id ? &*expression.node : nullptr;
@@ -615,6 +662,10 @@ void collectConditionNodes(
nodes->push_back(&*expression.node);
return;
}
if (expression.kind == ConditionExpressionKind::Wire)
{
return;
}
for (const ConditionExpression &child : expression.children)
{
collectConditionNodes(child, nodes);


+ 13
- 0
app/src/domain/control_logic_model.h Zobrazit soubor

@@ -204,19 +204,32 @@ struct LogicNode
enum class ConditionExpressionKind
{
Node,
Wire,
Series,
Parallel
};

struct WireSegment
{
static constexpr int kMinimumColumnSpan = 1;
static constexpr int kMaximumColumnSpan = 256;

int columnSpan = 1;

bool validate(std::string *error = nullptr) const;
};

// 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接
struct ConditionExpression
{
std::string id;
ConditionExpressionKind kind = ConditionExpressionKind::Node;
std::optional<LogicNode> node;
std::optional<WireSegment> wire;
std::vector<ConditionExpression> children;

static ConditionExpression fromNode(LogicNode node);
static ConditionExpression fromWire(std::string id, int column_span = 1);
bool validate(std::string *error = nullptr) const;
bool validateForRunning(std::string *error = nullptr) const;
};


+ 25
- 1
app/src/infrastructure/json_project_storage.cpp Zobrazit soubor

@@ -1468,6 +1468,8 @@ QString expressionKindText(ConditionExpressionKind kind)
{
case ConditionExpressionKind::Node:
return QStringLiteral("node");
case ConditionExpressionKind::Wire:
return QStringLiteral("wire");
case ConditionExpressionKind::Series:
return QStringLiteral("series");
case ConditionExpressionKind::Parallel:
@@ -1485,6 +1487,10 @@ QJsonObject serializeConditionExpression(const ConditionExpression &expression)
{
object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node));
}
else if (expression.kind == ConditionExpressionKind::Wire)
{
object.insert(QStringLiteral("columnSpan"), expression.wire->columnSpan);
}
else
{
QJsonArray children;
@@ -1525,11 +1531,29 @@ bool parseConditionExpression(
expression->node = std::move(parsed_node);
return true;
}
if (kind == "wire")
{
int column_span = 0;
if (!readInt(
object,
"columnSpan",
context,
WireSegment::kMinimumColumnSpan,
WireSegment::kMaximumColumnSpan,
&column_span,
state))
{
return false;
}
expression->kind = ConditionExpressionKind::Wire;
expression->wire = WireSegment{column_span};
return true;
}
if (kind != "series" && kind != "parallel")
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".kind 必须是 node、series 或 parallel");
context + ".kind 必须是 node、wire、series 或 parallel");
}
QJsonArray children;
if (!readArray(object, "children", context, &children, state))


+ 367
- 9
app/src/services/logic_editor_service.cpp Zobrazit soubor

@@ -75,18 +75,43 @@ ConditionExpression *findParentExpression(

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

NodeIdSet conditionNodeIds(const ConditionExpression &expression)
NodeIdSet conditionLeafIds(const ConditionExpression &expression)
{
std::vector<const LogicNode *> nodes;
collectConditionNodes(expression, &nodes);
NodeIdSet ids;
for (const LogicNode *node : nodes)
if (expression.kind == ConditionExpressionKind::Node
|| expression.kind == ConditionExpressionKind::Wire)
{
ids.insert(node->id);
ids.insert(expression.id);
return ids;
}
for (const ConditionExpression &child : expression.children)
{
NodeIdSet child_ids = conditionLeafIds(child);
ids.insert(child_ids.cbegin(), child_ids.cend());
}
return ids;
}

int expressionColumns(const ConditionExpression &expression)
{
if (expression.kind == ConditionExpressionKind::Node)
{
return 1;
}
if (expression.kind == ConditionExpressionKind::Wire)
{
return expression.wire->columnSpan;
}
int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1;
for (const ConditionExpression &child : expression.children)
{
const int child_columns = expressionColumns(child);
columns = expression.kind == ConditionExpressionKind::Series
? columns + child_columns : std::max(columns, child_columns);
}
return columns;
}

bool isSubset(const NodeIdSet &subset, const NodeIdSet &values)
{
return std::all_of(
@@ -124,7 +149,7 @@ bool addParallelForSelection(
const std::string &parallel_id,
const std::string &series_id)
{
const NodeIdSet expression_ids = conditionNodeIds(*expression);
const NodeIdSet expression_ids = conditionLeafIds(*expression);
if (sameValues(expression_ids, selected_ids))
{
addParallelSibling(expression, std::move(*branch), parallel_id);
@@ -136,7 +161,7 @@ bool addParallelForSelection(
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]));
child_ids.push_back(conditionLeafIds(expression->children[index]));
const bool intersects = std::any_of(
child_ids.back().cbegin(), child_ids.back().cend(),
[&selected_ids](const std::string &id)
@@ -212,7 +237,9 @@ bool addParallelForSelection(
bool removeExpressionRecursive(
ConditionExpression *expression, const std::string &expression_id)
{
if (expression == nullptr || expression->kind == ConditionExpressionKind::Node)
if (expression == nullptr
|| expression->kind == ConditionExpressionKind::Node
|| expression->kind == ConditionExpressionKind::Wire)
{
return false;
}
@@ -238,6 +265,65 @@ bool removeExpressionRecursive(
return false;
}

std::optional<int> selectedExpressionColumns(
const ConditionExpression &expression,
const NodeIdSet &selected_ids)
{
const NodeIdSet expression_ids = conditionLeafIds(expression);
if (sameValues(expression_ids, selected_ids))
{
return expressionColumns(expression);
}
if (expression.kind == ConditionExpressionKind::Node
|| expression.kind == ConditionExpressionKind::Wire)
{
return std::nullopt;
}

std::vector<NodeIdSet> child_ids;
std::vector<std::size_t> matching_children;
child_ids.reserve(expression.children.size());
for (std::size_t index = 0; index < expression.children.size(); ++index)
{
child_ids.push_back(conditionLeafIds(expression.children[index]));
const bool intersects = std::any_of(
child_ids.back().cbegin(), child_ids.back().cend(),
[&selected_ids](const std::string &id)
{
return selected_ids.count(id) != 0U;
});
if (intersects)
{
matching_children.push_back(index);
}
}
if (matching_children.size() == 1U)
{
return selectedExpressionColumns(
expression.children[matching_children.front()], selected_ids);
}
if (matching_children.empty()
|| expression.kind != ConditionExpressionKind::Series)
{
return std::nullopt;
}
const std::size_t first = matching_children.front();
const std::size_t last = matching_children.back();
if (last - first + 1U != matching_children.size())
{
return std::nullopt;
}
NodeIdSet range_ids;
int columns = 0;
for (std::size_t index = first; index <= last; ++index)
{
range_ids.insert(child_ids[index].cbegin(), child_ids[index].cend());
columns += expressionColumns(expression.children[index]);
}
return sameValues(range_ids, selected_ids)
? std::optional<int>{columns} : std::nullopt;
}

LadderRung *findEditableRung(
Project &project,
const std::string &logic_id,
@@ -329,6 +415,7 @@ bool LogicEditorService::expressionsEqual(
{
if (left.id != right.id || left.kind != right.kind
|| left.node.has_value() != right.node.has_value()
|| left.wire.has_value() != right.wire.has_value()
|| left.children.size() != right.children.size())
{
return false;
@@ -337,6 +424,11 @@ bool LogicEditorService::expressionsEqual(
{
return false;
}
if (left.wire.has_value()
&& left.wire->columnSpan != right.wire->columnSpan)
{
return false;
}
for (std::size_t index = 0; index < left.children.size(); ++index)
{
if (!expressionsEqual(left.children[index], right.children[index]))
@@ -809,6 +901,50 @@ LogicEditorResult LogicEditorService::appendCondition(
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr || findRung(logic_id, rung_id) == nullptr)
{
return failure(
logic == nullptr ? LogicEditorError::LogicNotFound
: LogicEditorError::RungNotFound,
logic == nullptr ? "未找到控制逻辑" : "未找到梯形图网络");
}
WireSegment wire{column_span};
std::string error;
if (!wire.validate(&error))
{
return failure(LogicEditorError::InvalidOperation, error);
}
const std::string wire_id = makeUniqueWireId(*logic);
ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span);
HistoryState before = captureState();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
if (!rung->condition.has_value())
{
rung->condition = std::move(leaf);
}
else if (rung->condition->kind == ConditionExpressionKind::Series)
{
rung->condition->children.push_back(std::move(leaf));
}
else
{
rung->condition = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(*rung->condition),
std::move(leaf));
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}

LogicEditorResult LogicEditorService::insertConditionAfter(
const std::string &logic_id,
const std::string &rung_id,
@@ -858,6 +994,91 @@ LogicEditorResult LogicEditorService::insertConditionAfter(
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::insertWireAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
int column_span)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *existing_rung = findRung(logic_id, rung_id);
const ConditionExpression *target = findExpression(
logic_id, rung_id, target_expression_id);
WireSegment wire{column_span};
std::string error;
if (logic == nullptr || existing_rung == nullptr || target == nullptr)
{
return failure(LogicEditorError::ExpressionNotFound, "未找到横线插入位置");
}
if ((target->kind != ConditionExpressionKind::Node
&& target->kind != ConditionExpressionKind::Wire)
|| !wire.validate(&error))
{
return failure(
LogicEditorError::InvalidOperation,
error.empty() ? "只能在触点或横线后插入横线" : error);
}
const std::string wire_id = makeUniqueWireId(*logic);
ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span);
HistoryState before = captureState();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
ConditionExpression *editable_target = findConditionExpression(
*rung->condition, target_expression_id);
ConditionExpression *parent = findParentExpression(
*rung->condition, target_expression_id);
if (parent != nullptr && parent->kind == ConditionExpressionKind::Series)
{
const auto target_iterator = std::find_if(
parent->children.begin(), parent->children.end(),
[&target_expression_id](const ConditionExpression &child)
{
return child.id == target_expression_id;
});
parent->children.insert(target_iterator + 1, std::move(leaf));
}
else
{
ConditionExpression original = std::move(*editable_target);
*editable_target = makeContainer(
makeUniqueExpressionId(*logic),
ConditionExpressionKind::Series,
std::move(original),
std::move(leaf));
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}

LogicEditorResult LogicEditorService::replaceWireWithCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &wire_expression_id,
const LogicNodeConfig &config)
{
const ControlLogic *logic = findLogic(logic_id);
const ConditionExpression *wire = findExpression(
logic_id, rung_id, wire_expression_id);
if (logic == nullptr || wire == nullptr
|| wire->kind != ConditionExpressionKind::Wire)
{
return failure(LogicEditorError::ExpressionNotFound, "未找到要替换的横线");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "横线只能替换为条件节点");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
HistoryState before = captureState();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
ConditionExpression *editable = findConditionExpression(
*rung->condition, wire_expression_id);
*editable = ConditionExpression::fromNode(makeNode(node_id, config));
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
@@ -885,7 +1106,7 @@ LogicEditorResult LogicEditorService::addParallelBranch(
LogicEditorError::InvalidOperation,
"并联选择中存在重复节点");
}
const NodeIdSet available_ids = conditionNodeIds(*existing_rung->condition);
const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition);
if (!isSubset(selected_ids, available_ids))
{
return failure(
@@ -918,6 +1139,72 @@ LogicEditorResult LogicEditorService::addParallelBranch(
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelWireBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_expression_ids)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (logic == nullptr || existing_rung == nullptr
|| !existing_rung->condition.has_value())
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图条件网络");
}
if (selected_expression_ids.empty())
{
return failure(
LogicEditorError::InvalidOperation,
"建立竖线连接前必须选择连续的触点或横线");
}
NodeIdSet selected_ids(
selected_expression_ids.cbegin(), selected_expression_ids.cend());
if (selected_ids.size() != selected_expression_ids.size())
{
return failure(LogicEditorError::InvalidOperation, "并联选择中存在重复对象");
}
const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition);
if (!isSubset(selected_ids, available_ids))
{
return failure(LogicEditorError::ExpressionNotFound, "并联选择中包含未知对象");
}
const std::optional<int> column_span = selectedExpressionColumns(
*existing_rung->condition, selected_ids);
if (!column_span.has_value()
|| *column_span > WireSegment::kMaximumColumnSpan)
{
return failure(
LogicEditorError::InvalidOperation,
"竖线连接只能围绕同一支路中的连续逻辑范围");
}

const std::string wire_id = makeUniqueWireId(*logic);
ConditionExpression wire = ConditionExpression::fromWire(
wire_id, *column_span);
const std::string parallel_id = makeUniqueExpressionId(*logic);
std::string series_id = parallel_id + "-range";
while (findExpression(logic_id, rung_id, series_id) != nullptr)
{
series_id += "-range";
}
HistoryState before = captureState();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
if (!addParallelForSelection(
&*rung->condition,
selected_ids,
&wire,
parallel_id,
series_id))
{
return failure(
LogicEditorError::InvalidOperation,
"竖线连接只能围绕同一支路中的连续逻辑范围");
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, wire_id};
}

LogicEditorResult LogicEditorService::setOutput(
const std::string &logic_id,
const std::string &rung_id,
@@ -1100,6 +1387,58 @@ LogicEditorResult LogicEditorService::removeExpression(
return {true, LogicEditorError::None, {}, expression_id};
}

LogicEditorResult LogicEditorService::removeExpressions(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &expression_ids)
{
const LadderRung *existing_rung = findRung(logic_id, rung_id);
if (existing_rung == nullptr || !existing_rung->condition.has_value())
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图条件网络");
}
if (expression_ids.empty())
{
return failure(LogicEditorError::InvalidOperation, "请先选择要删除的条件或横线");
}
NodeIdSet unique_ids;
for (const std::string &expression_id : expression_ids)
{
const ConditionExpression *expression = findExpression(
logic_id, rung_id, expression_id);
if (!unique_ids.insert(expression_id).second)
{
return failure(LogicEditorError::InvalidOperation, "删除列表中存在重复对象");
}
if (expression == nullptr)
{
return failure(
LogicEditorError::ExpressionNotFound,
"删除列表中包含未知条件表达式");
}
}

HistoryState before = captureState();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
for (const std::string &expression_id : expression_ids)
{
if (!rung->condition.has_value())
{
break;
}
if (rung->condition->id == expression_id)
{
rung->condition.reset();
break;
}
removeExpressionRecursive(&*rung->condition, expression_id);
}
normalizeConditionExpression(&rung->condition);
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, expression_ids.front()};
}

bool LogicEditorService::canUndo() const
{
return history_.canUndo();
@@ -1225,6 +1564,25 @@ std::string LogicEditorService::makeUniqueNodeId(
}
}

std::string LogicEditorService::makeUniqueWireId(const ControlLogic &logic)
{
for (std::size_t index = 1;; ++index)
{
const std::string candidate = "wire-" + std::to_string(index);
const bool found = std::any_of(
logic.rungs.cbegin(), logic.rungs.cend(),
[&candidate](const LadderRung &rung)
{
return rung.condition.has_value()
&& findConditionExpression(*rung.condition, candidate) != nullptr;
});
if (!found)
{
return candidate;
}
}
}

std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic)
{
for (std::size_t index = 1;; ++index)


+ 23
- 0
app/src/services/logic_editor_service.h Zobrazit soubor

@@ -68,16 +68,34 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config);
LogicEditorResult appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span = 1);
LogicEditorResult insertConditionAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_node_id,
const LogicNodeConfig &config);
LogicEditorResult insertWireAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
int column_span = 1);
LogicEditorResult replaceWireWithCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &wire_expression_id,
const LogicNodeConfig &config);
LogicEditorResult addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config);
LogicEditorResult addParallelWireBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_expression_ids);
LogicEditorResult setOutput(
const std::string &logic_id,
const std::string &rung_id,
@@ -96,6 +114,10 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id);
LogicEditorResult removeExpressions(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &expression_ids);

bool canUndo() const;
bool canRedo() const;
@@ -129,6 +151,7 @@ private:
static std::string nodePrefix(const LogicNodeConfig &config);
static std::string makeUniqueNodeId(
const ControlLogic &logic, const std::string &prefix);
static std::string makeUniqueWireId(const ControlLogic &logic);
static std::string makeUniqueExpressionId(const ControlLogic &logic);
static std::string makeUniqueRungId(const ControlLogic &logic);
static LogicEditorResult failure(


+ 48
- 2
app/src/services/software_logic_executor.cpp Zobrazit soubor

@@ -74,7 +74,10 @@ bool coilModesAreCompatible(CoilMode existing, CoilMode current)
void LogicTraceValues::clear()
{
nodeValues.clear();
nodePowerValues.clear();
expressionValues.clear();
expressionInputValues.clear();
expressionPowerValues.clear();
rungValues.clear();
tonValues.clear();
counterValues.clear();
@@ -95,7 +98,10 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic(
if (values != logicValues.cend())
{
projection.nodeValues = values->second.nodeValues;
projection.nodePowerValues = values->second.nodePowerValues;
projection.expressionValues = values->second.expressionValues;
projection.expressionInputValues = values->second.expressionInputValues;
projection.expressionPowerValues = values->second.expressionPowerValues;
projection.rungValues = values->second.rungValues;
projection.tonValues = values->second.tonValues;
projection.counterValues = values->second.counterValues;
@@ -224,7 +230,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(

bool rung_value = false;
LogicScanResult result = evaluateExpression(
logic.id, *rung.condition, repository, logic_trace, &rung_value);
logic.id,
*rung.condition,
repository,
logic_trace,
true,
&rung_value);
if (!result.succeeded)
{
result.logicId = logic.id;
@@ -235,6 +246,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
{
logic_trace->rungValues[rung.id] = rung_value;
logic_trace->nodeValues[rung.output->id] = rung_value;
logic_trace->nodePowerValues[rung.output->id] = rung_value;
}
bool output_value = false;
result = executeOutput(
@@ -254,6 +266,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
if (logic_trace != nullptr)
{
logic_trace->nodeValues[rung.output->id] = output_value;
logic_trace->nodePowerValues[rung.output->id] = output_value;
}
}
}
@@ -268,7 +281,10 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
if (values != trace->logicValues.cend())
{
trace->nodeValues = values->second.nodeValues;
trace->nodePowerValues = values->second.nodePowerValues;
trace->expressionValues = values->second.expressionValues;
trace->expressionInputValues = values->second.expressionInputValues;
trace->expressionPowerValues = values->second.expressionPowerValues;
trace->rungValues = values->second.rungValues;
trace->tonValues = values->second.tonValues;
trace->counterValues = values->second.counterValues;
@@ -284,12 +300,27 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression(
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceValues *trace,
bool input_power,
bool *value)
{
if (value == nullptr)
{
return failure(LogicScanError::InvalidLogic, "缺少表达式结果接收对象");
}
if (trace != nullptr)
{
trace->expressionInputValues[expression.id] = input_power;
}
if (expression.kind == ConditionExpressionKind::Wire)
{
*value = true;
if (trace != nullptr)
{
trace->expressionValues[expression.id] = true;
trace->expressionPowerValues[expression.id] = input_power;
}
return success();
}
if (expression.kind == ConditionExpressionKind::Node)
{
LogicScanResult result = evaluateCondition(
@@ -297,28 +328,43 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression(
if (result.succeeded && trace != nullptr)
{
trace->nodeValues[expression.node->id] = *value;
trace->nodePowerValues[expression.node->id] = input_power && *value;
trace->expressionValues[expression.id] = *value;
trace->expressionPowerValues[expression.id] = input_power && *value;
}
return result;
}

bool accumulated = expression.kind == ConditionExpressionKind::Series;
bool power = input_power;
for (const ConditionExpression &child : expression.children)
{
bool child_value = false;
const bool child_input = expression.kind == ConditionExpressionKind::Series
? power : input_power;
LogicScanResult result = evaluateExpression(
logic_id, child, repository, trace, &child_value);
logic_id,
child,
repository,
trace,
child_input,
&child_value);
if (!result.succeeded)
{
return result;
}
accumulated = expression.kind == ConditionExpressionKind::Series
? accumulated && child_value : accumulated || child_value;
if (expression.kind == ConditionExpressionKind::Series)
{
power = power && child_value;
}
}
*value = accumulated;
if (trace != nullptr)
{
trace->expressionValues[expression.id] = accumulated;
trace->expressionPowerValues[expression.id] = input_power && accumulated;
}
return success();
}


+ 4
- 0
app/src/services/software_logic_executor.h Zobrazit soubor

@@ -56,7 +56,10 @@ struct WordTraceValue
struct LogicTraceValues
{
std::unordered_map<std::string, bool> nodeValues;
std::unordered_map<std::string, bool> nodePowerValues;
std::unordered_map<std::string, bool> expressionValues;
std::unordered_map<std::string, bool> expressionInputValues;
std::unordered_map<std::string, bool> expressionPowerValues;
std::unordered_map<std::string, bool> rungValues;
std::unordered_map<std::string, TonTraceValue> tonValues;
std::unordered_map<std::string, CounterTraceValue> counterValues;
@@ -111,6 +114,7 @@ private:
const ConditionExpression &expression,
RegisterRepository &repository,
LogicTraceValues *trace,
bool input_power,
bool *value);
LogicScanResult executeOutput(
const std::string &logic_id,


+ 452
- 17
app/src/ui/logic_editor_widget.cpp Zobrazit soubor

@@ -182,6 +182,10 @@ ExpressionMetrics measureExpression(const ConditionExpression &expression)
{
return {};
}
if (expression.kind == ConditionExpressionKind::Wire)
{
return {expression.wire->columnSpan, 1};
}

ExpressionMetrics metrics{0, 0};
if (expression.kind == ConditionExpressionKind::Series)
@@ -273,14 +277,16 @@ public:
const QPointF &center,
const QString &register_comment,
bool active,
bool faulted)
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)
faulted_(faulted),
condition_(condition)
{
setPos(center);
setFlag(ItemIsSelectable, true);
@@ -518,6 +524,7 @@ public:

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_;
@@ -527,6 +534,110 @@ private:
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 &center,
int column_span,
bool active)
: expression_id_(expression_id),
rung_id_(rung_id),
width_(static_cast<qreal>(column_span) * kCellWidth),
active_(active)
{
setPos(center);
setFlag(ItemIsSelectable, true);
setZValue(2.0);
setToolTip(LogicEditorWidget::tr("横线:%1 列,可用触点直接替换")
.arg(column_span));
}

QRectF boundingRect() const override
{
return {-width_ / 2.0, -kCellHeight / 2.0, width_, kCellHeight};
}

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override
{
const bool selected = (option->state & QStyle::State_Selected) != 0;
if (selected)
{
painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor);
painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine));
painter->drawRect(boundingRect().adjusted(3, 3, -3, -3));
}
painter->setPen(ladderPen(active_));
painter->drawLine(QPointF(-width_ / 2.0, 0), QPointF(width_ / 2.0, 0));
}

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

private:
std::string expression_id_;
std::string rung_id_;
qreal width_ = kCellWidth;
bool active_ = false;
};

class LogicEditorWidget::VerticalConnectorItem final : public QGraphicsItem
{
public:
VerticalConnectorItem(
const std::string &branch_expression_id,
const std::string &rung_id,
qreal x,
qreal top,
qreal bottom,
bool active)
: branch_expression_id_(branch_expression_id),
rung_id_(rung_id),
height_(bottom - top),
active_(active)
{
setPos(x, top);
setFlag(ItemIsSelectable, true);
setZValue(2.5);
setToolTip(LogicEditorWidget::tr("竖线连接:删除将移除对应并联支路"));
}

QRectF boundingRect() const override
{
return {-8.0, 0.0, 16.0, height_};
}

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override
{
const bool selected = (option->state & QStyle::State_Selected) != 0;
painter->setPen(QPen(
selected ? kSelectionBorderColor
: active_ ? kActiveColor : kLadderColor,
selected || active_ ? 3.0 : kLadderLineWidth));
painter->drawLine(QPointF(0, 0), QPointF(0, height_));
if (selected)
{
painter->setPen(QPen(kSelectionBorderColor, 1.0, Qt::DashLine));
painter->drawRect(boundingRect().adjusted(1, 1, -1, -1));
}
}

const std::string &branchExpressionId() const
{
return branch_expression_id_;
}
const std::string &rungId() const { return rung_id_; }

private:
std::string branch_expression_id_;
std::string rung_id_;
qreal height_ = 0.0;
bool active_ = false;
};

class LogicEditorWidget::RungItem final : public QGraphicsItem
@@ -627,15 +738,31 @@ RenderResult renderExpression(
bool trace_enabled,
const std::string &fault_node_id)
{
const bool active = trace_enabled && traceValue(
trace, &LogicTraceSnapshot::expressionValues, expression.id);
if (expression.kind == ConditionExpressionKind::Wire)
{
const bool active = trace_enabled && traceValue(
trace, &LogicTraceSnapshot::expressionPowerValues, expression.id);
const qreal width = static_cast<qreal>(expression.wire->columnSpan) * kCellWidth;
const QPointF center(
top_left.x() + width / 2.0,
top_left.y() + kCellHeight / 2.0);
scene.addItem(new LogicEditorWidget::WireItem(
expression.id,
rung_id,
center,
expression.wire->columnSpan,
active));
return {
QPointF(top_left.x(), center.y()),
QPointF(top_left.x() + width, center.y())};
}
if (expression.kind == ConditionExpressionKind::Node)
{
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);
trace, &LogicTraceSnapshot::nodePowerValues, expression.node->id);
scene.addLine(
QLineF(
QPointF(top_left.x(), center.y()),
@@ -655,7 +782,8 @@ RenderResult renderExpression(
: QString{};
}(),
node_active,
expression.node->id == fault_node_id));
expression.node->id == fault_node_id,
true));
return {
QPointF(top_left.x(), center.y()),
QPointF(top_left.x() + kCellWidth, center.y())};
@@ -685,7 +813,13 @@ RenderResult renderExpression(
}
else
{
scene.addLine(QLineF(previous.output, current.input), ladderPen(active));
const bool previous_active = trace_enabled && traceValue(
trace,
&LogicTraceSnapshot::expressionPowerValues,
expression.children[index - 1U].id);
scene.addLine(
QLineF(previous.output, current.input),
ladderPen(previous_active));
}
previous = current;
x += static_cast<qreal>(child_metrics.columns) * kCellWidth;
@@ -715,14 +849,36 @@ RenderResult renderExpression(
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));
scene.addLine(QLineF(right_join, top_y, right_join, bottom_y), ladderPen(active));
const bool parallel_input_active = trace_enabled && traceValue(
trace, &LogicTraceSnapshot::expressionInputValues, expression.id);
for (std::size_t index = 1; index < branches.size(); ++index)
{
const qreal segment_top = branches[index - 1U].input.y();
const qreal segment_bottom = branches[index].input.y();
const bool branch_active = trace_enabled && traceValue(
trace,
&LogicTraceSnapshot::expressionPowerValues,
expression.children[index].id);
scene.addItem(new LogicEditorWidget::VerticalConnectorItem(
expression.children[index].id,
rung_id,
left_join,
segment_top,
segment_bottom,
parallel_input_active));
scene.addItem(new LogicEditorWidget::VerticalConnectorItem(
expression.children[index].id,
rung_id,
right_join,
segment_top,
segment_bottom,
branch_active));
}
for (std::size_t index = 0; index < branches.size(); ++index)
{
const bool branch_active = trace_enabled && traceValue(
trace,
&LogicTraceSnapshot::expressionValues,
&LogicTraceSnapshot::expressionPowerValues,
expression.children[index].id);
scene.addLine(
QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input),
@@ -898,7 +1054,8 @@ void LogicEditorWidget::reloadLogic()
rung.output->id)
: rung_active;
}(),
rung.output->id == fault_node_id_));
rung.output->id == fault_node_id_,
false));
}
else
{
@@ -938,6 +1095,38 @@ void LogicEditorWidget::selectNode(const std::string &node_id)
}
}

void LogicEditorWidget::selectExpression(const std::string &expression_id)
{
for (QGraphicsItem *item : scene_->items())
{
bool selected = false;
if (NodeItem *node = dynamic_cast<NodeItem *>(item))
{
selected = node->isConditionNode() && node->nodeId() == expression_id;
if (selected)
{
current_rung_id_ = node->rungId();
ensureVisible(node);
}
}
else if (WireItem *wire = dynamic_cast<WireItem *>(item))
{
selected = wire->expressionId() == expression_id;
if (selected)
{
current_rung_id_ = wire->rungId();
ensureVisible(wire);
}
}
item->setSelected(selected);
}
}

void LogicEditorWidget::selectWire(const std::string &wire_id)
{
selectExpression(wire_id);
}

std::string LogicEditorWidget::selectedNodeId() const
{
const std::vector<std::string> ids = selectedNodeIds();
@@ -973,6 +1162,73 @@ std::vector<std::string> LogicEditorWidget::selectedNodeIds() const
return ids;
}

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

std::vector<std::string> LogicEditorWidget::selectedWireIds() const
{
std::vector<std::string> ids;
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const WireItem *wire = dynamic_cast<const WireItem *>(item))
{
ids.push_back(wire->expressionId());
}
}
return ids;
}

std::vector<std::string> LogicEditorWidget::selectedBranchIds() const
{
std::vector<std::string> ids;
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const VerticalConnectorItem *connector =
dynamic_cast<const VerticalConnectorItem *>(item))
{
if (std::find(ids.cbegin(), ids.cend(), connector->branchExpressionId())
== ids.cend())
{
ids.push_back(connector->branchExpressionId());
}
}
}
return ids;
}

std::string LogicEditorWidget::selectedRungId() const
{
std::string rung_id;
@@ -986,6 +1242,23 @@ std::string LogicEditorWidget::selectedRungId() const
}
rung_id = node->rungId();
}
else if (const WireItem *wire = dynamic_cast<const WireItem *>(item))
{
if (!rung_id.empty() && rung_id != wire->rungId())
{
return {};
}
rung_id = wire->rungId();
}
else if (const VerticalConnectorItem *connector =
dynamic_cast<const VerticalConnectorItem *>(item))
{
if (!rung_id.empty() && rung_id != connector->rungId())
{
return {};
}
rung_id = connector->rungId();
}
else if (const RungItem *rung = dynamic_cast<const RungItem *>(item))
{
if (rung_id.empty())
@@ -1015,12 +1288,19 @@ LogicEditorResult LogicEditorWidget::addRung()

LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config)
{
const std::vector<std::string> selected_expressions = selectedExpressionIds();
const std::vector<std::string> selected_wires = selectedWireIds();
const std::vector<std::string> selected_ids = selectedNodeIds();
LogicEditorResult result;
if (selected_ids.size() > 1U)
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)
{
@@ -1038,7 +1318,7 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config)
if (result.succeeded)
{
reloadLogic();
selectNode(result.id);
selectExpression(result.id);
emit graphChanged();
}
else
@@ -1075,7 +1355,133 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co
if (result.succeeded)
{
reloadLogic();
selectNode(result.id);
selectExpression(result.id);
emit graphChanged();
}
else
{
reportFailure(result);
}
return result;
}

LogicEditorResult LogicEditorWidget::addHorizontalWire()
{
const std::vector<std::string> selected_ids = selectedExpressionIds();
LogicEditorResult result;
if (selected_ids.size() > 1U)
{
result = {false, LogicEditorError::InvalidOperation,
"插入横线时只能选择一个条件对象", {}};
}
else if (selected_ids.size() == 1U)
{
result = editor_service_.insertWireAfter(
logic_id_, currentRungId(), selected_ids.front());
}
else
{
result = editor_service_.appendWire(logic_id_, currentRungId());
}
if (result.succeeded)
{
reloadLogic();
selectWire(result.id);
emit graphChanged();
}
else
{
reportFailure(result);
}
return result;
}

LogicEditorResult LogicEditorWidget::addVerticalWire()
{
const std::string rung_id = selectedRungId();
const std::vector<std::string> selected_ids = selectedExpressionIds();
LogicEditorResult result;
if (rung_id.empty() || selected_ids.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"请在同一网络中选择要连接的连续条件或横线", {}};
}
else
{
result = editor_service_.addParallelWireBranch(
logic_id_, rung_id, selected_ids);
}
if (result.succeeded)
{
reloadLogic();
selectWire(result.id);
emit graphChanged();
}
else
{
reportFailure(result);
}
return result;
}

LogicEditorResult LogicEditorWidget::deleteHorizontalWire()
{
const std::vector<std::string> wire_ids = selectedWireIds();
const std::string rung_id = selectedRungId();
if (wire_ids.empty() || rung_id.empty())
{
const LogicEditorResult result = {
false,
LogicEditorError::InvalidOperation,
"请先选择要删除的横线",
{}};
reportFailure(result);
return result;
}
const LogicEditorResult result = editor_service_.removeExpressions(
logic_id_, rung_id, wire_ids);
if (result.succeeded)
{
reloadLogic();
emit nodeSelected({});
emit graphChanged();
}
else
{
reportFailure(result);
}
return result;
}

LogicEditorResult LogicEditorWidget::deleteVerticalWire()
{
const std::vector<std::string> branch_ids = selectedBranchIds();
if (branch_ids.empty())
{
const LogicEditorResult result = {
false,
LogicEditorError::InvalidOperation,
"请先选择要删除的竖线连接",
{}};
reportFailure(result);
return result;
}
const std::string rung_id = selectedRungId();
LogicEditorResult result;
if (rung_id.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"竖线连接必须位于同一网络", {}};
}
else
{
result = editor_service_.removeExpressions(
logic_id_, rung_id, branch_ids);
}
if (result.succeeded)
{
reloadLogic();
emit nodeSelected({});
emit graphChanged();
}
else
@@ -1105,9 +1511,38 @@ LogicEditorResult LogicEditorWidget::setOutput(

LogicEditorResult LogicEditorWidget::deleteSelected()
{
const std::vector<std::string> expression_ids = selectedExpressionIds();
const std::vector<std::string> wire_ids = selectedWireIds();
const std::vector<std::string> branch_ids = selectedBranchIds();
const std::vector<std::string> node_ids = selectedNodeIds();
LogicEditorResult result;
if (!node_ids.empty())
if (!branch_ids.empty())
{
result = editor_service_.removeExpressions(
logic_id_, selectedRungId(), branch_ids);
}
else if (!wire_ids.empty())
{
bool output_selected = false;
for (QGraphicsItem *item : scene_->selectedItems())
{
if (const NodeItem *node = dynamic_cast<const NodeItem *>(item))
{
output_selected = output_selected || !node->isConditionNode();
}
}
if (output_selected)
{
result = {false, LogicEditorError::InvalidOperation,
"不能同时删除横线和输出节点", {}};
}
else
{
result = editor_service_.removeExpressions(
logic_id_, selectedRungId(), expression_ids);
}
}
else if (!node_ids.empty())
{
result = editor_service_.removeNodes(logic_id_, node_ids);
}


+ 11
- 0
app/src/ui/logic_editor_widget.h Zobrazit soubor

@@ -18,6 +18,8 @@ class LogicEditorWidget final : public QGraphicsView

public:
class NodeItem;
class WireItem;
class VerticalConnectorItem;
class RungItem;

explicit LogicEditorWidget(
@@ -39,6 +41,10 @@ public:
LogicEditorResult addRung();
LogicEditorResult addCondition(const LogicNodeConfig &config);
LogicEditorResult addParallelBranch(const LogicNodeConfig &config);
LogicEditorResult addHorizontalWire();
LogicEditorResult addVerticalWire();
LogicEditorResult deleteHorizontalWire();
LogicEditorResult deleteVerticalWire();
LogicEditorResult setOutput(
const LogicNodeConfig &config, bool configured = false);
LogicEditorResult deleteSelected();
@@ -55,6 +61,11 @@ private:
void handleSelectionChanged();
void reportFailure(const LogicEditorResult &result);
std::string currentRungId() const;
void selectExpression(const std::string &expression_id);
void selectWire(const std::string &wire_id);
std::vector<std::string> selectedExpressionIds() const;
std::vector<std::string> selectedWireIds() const;
std::vector<std::string> selectedBranchIds() const;

LogicEditorService &editor_service_;
QGraphicsScene *scene_ = nullptr;


+ 72
- 0
app/src/ui/main_window.cpp Zobrazit soubor

@@ -395,6 +395,14 @@ void MainWindow::configureActions()

connect(ui_->addRungAction, &QAction::triggered,
this, &MainWindow::addLogicRung);
connect(ui_->insertHorizontalWireAction, &QAction::triggered,
this, &MainWindow::addLogicHorizontalWire);
connect(ui_->insertVerticalWireAction, &QAction::triggered,
this, &MainWindow::addLogicVerticalWire);
connect(ui_->deleteHorizontalWireAction, &QAction::triggered,
this, &MainWindow::deleteLogicHorizontalWire);
connect(ui_->deleteVerticalWireAction, &QAction::triggered,
this, &MainWindow::deleteLogicVerticalWire);
connect(ui_->parallelInsertAction, &QAction::triggered,
this,
[this]
@@ -727,6 +735,10 @@ void MainWindow::configureAppearance()
ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete));
ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung));
ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch));
ui_->insertHorizontalWireAction->setIcon(makeUiIcon(UiIcon::HorizontalWire));
ui_->insertVerticalWireAction->setIcon(makeUiIcon(UiIcon::VerticalWire));
ui_->deleteHorizontalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
ui_->deleteVerticalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
ui_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact));
ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact));
ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact));
@@ -1030,6 +1042,10 @@ void MainWindow::updateEditActions()
|| (logic_active && logic_editor_service_.canRedo())));
ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active));
ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active));
ui_->insertHorizontalWireAction->setEnabled(editable && logic_active);
ui_->insertVerticalWireAction->setEnabled(editable && logic_active);
ui_->deleteHorizontalWireAction->setEnabled(editable && logic_active);
ui_->deleteVerticalWireAction->setEnabled(editable && logic_active);
}

void MainWindow::clearEditorHistories()
@@ -1194,6 +1210,62 @@ void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config)
statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000);
}

void MainWindow::addLogicHorizontalWire()
{
const LogicEditorResult result = logic_editor_widget_->addHorizontalWire();
if (!result.succeeded)
{
showProjectResult(tr("插入横线"), fromUtf8(result.message), false);
return;
}
selected_logic_node_id_.clear();
showLogicNodeProperties({});
refreshProjectUi();
statusBar()->showMessage(tr("横线已插入,可直接用触点替换"), 3000);
}

void MainWindow::addLogicVerticalWire()
{
const LogicEditorResult result = logic_editor_widget_->addVerticalWire();
if (!result.succeeded)
{
showProjectResult(tr("插入竖线"), fromUtf8(result.message), false);
return;
}
selected_logic_node_id_.clear();
showLogicNodeProperties({});
refreshProjectUi();
statusBar()->showMessage(tr("已建立横线旁路和竖线连接"), 3000);
}

void MainWindow::deleteLogicHorizontalWire()
{
const LogicEditorResult result = logic_editor_widget_->deleteHorizontalWire();
if (!result.succeeded)
{
showProjectResult(tr("删除横线"), fromUtf8(result.message), false);
return;
}
selected_logic_node_id_.clear();
showLogicNodeProperties({});
refreshProjectUi();
statusBar()->showMessage(tr("横线已删除"), 3000);
}

void MainWindow::deleteLogicVerticalWire()
{
const LogicEditorResult result = logic_editor_widget_->deleteVerticalWire();
if (!result.succeeded)
{
showProjectResult(tr("删除竖线"), fromUtf8(result.message), false);
return;
}
selected_logic_node_id_.clear();
showLogicNodeProperties({});
refreshProjectUi();
statusBar()->showMessage(tr("竖线及对应并联支路已删除"), 3000);
}

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


+ 4
- 0
app/src/ui/main_window.h Zobrazit soubor

@@ -136,6 +136,10 @@ private:
void addLogicCondition(const LogicNodeConfig &config);
// 为当前选中的连续逻辑范围建立并联支路
void addLogicParallelBranch(const LogicNodeConfig &config);
void addLogicHorizontalWire();
void addLogicVerticalWire();
void deleteLogicHorizontalWire();
void deleteLogicVerticalWire();
// 设置当前网络右侧的输出线圈
void setLogicOutput(const LogicNodeConfig &config);
void configureAndSetLogicOutput(const LogicNodeConfig &config);


+ 48
- 0
app/src/ui/main_window.ui Zobrazit soubor

@@ -250,6 +250,8 @@
<addaction name="redoAction"/>
<addaction name="separator"/>
<addaction name="deleteSelectionAction"/>
<addaction name="deleteHorizontalWireAction"/>
<addaction name="deleteVerticalWireAction"/>
<addaction name="clearSelectionAction"/>
</widget>
<widget class="QMenu" name="runMenu">
@@ -331,6 +333,8 @@
<bool>false</bool>
</attribute>
<addaction name="addRungAction"/>
<addaction name="insertHorizontalWireAction"/>
<addaction name="insertVerticalWireAction"/>
<addaction name="parallelInsertAction"/>
<addaction name="deleteLogicAction"/>
<addaction name="separator"/>
@@ -1059,6 +1063,50 @@
<string>在梯形图末尾新增网络</string>
</property>
</action>
<action name="insertHorizontalWireAction">
<property name="text">
<string>横线</string>
</property>
<property name="toolTip">
<string>在所选条件或横线后插入横线;没有选择时追加到当前网络</string>
</property>
<property name="shortcut">
<string>F11</string>
</property>
</action>
<action name="insertVerticalWireAction">
<property name="text">
<string>竖线</string>
</property>
<property name="toolTip">
<string>为所选连续逻辑范围建立横线旁路和竖线连接</string>
</property>
<property name="shortcut">
<string>F12</string>
</property>
</action>
<action name="deleteHorizontalWireAction">
<property name="text">
<string>删除横线</string>
</property>
<property name="toolTip">
<string>删除画布中选中的横线</string>
</property>
<property name="shortcut">
<string>Shift+F11</string>
</property>
</action>
<action name="deleteVerticalWireAction">
<property name="text">
<string>删除竖线</string>
</property>
<property name="toolTip">
<string>删除画布中选中的竖线及其对应并联支路</string>
</property>
<property name="shortcut">
<string>Shift+F12</string>
</property>
</action>
<action name="parallelInsertAction">
<property name="text">
<string>并联支路</string>


+ 18
- 0
app/src/ui/toolbar_icon_factory.cpp Zobrazit soubor

@@ -359,6 +359,24 @@ QPixmap renderIcon(UiIcon icon, int size)
painter.drawLine(QPointF(7, 12), QPointF(17, 12));
break;
}
case UiIcon::HorizontalWire:
{
painter.drawLine(QPointF(3, 12), QPointF(21, 12));
painter.setPen(iconPen(kAccent, 2.0));
painter.drawLine(QPointF(9, 7), QPointF(15, 7));
painter.drawLine(QPointF(12, 4), QPointF(12, 10));
break;
}
case UiIcon::VerticalWire:
{
painter.drawLine(QPointF(5, 7), QPointF(19, 7));
painter.drawLine(QPointF(5, 17), QPointF(19, 17));
painter.drawLine(QPointF(12, 7), QPointF(12, 17));
painter.setPen(iconPen(kAccent, 2.0));
painter.drawLine(QPointF(17, 9), QPointF(17, 15));
painter.drawLine(QPointF(14, 12), QPointF(20, 12));
break;
}
case UiIcon::ParallelBranch:
{
painter.drawLine(QPointF(2, 12), QPointF(6, 12));


+ 2
- 0
app/src/ui/toolbar_icon_factory.h Zobrazit soubor

@@ -28,6 +28,8 @@ enum class UiIcon
AlarmSettings,
Delete,
AddRung,
HorizontalWire,
VerticalWire,
ParallelBranch,
NormallyOpenContact,
NormallyClosedContact,


+ 20
- 0
app/tests/domain_tests.cpp Zobrazit soubor

@@ -603,6 +603,26 @@ void testLadderLogicBoundaries()
logic.rungs.push_back(rung);
require(logic.validate(), "stop AND (start OR run) self-hold ladder must be valid");

ConditionExpression wire = ConditionExpression::fromWire("wire-1", 2);
require(wire.validate() && wire.validateForRunning()
&& wire.wire->columnSpan == 2,
"a configured horizontal wire must be a valid runnable expression leaf");
ConditionExpression invalid_wire = ConditionExpression::fromWire("wire-invalid", 0);
require(!invalid_wire.validate(), "a zero-column horizontal wire must be rejected");
invalid_wire = ConditionExpression::fromWire(
"wire-too-wide", WireSegment::kMaximumColumnSpan + 1);
require(!invalid_wire.validate(), "an oversized horizontal wire must be rejected");

ConditionExpression wired_series;
wired_series.id = "wired-series";
wired_series.kind = ConditionExpressionKind::Series;
wired_series.children = {
ConditionExpression::fromNode(stop),
ConditionExpression::fromWire("wire-series"),
start_parallel};
require(wired_series.validateForRunning(),
"a wire must preserve a valid structured series expression");

logic.rungs.front().condition->children.front() =
ConditionExpression::fromNode(coil);
require(!logic.validate(), "a ladder condition expression must reject coils");


+ 61
- 0
app/tests/logic_editor_service_tests.cpp Zobrazit soubor

@@ -132,6 +132,66 @@ void testRangeParallelInsertion()
"a non-contiguous selection must be rejected");
}

void testStructuredWireEditing()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = service.firstRungId(logic_id);
service.appendCondition(logic_id, rung_id, contact(0));
service.appendCondition(logic_id, rung_id, contact(1));
service.appendCondition(logic_id, rung_id, contact(2));

const LogicEditorResult branch = service.addParallelWireBranch(
logic_id, rung_id, {"contact-2", "contact-3"});
require(branch.succeeded && branch.id == "wire-1",
"a continuous range must accept a structured horizontal bypass");
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.at(1).kind
== ConditionExpressionKind::Parallel,
"a vertical connection must produce a parallel expression");
const ConditionExpression &wire =
rung->condition->children.at(1).children.at(1);
require(wire.kind == ConditionExpressionKind::Wire
&& wire.wire->columnSpan == 2,
"the bypass wire span must match the selected two-column range");

const LogicEditorResult replacement = service.replaceWireWithCondition(
logic_id, rung_id, branch.id, contact(3));
require(replacement.succeeded && replacement.id == "contact-4",
"a selected wire must be replaceable by a configured node type");
const LogicEditorResult extension = service.insertWireAfter(
logic_id, rung_id, replacement.id);
require(extension.succeeded && extension.id == "wire-1",
"a wire id must become reusable after its expression is replaced");

rung = service.findRung(logic_id, rung_id);
const ConditionExpression *extended_branch = service.findExpression(
logic_id, rung_id, rung->condition->children.at(1).children.at(1).id);
require(extended_branch != nullptr
&& extended_branch->kind == ConditionExpressionKind::Series
&& extended_branch->children.at(1).kind
== ConditionExpressionKind::Wire,
"inserting a horizontal wire after a branch node must preserve structure");
const std::string extended_branch_id = extended_branch->id;
require(!service.addParallelWireBranch(
logic_id, rung_id, {"contact-1", "contact-3"}).succeeded,
"non-contiguous wire connection targets must be rejected");

require(service.removeExpressions(
logic_id, rung_id, {extended_branch_id}).succeeded,
"deleting a selected vertical connection branch must succeed atomically");
rung = service.findRung(logic_id, rung_id);
require(rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.size() == 3U,
"removing a bypass branch must normalize back to the original series");
require(service.undo().succeeded
&& service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr,
"wire branch deletion must participate in ladder undo history");
}

void testLogicLifecycleAndOrdering()
{
TestProjectStorage storage;
@@ -305,6 +365,7 @@ int main()
{
testStructuredEditingAndNormalization();
testRangeParallelInsertion();
testStructuredWireEditing();
testLogicLifecycleAndOrdering();
testEdgeTimerNodesAndRungComments();
testHistoryAndAtomicBatchDelete();


+ 69
- 2
app/tests/main_window_tests.cpp Zobrazit soubor

@@ -670,6 +670,14 @@ void testModeActionsControlEditingAvailability()
window, "addNormalCoilAction");
QAction *parallel_insert_action = requiredChild<QAction>(
window, "parallelInsertAction");
QAction *insert_horizontal_wire_action = requiredChild<QAction>(
window, "insertHorizontalWireAction");
QAction *insert_vertical_wire_action = requiredChild<QAction>(
window, "insertVerticalWireAction");
QAction *delete_horizontal_wire_action = requiredChild<QAction>(
window, "deleteHorizontalWireAction");
QAction *delete_vertical_wire_action = requiredChild<QAction>(
window, "deleteVerticalWireAction");
QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock");
QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock");
QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel");
@@ -725,6 +733,10 @@ void testModeActionsControlEditingAvailability()
"configureAlarmsAction",
"deleteControlAction",
"addRungAction",
"insertHorizontalWireAction",
"insertVerticalWireAction",
"deleteHorizontalWireAction",
"deleteVerticalWireAction",
"parallelInsertAction",
"addNormallyOpenAction",
"addNormallyClosedAction",
@@ -799,7 +811,15 @@ void testModeActionsControlEditingAvailability()
require(editing_action->isChecked(), "editing action must be selected initially");
require(undo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Z)
&& redo_action->shortcut() == QKeySequence(Qt::CTRL | Qt::Key_Y)
&& delete_selection_action->shortcut() == QKeySequence(Qt::Key_Delete),
&& delete_selection_action->shortcut() == QKeySequence(Qt::Key_Delete)
&& insert_horizontal_wire_action->shortcut()
== QKeySequence(Qt::Key_F11)
&& insert_vertical_wire_action->shortcut()
== QKeySequence(Qt::Key_F12)
&& delete_horizontal_wire_action->shortcut()
== QKeySequence(Qt::SHIFT | Qt::Key_F11)
&& delete_vertical_wire_action->shortcut()
== QKeySequence(Qt::SHIFT | Qt::Key_F12),
"core editor actions must expose the agreed keyboard shortcuts");
require(project_dock->isEnabled(), "project dock must be enabled while editing");
require(properties_dock->isEnabled(), "properties dock must be enabled while editing");
@@ -941,7 +961,54 @@ void testModeActionsControlEditingAvailability()
collectConditionNodes(*restored_rung.condition, &restored_nodes);
require(restored_nodes.size() == 1U,
"logic redo must restore the original condition node");
logic_editor->selectNode(restored_nodes.front()->id);
const std::string restored_node_id = restored_nodes.front()->id;
logic_editor->selectNode(restored_node_id);

insert_horizontal_wire_action->trigger();
const LadderRung &wired_rung = logic_editor_service.findLogic(
logic_editor_service.firstLogicId())->rungs.front();
require(wired_rung.condition->kind == ConditionExpressionKind::Series
&& wired_rung.condition->children.at(1).kind
== ConditionExpressionKind::Wire,
"F11 must insert a selectable horizontal wire after the current condition");
delete_horizontal_wire_action->trigger();
require(logic_editor_service.findLogic(logic_editor_service.firstLogicId())
->rungs.front().condition->kind == ConditionExpressionKind::Node,
"Shift+F11 must delete the selected horizontal wire");
logic_editor->selectNode(restored_node_id);
insert_horizontal_wire_action->trigger();
add_normally_open_action->trigger();
const LadderRung &replaced_wire_rung = logic_editor_service.findLogic(
logic_editor_service.firstLogicId())->rungs.front();
require(replaced_wire_rung.condition->kind == ConditionExpressionKind::Series
&& replaced_wire_rung.condition->children.at(1).kind
== ConditionExpressionKind::Node,
"adding a contact on a selected wire must replace the wire in place");
undo_action->trigger();
undo_action->trigger();

logic_editor->selectNode(restored_node_id);
insert_vertical_wire_action->trigger();
const LadderRung &vertical_rung = logic_editor_service.findLogic(
logic_editor_service.firstLogicId())->rungs.front();
require(vertical_rung.condition->kind == ConditionExpressionKind::Parallel
&& vertical_rung.condition->children.at(1).kind
== ConditionExpressionKind::Wire,
"F12 must create a structured wire bypass with vertical connectors");
logic_editor->scene()->clearSelection();
for (QGraphicsItem *item : logic_editor->scene()->items())
{
if (qFuzzyCompare(item->zValue(), 2.5))
{
item->setSelected(true);
}
}
delete_vertical_wire_action->trigger();
require(logic_editor_service.findLogic(logic_editor_service.firstLogicId())
->rungs.front().condition->kind == ConditionExpressionKind::Node,
"Shift+F12 must remove the selected vertical connection branch");

logic_editor->selectNode(restored_node_id);
parallel_insert_action->trigger();
add_normal_coil_action->trigger();
const ControlLogic *logic = logic_editor_service.findLogic(


+ 39
- 0
app/tests/offline_simulation_service_tests.cpp Zobrazit soubor

@@ -257,6 +257,44 @@ void testNestedSeriesParallelExpression()
require(readBit(repository, 10), "A branch must independently energize output");
}

void testWirePassThroughAndPowerTrace()
{
VirtualRegisterRepository repository;
SoftwareLogicExecutor executor;
ConditionExpression root;
root.id = "wire-series";
root.kind = ConditionExpressionKind::Series;
root.children = {
ConditionExpression::fromNode(contact("wire-input", 0)),
ConditionExpression::fromWire("wire-segment", 2),
ConditionExpression::fromNode(contact("wire-output", 1))};
LadderRung wired_rung;
wired_rung.id = "wired-rung";
wired_rung.name = "wired-rung";
wired_rung.condition = root;
wired_rung.output = coil("wired-coil", 10);
const ControlLogic program = logic({wired_rung});

writeBit(repository, 1, true);
LogicTraceSnapshot trace;
require(executor.executeScan({program}, repository, &trace).succeeded,
"wire expression scan must succeed");
require(!readBit(repository, 10),
"a horizontal wire must not bypass a false upstream series contact");
require(trace.expressionValues.at("wire-segment")
&& !trace.expressionInputValues.at("wire-segment")
&& !trace.expressionPowerValues.at("wire-segment"),
"a wire must remain logically true without showing false upstream power");

writeBit(repository, 0, true);
require(executor.executeScan({program}, repository, &trace).succeeded,
"powered wire expression scan must succeed");
require(readBit(repository, 10)
&& trace.expressionInputValues.at("wire-segment")
&& trace.expressionPowerValues.at("wire-segment"),
"a powered horizontal wire must pass current to the downstream contact");
}

void testSeriesParallelContactsAndSequentialVisibility()
{
VirtualRegisterRepository repository;
@@ -862,6 +900,7 @@ int main(int argc, char *argv[])
{
testSeriesParallelContactsAndSequentialVisibility();
testNestedSeriesParallelExpression();
testWirePassThroughAndPowerTrace();
testAllComparisons();
testSetResetAndDisabledLogic();
testMultipleLogicScanOrderAndTraceIsolation();


+ 5
- 1
app/tests/project_management_tests.cpp Zobrazit soubor

@@ -148,6 +148,7 @@ Project makeExampleProject()
series.kind = ConditionExpressionKind::Series;
series.children = {
std::move(parallel),
ConditionExpression::fromWire("start-wire", 2),
ConditionExpression::fromNode(compare)};
rung.condition = std::move(series);
rung.output = coil;
@@ -496,6 +497,9 @@ void testExampleProjectRoundTrip()
== ConditionExpressionKind::Parallel
&& rung.condition->children.front().children.size() == 2U,
"parallel expression branches must survive round trip");
require(rung.condition->children.at(1).kind == ConditionExpressionKind::Wire
&& rung.condition->children.at(1).wire->columnSpan == 2,
"horizontal wire span must survive JSON round trip");
require(rung.output.has_value(),
"ladder output must survive round trip");

@@ -553,7 +557,7 @@ void testExampleProjectRoundTrip()
"ADD and SUB operations must survive round trip");

const auto &compare = std::get<CompareNodeConfig>(
rung.condition->children.at(1).node->config);
rung.condition->children.at(2).node->config);
require(compare.address.index() == 2 && compare.value == 100,
"comparison configuration must survive round trip");
require(service.saveAs(second_path.toStdString()).succeeded,


Načítá se…
Zrušit
Uložit