Sfoglia il codice sorgente

feat: 完善梯形图编辑并整理寄存器仓库结构

main
suyu 4 settimane fa
parent
commit
b852940d7d
20 ha cambiato i file con 839 aggiunte e 34 eliminazioni
  1. +2
    -1
      app/integrated_platform.pro
  2. +0
    -28
      app/src/domain/register_repository.h
  3. +1
    -1
      app/src/domain/virtual_register_repository.cpp
  4. +32
    -0
      app/src/domain/virtual_register_repository.h
  5. +1
    -0
      app/src/main.cpp
  6. +477
    -0
      app/src/services/logic_editor_service.cpp
  7. +6
    -0
      app/src/services/logic_editor_service.h
  8. +1
    -0
      app/src/services/offline_simulation_service.h
  9. +48
    -2
      app/src/ui/logic_editor_widget.cpp
  10. +1
    -0
      app/tests/alarm_service_tests.cpp
  11. +1
    -0
      app/tests/domain_tests.cpp
  12. +1
    -0
      app/tests/hmi_editor_service_tests.cpp
  13. +87
    -0
      app/tests/logic_editor_service_tests.cpp
  14. +173
    -0
      app/tests/main_window_tests.cpp
  15. +1
    -0
      app/tests/offline_simulation_service_tests.cpp
  16. +1
    -0
      app/tests/performance_tests.cpp
  17. +1
    -0
      app/tests/plc_runtime_tests.cpp
  18. +3
    -2
      app/tests/pri/test_layers.pri
  19. +1
    -0
      app/tests/register_monitor_service_tests.cpp
  20. +1
    -0
      app/tests/runtime_mode_service_tests.cpp

+ 2
- 1
app/integrated_platform.pro Vedi File

@@ -29,7 +29,7 @@ SOURCES += \
src/ui/property_panel_controller.cpp \
src/ui/runtime_panel_controller.cpp \
src/domain/register_address.cpp \
src/domain/register_repository.cpp \
src/domain/virtual_register_repository.cpp \
src/domain/active_register_repository.cpp \
src/domain/register_monitor_model.cpp \
src/domain/alarm_model.cpp \
@@ -72,6 +72,7 @@ HEADERS += \
src/ui/runtime_panel_controller.h \
src/domain/register_address.h \
src/domain/register_repository.h \
src/domain/virtual_register_repository.h \
src/domain/active_register_repository.h \
src/domain/register_monitor_model.h \
src/domain/alarm_model.h \


+ 0
- 28
app/src/domain/register_repository.h Vedi File

@@ -2,8 +2,6 @@

#include "register_address.h"

#include <array>
#include <cstddef>
#include <cstdint>

// 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息
@@ -56,29 +54,3 @@ public:
virtual RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) = 0;
};

// 基于内存的离线 M/D 寄存器仓库;地址范围与真实 PLC 保持一致
class VirtualRegisterRepository : public RegisterRepository
{
public:
VirtualRegisterRepository();

// 读取内存中的 M 区位值
BitReadResult readBit(const RegisterAddress &address) const override;
// 更新内存中的 M 区位值
RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override;
// 读取内存中的 D 区字值
WordReadResult readWord(const RegisterAddress &address) const override;
// 更新内存中的 D 区字值
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

// 将所有虚拟寄存器恢复为默认值,开始新的离线会话
void clear();

private:
static constexpr std::size_t kRegisterCount =
static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1);
std::array<bool, kRegisterCount> bits_{}; // M 区位寄存器内存
std::array<std::int16_t, kRegisterCount> words_{}; // D 区字寄存器内存
};

app/src/domain/register_repository.cpp → app/src/domain/virtual_register_repository.cpp Vedi File

@@ -1,4 +1,4 @@
#include "register_repository.h"
#include "virtual_register_repository.h"

