浏览代码

feat: add ladder command input editing

main
suyu 3 周前
父节点
当前提交
6d9cd22729
共有 9 个文件被更改,包括 2184 次插入92 次删除
  1. +2
    -0
      app/integrated_platform.pro
  2. +590
    -0
      app/src/services/logic_command_service.cpp
  3. +106
    -0
      app/src/services/logic_command_service.h
  4. +191
    -23
      app/src/services/logic_editor_service.cpp
  5. +22
    -6
      app/src/services/logic_editor_service.h
  6. +902
    -61
      app/src/ui/logic_editor_widget.cpp
  7. +63
    -0
      app/src/ui/logic_editor_widget.h
  8. +304
    -0
      app/tests/logic_editor_service_tests.cpp
  9. +4
    -2
      app/tests/pri/test_layers.pri

+ 2
- 0
app/integrated_platform.pro 查看文件

@@ -45,6 +45,7 @@ SOURCES += \
src/services/hmi_editor_service.cpp \
src/services/hmi_navigation_service.cpp \
src/services/logic_editor_service.cpp \
src/services/logic_command_service.cpp \
src/services/hmi_runtime_service.cpp \
src/services/software_logic_executor.cpp \
src/services/offline_simulation_service.cpp \
@@ -96,6 +97,7 @@ HEADERS += \
src/services/hmi_editor_service.h \
src/services/hmi_navigation_service.h \
src/services/logic_editor_service.h \
src/services/logic_command_service.h \
src/services/hmi_runtime_service.h \
src/services/software_logic_executor.h \
src/services/offline_simulation_service.h \


+ 590
- 0
app/src/services/logic_command_service.cpp 查看文件

@@ -0,0 +1,590 @@
#include "logic_command_service.h"

#include "logic_editor_service.h"

#include <algorithm>
#include <cctype>
#include <limits>
#include <sstream>
#include <unordered_map>

namespace {

std::vector<std::string> splitTokens(const std::string &text)
{
std::istringstream stream(text);
std::vector<std::string> tokens;
std::string token;
while (stream >> token)
{
tokens.push_back(std::move(token));
}
return tokens;
}

std::string upperAscii(std::string value)
{
std::transform(
value.begin(), value.end(), value.begin(),
[](unsigned char character)
{
return static_cast<char>(std::toupper(character));
});
return value;
}

LogicCommandParseResult parseFailure(const std::string &message)
{
return {false, message, {}};
}

bool requiresOperands(
const std::vector<std::string> &tokens,
std::size_t count,
const std::string &example,
LogicCommandParseResult *failure)
{
if (tokens.size() == count + 1U)
{
return true;
}
*failure = parseFailure(
tokens.front() + " 需要 " + std::to_string(count)
+ " 个操作数,例如 " + example);
return false;
}

bool parseAddress(
const std::string &text,
RegisterArea expected_area,
const std::string &instruction,
RegisterAddress *address,
LogicCommandParseResult *failure)
{
const RegisterAddressParseResult parsed = parseRegisterAddress(text);
const char expected_prefix = expected_area == RegisterArea::M ? 'M' : 'D';
if (!parsed.succeeded)
{
const std::string range = std::string(1, expected_prefix) + "0~"
+ expected_prefix + std::to_string(RegisterAddress::kMaximumIndex);
*failure = parseFailure(
parsed.error == RegisterAddressParseError::OutOfRange
? std::string(1, expected_prefix) + " 地址超出范围,应为 " + range
: "地址格式错误,应为 " + range);
return false;
}
if (parsed.address.area() != expected_area)
{
*failure = parseFailure(
instruction + " 只支持 " + std::string(1, expected_prefix) + " 地址");
return false;
}
*address = parsed.address;
return true;
}

bool parseInt16(
const std::string &text,
const std::string &instruction,
std::int16_t *value,
LogicCommandParseResult *failure);

bool parseWordOperand(
const std::string &text,
const std::string &instruction,
WordOperand *operand,
LogicCommandParseResult *failure)
{
const RegisterAddressParseResult parsed = parseRegisterAddress(text);
if (parsed.succeeded)
{
if (parsed.address.area() != RegisterArea::D)
{
*failure = parseFailure(instruction + " 的寄存器操作数必须使用 D 地址");
return false;
}
*operand = WordOperand{
WordOperandKind::Register, parsed.address, 0};
return true;
}
if (!text.empty()
&& (text.front() == 'D' || text.front() == 'd'
|| text.front() == 'M' || text.front() == 'm'))
{
RegisterAddress address{RegisterArea::D, 0};
return parseAddress(
text, RegisterArea::D, instruction, &address, failure)
&& ((*operand = WordOperand{
WordOperandKind::Register, address, 0}), true);
}
std::int16_t constant = 0;
if (!parseInt16(text, instruction, &constant, failure))
{
return false;
}
*operand = WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
constant};
return true;
}

bool isLoad(LogicCommandOpcode opcode)
{
return opcode == LogicCommandOpcode::Load
|| opcode == LogicCommandOpcode::LoadInverse
|| opcode == LogicCommandOpcode::LoadRising
|| opcode == LogicCommandOpcode::LoadFalling
|| opcode == LogicCommandOpcode::CompareEqual
|| opcode == LogicCommandOpcode::CompareNotEqual
|| opcode == LogicCommandOpcode::CompareLessThan
|| opcode == LogicCommandOpcode::CompareLessThanOrEqual
|| opcode == LogicCommandOpcode::CompareGreaterThan
|| opcode == LogicCommandOpcode::CompareGreaterThanOrEqual;
}

bool isOr(LogicCommandOpcode opcode)
{
return opcode == LogicCommandOpcode::Or
|| opcode == LogicCommandOpcode::OrInverse;
}

bool isCondition(LogicCommandOpcode opcode)
{
return isLoad(opcode) || isOr(opcode)
|| opcode == LogicCommandOpcode::And
|| opcode == LogicCommandOpcode::AndInverse;
}

bool isConditionTarget(LogicCommandTargetKind kind)
{
return kind != LogicCommandTargetKind::Output;
}

bool isOutput(LogicCommandOpcode opcode)
{
return opcode == LogicCommandOpcode::Output
|| opcode == LogicCommandOpcode::Set
|| opcode == LogicCommandOpcode::Reset
|| opcode == LogicCommandOpcode::Move
|| opcode == LogicCommandOpcode::Add
|| opcode == LogicCommandOpcode::Subtract;
}

bool parseInt16(
const std::string &text,
const std::string &instruction,
std::int16_t *value,
LogicCommandParseResult *failure)
{
try
{
std::size_t consumed = 0;
const long parsed = std::stol(text, &consumed, 10);
if (consumed != text.size()
|| parsed < std::numeric_limits<std::int16_t>::min()
|| parsed > std::numeric_limits<std::int16_t>::max())
{
throw std::out_of_range("int16");
}
*value = static_cast<std::int16_t>(parsed);
return true;
}
catch (const std::exception &)
{
*failure = parseFailure(
instruction + " 的第二个操作数应为 -32768~32767 的常量");
return false;
}
}

