diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index c896ba6..7b153b9 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -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 \ diff --git a/app/src/domain/register_repository.h b/app/src/domain/register_repository.h index a90d477..6514a7c 100644 --- a/app/src/domain/register_repository.h +++ b/app/src/domain/register_repository.h @@ -2,8 +2,6 @@ #include "register_address.h" -#include -#include #include // 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 @@ -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(RegisterAddress::kMaximumIndex + 1); - std::array bits_{}; // M 区位寄存器内存 - std::array words_{}; // D 区字寄存器内存 -}; diff --git a/app/src/domain/register_repository.cpp b/app/src/domain/virtual_register_repository.cpp similarity index 98% rename from app/src/domain/register_repository.cpp rename to app/src/domain/virtual_register_repository.cpp index 56ac50f..45b6e26 100644 --- a/app/src/domain/register_repository.cpp +++ b/app/src/domain/virtual_register_repository.cpp @@ -1,4 +1,4 @@ -#include "register_repository.h" +#include "virtual_register_repository.h" VirtualRegisterRepository::VirtualRegisterRepository() { diff --git a/app/src/domain/virtual_register_repository.h b/app/src/domain/virtual_register_repository.h new file mode 100644 index 0000000..400de18 --- /dev/null +++ b/app/src/domain/virtual_register_repository.h @@ -0,0 +1,32 @@ +#pragma once + +#include "register_repository.h" + +#include +#include + +// 基于内存的离线 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(RegisterAddress::kMaximumIndex + 1); + std::array bits_{}; // M 区位寄存器内存 + std::array words_{}; // D 区字寄存器内存 +}; diff --git a/app/src/main.cpp b/app/src/main.cpp index 7d5c12e..b115119 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -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" diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index 16f630a..11e9dfb 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -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 ¶llel_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 ¶llel_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 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> &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 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 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(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(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(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(cells.size()); + + std::unordered_set 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, diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index 5aab3a1..a74f376 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -4,6 +4,7 @@ #include "editor_history.h" #include +#include #include class ProjectService; @@ -121,6 +122,11 @@ public: const std::string &logic_id, const std::string &rung_id, const std::vector &selected_expression_ids); + // 按选中的横线网格建立旁路;网格必须来自同一条横线且列号连续 + LogicEditorResult addParallelWireBranchAtCells( + const std::string &logic_id, + const std::string &rung_id, + const std::vector> &selected_wire_cells); // 输出槽编辑;每个网络最多一个输出节点 LogicEditorResult setOutput( const std::string &logic_id, diff --git a/app/src/services/offline_simulation_service.h b/app/src/services/offline_simulation_service.h index 93a4545..c0a04cb 100644 --- a/app/src/services/offline_simulation_service.h +++ b/app/src/services/offline_simulation_service.h @@ -1,5 +1,6 @@ #pragma once +#include "domain/virtual_register_repository.h" #include "software_logic_executor.h" #include diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 1c601f9..fc2aa42 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -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> selected_empty_slots = + selectedEmptySlots(); + const std::vector> selected_wire_cells = + selectedWireCells(); const std::vector selected_ids = selectedNodeIds(); std::vector 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> selected_empty_slots = + selectedEmptySlots(); + const std::vector> selected_wire_cells = + selectedWireCells(); + const std::vector selected_nodes = selectedNodeIds(); const std::vector 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( diff --git a/app/tests/alarm_service_tests.cpp b/app/tests/alarm_service_tests.cpp index 5e3f5b8..795f7dc 100644 --- a/app/tests/alarm_service_tests.cpp +++ b/app/tests/alarm_service_tests.cpp @@ -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" diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 67d13e7..ad512a3 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -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 diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index c19fcfb..eb78fd6 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -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" diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index 6aea30b..e06f2f8 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -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 ¶llel = 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 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(); diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index cf4965f..48bda9a 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -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 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 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 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 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(); diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index cc4d83f..7d1c374 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -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" diff --git a/app/tests/performance_tests.cpp b/app/tests/performance_tests.cpp index a43bc3e..c779925 100644 --- a/app/tests/performance_tests.cpp +++ b/app/tests/performance_tests.cpp @@ -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 diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp index 8f3f77a..57c0600 100644 --- a/app/tests/plc_runtime_tests.cpp +++ b/app/tests/plc_runtime_tests.cpp @@ -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" diff --git a/app/tests/pri/test_layers.pri b/app/tests/pri/test_layers.pri index 882abf8..e87a2d2 100644 --- a/app/tests/pri/test_layers.pri +++ b/app/tests/pri/test_layers.pri @@ -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 \ diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index 2b132b6..411ccf8 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -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" diff --git a/app/tests/runtime_mode_service_tests.cpp b/app/tests/runtime_mode_service_tests.cpp index 548d4cd..5acd504 100644 --- a/app/tests/runtime_mode_service_tests.cpp +++ b/app/tests/runtime_mode_service_tests.cpp @@ -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