VirtualRegisterRepository::VirtualRegisterRepository()
{

+ 32
- 0
app/src/domain/virtual_register_repository.h Vedi File

@@ -0,0 +1,32 @@
#pragma once

#include "register_repository.h"

#include <array>
#include <cstddef>

// 基于内存的离线 M/D 寄存器仓库;地址范围与真实 PLC 保持一致
class VirtualRegisterRepository : public RegisterRepository
{
public:
VirtualRegisterRepository();

// 读取内存中的 M 区位值
BitReadResult readBit(const RegisterAddress &address) const override;
// 更新内存中的 M 区位值
RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override;
// 读取内存中的 D 区字值
WordReadResult readWord(const RegisterAddress &address) const override;
// 更新内存中的 D 区字值
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

// 将所有虚拟寄存器恢复为默认值,开始新的离线会话
void clear();

private:
static constexpr std::size_t kRegisterCount =
static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1);
std::array<bool, kRegisterCount> bits_{}; // M 区位寄存器内存
std::array<std::int16_t, kRegisterCount> words_{}; // D 区字寄存器内存
};

+ 1
- 0
app/src/main.cpp Vedi File

@@ -12,6 +12,7 @@
#include "infrastructure/plc_communication_service.h"
#include "infrastructure/plc_register_repository.h"
#include "domain/active_register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h"
#include "services/alarm_editor_service.h"


+ 477
- 0
app/src/services/logic_editor_service.cpp Vedi File

@@ -259,6 +259,205 @@ bool addParallelForSelection(
return true;
}

bool addParallelForWireCellRange(
ConditionExpression *expression,
const std::string &wire_id,
int first_column,
int selected_columns,
ConditionExpression *branch,
const std::string &parallel_id,
const std::string &series_id,
const std::string &leading_wire_id,
const std::string &trailing_wire_id)
{
if (expression == nullptr)
{
return false;
}
if (expression->kind == ConditionExpressionKind::Wire
&& expression->id == wire_id)
{
const int total_columns = expression->wire->columnSpan;
if (first_column < 0 || selected_columns <= 0
|| first_column + selected_columns > total_columns)
{
return false;
}
if (first_column == 0 && selected_columns == total_columns)
{
addParallelSibling(expression, std::move(*branch), parallel_id);
return true;
}

ConditionExpression replacement;
replacement.id = series_id;
replacement.kind = ConditionExpressionKind::Series;
if (first_column > 0)
{
replacement.children.push_back(ConditionExpression::fromWire(
leading_wire_id, first_column));
}
ConditionExpression selected_wire = ConditionExpression::fromWire(
wire_id, selected_columns);
replacement.children.push_back(makeContainer(
parallel_id,
ConditionExpressionKind::Parallel,
std::move(selected_wire),
std::move(*branch)));
const int trailing_columns =
total_columns - first_column - selected_columns;
if (trailing_columns > 0)
{
replacement.children.push_back(ConditionExpression::fromWire(
trailing_wire_id, trailing_columns));
}
*expression = std::move(replacement);
return true;
}
if (expression->kind == ConditionExpressionKind::Node
|| expression->kind == ConditionExpressionKind::Wire)
{
return false;
}
for (ConditionExpression &child : expression->children)
{
if (addParallelForWireCellRange(
&child,
wire_id,
first_column,
selected_columns,
branch,
parallel_id,
series_id,
leading_wire_id,
trailing_wire_id))
{
return true;
}
}
return false;
}

struct WireCellLocation
{
const ConditionExpression *series = nullptr;
std::size_t child_index = 0U;
int child_start = 0;
};

bool findWireCellLocation(
const ConditionExpression &expression,
const std::string &wire_id,
WireCellLocation *location)
{
if (expression.kind == ConditionExpressionKind::Node
|| expression.kind == ConditionExpressionKind::Wire)
{
return false;
}

if (expression.kind == ConditionExpressionKind::Series)
{
int child_start = 0;
for (std::size_t index = 0; index < expression.children.size(); ++index)
{
const ConditionExpression &child = expression.children[index];
if (child.kind == ConditionExpressionKind::Wire
&& child.id == wire_id)
{
if (location != nullptr)
{
*location = {&expression, index, child_start};
}
return true;
}
if (findWireCellLocation(child, wire_id, location))
{
return true;
}
child_start += expressionColumns(child);
}
return false;
}

for (const ConditionExpression &child : expression.children)
{
if (findWireCellLocation(child, wire_id, location))
{
return true;
}
}
return false;
}