LogicCommandResult executionFailure(
const std::string &message, LogicCommandOpcode opcode)
{
return {false, message, {}, {}, opcode};
}

} // namespace

LogicCommandService::LogicCommandService(LogicEditorService &editor_service)
: editor_service_(editor_service)
{
}

LogicCommandParseResult LogicCommandService::parse(const std::string &text)
{
std::vector<std::string> tokens = splitTokens(text);
if (tokens.empty())
{
return parseFailure("请输入 PLC 指令");
}
tokens.front() = upperAscii(tokens.front());
const std::string &mnemonic = tokens.front();
LogicCommandParseResult failure;

const std::unordered_map<std::string, LogicCommandOpcode> condition_opcodes{
{"LD", LogicCommandOpcode::Load},
{"LDI", LogicCommandOpcode::LoadInverse},
{"LDP", LogicCommandOpcode::LoadRising},
{"LDF", LogicCommandOpcode::LoadFalling},
{"AND", LogicCommandOpcode::And},
{"ANI", LogicCommandOpcode::AndInverse},
{"OR", LogicCommandOpcode::Or},
{"ORI", LogicCommandOpcode::OrInverse}};
const std::unordered_map<std::string, LogicCommandOpcode> compare_opcodes{
{"LD=", LogicCommandOpcode::CompareEqual},
{"LD<>", LogicCommandOpcode::CompareNotEqual},
{"LD<", LogicCommandOpcode::CompareLessThan},
{"LD<=", LogicCommandOpcode::CompareLessThanOrEqual},
{"LD>", LogicCommandOpcode::CompareGreaterThan},
{"LD>=", LogicCommandOpcode::CompareGreaterThanOrEqual}};
const auto compare = compare_opcodes.find(mnemonic);
if (compare != compare_opcodes.end())
{
if (!requiresOperands(tokens, 2U, mnemonic + " D0 0", &failure))
{
return failure;
}
RegisterAddress address{RegisterArea::D, 0};
if (!parseAddress(tokens[1], RegisterArea::D, mnemonic, &address, &failure))
{
return failure;
}
std::int16_t value = 0;
if (!parseInt16(tokens[2], mnemonic, &value, &failure))
{
return failure;
}
ComparisonOperator operation = ComparisonOperator::Equal;
switch (compare->second)
{
case LogicCommandOpcode::CompareEqual:
operation = ComparisonOperator::Equal;
break;
case LogicCommandOpcode::CompareNotEqual:
operation = ComparisonOperator::NotEqual;
break;
case LogicCommandOpcode::CompareLessThan:
operation = ComparisonOperator::LessThan;
break;
case LogicCommandOpcode::CompareLessThanOrEqual:
operation = ComparisonOperator::LessThanOrEqual;
break;
case LogicCommandOpcode::CompareGreaterThan:
operation = ComparisonOperator::GreaterThan;
break;
case LogicCommandOpcode::CompareGreaterThanOrEqual:
operation = ComparisonOperator::GreaterThanOrEqual;
break;
default:
break;
}
return {true, {}, {compare->second,
CompareNodeConfig{address, operation, value}}};
}
const auto condition = condition_opcodes.find(mnemonic);
if (condition != condition_opcodes.end())
{
if (!requiresOperands(tokens, 1U, mnemonic + " M0", &failure))
{
return failure;
}
RegisterAddress address{RegisterArea::M, 0};
if (!parseAddress(tokens[1], RegisterArea::M, mnemonic, &address, &failure))
{
return failure;
}
LogicNodeConfig config;
if (condition->second == LogicCommandOpcode::LoadRising
|| condition->second == LogicCommandOpcode::LoadFalling)
{
config = EdgeContactNodeConfig{
address,
condition->second == LogicCommandOpcode::LoadRising
? EdgeMode::Rising : EdgeMode::Falling};
}
else
{
const bool inverse = condition->second == LogicCommandOpcode::LoadInverse
|| condition->second == LogicCommandOpcode::AndInverse
|| condition->second == LogicCommandOpcode::OrInverse;
config = ContactNodeConfig{
address,
inverse ? ContactMode::NormallyClosed
: ContactMode::NormallyOpen};
}
return {true, {}, {condition->second, std::move(config)}};
}

const std::unordered_map<std::string, LogicCommandOpcode> coil_opcodes{
{"OUT", LogicCommandOpcode::Output},
{"SET", LogicCommandOpcode::Set},
{"RST", LogicCommandOpcode::Reset}};
const auto coil = coil_opcodes.find(mnemonic);
if (coil != coil_opcodes.end())
{
if (!requiresOperands(tokens, 1U, mnemonic + " M0", &failure))
{
return failure;
}
RegisterAddress address{RegisterArea::M, 0};
if (!parseAddress(tokens[1], RegisterArea::M, mnemonic, &address, &failure))
{
return failure;
}
const CoilMode mode = coil->second == LogicCommandOpcode::Set
? CoilMode::Set
: coil->second == LogicCommandOpcode::Reset
? CoilMode::Reset : CoilMode::Normal;
return {true, {}, {coil->second, CoilNodeConfig{address, mode}}};
}

if (mnemonic == "MOV")
{
if (!requiresOperands(tokens, 2U, "MOV D0 D1", &failure))
{
return failure;
}
RegisterAddress destination{RegisterArea::D, 0};
WordOperand source;
if (!parseWordOperand(tokens[1], mnemonic, &source, &failure)
|| !parseAddress(tokens[2], RegisterArea::D, mnemonic, &destination, &failure))
{
return failure;
}
return {
true, {},
{LogicCommandOpcode::Move,
MoveNodeConfig{source, destination}}};
}

if (mnemonic == "ADD" || mnemonic == "SUB")
{
if (!requiresOperands(tokens, 3U, mnemonic + " D0 D1 D2", &failure))
{
return failure;
}
WordOperand left;
WordOperand right;
RegisterAddress destination{RegisterArea::D, 0};
if (!parseWordOperand(tokens[1], mnemonic, &left, &failure)
|| !parseWordOperand(tokens[2], mnemonic, &right, &failure)
|| !parseAddress(tokens[3], RegisterArea::D, mnemonic, &destination, &failure))
{
return failure;
}
const LogicCommandOpcode opcode = mnemonic == "ADD"
? LogicCommandOpcode::Add : LogicCommandOpcode::Subtract;
return {
true, {},
{opcode,
ArithmeticNodeConfig{
opcode == LogicCommandOpcode::Add
? ArithmeticOperation::Add : ArithmeticOperation::Subtract,
left,
right,
destination}}};
}

return parseFailure("当前指令暂不支持:" + mnemonic);
}