bool addParallelForWireCellSeriesRange(
ConditionExpression *root,
const std::string &series_id,
std::size_t first_child,
std::size_t last_child,
int leading_columns,
int selected_columns,
int trailing_columns,
ConditionExpression *branch,
const std::string &selected_wire_id,
const std::string &parallel_id,
const std::string &leading_wire_id,
const std::string &trailing_wire_id)
{
if (root == nullptr || branch == nullptr)
{
return false;
}
ConditionExpression *series = findConditionExpression(*root, series_id);
if (series == nullptr || series->kind != ConditionExpressionKind::Series
|| first_child > last_child || last_child >= series->children.size()
|| leading_columns < 0 || selected_columns <= 0 || trailing_columns < 0)
{
return false;
}
for (std::size_t index = first_child; index <= last_child; ++index)
{
if (series->children[index].kind != ConditionExpressionKind::Wire)
{
return false;
}
}

ConditionExpression selected_wire = ConditionExpression::fromWire(
selected_wire_id, selected_columns);
ConditionExpression parallel = makeContainer(
parallel_id,
ConditionExpressionKind::Parallel,
std::move(selected_wire),
std::move(*branch));
std::vector<ConditionExpression> replacement;
replacement.reserve(
series->children.size() - (last_child - first_child) + 3U);
for (std::size_t index = 0; index < first_child; ++index)
{
replacement.push_back(std::move(series->children[index]));
}
if (leading_columns > 0)
{
replacement.push_back(ConditionExpression::fromWire(
leading_wire_id, leading_columns));
}
replacement.push_back(std::move(parallel));
if (trailing_columns > 0)
{
replacement.push_back(ConditionExpression::fromWire(
trailing_wire_id, trailing_columns));
}
for (std::size_t index = last_child + 1U;
index < series->children.size();
++index)
{
replacement.push_back(std::move(series->children[index]));
}
series->children = std::move(replacement);
return true;
}

bool removeExpressionRecursive(
ConditionExpression *expression, const std::string &expression_id)
{
@@ -1636,6 +1835,284 @@ LogicEditorResult LogicEditorService::addParallelWireBranch(
return {true, LogicEditorError::None, {}, wire_id};
}

LogicEditorResult LogicEditorService::addParallelWireBranchAtCells(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::pair<std::string, int>> &selected_wire_cells)
{
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_wire_cells.empty())
{
return failure(
LogicEditorError::InvalidOperation,
"建立竖线连接前必须选择横线网格");
}

struct SelectedWireCell
{
std::string wire_id;
int offset = 0;
int absolute_column = 0;
};
std::vector<SelectedWireCell> cells;
cells.reserve(selected_wire_cells.size());
for (const auto &cell : selected_wire_cells)
{
if (cell.first.empty())
{
return failure(
LogicEditorError::ExpressionNotFound,
"未找到选中的横线");
}
const ConditionExpression *wire = findExpression(
logic_id, rung_id, cell.first);
if (wire == nullptr || wire->kind != ConditionExpressionKind::Wire
|| !wire->wire.has_value()
|| cell.second < 0 || cell.second >= wire->wire->columnSpan)
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格列偏移超出横线范围");
}
cells.push_back({cell.first, cell.second, 0});
}

const bool same_wire = std::all_of(
cells.cbegin(), cells.cend(),
[&cells](const SelectedWireCell &cell)
{
return cell.wire_id == cells.front().wire_id;
});
const std::string wire_expression_id = cells.front().wire_id;
const ConditionExpression *selected_wire = findExpression(
logic_id, rung_id, wire_expression_id);
if (selected_wire == nullptr || !selected_wire->wire.has_value())
{
return failure(LogicEditorError::ExpressionNotFound, "未找到选中的横线");
}
int leading_columns = 0;
int trailing_columns = 0;
std::string selected_series_id;
std::size_t selected_first_child = 0U;
std::size_t selected_last_child = 0U;
if (same_wire)
{
std::vector<int> offsets;
offsets.reserve(cells.size());
for (const SelectedWireCell &cell : cells)
{
offsets.push_back(cell.offset);
}
std::sort(offsets.begin(), offsets.end());
if (std::adjacent_find(offsets.cbegin(), offsets.cend())
!= offsets.cend()
|| offsets.back() - offsets.front() + 1
!= static_cast<int>(offsets.size()))
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格必须是连续列");
}
for (SelectedWireCell &cell : cells)
{
cell.absolute_column = cell.offset;
}
leading_columns = offsets.front();
trailing_columns = selected_wire->wire->columnSpan
- offsets.front() - static_cast<int>(offsets.size());
std::sort(
cells.begin(), cells.end(),
[](const SelectedWireCell &left, const SelectedWireCell &right)
{
return left.absolute_column < right.absolute_column;
});
}
else
{
const ConditionExpression *source_series = nullptr;
for (SelectedWireCell &cell : cells)
{
WireCellLocation location;
if (!findWireCellLocation(
*existing_rung->condition, cell.wire_id, &location)
|| location.series == nullptr)
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格必须位于同一条连续串联路径");
}
if (source_series == nullptr)
{
source_series = location.series;
}
else if (source_series != location.series)
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格必须位于同一条连续串联路径");
}
cell.absolute_column = location.child_start + cell.offset;
}
std::sort(
cells.begin(), cells.end(),
[](const SelectedWireCell &left, const SelectedWireCell &right)
{
return left.absolute_column < right.absolute_column;
});
if (std::adjacent_find(
cells.cbegin(), cells.cend(),
[](const SelectedWireCell &left, const SelectedWireCell &right)
{
return left.absolute_column == right.absolute_column;
}) != cells.cend()
|| cells.back().absolute_column - cells.front().absolute_column + 1
!= static_cast<int>(cells.size()))
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格必须是连续列");
}

int child_start = 0;
std::size_t first_child = source_series->children.size();
std::size_t last_child = source_series->children.size();
const int range_begin = cells.front().absolute_column;
const int range_end = cells.back().absolute_column + 1;
for (std::size_t index = 0; index < source_series->children.size(); ++index)
{
const ConditionExpression &child = source_series->children[index];
const int child_end = child_start + expressionColumns(child);
if (child_end > range_begin && child_start < range_end)
{
if (child.kind != ConditionExpressionKind::Wire)
{
return failure(
LogicEditorError::InvalidOperation,
"竖线网格不能跨越触点或并联支路");
}
if (first_child == source_series->children.size())
{
first_child = index;
}
last_child = index;
}
child_start = child_end;
}
if (first_child == source_series->children.size())
{
return failure(
LogicEditorError::InvalidOperation,
"未找到连续的横线网格范围");
}
selected_series_id = source_series->id;
selected_first_child = first_child;
selected_last_child = last_child;
int layout_start = 0;
for (std::size_t index = 0; index < first_child; ++index)
{
layout_start += expressionColumns(source_series->children[index]);
}
int selected_end = layout_start;
for (std::size_t index = first_child;
index <= last_child;
++index)
{
selected_end += expressionColumns(source_series->children[index]);
}
leading_columns = cells.front().absolute_column - layout_start;
trailing_columns = selected_end - (cells.back().absolute_column + 1);
}

const int first_column = cells.front().absolute_column;
const int selected_columns = static_cast<int>(cells.size());

std::unordered_set<std::string> generated_wire_ids;
const auto makeGeneratedWireId = [&]()
{
std::string candidate = makeUniqueWireId(*logic);
while (generated_wire_ids.count(candidate) != 0U
|| findExpression(logic_id, rung_id, candidate) != nullptr)
{
candidate += "-split";
}
generated_wire_ids.insert(candidate);
return candidate;
};

const std::string branch_wire_id = makeGeneratedWireId();
const std::string selected_wire_id = same_wire
? wire_expression_id : cells.front().wire_id;
const std::string leading_wire_id = leading_columns > 0
? makeGeneratedWireId() : std::string{};
const std::string trailing_wire_id = trailing_columns > 0
? makeGeneratedWireId() : std::string{};
ConditionExpression branch = ConditionExpression::fromWire(
branch_wire_id, selected_columns);
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();
const bool modified_before = project_service_.isModified();
LadderRung *rung = findEditableRung(
project_service_.editProject(), logic_id, rung_id);
bool inserted = false;
if (same_wire)
{
inserted = addParallelForWireCellRange(
&*rung->condition,
wire_expression_id,
first_column,
selected_columns,
&branch,
parallel_id,
series_id,
leading_wire_id,
trailing_wire_id);
}
else
{
inserted = addParallelForWireCellSeriesRange(
&*rung->condition,
selected_series_id,
selected_first_child,
selected_last_child,
leading_columns,
selected_columns,
trailing_columns,
&branch,
selected_wire_id,
parallel_id,
leading_wire_id,
trailing_wire_id);
}
if (!inserted)
{
rollbackEdit(std::move(before), modified_before);
return failure(
LogicEditorError::InvalidOperation,
"竖线连接只能围绕同一条横线中的连续网格建立");
}
normalizeConditionExpression(&rung->condition);
std::string validation_error;
if (!rung->validate(&validation_error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, branch_wire_id};
}