const std::vector<LogicCommandSuggestion> &LogicCommandService::suggestions()
{
static const std::vector<LogicCommandSuggestion> values{
{"LD", "常开触点", "M 地址"},
{"LDI", "常闭触点", "M 地址"},
{"LDP", "上升沿触点", "M 地址"},
{"LDF", "下降沿触点", "M 地址"},
{"LD=", "D 值等于常量", "D 地址 + 常量"},
{"LD<>", "D 值不等于常量", "D 地址 + 常量"},
{"LD<", "D 值小于常量", "D 地址 + 常量"},
{"LD<=", "D 值小于等于常量", "D 地址 + 常量"},
{"LD>", "D 值大于常量", "D 地址 + 常量"},
{"LD>=", "D 值大于等于常量", "D 地址 + 常量"},
{"AND", "串联常开触点", "M 地址"},
{"ANI", "串联常闭触点", "M 地址"},
{"OR", "并联常开触点", "M 地址"},
{"ORI", "并联常闭触点", "M 地址"},
{"OUT", "普通线圈", "M 地址"},
{"SET", "置位线圈", "M 地址"},
{"RST", "复位线圈", "M 地址"},
{"MOV", "数据传送", "D 地址 -> D 地址"},
{"ADD", "加法", "D 地址 + D 地址 -> D 地址"},
{"SUB", "减法", "D 地址 - D 地址 -> D 地址"},
{"TON", "暂不支持", "T 地址", false},
{"CTU", "暂不支持", "C 地址", false}};
return values;
}

LogicCommandResult LogicCommandService::execute(
const LogicCommandRequest &request)
{
const LogicCommandParseResult parsed = parse(request.text);
if (!parsed.succeeded)
{
return executionFailure(parsed.message, parsed.command.opcode);
}
const LogicCommandOpcode opcode = parsed.command.opcode;
LogicEditorResult edited;

if (request.target.kind == LogicCommandTargetKind::ExistingNode)
{
const LogicNode *existing = editor_service_.findNode(
request.logicId, request.target.expressionId);
if (existing == nullptr)
{
return executionFailure("未找到要编辑的逻辑节点", opcode);
}
if (existing->isCondition())
{
if (!isLoad(opcode))
{
return executionFailure(
"已有条件节点只能替换为 LD/LDI/LDP/LDF 指令", opcode);
}
}
else if (existing->isOutput())
{
if (!isOutput(opcode))
{
return executionFailure(
"已有输出节点只能替换为 OUT/SET/RST/MOV/ADD/SUB 指令",
opcode);
}
}
else
{
return executionFailure("当前节点类型不支持命令替换", opcode);
}
edited = editor_service_.updateNodeConfig(
request.logicId, request.target.expressionId,
parsed.command.config);
}
else if (isCondition(opcode))
{
if (isLoad(opcode) && request.continuing)
{
edited = editor_service_.appendCondition(
request.logicId, {}, parsed.command.config, true);
}
else if (isOr(opcode))
{
const std::string rung_id = request.continuing
? request.currentRungId : request.target.rungId;
if (request.continuing)
{
edited = editor_service_.addParallelToWholeCondition(
request.logicId, rung_id, parsed.command.config, true);
}
else if (request.parallelNodeIds.empty())
{
return executionFailure(
"OR/ORI 需要先选中同一网络中的连续条件", opcode);
}
else
{
edited = editor_service_.addParallelBranch(
request.logicId,
rung_id,
request.parallelNodeIds,
parsed.command.config,
true);
}
}
else if (request.continuing)
{
const LadderRung *rung = editor_service_.findRung(
request.logicId, request.currentRungId);
if (rung != nullptr && rung->output.has_value())
{
return executionFailure(
"当前网络已有输出,下一条只能输入 LD/LDI/LDP/LDF 新建网络",
opcode);
}
edited = editor_service_.appendCondition(
request.logicId, request.currentRungId,
parsed.command.config, true);
}
else
{
if (!isConditionTarget(request.target.kind))
{
return executionFailure(
"条件指令只能输入在第 1~10 列条件区", opcode);
}
if ((opcode == LogicCommandOpcode::And
|| opcode == LogicCommandOpcode::AndInverse)
&& (!request.target.rungId.empty()
&& (editor_service_.findRung(
request.logicId, request.target.rungId) == nullptr
|| !editor_service_.findRung(
request.logicId, request.target.rungId)
->condition.has_value())))
{
return executionFailure(
"AND/ANI 前必须先输入 LD/LDI/LDP/LDF", opcode);
}
switch (request.target.kind)
{
case LogicCommandTargetKind::EmptyColumn:
edited = editor_service_.insertConditionAtColumn(
request.logicId, request.target.rungId,
request.target.column, parsed.command.config, true);
break;
case LogicCommandTargetKind::BranchEmptyColumn:
edited = editor_service_.insertConditionInBranchAtColumn(
request.logicId, request.target.rungId,
request.target.expressionId, request.target.column,
parsed.command.config, true);
break;
case LogicCommandTargetKind::WireColumn:
edited = editor_service_.replaceWireColumnWithCondition(
request.logicId, request.target.rungId,
request.target.expressionId, request.target.column,
parsed.command.config, true);
break;
case LogicCommandTargetKind::GapColumn:
edited = editor_service_.replaceGapColumnWithCondition(
request.logicId, request.target.rungId,
request.target.expressionId, request.target.column,
parsed.command.config, true);
break;
case LogicCommandTargetKind::Output:
break;
case LogicCommandTargetKind::ExistingNode:
break;
}
}
}
else
{
const std::string rung_id = request.continuing
? request.currentRungId : request.target.rungId;
if (!request.continuing
&& request.target.kind != LogicCommandTargetKind::Output)
{
return executionFailure(
"输出指令只能输入在第 11 列输出区", opcode);
}
const LadderRung *rung = editor_service_.findRung(
request.logicId, rung_id);
if (request.continuing && rung != nullptr && rung->output.has_value())
{
return executionFailure(
"当前网络已经有输出,下一条请输入 LD/LDI/LDP/LDF 新建网络",
opcode);
}
edited = editor_service_.setOutput(
request.logicId, rung_id, parsed.command.config, true);
}

if (!edited.succeeded)
{
return executionFailure(edited.message, opcode);
}
return {
true, {}, edited.id,
editor_service_.rungIdForNode(request.logicId, edited.id),
opcode};
}

+ 106
- 0
app/src/services/logic_command_service.h 查看文件

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

#include "domain/control_logic_model.h"

#include <string>
#include <vector>

class LogicEditorService;

enum class LogicCommandOpcode
{
Load,
LoadInverse,
LoadRising,
LoadFalling,
CompareEqual,
CompareNotEqual,
CompareLessThan,
CompareLessThanOrEqual,
CompareGreaterThan,
CompareGreaterThanOrEqual,
And,
AndInverse,
Or,
OrInverse,
Output,
Set,
Reset,
Move,
Add,
Subtract
};