LogicEditorResult LogicEditorService::setOutput(
const std::string &logic_id,
const std::string &rung_id,


+ 6
- 0
app/src/services/logic_editor_service.h Vedi File

@@ -4,6 +4,7 @@
#include "editor_history.h"

#include <string>
#include <utility>
#include <vector>

class ProjectService;
@@ -121,6 +122,11 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_expression_ids);
// 按选中的横线网格建立旁路;网格必须来自同一条横线且列号连续
LogicEditorResult addParallelWireBranchAtCells(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::pair<std::string, int>> &selected_wire_cells);
// 输出槽编辑;每个网络最多一个输出节点
LogicEditorResult setOutput(
const std::string &logic_id,


+ 1
- 0
app/src/services/offline_simulation_service.h Vedi File

@@ -1,5 +1,6 @@
#pragma once

#include "domain/virtual_register_repository.h"
#include "software_logic_executor.h"

#include <QObject>


+ 48
- 2
app/src/ui/logic_editor_widget.cpp Vedi File

@@ -1618,6 +1618,10 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config)
LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config)
{
const std::string rung_id = selectedRungId();
const std::vector<std::pair<std::string, int>> selected_empty_slots =
selectedEmptySlots();
const std::vector<std::pair<std::string, int>> selected_wire_cells =
selectedWireCells();
const std::vector<std::string> selected_ids = selectedNodeIds();
std::vector<std::string> condition_ids;
for (const std::string &node_id : selected_ids)
@@ -1629,7 +1633,23 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co
}
}
LogicEditorResult result;
if (rung_id.empty() || condition_ids.empty())
if (!selected_empty_slots.empty()
&& (!selected_ids.empty() || !selected_wire_cells.empty()))
{
result = {false, LogicEditorError::InvalidOperation,
"并联支路不能混合选择空白网格和逻辑对象", {}};
}
else if (!selected_empty_slots.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"空白网格没有可并联的逻辑,请先选择触点或横线", {}};
}
else if (!selected_wire_cells.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"并联支路需要选择触点;横线网格请使用竖线连接", {}};
}
else if (rung_id.empty() || condition_ids.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"请在同一网络中选择要并联的连续节点", {}};
@@ -1686,13 +1706,39 @@ LogicEditorResult LogicEditorWidget::addHorizontalWire()
LogicEditorResult LogicEditorWidget::addVerticalWire()
{
const std::string rung_id = selectedRungId();
const std::vector<std::pair<std::string, int>> selected_empty_slots =
selectedEmptySlots();
const std::vector<std::pair<std::string, int>> selected_wire_cells =
selectedWireCells();
const std::vector<std::string> selected_nodes = selectedNodeIds();
const std::vector<std::string> selected_ids = selectedExpressionIds();
LogicEditorResult result;
if (rung_id.empty() || selected_ids.empty())
if (!selected_empty_slots.empty()
&& (!selected_wire_cells.empty() || !selected_nodes.empty()))
{
result = {false, LogicEditorError::InvalidOperation,
"竖线不能混合选择空白网格和逻辑对象", {}};
}
else if (!selected_empty_slots.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"空白网格没有可连接的逻辑,请先插入触点或横线", {}};
}
else if (!selected_wire_cells.empty() && !selected_nodes.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"竖线只能选择连续触点或同一条横线的网格", {}};
}
else if (rung_id.empty() || selected_ids.empty())
{
result = {false, LogicEditorError::InvalidOperation,
"请在同一网络中选择要连接的连续条件或横线", {}};
}
else if (!selected_wire_cells.empty())
{
result = editor_service_.addParallelWireBranchAtCells(
logic_id_, rung_id, selected_wire_cells);
}
else
{
result = editor_service_.addParallelWireBranch(


+ 1
- 0
app/tests/alarm_service_tests.cpp Vedi File

@@ -1,5 +1,6 @@
#include "domain/project_storage.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/alarm_editor_service.h"
#include "services/alarm_service.h"
#include "services/project_service.h"


+ 1
- 0
app/tests/domain_tests.cpp Vedi File

@@ -6,6 +6,7 @@
#include "domain/register_address.h"
#include "domain/register_repository.h"
#include "domain/runtime_state.h"
#include "domain/virtual_register_repository.h"
#include "support/test_support.h"

#include <cstdint>


+ 1
- 0
app/tests/hmi_editor_service_tests.cpp Vedi File

@@ -1,5 +1,6 @@
#include "domain/project_storage.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h"
#include "services/hmi_navigation_service.h"


+ 87
- 0
app/tests/logic_editor_service_tests.cpp Vedi File

@@ -343,6 +343,91 @@ void testStructuredWireEditing()
"wire branch deletion must participate in ladder undo history");
}