struct ParsedLogicCommand
{
LogicCommandOpcode opcode = LogicCommandOpcode::Load;
LogicNodeConfig config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen};
};

struct LogicCommandParseResult
{
bool succeeded = false;
std::string message;
ParsedLogicCommand command;
};

struct LogicCommandSuggestion
{
std::string mnemonic;
std::string description;
std::string operand_hint;
bool supported = true;
};

enum class LogicCommandTargetKind
{
EmptyColumn,
BranchEmptyColumn,
WireColumn,
GapColumn,
Output,
ExistingNode
};

struct LogicCommandTarget
{
LogicCommandTargetKind kind = LogicCommandTargetKind::EmptyColumn;
std::string rungId;
std::string expressionId;
int column = 0;
};

struct LogicCommandRequest
{
std::string logicId;
std::string text;
LogicCommandTarget target;
bool continuing = false;
std::string currentRungId;
std::vector<std::string> parallelNodeIds;
};

struct LogicCommandResult
{
bool succeeded = false;
std::string message;
std::string id;
std::string rungId;
LogicCommandOpcode opcode = LogicCommandOpcode::Load;
};

// 将单条命令语解析并原子转换为现有结构化梯形图编辑操作
class LogicCommandService
{
public:
explicit LogicCommandService(LogicEditorService &editor_service);

static LogicCommandParseResult parse(const std::string &text);
static const std::vector<LogicCommandSuggestion> &suggestions();

LogicCommandResult execute(const LogicCommandRequest &request);

private:
LogicEditorService &editor_service_;
};

+ 191
- 23
app/src/services/logic_editor_service.cpp 查看文件

@@ -68,9 +68,12 @@ std::string rungLimitMessage(
: std::string{};
}

LogicNode makeNode(const std::string &id, const LogicNodeConfig &config)
LogicNode makeNode(
const std::string &id,
const LogicNodeConfig &config,
bool configured = false)
{
return {id, config, false};
return {id, config, configured};
}

ConditionExpression makeContainer(
@@ -165,6 +168,28 @@ int expressionColumns(const ConditionExpression &expression)
return columns;
}

std::optional<std::pair<std::string, int>> firstGapCell(
const ConditionExpression &expression)
{
if (expression.kind == ConditionExpressionKind::Gap)
{
return std::make_pair(expression.id, 0);
}
if (expression.kind == ConditionExpressionKind::Node
|| expression.kind == ConditionExpressionKind::Wire)
{
return std::nullopt;
}
for (const ConditionExpression &child : expression.children)
{
if (const auto gap = firstGapCell(child); gap.has_value())
{
return gap;
}
}
return std::nullopt;
}