void testWireCellParallelSelectionUsesExactColumns()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(service, logic_id);
const LogicEditorResult source = service.appendWire(
logic_id, rung_id, 3);
require(source.succeeded, "wire-cell parallel setup must create a three-column wire");

const LogicEditorResult branch = service.addParallelWireBranchAtCells(
logic_id,
rung_id,
{{source.id, 0}, {source.id, 1}});
require(branch.succeeded,
"a selected two-cell wire range must create a parallel bypass");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.size() == 2U,
"a partial wire selection must preserve the surrounding series layout");
const ConditionExpression &parallel = rung->condition->children.front();
require(parallel.kind == ConditionExpressionKind::Parallel
&& parallel.children.size() == 2U
&& parallel.children.front().kind == ConditionExpressionKind::Wire
&& parallel.children.front().wire->columnSpan == 2
&& parallel.children.back().kind == ConditionExpressionKind::Wire
&& parallel.children.back().wire->columnSpan == 2
&& rung->condition->children.back().kind
== ConditionExpressionKind::Wire
&& rung->condition->children.back().wire->columnSpan == 1
&& conditionColumns(*rung->condition) == 3,
"the new bypass width must match the selected two cells, not the full source wire");

service.clearHistory();
const bool modified_before = project_service.isModified();
const LogicEditorResult non_contiguous =
service.addParallelWireBranchAtCells(
logic_id,
rung_id,
{{source.id, 0}, {source.id, 2}});
require(!non_contiguous.succeeded
&& project_service.isModified() == modified_before
&& !service.canUndo(),
"non-contiguous wire-cell selections must fail without mutation");
}

void testWireCellParallelSelectionAcrossAdjacentWires()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(service, logic_id);
std::vector<std::string> wire_ids;
for (int index = 0; index < 3; ++index)
{
const LogicEditorResult wire = service.appendWire(logic_id, rung_id);
require(wire.succeeded,
"adjacent wire setup must create each one-column segment");
wire_ids.push_back(wire.id);
}

const LogicEditorResult branch = service.addParallelWireBranchAtCells(
logic_id,
rung_id,
{{wire_ids.at(0), 0}, {wire_ids.at(1), 0}, {wire_ids.at(2), 0}});
require(branch.succeeded,
"visually continuous adjacent wire cells must create one bypass");

const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& rung->condition->kind == ConditionExpressionKind::Parallel
&& rung->condition->children.size() == 2U
&& rung->condition->children.front().kind
== ConditionExpressionKind::Wire
&& rung->condition->children.front().wire->columnSpan == 3
&& rung->condition->children.back().kind
== ConditionExpressionKind::Wire
&& rung->condition->children.back().wire->columnSpan == 3,
"three adjacent one-column wires must become a three-column parallel range");
}