bool isSubset(const NodeIdSet &subset, const NodeIdSet &values)
{
return std::all_of(
@@ -1158,13 +1183,36 @@ LogicEditorResult LogicEditorService::addRung(const std::string &logic_id)

LogicEditorResult LogicEditorService::removeRung(
const std::string &logic_id, const std::string &rung_id)
{
return removeRungs(logic_id, {rung_id});
}

LogicEditorResult LogicEditorService::removeRungs(
const std::string &logic_id,
const std::vector<std::string> &rung_ids)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
{
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
if (findRung(logic_id, rung_id) == nullptr)
if (rung_ids.empty())
{
return failure(LogicEditorError::InvalidOperation, "请先选择要删除的梯形图网络");
}
std::unordered_set<std::string> selected_ids;
for (const std::string &rung_id : rung_ids)
{
if (!selected_ids.insert(rung_id).second)
{
return failure(LogicEditorError::InvalidOperation, "删除列表中存在重复网络");
}
if (findRung(logic_id, rung_id) == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
}
if (selected_ids.empty())
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
@@ -1176,10 +1224,13 @@ LogicEditorResult LogicEditorService::removeRung(
target->rungs.erase(
std::remove_if(
target->rungs.begin(), target->rungs.end(),
[&rung_id](const LadderRung &rung) { return rung.id == rung_id; }),
[&selected_ids](const LadderRung &rung)
{
return selected_ids.find(rung.id) != selected_ids.end();
}),
target->rungs.end());
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, rung_id};
return {true, LogicEditorError::None, {}, rung_ids.front()};
}

LogicEditorResult LogicEditorService::updateRungComment(
@@ -1219,7 +1270,8 @@ LogicEditorResult LogicEditorService::updateRungComment(
LogicEditorResult LogicEditorService::appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr)
@@ -1227,7 +1279,9 @@ LogicEditorResult LogicEditorService::appendCondition(
return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑");
}
const bool create_rung = rung_id.empty();
if (!create_rung && findRung(logic_id, rung_id) == nullptr)
const LadderRung *existing_rung = create_rung
? nullptr : findRung(logic_id, rung_id);
if (!create_rung && existing_rung == nullptr)
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
@@ -1243,11 +1297,22 @@ LogicEditorResult LogicEditorService::appendCondition(
{
return failure(LogicEditorError::InvalidNode, "梯形图条件不能使用线圈节点");
}
if (existing_rung != nullptr && !existing_rung->output.has_value()
&& existing_rung->condition.has_value())
{
// 普通条件追加也优先填充删除后保留的 Gap,避免把新节点追加到网络末端
if (const auto gap = firstGapCell(*existing_rung->condition); gap.has_value())
{
return replaceGapColumnWithCondition(
logic_id, rung_id, gap->first, gap->second, config, configured);
}
}
// 先生成稳定节点 ID,再把新节点接到已有表达式的串联末尾
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
const std::string target_rung_id = create_rung
? makeUniqueRungId(*logic) : rung_id;
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
ConditionExpression leaf = ConditionExpression::fromNode(
makeNode(node_id, config, configured));
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
@@ -1358,7 +1423,8 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *existing_rung = findRung(logic_id, rung_id);
@@ -1388,7 +1454,8 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn(
// 点击远端空网格时,用持久化横线填充前置空档,再放入真实条件节点
const int leading_wire_columns = column - occupied_columns;
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
ConditionExpression leaf = ConditionExpression::fromNode(
makeNode(node_id, config, configured));
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
@@ -1459,7 +1526,8 @@ LogicEditorResult LogicEditorService::insertConditionInBranchAtColumn(
const std::string &rung_id,
const std::string &branch_expression_id,
int column,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
const LadderRung *existing_rung = findRung(logic_id, rung_id);
@@ -1503,7 +1571,7 @@ LogicEditorResult LogicEditorService::insertConditionInBranchAtColumn(
? makeUniqueWireId(*logic) : std::string{};
const std::string series_id = makeUniqueExpressionId(*logic);
ConditionExpression leaf = ConditionExpression::fromNode(
makeNode(node_id, config));
makeNode(node_id, config, configured));
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
@@ -1572,6 +1640,16 @@ LogicEditorResult LogicEditorService::appendWire(
{
return failure(LogicEditorError::RungNotFound, "未找到梯形图网络");
}
if (existing_rung != nullptr && column_span == 1
&& existing_rung->condition.has_value())
{
// 删除后的 Gap 仍保留原网格位置;无选中目标追加时优先填回第一个 Gap
if (const auto gap = firstGapCell(*existing_rung->condition); gap.has_value())
{
return replaceGapColumnWithWire(
logic_id, rung_id, gap->first, gap->second);
}
}
bool create_rung = rung_id.empty();
if (!create_rung && column_span == 1 && existing_rung->condition.has_value()
&& expressionColumns(*existing_rung->condition)
@@ -1762,7 +1840,25 @@ LogicEditorResult LogicEditorService::insertWireAfter(
{
return child.id == target_expression_id;
});
parent->children.insert(target_iterator + 1, std::move(leaf));
const auto insertion_position = target_iterator + 1;
if (insertion_position != parent->children.end()
&& insertion_position->kind == ConditionExpressionKind::Gap)
{
// 连续添加横线时优先消耗目标后面的 Gap,保持网络总列数不变
if (insertion_position->gap->columnSpan == 1)
{
*insertion_position = std::move(leaf);
}
else
{
--insertion_position->gap->columnSpan;
parent->children.insert(insertion_position, std::move(leaf));
}
}
else
{
parent->children.insert(insertion_position, std::move(leaf));
}
}
else
{
@@ -1830,7 +1926,8 @@ LogicEditorResult LogicEditorService::replaceWireColumnWithCondition(
const std::string &rung_id,
const std::string &wire_expression_id,
int column_offset,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
const ConditionExpression *wire = findExpression(
@@ -1866,7 +1963,7 @@ LogicEditorResult LogicEditorService::replaceWireColumnWithCondition(
wire_expression_id, leading_columns));
}
replacement.children.push_back(
ConditionExpression::fromNode(makeNode(node_id, config)));
ConditionExpression::fromNode(makeNode(node_id, config, configured)));
if (trailing_columns > 0)
{
replacement.children.push_back(ConditionExpression::fromWire(
@@ -1901,7 +1998,8 @@ LogicEditorResult LogicEditorService::replaceGapColumnWithCondition(
const std::string &rung_id,
const std::string &gap_expression_id,
int column_offset,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
const ConditionExpression *gap = findExpression(
@@ -1926,7 +2024,8 @@ LogicEditorResult LogicEditorService::replaceGapColumnWithCondition(
ConditionExpression replacement;
if (leading_columns == 0 && trailing_columns == 0)
{
replacement = ConditionExpression::fromNode(makeNode(node_id, config));
replacement = ConditionExpression::fromNode(
makeNode(node_id, config, configured));
}
else
{
@@ -1938,7 +2037,7 @@ LogicEditorResult LogicEditorService::replaceGapColumnWithCondition(
gap_expression_id, leading_columns));
}
replacement.children.push_back(
ConditionExpression::fromNode(makeNode(node_id, config)));
ConditionExpression::fromNode(makeNode(node_id, config, configured)));
if (trailing_columns > 0)
{
replacement.children.push_back(ConditionExpression::fromGap(
@@ -2032,7 +2131,8 @@ LogicEditorResult LogicEditorService::addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config)
const LogicNodeConfig &config,
bool configured)
{
const ControlLogic *logic = findLogic(logic_id);
if (logic == nullptr || findRung(logic_id, rung_id) == nullptr)
@@ -2068,7 +2168,8 @@ LogicEditorResult LogicEditorService::addParallelBranch(
"并联选择中包含未知节点");
}
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
ConditionExpression leaf = ConditionExpression::fromNode(
makeNode(node_id, config, configured));
const std::string parallel_id = makeUniqueExpressionId(*logic);
std::string series_id = parallel_id + "-range";
while (findExpression(logic_id, rung_id, series_id) != nullptr)
@@ -2105,6 +2206,73 @@ LogicEditorResult LogicEditorService::addParallelBranch(
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelToWholeCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured)
{
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,
"OR/ORI 前必须先有一段条件");
}
if (!isConditionConfig(config))
{
return failure(LogicEditorError::InvalidNode, "并联支路只能使用条件节点");
}

const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression branch = ConditionExpression::fromNode(
makeNode(node_id, config, configured));
const std::string parallel_id = makeUniqueExpressionId(*logic);
std::string branch_series_id = parallel_id + "-branch";
while (findExpression(logic_id, rung_id, branch_series_id) != nullptr)
{
branch_series_id += "-branch";
}
const int original_columns = expressionColumns(*existing_rung->condition);
const std::string padding_wire_id = original_columns > 1
? makeUniqueWireId(*logic) : std::string{};
HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();
auto editable_logic = std::find_if(
project.controlLogics.begin(), project.controlLogics.end(),
[&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; });
LadderRung *rung = findEditableRung(project, logic_id, rung_id);
ConditionExpression original = std::move(*rung->condition);
if (original_columns > 1)
{
ConditionExpression padded;
padded.id = branch_series_id;
padded.kind = ConditionExpressionKind::Series;
padded.children.push_back(std::move(branch));
padded.children.push_back(ConditionExpression::fromWire(
padding_wire_id, original_columns - 1));
branch = std::move(padded);
}
rung->condition = makeContainer(
parallel_id,
ConditionExpressionKind::Parallel,
std::move(original),
std::move(branch));

std::string validation_error;
if (!repairExplicitLayout(*editable_logic, *rung, &validation_error)
|| !rung->validate(&validation_error))
{
rollbackEdit(std::move(before), modified_before);
return failure(LogicEditorError::InvalidOperation, validation_error);
}
recordHistory(std::move(before));
return {true, LogicEditorError::None, {}, node_id};
}

LogicEditorResult LogicEditorService::addParallelWireBranch(
const std::string &logic_id,
const std::string &rung_id,
@@ -2544,13 +2712,13 @@ LogicEditorResult LogicEditorService::updateNodeConfig(
{
return failure(LogicEditorError::NodeNotFound, "未找到逻辑节点");
}
if (node->config.index() != config.index())
LogicNode candidate{node_id, config, true};
if (node->isCondition() != candidate.isCondition())
{
return failure(
LogicEditorError::UnsupportedNodeChange,
"节点创建后不能修改节点类别");
"不能把条件节点改成输出节点,或把输出节点改成条件节点");
}
LogicNode candidate{node_id, config, true};
std::string error;
if (!candidate.validate(&error))
{


+ 22
- 6
app/src/services/logic_editor_service.h 查看文件

@@ -108,6 +108,10 @@ public:
/** @brief 删除指定网络 */
LogicEditorResult removeRung(
const std::string &logic_id, const std::string &rung_id);
/** @brief 批量删除指定网络,失败时整体回滚 */
LogicEditorResult removeRungs(
const std::string &logic_id,
const std::vector<std::string> &rung_ids);
/** @brief 修改网络注释;注释长度和换行规则由领域校验约束 */
LogicEditorResult updateRungComment(
const std::string &logic_id,
@@ -118,7 +122,8 @@ public:
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/**
* @brief 按绝对条件列插入条件节点
* @param column 从 0 开始的条件列号;插入位置必须位于允许的条件区
@@ -127,7 +132,8 @@ public:
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/**
* @brief 在直属并联分支的指定视觉列插入条件节点
*
@@ -138,7 +144,8 @@ public:
const std::string &rung_id,
const std::string &branch_expression_id,
int column,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/** @brief 在网络条件末尾追加指定列宽的横线 */
LogicEditorResult appendWire(
const std::string &logic_id,
@@ -168,14 +175,16 @@ public:
const std::string &rung_id,
const std::string &wire_expression_id,
int column_offset,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/** @brief 用条件节点替换断路表达式中的一个指定列单元格 */
LogicEditorResult replaceGapColumnWithCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &gap_expression_id,
int column_offset,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/** @brief 用一格横线修复断路表达式中的指定网格 */
LogicEditorResult replaceGapColumnWithWire(
const std::string &logic_id,
@@ -190,7 +199,14 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config);
const LogicNodeConfig &config,
bool configured = false);
/** @brief 将当前网络的完整条件表达式与一个新条件并联 */
LogicEditorResult addParallelToWholeCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured = false);
/** @brief 将选中的横线表达式建立为并联旁路 */
LogicEditorResult addParallelWireBranch(
const std::string &logic_id,


+ 902
- 61
app/src/ui/logic_editor_widget.cpp
文件差异内容过多而无法显示
查看文件


+ 63
- 0
app/src/ui/logic_editor_widget.h 查看文件

@@ -9,6 +9,7 @@
#pragma once

#include "domain/control_logic_model.h"
#include "services/logic_command_service.h"
#include "services/logic_editor_service.h"
#include "services/software_logic_executor.h"

@@ -19,6 +20,11 @@
#include <vector>

class QGraphicsScene;
class QCompleter;
class QEvent;
class QLineEdit;
class QMouseEvent;
class QRubberBand;
class QResizeEvent;

/** 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交 */
@@ -35,6 +41,7 @@ public:
class VerticalConnectorItem;
class RungItem;
class EmptySlotItem;
class OutputSlotItem;

/** 创建梯形图画布并绑定编辑服务 */
explicit LogicEditorWidget(
@@ -95,8 +102,20 @@ signals:
void editorError(const QString &message);

protected:
/** 在任意网络区域开始鼠标框选 */
void mousePressEvent(QMouseEvent *event) override;
/** 更新鼠标框选区域 */
void mouseMoveEvent(QMouseEvent *event) override;
/** 完成鼠标框选并提交场景选择 */
void mouseReleaseEvent(QMouseEvent *event) override;
/** 在条件网格或输出槽双击时打开命令输入框 */
void mouseDoubleClickEvent(QMouseEvent *event) override;
/** 在窗口大小变化后重新布局梯形图场景 */
void resizeEvent(QResizeEvent *event) override;
/** 滚动画布时同步命令输入框位置 */
void scrollContentsBy(int dx, int dy) override;
/** 处理命令输入框的回车、取消和按键事件 */
bool eventFilter(QObject *watched, QEvent *event) override;

private:
/** 处理图形场景选择变化 */
@@ -123,9 +142,31 @@ private:
std::vector<std::pair<std::string, int>> selectedGapCells() const;
/** 判断当前是否选中了网络内的图元 */
bool hasSelectedRungItem() const;
/** 收集当前明确选中的网络图元 */
std::vector<std::string> selectedRungItemIds() const;
/** 打开指定目标位置的命令输入框 */
void beginCommandInput(
const LogicCommandTarget &target,
const QPointF &scene_center);
/** 提交当前命令输入;失败时保留输入框并标红 */
void commitCommandInput();
/** 关闭命令输入框并清理临时状态 */
void cancelCommandInput();
/** 根据网络、列和支路标识查找下一输入位置 */
bool beginNextCommandInput();
/** 按目标定位输入框的屏幕矩形 */
void positionCommandInput(const QPointF &scene_center);
/** 返回当前场景目标的中心位置 */
bool findCommandTargetCenter(
const LogicCommandTarget &target,
QPointF *scene_center) const;
/** 将成功命令选中并刷新属性面板 */
void selectCommandResult(const LogicCommandResult &result);

/** 梯形图编辑服务,不由控件拥有 */
LogicEditorService &editor_service_;
/** 命令语解析和结构化编辑适配服务 */
LogicCommandService command_service_;
/** 承载梯形图图元的场景 */
QGraphicsScene *scene_ = nullptr;
/** 当前显示的控制逻辑标识 */
@@ -140,4 +181,26 @@ private:
bool runtime_trace_enabled_ = false;
/** 是否允许编辑梯形图 */
bool editing_enabled_ = true;
/** 鼠标框选覆盖层 */
QRubberBand *selection_band_ = nullptr;
/** 框选起点(视口坐标) */
QPoint selection_origin_;
/** 框选时是否已超过拖拽阈值 */
bool selection_dragging_ = false;
/** 框选起始时的键盘修饰键 */
Qt::KeyboardModifiers selection_modifiers_ = Qt::NoModifier;
/** 命令输入框,仅在编辑态临时创建 */
QLineEdit *command_editor_ = nullptr;
/** 命令输入补全器 */
QCompleter *command_completer_ = nullptr;
/** 当前命令输入目标 */
LogicCommandTarget command_target_;
/** 是否沿当前命令序列继续输入 */
bool command_continuing_ = false;
/** OR/ORI 只增加支路,不推进主表达式列 */
bool command_keep_column_ = false;
/** 连续输入对应的当前网络 */
std::string command_current_rung_id_;
/** 编辑已有网络 OR 时保留的选择范围 */
std::vector<std::string> command_parallel_node_ids_;
};

+ 304
- 0
app/tests/logic_editor_service_tests.cpp 查看文件

@@ -1,6 +1,7 @@
#include "domain/project_storage.h"
#include "domain/project_limits.h"
#include "services/logic_editor_service.h"
#include "services/logic_command_service.h"
#include "services/editor_history.h"
#include "services/project_service.h"
#include "support/test_support.h"
@@ -130,6 +131,244 @@ const ConditionExpression *firstExpressionOfKind(
return nullptr;
}

void testLogicCommandParsing()
{
const LogicCommandParseResult load = LogicCommandService::parse("ldi m4000");
require(load.succeeded
&& load.command.opcode == LogicCommandOpcode::LoadInverse
&& std::get<ContactNodeConfig>(load.command.config).address.index() == 4000
&& std::get<ContactNodeConfig>(load.command.config).mode
== ContactMode::NormallyClosed,
"LDI must parse case-insensitively into a configured contact");

const LogicCommandParseResult add = LogicCommandService::parse(
"ADD D0 D1 D4000");
require(add.succeeded
&& add.command.opcode == LogicCommandOpcode::Add
&& std::get<ArithmeticNodeConfig>(add.command.config)
.destination.index() == 4000,
"ADD must parse three D operands through the shared address rules");
const LogicCommandParseResult compare = LogicCommandService::parse(
"LD<= D12 -3");
require(compare.succeeded
&& compare.command.opcode
== LogicCommandOpcode::CompareLessThanOrEqual
&& std::get<CompareNodeConfig>(compare.command.config).address.index() == 12
&& std::get<CompareNodeConfig>(compare.command.config).value == -3,
"LD comparison commands must parse a D address and signed constant");
const LogicCommandParseResult move_constant = LogicCommandService::parse(
"MOV -7 D20");
require(move_constant.succeeded
&& std::get<MoveNodeConfig>(move_constant.command.config).source.kind
== WordOperandKind::Constant
&& std::get<MoveNodeConfig>(move_constant.command.config).source.constant == -7,
"MOV must preserve constant sources when an existing output is reopened");
const LogicCommandParseResult add_constant = LogicCommandService::parse(
"ADD D0 1 D2");
require(add_constant.succeeded
&& std::get<ArithmeticNodeConfig>(add_constant.command.config).right.kind
== WordOperandKind::Constant,
"ADD must accept constant operands used by the instruction dialog");
require(!LogicCommandService::parse("LD D0").succeeded,
"contact commands must reject D addresses");
require(!LogicCommandService::parse("ADD D0 D1 D4001").succeeded,
"data commands must reject out-of-range D addresses");
require(!LogicCommandService::parse("TON T0").succeeded,
"unsupported commands must be rejected explicitly");
require(!LogicCommandService::parse("OUT M0 M1").succeeded,
"commands with the wrong operand count must be rejected");

TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService editor(project_service);
LogicCommandService commands(editor);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(editor, logic_id);
LogicCommandRequest and_without_load;
and_without_load.logicId = logic_id;
and_without_load.text = "AND M0";
and_without_load.target = {
LogicCommandTargetKind::EmptyColumn, rung_id, {}, 0};
require(!commands.execute(and_without_load).succeeded,
"AND without a preceding LD must be rejected");
}

void testContinuousLogicCommands()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService editor(project_service);
LogicCommandService commands(editor);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(editor, logic_id);

LogicCommandRequest request;
request.logicId = logic_id;
request.target = {LogicCommandTargetKind::EmptyColumn, rung_id, {}, 0};
request.text = "LD M1";
const LogicCommandResult load = commands.execute(request);
require(load.succeeded && load.rungId == rung_id,
"LD on an empty cell must create the first condition in that network");

request.continuing = true;
request.currentRungId = load.rungId;
request.text = "AND M2";
const LogicCommandResult and_result = commands.execute(request);
require(and_result.succeeded,
"AND must append to the current command-input network");
request.text = "OR M3";
const LogicCommandResult or_result = commands.execute(request);
require(or_result.succeeded,
"OR must parallel the complete accumulated condition: "
+ or_result.message);

const LadderRung *rung = editor.findRung(logic_id, rung_id);
const ConditionExpression *parallel = firstExpressionOfKind(
*rung->condition, ConditionExpressionKind::Parallel);
require(parallel != nullptr && parallel->children.size() == 2U
&& parallel->children.front().kind == ConditionExpressionKind::Series
&& conditionNodes(parallel->children.front()) == 2
&& conditionNodes(parallel->children.back()) == 1,
"LD M1, AND M2, OR M3 must form (M1 AND M2) OR M3");

request.text = "OUT M4";
const LogicCommandResult output = commands.execute(request);
rung = editor.findRung(logic_id, rung_id);
require(output.succeeded && rung != nullptr && rung->validateForRunning(),
"command-created contacts and output must be configured and runnable");

request.text = "LDI M5";
const LogicCommandResult next_load = commands.execute(request);
require(next_load.succeeded && next_load.rungId != rung_id
&& editor.findLogic(logic_id)->rungs.size() == 2U,
"a consecutive LD after an output must create the next network");
}

void testExistingNodeCommandReplacement()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService editor(project_service);
LogicCommandService commands(editor);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(editor, logic_id);
const LogicEditorResult contact_result = editor.appendCondition(
logic_id, rung_id, contact(10), true);
require(contact_result.succeeded,
"existing-node command fixture must create a contact");

LogicCommandRequest replace_contact;
replace_contact.logicId = logic_id;
replace_contact.text = "LDI M11";
replace_contact.target = {
LogicCommandTargetKind::ExistingNode,
rung_id,
contact_result.id,
0};
const LogicCommandResult replaced_contact = commands.execute(replace_contact);
const LogicNode *contact_node = editor.findNode(logic_id, contact_result.id);
require(replaced_contact.succeeded && contact_node != nullptr
&& std::holds_alternative<ContactNodeConfig>(contact_node->config)
&& std::get<ContactNodeConfig>(contact_node->config).address.index() == 11
&& std::get<ContactNodeConfig>(contact_node->config).mode
== ContactMode::NormallyClosed,
"an existing contact must accept a load command replacement");

const LogicEditorResult output_result = editor.setOutput(
logic_id,
rung_id,
CoilNodeConfig{
RegisterAddress{RegisterArea::M, 20}, CoilMode::Normal},
true);
require(output_result.succeeded,
"existing-node command fixture must create an output");
LogicCommandRequest replace_output;
replace_output.logicId = logic_id;
replace_output.text = "SET M21";
replace_output.target = {
LogicCommandTargetKind::ExistingNode,
rung_id,
output_result.id,
0};
const LogicCommandResult replaced_output = commands.execute(replace_output);
const LogicNode *output_node = editor.findNode(logic_id, output_result.id);
require(replaced_output.succeeded && output_node != nullptr
&& std::holds_alternative<CoilNodeConfig>(output_node->config)
&& std::get<CoilNodeConfig>(output_node->config).address.index() == 21
&& std::get<CoilNodeConfig>(output_node->config).mode == CoilMode::Set,
"an existing output must accept an output command replacement");

LogicCommandRequest wrong_type = replace_contact;
wrong_type.text = "OUT M30";
require(!commands.execute(wrong_type).succeeded,
"an existing condition must reject an output command replacement");
}

void testPositionedLogicCommandsAndAtomicFailures()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService editor(project_service);
LogicCommandService commands(editor);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string rung_id = makeEmptyRung(editor, logic_id);
const LogicEditorResult first = editor.appendCondition(
logic_id, rung_id, contact(10), true);
const LogicEditorResult second = editor.appendCondition(
logic_id, rung_id, contact(11), true);

LogicCommandRequest selected_or;
selected_or.logicId = logic_id;
selected_or.text = "ORI M12";
selected_or.target = {LogicCommandTargetKind::EmptyColumn, rung_id, {}, 2};
selected_or.parallelNodeIds = {first.id, second.id};
require(commands.execute(selected_or).succeeded,
"ORI on an existing network must use the selected continuous range");

const std::string wire_rung = makeEmptyRung(editor, logic_id);
const LogicEditorResult wire = editor.appendWire(logic_id, wire_rung, 3);
LogicCommandRequest wire_command;
wire_command.logicId = logic_id;
wire_command.text = "LDP M20";
wire_command.target = {
LogicCommandTargetKind::WireColumn, wire_rung, wire.id, 1};
const LogicCommandResult inserted = commands.execute(wire_command);
require(inserted.succeeded
&& editor.findNode(logic_id, inserted.id)->isConfigured(),
"a command on a wire cell must replace exactly that cell");

const std::string gap_rung = makeEmptyRung(editor, logic_id);
const LogicEditorResult gap_wire = editor.appendWire(logic_id, gap_rung, 2);
require(editor.disconnectWireCells(logic_id, gap_rung, {{gap_wire.id, 0}}).succeeded,
"gap command fixture must disconnect one wire cell");
const ConditionExpression *gap = firstExpressionOfKind(
*editor.findRung(logic_id, gap_rung)->condition,
ConditionExpressionKind::Gap);
LogicCommandRequest gap_command;
gap_command.logicId = logic_id;
gap_command.text = "LD M21";
gap_command.target = {
LogicCommandTargetKind::GapColumn, gap_rung, gap->id, 0};
require(commands.execute(gap_command).succeeded
&& gapColumns(*editor.findRung(logic_id, gap_rung)->condition) == 0,
"a command on a gap cell must repair that exact gap");

editor.clearHistory();
const std::size_t rung_count = editor.findLogic(logic_id)->rungs.size();
LogicCommandRequest invalid = gap_command;
invalid.text = "ADD D0 D1 D4001";
require(!commands.execute(invalid).succeeded
&& editor.findLogic(logic_id)->rungs.size() == rung_count
&& !editor.canUndo(),
"an invalid command must not change the project or undo history");

LogicCommandRequest wrong_position = gap_command;
wrong_position.text = "OUT M30";
require(!commands.execute(wrong_position).succeeded
&& !editor.canUndo(),
"an output command in the condition area must be rejected atomically");
}

void testEmptyLogicCreatesNetworksOnFirstEdit()
{
TestProjectStorage storage;
@@ -178,6 +417,33 @@ void testEmptyLogicCreatesNetworksOnFirstEdit()
"the first output must create a full-width explicit wire network");
}

void testBatchRungDeleteIsAtomic()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService service(project_service);
const std::string logic_id = service.ensureDefaultLogic().id;
const std::string first = service.addRung(logic_id).id;
const std::string second = service.addRung(logic_id).id;
const std::string third = service.addRung(logic_id).id;
require(service.findLogic(logic_id)->rungs.size() == 3U,
"the fixture must create three networks");

require(service.removeRungs(logic_id, {first, second}).succeeded
&& service.findLogic(logic_id)->rungs.size() == 1U
&& service.findRung(logic_id, third) != nullptr,
"a selected group of networks must be deleted together");
require(service.undo().succeeded
&& service.findLogic(logic_id)->rungs.size() == 3U,
"undo must restore a batch network deletion as one edit");

service.clearHistory();
require(!service.removeRungs(logic_id, {first, first}).succeeded
&& service.findLogic(logic_id)->rungs.size() == 3U
&& !service.canUndo(),
"duplicate network selection must fail without mutation or history");
}

void testAppendingWireMovesToNextNetworkAfterTenColumns()
{
TestProjectStorage storage;
@@ -203,6 +469,38 @@ void testAppendingWireMovesToNextNetworkAfterTenColumns()
"automatic network rollover must be undone as one edit");
}

void testAppendingWireReusesDeletedGapCells()
{
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 full_wire = service.appendWire(
logic_id, rung_id, ProjectLimits::kMaximumConditionColumns);
require(full_wire.succeeded, "gap reuse setup must create a full-width wire");
require(service.disconnectWires(logic_id, rung_id, {full_wire.id}).succeeded,
"deleting the full-width wire must create reusable gaps");

LogicEditorResult current = service.appendWire(logic_id, rung_id);
require(current.succeeded,
"the first appended wire must reuse the first deleted gap cell");
for (int index = 1; index < ProjectLimits::kMaximumConditionColumns; ++index)
{
current = service.insertWireAfter(logic_id, rung_id, current.id);
require(current.succeeded,
"inserting after a selected wire must reuse the next deleted gap cell");
require(service.findLogic(logic_id)->rungs.size() == 1U,
"reusing a gap must not create a new network");
}
const LadderRung *rung = service.findRung(logic_id, rung_id);
require(rung != nullptr && rung->condition.has_value()
&& conditionColumns(*rung->condition)
== ProjectLimits::kMaximumConditionColumns
&& gapColumns(*rung->condition) == 0,
"all deleted gap cells must be restored as horizontal wires");
}

void testStructuredEditingAndNormalization()
{
TestProjectStorage storage;
@@ -1464,8 +1762,14 @@ int main()
{
try
{
testLogicCommandParsing();
testContinuousLogicCommands();
testExistingNodeCommandReplacement();
testPositionedLogicCommandsAndAtomicFailures();
testEmptyLogicCreatesNetworksOnFirstEdit();
testBatchRungDeleteIsAtomic();
testAppendingWireMovesToNextNetworkAfterTenColumns();
testAppendingWireReusesDeletedGapCells();
testStructuredEditingAndNormalization();
testRangeParallelInsertion();
testParallelBranchGridInsertion();


+ 4
- 2
app/tests/pri/test_layers.pri 查看文件

@@ -91,11 +91,13 @@ SERVICE_HMI_HEADERS = \
../src/services/hmi_runtime_service.h

SERVICE_LOGIC_SOURCES = \
../src/services/logic_editor_service.cpp
../src/services/logic_editor_service.cpp \
../src/services/logic_command_service.cpp

SERVICE_LOGIC_HEADERS = \
../src/services/editor_history.h \
../src/services/logic_editor_service.h
../src/services/logic_editor_service.h \
../src/services/logic_command_service.h

SERVICE_OFFLINE_SOURCES = \
../src/services/software_logic_executor.cpp \


正在加载...
取消
保存