void testBatchDeleteAllNodesInParallelBranch()
{
TestProjectStorage storage;
@@ -951,6 +1036,8 @@ int main()
testRangeParallelInsertion();
testParallelBranchGridInsertion();
testStructuredWireEditing();
testWireCellParallelSelectionUsesExactColumns();
testWireCellParallelSelectionAcrossAdjacentWires();
testBatchDeleteAllNodesInParallelBranch();
testConditionColumnLimit();
testUnconditionalOutputEditing();


+ 173
- 0
app/tests/main_window_tests.cpp Vedi File

@@ -2,6 +2,7 @@
#include "domain/project_storage.h"
#include "domain/project_limits.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/alarm_editor_service.h"
#include "services/alarm_service.h"
#include "services/hmi_editor_service.h"
@@ -903,6 +904,175 @@ void testLogicParallelPaddingGridInsertion()
"using a distant branch cell must retain the skipped cell as a replaceable wire");
}

void testLogicParallelActionsRespectGridSelectionType()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(service, logic_id);

std::vector<std::string> top_ids;
for (int address = 0; address < 3; ++address)
{
const LogicEditorResult result = service.appendCondition(
logic_id, rung_id, logicContact(address));
require(result.succeeded,
"grid selection type setup must create the upper branch");
top_ids.push_back(result.id);
}
require(service.addParallelBranch(
logic_id, rung_id, top_ids, logicContact(10))
.succeeded,
"grid selection type setup must create a short parallel branch");

LogicEditorWidget editor(service);
editor.setLogicId(logic_id);
editor.resize(1400, 480);
editor.show();
QApplication::processEvents();

QList<QGraphicsItem *> branch_slots;
for (QGraphicsItem *item : editor.scene()->items())
{
if (item->toolTip().startsWith(QStringLiteral("并联空网格")))
{
branch_slots.push_back(item);
}
}
std::sort(
branch_slots.begin(), branch_slots.end(),
[](const QGraphicsItem *left, const QGraphicsItem *right)
{
return left->scenePos().x() < right->scenePos().x();
});
require(branch_slots.size() == 2,
"empty parallel branch must expose its two grid cells");
branch_slots.at(0)->setSelected(true);
branch_slots.at(1)->setSelected(true);
service.clearHistory();

const LogicEditorResult parallel = editor.addParallelBranch(logicContact(20));
require(!parallel.succeeded
&& parallel.message.find("空白网格没有可并联的逻辑")
!= std::string::npos
&& !service.canUndo(),
"parallel insertion must reject a selection made only of empty branch cells");
const LogicEditorResult vertical = editor.addVerticalWire();
require(!vertical.succeeded
&& vertical.message.find("空白网格没有可连接的逻辑")
!= std::string::npos
&& !service.canUndo(),
"vertical insertion must reject a selection made only of empty branch cells");
}

void testLogicVerticalWireUsesSelectedWireCells()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(service, logic_id);
const LogicEditorResult source = service.appendWire(
logic_id, rung_id, 3);
require(source.succeeded,
"wire-cell UI setup must create a three-column wire");

LogicEditorWidget editor(service);
editor.setLogicId(logic_id);
editor.resize(1400, 360);
editor.show();
QApplication::processEvents();

QList<QGraphicsItem *> wire_cells;
for (QGraphicsItem *item : editor.scene()->items())
{
if (item->toolTip().startsWith(QStringLiteral("横线网格")))
{
wire_cells.push_back(item);
}
}
std::sort(
wire_cells.begin(), wire_cells.end(),
[](const QGraphicsItem *left, const QGraphicsItem *right)
{
return left->scenePos().x() < right->scenePos().x();
});
require(wire_cells.size() == 3,
"a three-column wire must expose three selectable cells");
wire_cells.at(0)->setSelected(true);
wire_cells.at(1)->setSelected(true);

const LogicEditorResult branch = editor.addVerticalWire();
require(branch.succeeded,
"vertical insertion must accept two selected wire cells");
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& rung->condition->kind == ConditionExpressionKind::Series
&& rung->condition->children.front().kind
== ConditionExpressionKind::Parallel
&& rung->condition->children.front().children.back().kind
== ConditionExpressionKind::Wire
&& rung->condition->children.front().children.back()
.wire->columnSpan == 2
&& rung->condition->children.back().kind
== ConditionExpressionKind::Wire
&& rung->condition->children.back().wire->columnSpan == 1,
"vertical insertion must persist exactly the selected two wire columns");
}

void testLogicVerticalWireUsesAdjacentWireCells()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(service, logic_id);
for (int index = 0; index < 3; ++index)
{
require(service.appendWire(logic_id, rung_id).succeeded,
"adjacent wire UI setup must create one-column segments");
}

LogicEditorWidget editor(service);
editor.setLogicId(logic_id);
editor.resize(1400, 360);
editor.show();
QApplication::processEvents();

QList<QGraphicsItem *> wire_cells;
for (QGraphicsItem *item : editor.scene()->items())
{
if (item->toolTip().startsWith(QStringLiteral("横线网格")))
{
wire_cells.push_back(item);
}
}
std::sort(
wire_cells.begin(), wire_cells.end(),
[](const QGraphicsItem *left, const QGraphicsItem *right)
{
return left->scenePos().x() < right->scenePos().x();
});
require(wire_cells.size() == 3,
"adjacent one-column wires must expose three selectable cells");
for (QGraphicsItem *cell : wire_cells)
{
cell->setSelected(true);
}

const LogicEditorResult branch = editor.addVerticalWire();
require(branch.succeeded,
"vertical insertion must accept adjacent cells from separate wire expressions");
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& rung->condition->kind == ConditionExpressionKind::Parallel
&& rung->condition->children.size() == 2U
&& rung->condition->children.front().wire->columnSpan == 3
&& rung->condition->children.back().wire->columnSpan == 3,
"adjacent wire cells must persist a three-column bypass in the UI path");
}

void testLogicParallelBranchConnectsShortBranchToRightJoin()
{
TestProjectStorage storage;
@@ -2477,6 +2647,9 @@ int main(int argc, char *argv[])
testRuntimeAlarmListInteraction();
testLogicEmptyGridSlotInsertion();
testLogicParallelPaddingGridInsertion();
testLogicParallelActionsRespectGridSelectionType();
testLogicVerticalWireUsesSelectedWireCells();
testLogicVerticalWireUsesAdjacentWireCells();
testLogicParallelBranchConnectsShortBranchToRightJoin();
testLogicContinuousInsertionConsumesFullWire();
testLogicHorizontalWireCanBeInsertedAgainAfterUndoingFirstAutoRung();


+ 1
- 0
app/tests/offline_simulation_service_tests.cpp Vedi File

@@ -1,5 +1,6 @@
#include "domain/hmi_model.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/hmi_runtime_service.h"
#include "services/offline_simulation_service.h"
#include "services/software_logic_executor.h"


+ 1
- 0
app/tests/performance_tests.cpp Vedi File

@@ -1,5 +1,6 @@
#include "domain/control_logic_model.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/software_logic_executor.h"

#include <QTest>


+ 1
- 0
app/tests/plc_runtime_tests.cpp Vedi File

@@ -5,6 +5,7 @@
#include "infrastructure/plc_communication_service.h"
#include "support/test_support.h"
#include "infrastructure/plc_register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/offline_simulation_service.h"
#include "services/plc_communication_gateway.h"
#include "services/project_service.h"


+ 3
- 2
app/tests/pri/test_layers.pri Vedi File

@@ -2,11 +2,12 @@

DOMAIN_REGISTER_SOURCES = \
../src/domain/register_address.cpp \
../src/domain/register_repository.cpp
../src/domain/virtual_register_repository.cpp

DOMAIN_REGISTER_HEADERS = \
../src/domain/register_address.h \
../src/domain/register_repository.h
../src/domain/register_repository.h \
../src/domain/virtual_register_repository.h

DOMAIN_HMI_SOURCES = \
../src/domain/hmi_model.cpp \


+ 1
- 0
app/tests/register_monitor_service_tests.cpp Vedi File

@@ -1,6 +1,7 @@
#include "domain/active_register_repository.h"
#include "domain/register_monitor_model.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "services/register_monitor_service.h"
#include "support/test_support.h"



+ 1
- 0
app/tests/runtime_mode_service_tests.cpp Vedi File

@@ -4,6 +4,7 @@
#include "domain/active_register_repository.h"
#include "domain/project_storage.h"
#include "domain/register_repository.h"
#include "domain/virtual_register_repository.h"
#include "support/test_support.h"

#include <functional>


Caricamento…
Annulla
Salva