| @@ -164,7 +164,8 @@ void normalizeExpression(ConditionExpression *expression) | |||
| { | |||
| if (expression == nullptr | |||
| || expression->kind == ConditionExpressionKind::Node | |||
| || expression->kind == ConditionExpressionKind::Wire) | |||
| || expression->kind == ConditionExpressionKind::Wire | |||
| || expression->kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return; | |||
| } | |||
| @@ -243,6 +244,7 @@ bool validateConditionExpression( | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| if (!expression.node.has_value() || expression.wire.has_value() | |||
| || expression.gap.has_value() | |||
| || !expression.children.empty() || !expression.node->isCondition()) | |||
| { | |||
| setError(error, "条件叶节点必须包含一个条件节点且不能包含子表达式"); | |||
| @@ -255,6 +257,7 @@ bool validateConditionExpression( | |||
| if (expression.kind == ConditionExpressionKind::Wire) | |||
| { | |||
| if (expression.node.has_value() || !expression.wire.has_value() | |||
| || expression.gap.has_value() | |||
| || !expression.children.empty()) | |||
| { | |||
| setError(error, "横线叶节点必须包含横线配置且不能包含逻辑节点或子表达式"); | |||
| @@ -268,6 +271,22 @@ bool validateConditionExpression( | |||
| *rows = 1; | |||
| return true; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| if (expression.node.has_value() || expression.wire.has_value() | |||
| || !expression.gap.has_value() || !expression.children.empty()) | |||
| { | |||
| setError(error, "断路叶节点必须包含断路配置且不能包含逻辑节点或子表达式"); | |||
| return false; | |||
| } | |||
| if (!expression.gap->validate(error)) | |||
| { | |||
| return false; | |||
| } | |||
| *columns = expression.gap->columnSpan; | |||
| *rows = 1; | |||
| return true; | |||
| } | |||
| if (expression.kind != ConditionExpressionKind::Series | |||
| && expression.kind != ConditionExpressionKind::Parallel) | |||
| { | |||
| @@ -275,6 +294,7 @@ bool validateConditionExpression( | |||
| return false; | |||
| } | |||
| if (expression.node.has_value() || expression.wire.has_value() | |||
| || expression.gap.has_value() | |||
| || expression.children.size() < 2U) | |||
| { | |||
| setError(error, "串联和并联表达式至少需要两个子表达式"); | |||
| @@ -288,6 +308,7 @@ bool validateConditionExpression( | |||
| int total_columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; | |||
| int total_rows = expression.kind == ConditionExpressionKind::Parallel ? 0 : 1; | |||
| int parallel_columns = -1; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| if (child.kind == expression.kind) | |||
| @@ -316,6 +337,12 @@ bool validateConditionExpression( | |||
| } | |||
| else | |||
| { | |||
| if (parallel_columns >= 0 && child_columns != parallel_columns) | |||
| { | |||
| setError(error, "并联各支路必须使用显式横线或断路补齐到相同列宽"); | |||
| return false; | |||
| } | |||
| parallel_columns = child_columns; | |||
| total_columns = std::max(total_columns, child_columns); | |||
| if (total_rows > ProjectLimits::kMaximumLogicRows - child_rows) | |||
| { | |||
| @@ -336,6 +363,30 @@ bool validateConditionExpression( | |||
| return true; | |||
| } | |||
| int conditionColumnCount(const ConditionExpression &expression) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| return 1; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Wire) | |||
| { | |||
| return expression.wire->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return expression.gap->columnSpan; | |||
| } | |||
| int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| const int child_columns = conditionColumnCount(child); | |||
| columns = expression.kind == ConditionExpressionKind::Series | |||
| ? columns + child_columns : std::max(columns, child_columns); | |||
| } | |||
| return columns; | |||
| } | |||
| } // namespace | |||
| bool WordOperand::validate(std::string *error) const | |||
| @@ -481,6 +532,16 @@ bool WireSegment::validate(std::string *error) const | |||
| return true; | |||
| } | |||
| bool GapSegment::validate(std::string *error) const | |||
| { | |||
| if (columnSpan < kMinimumColumnSpan || columnSpan > kMaximumColumnSpan) | |||
| { | |||
| setError(error, "断路跨度必须在 1~10 列范围内"); | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| ConditionExpression ConditionExpression::fromWire( | |||
| std::string expression_id, int column_span) | |||
| { | |||
| @@ -491,6 +552,16 @@ ConditionExpression ConditionExpression::fromWire( | |||
| return expression; | |||
| } | |||
| ConditionExpression ConditionExpression::fromGap( | |||
| std::string expression_id, int column_span) | |||
| { | |||
| ConditionExpression expression; | |||
| expression.id = std::move(expression_id); | |||
| expression.kind = ConditionExpressionKind::Gap; | |||
| expression.gap = GapSegment{column_span}; | |||
| return expression; | |||
| } | |||
| bool ConditionExpression::validate(std::string *error) const | |||
| { | |||
| std::size_t node_count = 0U; | |||
| @@ -519,6 +590,11 @@ bool ConditionExpression::validateForRunning(std::string *error) const | |||
| { | |||
| return true; | |||
| } | |||
| if (kind == ConditionExpressionKind::Gap) | |||
| { | |||
| setError(error, "梯形图条件区存在未连接的空白网格"); | |||
| return false; | |||
| } | |||
| for (const ConditionExpression &child : children) | |||
| { | |||
| if (!child.validateForRunning(error)) | |||
| @@ -539,6 +615,7 @@ void normalizeConditionExpression(std::optional<ConditionExpression> *expression | |||
| normalizeExpression(&expression->value()); | |||
| if (expression->value().kind != ConditionExpressionKind::Node | |||
| && expression->value().kind != ConditionExpressionKind::Wire | |||
| && expression->value().kind != ConditionExpressionKind::Gap | |||
| && expression->value().children.empty()) | |||
| { | |||
| expression->reset(); | |||
| @@ -552,6 +629,10 @@ const LogicNode *findConditionNode( | |||
| { | |||
| return nullptr; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return nullptr; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| return expression.node->id == node_id ? &*expression.node : nullptr; | |||
| @@ -614,6 +695,10 @@ void collectConditionNodes( | |||
| { | |||
| return; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return; | |||
| } | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| collectConditionNodes(child, nodes); | |||
| @@ -707,6 +792,14 @@ bool LadderRung::validateStructure(std::string *error) const | |||
| node_ids.push_back(node->id); | |||
| } | |||
| } | |||
| if (output.has_value() | |||
| && (!condition.has_value() | |||
| || conditionColumnCount(*condition) | |||
| != ProjectLimits::kMaximumConditionColumns)) | |||
| { | |||
| setError(error, "带输出的梯形图网络必须用条件、横线或断路显式占满前 10 列"); | |||
| return false; | |||
| } | |||
| if (output.has_value()) | |||
| { | |||
| if (!output->validate(error) || !output->isOutput()) | |||
| @@ -152,6 +152,7 @@ enum class ConditionExpressionKind | |||
| { | |||
| Node, // 一个实际的条件节点 | |||
| Wire, // 一段横线 | |||
| Gap, // 一段明确断开的空白网格 | |||
| Series, // 多个条件串联,必须全部满足 | |||
| Parallel // 多个条件并联,满足任意一条即可 | |||
| }; | |||
| @@ -168,6 +169,18 @@ struct WireSegment | |||
| bool validate(std::string *error = nullptr) const; | |||
| }; | |||
| // 条件区中的持久化断路;columnSpan 表示连续空白网格列数 | |||
| struct GapSegment | |||
| { | |||
| static constexpr int kMinimumColumnSpan = 1; | |||
| static constexpr int kMaximumColumnSpan = | |||
| ProjectLimits::kMaximumConditionColumns; | |||
| int columnSpan = 1; // 断路占用的网格列数 | |||
| bool validate(std::string *error = nullptr) const; | |||
| }; | |||
| // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 | |||
| struct ConditionExpression | |||
| { | |||
| @@ -175,12 +188,15 @@ struct ConditionExpression | |||
| ConditionExpressionKind kind = ConditionExpressionKind::Node; // 表达式类型 | |||
| std::optional<LogicNode> node; // kind 为 Node 时保存的节点 | |||
| std::optional<WireSegment> wire; // kind 为 Wire 时保存的横线 | |||
| std::optional<GapSegment> gap; // kind 为 Gap 时保存的断路 | |||
| std::vector<ConditionExpression> children; // 串联或并联的子表达式 | |||
| // 把一个逻辑节点包装成条件表达式 | |||
| static ConditionExpression fromNode(LogicNode node); | |||
| // 创建一段指定列数的横线表达式 | |||
| static ConditionExpression fromWire(std::string id, int column_span = 1); | |||
| // 创建一段指定列数的断路表达式 | |||
| static ConditionExpression fromGap(std::string id, int column_span = 1); | |||
| // 编辑态校验允许空配置节点,但必须保持树结构合法 | |||
| bool validate(std::string *error = nullptr) const; | |||
| // 运行态校验额外要求每个节点都已经配置完成 | |||
| @@ -214,13 +230,13 @@ void collectConditionNodes( | |||
| void collectConditionExpressionIds( | |||
| const ConditionExpression &expression, std::vector<std::string> *ids); | |||
| // 条件表达式为空时表示恒真网络,输出指令固定在最右侧 | |||
| // 输出指令固定在第 11 列;有输出时前 10 列必须由显式条件、横线或断路占满 | |||
| struct LadderRung | |||
| { | |||
| std::string id; // 网络唯一 ID | |||
| std::string name; // 网络名称 | |||
| std::string comment; // 网络注释 | |||
| std::optional<ConditionExpression> condition; // 条件表达式,没有时表示恒真 | |||
| std::optional<ConditionExpression> condition; // 条件区表达式;空值只用于空网络草稿 | |||
| std::optional<LogicNode> output; // 网络右侧的输出指令 | |||
| // 编辑态允许没有条件或输出,便于逐步搭建网络 | |||
| @@ -1308,6 +1308,8 @@ QString expressionKindText(ConditionExpressionKind kind) | |||
| return QStringLiteral("node"); | |||
| case ConditionExpressionKind::Wire: | |||
| return QStringLiteral("wire"); | |||
| case ConditionExpressionKind::Gap: | |||
| return QStringLiteral("gap"); | |||
| case ConditionExpressionKind::Series: | |||
| return QStringLiteral("series"); | |||
| case ConditionExpressionKind::Parallel: | |||
| @@ -1329,6 +1331,10 @@ QJsonObject serializeConditionExpression(const ConditionExpression &expression) | |||
| { | |||
| object.insert(QStringLiteral("columnSpan"), expression.wire->columnSpan); | |||
| } | |||
| else if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| object.insert(QStringLiteral("columnSpan"), expression.gap->columnSpan); | |||
| } | |||
| else | |||
| { | |||
| QJsonArray children; | |||
| @@ -1404,11 +1410,29 @@ bool parseConditionExpression( | |||
| expression->wire = WireSegment{column_span}; | |||
| return true; | |||
| } | |||
| if (kind == "gap") | |||
| { | |||
| int column_span = 0; | |||
| if (!readInt( | |||
| object, | |||
| "columnSpan", | |||
| context, | |||
| GapSegment::kMinimumColumnSpan, | |||
| GapSegment::kMaximumColumnSpan, | |||
| &column_span, | |||
| state)) | |||
| { | |||
| return false; | |||
| } | |||
| expression->kind = ConditionExpressionKind::Gap; | |||
| expression->gap = GapSegment{column_span}; | |||
| return true; | |||
| } | |||
| if (kind != "series" && kind != "parallel") | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| context + ".kind 必须是 node、wire、series 或 parallel"); | |||
| context + ".kind 必须是 node、wire、gap、series 或 parallel"); | |||
| } | |||
| QJsonArray children; | |||
| if (!readArray( | |||
| @@ -1875,7 +1899,6 @@ ProjectLoadResult JsonProjectStorage::load(const std::string &file_path) | |||
| { | |||
| return {false, {}, state.error, state.message}; | |||
| } | |||
| // JSON 字段合法不代表业务关系合法,还需执行领域层整体校验 | |||
| std::string validation_error; | |||
| if (!project.validate(&validation_error)) | |||
| @@ -2,7 +2,7 @@ | |||
| #include "domain/project_storage.h" | |||
| // 使用严格 1.0 JSON 文件保存和加载工程;加载成功后才替换调用方工程 | |||
| // 严格读写当前 1.0 JSON 工程格式 | |||
| class JsonProjectStorage final : public ProjectStorage | |||
| { | |||
| public: | |||
| @@ -29,6 +29,8 @@ | |||
| // 程序从这里开始,argc 和 argv 保存用户启动程序时附带的命令行参数 | |||
| int main(int argc, char *argv[]) | |||
| { | |||
| // Windows 默认会给对话框添加上下文帮助按钮,应用内统一隐藏 | |||
| QApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); | |||
| // 创建整个 Qt 应用,后面的窗口、按钮和消息循环都要依靠它 | |||
| QApplication application(argc, argv); | |||
| // 设置程序名称,Qt 的窗口信息和配置中会用到这个名字 | |||
| @@ -147,6 +147,10 @@ int expressionColumns(const ConditionExpression &expression) | |||
| { | |||
| return expression.wire->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return expression.gap->columnSpan; | |||
| } | |||
| int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| @@ -369,7 +373,8 @@ bool addParallelForWireCellRange( | |||
| return true; | |||
| } | |||
| if (expression->kind == ConditionExpressionKind::Node | |||
| || expression->kind == ConditionExpressionKind::Wire) | |||
| || expression->kind == ConditionExpressionKind::Wire | |||
| || expression->kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return false; | |||
| } | |||
| @@ -405,7 +410,8 @@ bool findWireCellLocation( | |||
| WireCellLocation *location) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Node | |||
| || expression.kind == ConditionExpressionKind::Wire) | |||
| || expression.kind == ConditionExpressionKind::Wire | |||
| || expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return false; | |||
| } | |||
| @@ -517,7 +523,8 @@ bool removeExpressionRecursive( | |||
| { | |||
| if (expression == nullptr | |||
| || expression->kind == ConditionExpressionKind::Node | |||
| || expression->kind == ConditionExpressionKind::Wire) | |||
| || expression->kind == ConditionExpressionKind::Wire | |||
| || expression->kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return false; | |||
| } | |||
| @@ -553,7 +560,8 @@ std::optional<int> selectedExpressionColumns( | |||
| return expressionColumns(expression); | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Node | |||
| || expression.kind == ConditionExpressionKind::Wire) | |||
| || expression.kind == ConditionExpressionKind::Wire | |||
| || expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return std::nullopt; | |||
| } | |||
| @@ -620,6 +628,52 @@ LadderRung *findEditableRung( | |||
| return rung == logic->rungs.end() ? nullptr : &*rung; | |||
| } | |||
| bool trimTrailingGridColumns(ConditionExpression *expression, int *columns) | |||
| { | |||
| if (expression == nullptr || columns == nullptr || *columns <= 0) | |||
| { | |||
| return true; | |||
| } | |||
| auto trim_leaf = [columns](int *span) | |||
| { | |||
| const int removed = std::min(*span, *columns); | |||
| *span -= removed; | |||
| *columns -= removed; | |||
| }; | |||
| if (expression->kind == ConditionExpressionKind::Wire) | |||
| { | |||
| trim_leaf(&expression->wire->columnSpan); | |||
| return *columns == 0 && expression->wire->columnSpan > 0; | |||
| } | |||
| if (expression->kind == ConditionExpressionKind::Gap) | |||
| { | |||
| trim_leaf(&expression->gap->columnSpan); | |||
| return *columns == 0 && expression->gap->columnSpan > 0; | |||
| } | |||
| if (expression->kind != ConditionExpressionKind::Series) | |||
| { | |||
| return false; | |||
| } | |||
| while (*columns > 0 && !expression->children.empty()) | |||
| { | |||
| ConditionExpression &last = expression->children.back(); | |||
| int *span = last.kind == ConditionExpressionKind::Wire | |||
| ? &last.wire->columnSpan | |||
| : last.kind == ConditionExpressionKind::Gap | |||
| ? &last.gap->columnSpan : nullptr; | |||
| if (span == nullptr) | |||
| { | |||
| return false; | |||
| } | |||
| trim_leaf(span); | |||
| if (*span == 0) | |||
| { | |||
| expression->children.pop_back(); | |||
| } | |||
| } | |||
| return *columns == 0 && !expression->children.empty(); | |||
| } | |||
| } // namespace | |||
| LogicEditorService::LogicEditorService(ProjectService &project_service) | |||
| @@ -705,6 +759,7 @@ bool LogicEditorService::expressionsEqual( | |||
| if (left.id != right.id || left.kind != right.kind | |||
| || left.node.has_value() != right.node.has_value() | |||
| || left.wire.has_value() != right.wire.has_value() | |||
| || left.gap.has_value() != right.gap.has_value() | |||
| || left.children.size() != right.children.size()) | |||
| { | |||
| return false; | |||
| @@ -718,6 +773,11 @@ bool LogicEditorService::expressionsEqual( | |||
| { | |||
| return false; | |||
| } | |||
| if (left.gap.has_value() | |||
| && left.gap->columnSpan != right.gap->columnSpan) | |||
| { | |||
| return false; | |||
| } | |||
| for (std::size_t index = 0; index < left.children.size(); ++index) | |||
| { | |||
| if (!expressionsEqual(left.children[index], right.children[index])) | |||
| @@ -1194,7 +1254,69 @@ LogicEditorResult LogicEditorService::appendCondition( | |||
| std::nullopt}); | |||
| } | |||
| LadderRung *rung = findEditableRung(project, logic_id, target_rung_id); | |||
| if (!rung->condition.has_value()) | |||
| if (rung->output.has_value() && rung->condition.has_value()) | |||
| { | |||
| ConditionExpression *span = &*rung->condition; | |||
| std::vector<ConditionExpression> *siblings = nullptr; | |||
| std::size_t span_index = 0U; | |||
| if (span->kind == ConditionExpressionKind::Series) | |||
| { | |||
| siblings = &span->children; | |||
| span_index = siblings->size(); | |||
| while (span_index > 0U) | |||
| { | |||
| const ConditionExpressionKind kind = | |||
| siblings->at(span_index - 1U).kind; | |||
| if (kind != ConditionExpressionKind::Wire | |||
| && kind != ConditionExpressionKind::Gap) | |||
| { | |||
| break; | |||
| } | |||
| --span_index; | |||
| } | |||
| span = span_index < siblings->size() | |||
| ? &siblings->at(span_index) : nullptr; | |||
| } | |||
| if (span == nullptr | |||
| || (span->kind != ConditionExpressionKind::Wire | |||
| && span->kind != ConditionExpressionKind::Gap)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "输出网络的 10 列条件区已经没有可替换的横线或断路"); | |||
| } | |||
| const bool wire_span = span->kind == ConditionExpressionKind::Wire; | |||
| const int span_columns = wire_span | |||
| ? span->wire->columnSpan : span->gap->columnSpan; | |||
| const std::string span_id = span->id; | |||
| if (span_columns == 1) | |||
| { | |||
| *span = std::move(leaf); | |||
| } | |||
| else if (siblings != nullptr) | |||
| { | |||
| *span = std::move(leaf); | |||
| siblings->insert( | |||
| siblings->begin() + static_cast<std::ptrdiff_t>(span_index + 1U), | |||
| wire_span | |||
| ? ConditionExpression::fromWire(span_id, span_columns - 1) | |||
| : ConditionExpression::fromGap(span_id, span_columns - 1)); | |||
| } | |||
| else | |||
| { | |||
| const std::string container_id = makeUniqueExpressionId(*editable_logic); | |||
| ConditionExpression remaining = wire_span | |||
| ? ConditionExpression::fromWire(span_id, span_columns - 1) | |||
| : ConditionExpression::fromGap(span_id, span_columns - 1); | |||
| *span = makeContainer( | |||
| container_id, | |||
| ConditionExpressionKind::Series, | |||
| std::move(leaf), | |||
| std::move(remaining)); | |||
| } | |||
| } | |||
| else if (!rung->condition.has_value()) | |||
| { | |||
| rung->condition = std::move(leaf); | |||
| } | |||
| @@ -1205,14 +1327,15 @@ LogicEditorResult LogicEditorService::appendCondition( | |||
| else | |||
| { | |||
| rung->condition = makeContainer( | |||
| makeUniqueExpressionId(*logic), | |||
| makeUniqueExpressionId(*editable_logic), | |||
| ConditionExpressionKind::Series, | |||
| std::move(*rung->condition), | |||
| std::move(leaf)); | |||
| } | |||
| std::string validation_error; | |||
| // 编辑服务修改后立即做整条网络校验;失败就恢复包括脏标记在内的完整快照 | |||
| if (!rung->validate(&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); | |||
| @@ -1259,6 +1382,9 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||
| 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); | |||
| if (leading_wire_columns > 0) | |||
| @@ -1308,7 +1434,8 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||
| std::string validation_error; | |||
| // 原子提交要求候选表达式整体通过校验,否则撤销本次所有局部拼接 | |||
| if (!rung->validate(&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); | |||
| @@ -1369,8 +1496,12 @@ LogicEditorResult LogicEditorService::insertConditionInBranchAtColumn( | |||
| makeNode(node_id, config)); | |||
| 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_service_.editProject(), logic_id, rung_id); | |||
| project, logic_id, rung_id); | |||
| ConditionExpression *editable_branch = findConditionExpression( | |||
| *rung->condition, branch_expression_id); | |||
| @@ -1399,7 +1530,8 @@ LogicEditorResult LogicEditorService::insertConditionInBranchAtColumn( | |||
| } | |||
| std::string validation_error; | |||
| if (!rung->validate(&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); | |||
| @@ -1514,6 +1646,9 @@ LogicEditorResult LogicEditorService::insertConditionAfter( | |||
| 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 *target = findConditionExpression(*rung->condition, target_node_id); | |||
| ConditionExpression *parent = findParentExpression(*rung->condition, target_node_id); | |||
| @@ -1527,16 +1662,20 @@ LogicEditorResult LogicEditorService::insertConditionAfter( | |||
| }); | |||
| const auto insertion_position = target_iterator + 1; | |||
| if (insertion_position != parent->children.end() | |||
| && insertion_position->kind == ConditionExpressionKind::Wire) | |||
| { | |||
| // 横线代表已占用的网格位置,连续插入时先消费一格而不是扩展网络 | |||
| if (insertion_position->wire->columnSpan == 1) | |||
| && (insertion_position->kind == ConditionExpressionKind::Wire | |||
| || insertion_position->kind == ConditionExpressionKind::Gap)) | |||
| { | |||
| int &span = insertion_position->kind == ConditionExpressionKind::Wire | |||
| ? insertion_position->wire->columnSpan | |||
| : insertion_position->gap->columnSpan; | |||
| // 连续插入优先消费后方显式横线或断路的一格 | |||
| if (span == 1) | |||
| { | |||
| *insertion_position = std::move(leaf); | |||
| } | |||
| else | |||
| { | |||
| --insertion_position->wire->columnSpan; | |||
| --span; | |||
| parent->children.insert(insertion_position, std::move(leaf)); | |||
| } | |||
| } | |||
| @@ -1549,13 +1688,14 @@ LogicEditorResult LogicEditorService::insertConditionAfter( | |||
| { | |||
| ConditionExpression original = std::move(*target); | |||
| *target = makeContainer( | |||
| makeUniqueExpressionId(*logic), | |||
| makeUniqueExpressionId(*editable_logic), | |||
| ConditionExpressionKind::Series, | |||
| std::move(original), | |||
| std::move(leaf)); | |||
| } | |||
| std::string validation_error; | |||
| if (!rung->validate(&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); | |||
| @@ -1592,8 +1732,12 @@ LogicEditorResult LogicEditorService::insertWireAfter( | |||
| ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span); | |||
| 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_service_.editProject(), logic_id, rung_id); | |||
| project, logic_id, rung_id); | |||
| ConditionExpression *editable_target = findConditionExpression( | |||
| *rung->condition, target_expression_id); | |||
| ConditionExpression *parent = findParentExpression( | |||
| @@ -1612,13 +1756,14 @@ LogicEditorResult LogicEditorService::insertWireAfter( | |||
| { | |||
| ConditionExpression original = std::move(*editable_target); | |||
| *editable_target = makeContainer( | |||
| makeUniqueExpressionId(*logic), | |||
| makeUniqueExpressionId(*editable_logic), | |||
| ConditionExpressionKind::Series, | |||
| std::move(original), | |||
| std::move(leaf)); | |||
| } | |||
| std::string validation_error; | |||
| if (!rung->validate(&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); | |||
| @@ -1645,6 +1790,11 @@ LogicEditorResult LogicEditorService::replaceWireWithCondition( | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "横线只能替换为条件节点"); | |||
| } | |||
| if (wire->wire->columnSpan > 1) | |||
| { | |||
| return replaceWireColumnWithCondition( | |||
| logic_id, rung_id, wire_expression_id, 0, config); | |||
| } | |||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| @@ -1715,12 +1865,85 @@ LogicEditorResult LogicEditorService::replaceWireColumnWithCondition( | |||
| 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 *editable = findConditionExpression( | |||
| *rung->condition, wire_expression_id); | |||
| *editable = std::move(replacement); | |||
| normalizeConditionExpression(&rung->condition); | |||
| std::string validation_error; | |||
| if (!repairExplicitLayout(*editable_logic, *rung, &validation_error) | |||
| || !rung->validate(&validation_error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidNode, validation_error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::replaceGapColumnWithCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &gap_expression_id, | |||
| int column_offset, | |||
| const LogicNodeConfig &config) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| const ConditionExpression *gap = findExpression( | |||
| logic_id, rung_id, gap_expression_id); | |||
| if (logic == nullptr || gap == nullptr | |||
| || gap->kind != ConditionExpressionKind::Gap) | |||
| { | |||
| return failure(LogicEditorError::ExpressionNotFound, "未找到要填充的断路网格"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "断路网格只能填入条件节点"); | |||
| } | |||
| if (column_offset < 0 || column_offset >= gap->gap->columnSpan) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "断路网格偏移超出有效范围"); | |||
| } | |||
| const int leading_columns = column_offset; | |||
| const int trailing_columns = gap->gap->columnSpan - column_offset - 1; | |||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | |||
| ConditionExpression replacement; | |||
| if (leading_columns == 0 && trailing_columns == 0) | |||
| { | |||
| replacement = ConditionExpression::fromNode(makeNode(node_id, config)); | |||
| } | |||
| else | |||
| { | |||
| replacement.id = makeUniqueExpressionId(*logic); | |||
| replacement.kind = ConditionExpressionKind::Series; | |||
| if (leading_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromGap( | |||
| gap_expression_id, leading_columns)); | |||
| } | |||
| replacement.children.push_back( | |||
| ConditionExpression::fromNode(makeNode(node_id, config))); | |||
| if (trailing_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromGap( | |||
| leading_columns > 0 ? makeUniqueGapId(*logic) : gap_expression_id, | |||
| trailing_columns)); | |||
| } | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| LadderRung *rung = findEditableRung( | |||
| project_service_.editProject(), logic_id, rung_id); | |||
| ConditionExpression *editable = findConditionExpression( | |||
| *rung->condition, gap_expression_id); | |||
| *editable = std::move(replacement); | |||
| normalizeConditionExpression(&rung->condition); | |||
| std::string validation_error; | |||
| if (!rung->validate(&validation_error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| @@ -1730,6 +1953,69 @@ LogicEditorResult LogicEditorService::replaceWireColumnWithCondition( | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::replaceGapColumnWithWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &gap_expression_id, | |||
| int column_offset) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| const ConditionExpression *gap = findExpression( | |||
| logic_id, rung_id, gap_expression_id); | |||
| if (logic == nullptr || gap == nullptr | |||
| || gap->kind != ConditionExpressionKind::Gap) | |||
| { | |||
| return failure(LogicEditorError::ExpressionNotFound, "未找到要修复的断路网格"); | |||
| } | |||
| if (column_offset < 0 || column_offset >= gap->gap->columnSpan) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "断路网格偏移超出有效范围"); | |||
| } | |||
| const int leading_columns = column_offset; | |||
| const int trailing_columns = gap->gap->columnSpan - column_offset - 1; | |||
| const std::string wire_id = makeUniqueWireId(*logic); | |||
| ConditionExpression replacement; | |||
| if (leading_columns == 0 && trailing_columns == 0) | |||
| { | |||
| replacement = ConditionExpression::fromWire(wire_id, 1); | |||
| } | |||
| else | |||
| { | |||
| replacement.id = makeUniqueExpressionId(*logic); | |||
| replacement.kind = ConditionExpressionKind::Series; | |||
| if (leading_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromGap( | |||
| gap_expression_id, leading_columns)); | |||
| } | |||
| replacement.children.push_back( | |||
| ConditionExpression::fromWire(wire_id, 1)); | |||
| if (trailing_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromGap( | |||
| leading_columns > 0 ? makeUniqueGapId(*logic) : gap_expression_id, | |||
| trailing_columns)); | |||
| } | |||
| } | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| LadderRung *rung = findEditableRung( | |||
| project_service_.editProject(), logic_id, rung_id); | |||
| ConditionExpression *editable = findConditionExpression( | |||
| *rung->condition, gap_expression_id); | |||
| *editable = std::move(replacement); | |||
| 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, {}, wire_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::addParallelBranch( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| @@ -1757,8 +2043,13 @@ LogicEditorResult LogicEditorService::addParallelBranch( | |||
| LogicEditorError::InvalidOperation, | |||
| "并联选择中存在重复节点"); | |||
| } | |||
| const NodeIdSet available_ids = conditionLeafIds(*existing_rung->condition); | |||
| if (!isSubset(selected_ids, available_ids)) | |||
| const bool all_conditions = std::all_of( | |||
| selected_ids.cbegin(), selected_ids.cend(), | |||
| [&existing_rung](const std::string &node_id) | |||
| { | |||
| return findConditionNode(*existing_rung->condition, node_id) != nullptr; | |||
| }); | |||
| if (!all_conditions) | |||
| { | |||
| return failure( | |||
| LogicEditorError::NodeNotFound, | |||
| @@ -1775,6 +2066,9 @@ LogicEditorResult LogicEditorService::addParallelBranch( | |||
| 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); | |||
| if (!addParallelForSelection( | |||
| &*rung->condition, | |||
| @@ -1789,7 +2083,8 @@ LogicEditorResult LogicEditorService::addParallelBranch( | |||
| "并联选择必须是一个连续的逻辑范围"); | |||
| } | |||
| std::string validation_error; | |||
| if (!rung->validate(&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); | |||
| @@ -2197,6 +2492,7 @@ LogicEditorResult LogicEditorService::setOutput( | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| 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(), | |||
| @@ -2210,7 +2506,16 @@ LogicEditorResult LogicEditorService::setOutput( | |||
| std::nullopt, | |||
| std::nullopt}); | |||
| } | |||
| findEditableRung(project, logic_id, target_rung_id)->output = std::move(node); | |||
| LadderRung *editable_rung = findEditableRung( | |||
| project, logic_id, target_rung_id); | |||
| editable_rung->output = std::move(node); | |||
| if (!repairExplicitLayout(*editable_logic, *editable_rung)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "输出前的显式横线无法在 10 列条件区内完成布局"); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| @@ -2353,6 +2658,10 @@ LogicEditorResult LogicEditorService::pasteConditionNodes( | |||
| current = replaceWireColumnWithCondition( | |||
| logic_id, rung_id, target.expressionId, target.column, source.config); | |||
| break; | |||
| case LogicConditionPasteTargetKind::ReplaceGapColumn: | |||
| current = replaceGapColumnWithCondition( | |||
| logic_id, rung_id, target.expressionId, target.column, source.config); | |||
| break; | |||
| case LogicConditionPasteTargetKind::Append: | |||
| default: | |||
| current = appendCondition(logic_id, rung_id, source.config); | |||
| @@ -2530,6 +2839,11 @@ LogicEditorResult LogicEditorService::pasteRung( | |||
| clone.id = unique_wire_id(); | |||
| clone.wire = source_expression.wire; | |||
| } | |||
| else if (source_expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| clone.id = unique_expression_id(); | |||
| clone.gap = source_expression.gap; | |||
| } | |||
| else | |||
| { | |||
| clone.id = unique_expression_id(); | |||
| @@ -2615,18 +2929,16 @@ LogicEditorResult LogicEditorService::removeNodes( | |||
| { | |||
| for (const std::string &node_id : node_ids) | |||
| { | |||
| if (rung.condition->kind == ConditionExpressionKind::Node | |||
| && rung.condition->id == node_id) | |||
| ConditionExpression *expression = findConditionExpression( | |||
| *rung.condition, node_id); | |||
| if (expression != nullptr | |||
| && expression->kind == ConditionExpressionKind::Node) | |||
| { | |||
| rung.condition.reset(); | |||
| break; | |||
| *expression = ConditionExpression::fromGap( | |||
| makeUniqueGapId(*editable_logic), 1); | |||
| } | |||
| removeExpressionRecursive(&*rung.condition, node_id); | |||
| } | |||
| if (rung.condition.has_value()) | |||
| { | |||
| normalizeConditionExpression(&rung.condition); | |||
| } | |||
| normalizeConditionExpression(&rung.condition); | |||
| } | |||
| } | |||
| for (const LadderRung &rung : editable_logic->rungs) | |||
| @@ -2652,7 +2964,11 @@ LogicEditorResult LogicEditorService::removeExpression( | |||
| return failure(LogicEditorError::ExpressionNotFound, "未找到条件支路"); | |||
| } | |||
| 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); | |||
| if (rung->condition->id == expression_id) | |||
| { | |||
| @@ -2663,6 +2979,13 @@ LogicEditorResult LogicEditorService::removeExpression( | |||
| removeExpressionRecursive(&*rung->condition, expression_id); | |||
| normalizeConditionExpression(&rung->condition); | |||
| } | |||
| 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, {}, expression_id}; | |||
| } | |||
| @@ -2699,8 +3022,13 @@ LogicEditorResult LogicEditorService::removeExpressions( | |||
| } | |||
| 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_service_.editProject(), logic_id, rung_id); | |||
| project, logic_id, rung_id); | |||
| for (const std::string &expression_id : expression_ids) | |||
| { | |||
| if (!rung->condition.has_value()) | |||
| @@ -2715,10 +3043,164 @@ LogicEditorResult LogicEditorService::removeExpressions( | |||
| removeExpressionRecursive(&*rung->condition, expression_id); | |||
| } | |||
| normalizeConditionExpression(&rung->condition); | |||
| 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, {}, expression_ids.front()}; | |||
| } | |||
| LogicEditorResult LogicEditorService::disconnectWireCells( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::pair<std::string, int>> &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 (wire_cells.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "请先选择要断开的横线网格"); | |||
| } | |||
| std::vector<std::pair<std::string, int>> cells = wire_cells; | |||
| std::sort( | |||
| cells.begin(), cells.end(), | |||
| [](const auto &left, const auto &right) | |||
| { | |||
| return left.first < right.first | |||
| || (left.first == right.first && left.second > right.second); | |||
| }); | |||
| if (std::adjacent_find(cells.cbegin(), cells.cend()) != cells.cend()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "横线断开列表中存在重复网格"); | |||
| } | |||
| for (const auto &cell : cells) | |||
| { | |||
| const ConditionExpression *wire = findExpression( | |||
| logic_id, rung_id, cell.first); | |||
| if (wire == nullptr || wire->kind != ConditionExpressionKind::Wire | |||
| || cell.second < 0 || cell.second >= wire->wire->columnSpan) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "横线网格偏移超出有效范围"); | |||
| } | |||
| } | |||
| 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); | |||
| for (const auto &cell : cells) | |||
| { | |||
| ConditionExpression *wire = findConditionExpression( | |||
| *rung->condition, cell.first); | |||
| if (wire == nullptr || wire->kind != ConditionExpressionKind::Wire | |||
| || cell.second < 0 || cell.second >= wire->wire->columnSpan) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, "横线网格断开位置已经失效"); | |||
| } | |||
| const int leading_columns = cell.second; | |||
| const int trailing_columns = wire->wire->columnSpan - cell.second - 1; | |||
| if (leading_columns == 0 && trailing_columns == 0) | |||
| { | |||
| *wire = ConditionExpression::fromGap( | |||
| makeUniqueGapId(*editable_logic), 1); | |||
| continue; | |||
| } | |||
| const std::string original_wire_id = wire->id; | |||
| ConditionExpression replacement; | |||
| replacement.id = makeUniqueExpressionId(*editable_logic); | |||
| replacement.kind = ConditionExpressionKind::Series; | |||
| if (leading_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromWire( | |||
| original_wire_id, leading_columns)); | |||
| } | |||
| replacement.children.push_back(ConditionExpression::fromGap( | |||
| makeUniqueGapId(*editable_logic), 1)); | |||
| if (trailing_columns > 0) | |||
| { | |||
| replacement.children.push_back(ConditionExpression::fromWire( | |||
| leading_columns > 0 | |||
| ? makeUniqueWireId(*editable_logic) : original_wire_id, | |||
| trailing_columns)); | |||
| } | |||
| *wire = std::move(replacement); | |||
| 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, {}, cells.front().first}; | |||
| } | |||
| LogicEditorResult LogicEditorService::disconnectWires( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &wire_ids) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| const LadderRung *existing_rung = findRung(logic_id, rung_id); | |||
| if (logic == nullptr || existing_rung == nullptr | |||
| || !existing_rung->condition.has_value()) | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图条件网络"); | |||
| } | |||
| if (wire_ids.empty()) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "请先选择要断开的横线"); | |||
| } | |||
| NodeIdSet unique_ids; | |||
| for (const std::string &wire_id : wire_ids) | |||
| { | |||
| const ConditionExpression *wire = findExpression(logic_id, rung_id, wire_id); | |||
| if (!unique_ids.insert(wire_id).second | |||
| || wire == nullptr || wire->kind != ConditionExpressionKind::Wire) | |||
| { | |||
| return failure(LogicEditorError::ExpressionNotFound, "断开列表中包含无效横线"); | |||
| } | |||
| } | |||
| 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); | |||
| for (const std::string &wire_id : wire_ids) | |||
| { | |||
| ConditionExpression *wire = findConditionExpression( | |||
| *rung->condition, wire_id); | |||
| *wire = ConditionExpression::fromGap( | |||
| makeUniqueGapId(*editable_logic), wire->wire->columnSpan); | |||
| } | |||
| 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, {}, wire_ids.front()}; | |||
| } | |||
| bool LogicEditorService::canUndo() const | |||
| { | |||
| return history_.canUndo(); | |||
| @@ -2843,6 +3325,25 @@ std::string LogicEditorService::makeUniqueWireId(const ControlLogic &logic) | |||
| } | |||
| } | |||
| std::string LogicEditorService::makeUniqueGapId(const ControlLogic &logic) | |||
| { | |||
| for (std::size_t index = 1;; ++index) | |||
| { | |||
| const std::string candidate = "gap-" + std::to_string(index); | |||
| const bool found = std::any_of( | |||
| logic.rungs.cbegin(), logic.rungs.cend(), | |||
| [&candidate](const LadderRung &rung) | |||
| { | |||
| return rung.condition.has_value() | |||
| && findConditionExpression(*rung.condition, candidate) != nullptr; | |||
| }); | |||
| if (!found) | |||
| { | |||
| return candidate; | |||
| } | |||
| } | |||
| } | |||
| std::string LogicEditorService::makeUniqueExpressionId(const ControlLogic &logic) | |||
| { | |||
| for (std::size_t index = 1;; ++index) | |||
| @@ -2875,6 +3376,108 @@ std::string LogicEditorService::makeUniqueRungId(const ControlLogic &logic) | |||
| } | |||
| } | |||
| bool LogicEditorService::repairExplicitLayout( | |||
| ControlLogic &logic, LadderRung &rung, std::string *error) | |||
| { | |||
| const auto append_wire = [&logic](ConditionExpression *expression, int columns) | |||
| { | |||
| if (expression == nullptr || columns <= 0) | |||
| { | |||
| return; | |||
| } | |||
| ConditionExpression wire = ConditionExpression::fromWire( | |||
| makeUniqueWireId(logic), columns); | |||
| if (expression->kind == ConditionExpressionKind::Series) | |||
| { | |||
| expression->children.push_back(std::move(wire)); | |||
| return; | |||
| } | |||
| const std::string container_id = makeUniqueExpressionId(logic); | |||
| ConditionExpression original = std::move(*expression); | |||
| *expression = makeContainer( | |||
| container_id, | |||
| ConditionExpressionKind::Series, | |||
| std::move(original), | |||
| std::move(wire)); | |||
| }; | |||
| std::function<void(ConditionExpression *)> materialize_parallel_wires; | |||
| materialize_parallel_wires = | |||
| [&](ConditionExpression *expression) | |||
| { | |||
| if (expression == nullptr | |||
| || expression->kind == ConditionExpressionKind::Node | |||
| || expression->kind == ConditionExpressionKind::Wire | |||
| || expression->kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return; | |||
| } | |||
| for (ConditionExpression &child : expression->children) | |||
| { | |||
| materialize_parallel_wires(&child); | |||
| } | |||
| if (expression->kind != ConditionExpressionKind::Parallel) | |||
| { | |||
| return; | |||
| } | |||
| int maximum_columns = 0; | |||
| for (const ConditionExpression &child : expression->children) | |||
| { | |||
| maximum_columns = std::max( | |||
| maximum_columns, expressionColumns(child)); | |||
| } | |||
| for (ConditionExpression &child : expression->children) | |||
| { | |||
| append_wire( | |||
| &child, maximum_columns - expressionColumns(child)); | |||
| } | |||
| }; | |||
| if (rung.condition.has_value()) | |||
| { | |||
| materialize_parallel_wires(&*rung.condition); | |||
| } | |||
| if (!rung.output.has_value()) | |||
| { | |||
| normalizeConditionExpression(&rung.condition); | |||
| return true; | |||
| } | |||
| if (!rung.condition.has_value()) | |||
| { | |||
| rung.condition = ConditionExpression::fromWire( | |||
| makeUniqueWireId(logic), ProjectLimits::kMaximumConditionColumns); | |||
| return true; | |||
| } | |||
| int columns = expressionColumns(*rung.condition); | |||
| if (columns > ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| int overflow = columns - ProjectLimits::kMaximumConditionColumns; | |||
| if (!trimTrailingGridColumns(&*rung.condition, &overflow)) | |||
| { | |||
| if (error != nullptr) | |||
| { | |||
| *error = "显式梯形图布局无法在 10 列条件区内完成"; | |||
| } | |||
| return false; | |||
| } | |||
| normalizeConditionExpression(&rung.condition); | |||
| columns = expressionColumns(*rung.condition); | |||
| } | |||
| append_wire( | |||
| &*rung.condition, | |||
| ProjectLimits::kMaximumConditionColumns - columns); | |||
| normalizeConditionExpression(&rung.condition); | |||
| const bool valid_width = rung.condition.has_value() | |||
| && expressionColumns(*rung.condition) | |||
| == ProjectLimits::kMaximumConditionColumns; | |||
| if (!valid_width && error != nullptr) | |||
| { | |||
| *error = "显式梯形图布局无法在 10 列条件区内完成"; | |||
| } | |||
| return valid_width; | |||
| } | |||
| LogicEditorResult LogicEditorService::failure( | |||
| LogicEditorError error, const std::string &message) | |||
| { | |||
| @@ -40,7 +40,8 @@ enum class LogicConditionPasteTargetKind | |||
| BranchEmptyColumn, | |||
| AfterNode, | |||
| ReplaceWire, | |||
| ReplaceWireColumn | |||
| ReplaceWireColumn, | |||
| ReplaceGapColumn | |||
| }; | |||
| struct LogicConditionPasteTarget | |||
| @@ -168,6 +169,19 @@ public: | |||
| const std::string &wire_expression_id, | |||
| int column_offset, | |||
| const LogicNodeConfig &config); | |||
| /** @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); | |||
| /** @brief 用一格横线修复断路表达式中的指定网格 */ | |||
| LogicEditorResult replaceGapColumnWithWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &gap_expression_id, | |||
| int column_offset); | |||
| /** | |||
| * @brief 将选中的条件节点建立为并联分支 | |||
| * @param selected_node_ids 同一网络中按视觉连续范围选择的条件节点 ID | |||
| @@ -255,6 +269,16 @@ public: | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &expression_ids); | |||
| /** @brief 把选中的横线网格替换为断路,保留网络列位置 */ | |||
| LogicEditorResult disconnectWireCells( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::pair<std::string, int>> &wire_cells); | |||
| /** @brief 把整段横线替换为等宽断路 */ | |||
| LogicEditorResult disconnectWires( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &wire_ids); | |||
| // 当前编辑会话的撤销/重做 | |||
| /** @brief 判断是否存在可撤销的梯形图编辑操作 */ | |||
| @@ -306,10 +330,15 @@ private: | |||
| const ControlLogic &logic, const std::string &prefix); | |||
| // 生成唯一横线表达式 ID | |||
| static std::string makeUniqueWireId(const ControlLogic &logic); | |||
| // 生成控制逻辑内唯一的断路表达式 ID | |||
| static std::string makeUniqueGapId(const ControlLogic &logic); | |||
| // 生成唯一容器表达式 ID | |||
| static std::string makeUniqueExpressionId(const ControlLogic &logic); | |||
| // 生成唯一网络 ID | |||
| static std::string makeUniqueRungId(const ControlLogic &logic); | |||
| // 把并联补线和输出前连接线转换为显式 Wire,并保持输出网络十列布局 | |||
| static bool repairExplicitLayout( | |||
| ControlLogic &logic, LadderRung &rung, std::string *error = nullptr); | |||
| // 创建统一的失败结果 | |||
| static LogicEditorResult failure( | |||
| LogicEditorError error, const std::string &message); | |||
| @@ -208,7 +208,7 @@ LogicScanResult SoftwareLogicExecutor::executeScan( | |||
| continue; | |||
| } | |||
| // 没有条件节点的网络等价于左侧电源线直接接通,作为恒真条件执行 | |||
| // 输出网络的显式 Wire 会计算为真;空网络只作为编辑草稿跳过 | |||
| bool rung_value = true; | |||
| LogicScanResult result = success(); | |||
| if (rung.condition.has_value()) | |||
| @@ -302,6 +302,15 @@ LogicScanResult SoftwareLogicExecutor::evaluateExpression( | |||
| } | |||
| return success(); | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "梯形图条件区存在未连接的空白网格", | |||
| logic_id, | |||
| {}, | |||
| expression.id); | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| LogicScanResult result = evaluateCondition( | |||
| @@ -150,6 +150,10 @@ ExpressionMetrics measureExpression(const ConditionExpression &expression) | |||
| { | |||
| return {expression.wire->columnSpan, 1}; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return {expression.gap->columnSpan, 1}; | |||
| } | |||
| ExpressionMetrics metrics{0, 0}; | |||
| if (expression.kind == ConditionExpressionKind::Series) | |||
| @@ -523,6 +527,54 @@ private: | |||
| int column_offset_ = 0; | |||
| }; | |||
| class LogicEditorWidget::GapCellItem final : public QGraphicsItem | |||
| { | |||
| public: | |||
| GapCellItem( | |||
| const std::string &expression_id, | |||
| const std::string &rung_id, | |||
| int column_offset, | |||
| const QPointF ¢er) | |||
| : expression_id_(expression_id), | |||
| rung_id_(rung_id), | |||
| column_offset_(column_offset) | |||
| { | |||
| setPos(center); | |||
| setFlag(ItemIsSelectable, true); | |||
| setZValue(2.2); | |||
| setToolTip(LogicEditorWidget::tr("断路网格:第 %1 格,可补横线或插入触点") | |||
| .arg(column_offset + 1)); | |||
| } | |||
| QRectF boundingRect() const override | |||
| { | |||
| return {-kCellWidth / 2.0, -kCellHeight / 2.0, kCellWidth, kCellHeight}; | |||
| } | |||
| void paint( | |||
| QPainter *painter, | |||
| const QStyleOptionGraphicsItem *option, | |||
| QWidget *) override | |||
| { | |||
| if ((option->state & QStyle::State_Selected) == 0) | |||
| { | |||
| return; | |||
| } | |||
| painter->fillRect(boundingRect().adjusted(2, 2, -2, -2), kSelectionColor); | |||
| painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); | |||
| painter->drawRect(boundingRect().adjusted(3, 3, -3, -3)); | |||
| } | |||
| const std::string &expressionId() const { return expression_id_; } | |||
| const std::string &rungId() const { return rung_id_; } | |||
| int columnOffset() const { return column_offset_; } | |||
| private: | |||
| std::string expression_id_; | |||
| std::string rung_id_; | |||
| int column_offset_ = 0; | |||
| }; | |||
| class LogicEditorWidget::EmptySlotItem final : public QGraphicsItem | |||
| { | |||
| public: | |||
| @@ -790,6 +842,28 @@ RenderResult renderExpression( | |||
| QPointF(top_left.x(), center.y()), | |||
| QPointF(top_left.x() + width, center.y())}; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| const qreal width = static_cast<qreal>(expression.gap->columnSpan) | |||
| * kCellWidth; | |||
| const QPointF center( | |||
| top_left.x() + width / 2.0, | |||
| top_left.y() + kCellHeight / 2.0); | |||
| for (int column = 0; column < expression.gap->columnSpan; ++column) | |||
| { | |||
| scene.addItem(new LogicEditorWidget::GapCellItem( | |||
| expression.id, | |||
| rung_id, | |||
| column, | |||
| QPointF( | |||
| top_left.x() + (static_cast<qreal>(column) + 0.5) | |||
| * kCellWidth, | |||
| center.y()))); | |||
| } | |||
| return { | |||
| QPointF(top_left.x(), center.y()), | |||
| QPointF(top_left.x() + width, center.y())}; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| const QPointF center( | |||
| @@ -917,30 +991,6 @@ RenderResult renderExpression( | |||
| scene.addLine( | |||
| QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), | |||
| ladderPen(branch_active)); | |||
| if (branches[index].output.x() < right_join - 0.1) | |||
| { | |||
| // 分支宽度不足时补出到右侧汇合点的结构连接,不写入隐式 Wire | |||
| scene.addLine( | |||
| QLineF( | |||
| branches[index].output, | |||
| QPointF(right_join, branches[index].output.y())), | |||
| ladderPen(branch_active)); | |||
| } | |||
| const int branch_columns = measureExpression( | |||
| expression.children[index]).columns; | |||
| for (int column = branch_columns; column < metrics.columns; ++column) | |||
| { | |||
| scene.addItem(new LogicEditorWidget::EmptySlotItem( | |||
| rung_id, | |||
| column, | |||
| QPointF( | |||
| top_left.x() | |||
| + (static_cast<qreal>(column) + 0.5) * kCellWidth, | |||
| branches[index].output.y()), | |||
| false, | |||
| expression.children[index].id, | |||
| branch_active)); | |||
| } | |||
| } | |||
| return {QPointF(left_join, top_y), QPointF(right_join, top_y)}; | |||
| } | |||
| @@ -1307,6 +1357,19 @@ std::vector<std::pair<std::string, int>> LogicEditorWidget::selectedWireCells() | |||
| return cells; | |||
| } | |||
| std::vector<std::pair<std::string, int>> LogicEditorWidget::selectedGapCells() const | |||
| { | |||
| std::vector<std::pair<std::string, int>> cells; | |||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||
| { | |||
| if (const GapCellItem *cell = dynamic_cast<const GapCellItem *>(item)) | |||
| { | |||
| cells.emplace_back(cell->expressionId(), cell->columnOffset()); | |||
| } | |||
| } | |||
| return cells; | |||
| } | |||
| std::vector<std::string> LogicEditorWidget::selectedBranchIds() const | |||
| { | |||
| std::vector<std::string> ids; | |||
| @@ -1355,6 +1418,15 @@ std::string LogicEditorWidget::selectedRungId() const | |||
| } | |||
| rung_id = cell->rungId(); | |||
| } | |||
| else if (const GapCellItem *cell = | |||
| dynamic_cast<const GapCellItem *>(item)) | |||
| { | |||
| if (!rung_id.empty() && rung_id != cell->rungId()) | |||
| { | |||
| return {}; | |||
| } | |||
| rung_id = cell->rungId(); | |||
| } | |||
| else if (const VerticalConnectorItem *connector = | |||
| dynamic_cast<const VerticalConnectorItem *>(item)) | |||
| { | |||
| @@ -1418,16 +1490,23 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | |||
| selectedEmptySlots(); | |||
| const std::vector<std::pair<std::string, int>> selected_wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::pair<std::string, int>> selected_gap_cells = | |||
| selectedGapCells(); | |||
| const std::vector<std::string> selected_expressions = selectedExpressionIds(); | |||
| const std::vector<std::string> selected_wires = selectedWireIds(); | |||
| const std::vector<std::string> selected_ids = selectedNodeIds(); | |||
| LogicEditorResult result; | |||
| if (selected_empty_slots.size() > 1U || selected_wire_cells.size() > 1U | |||
| || selected_gap_cells.size() > 1U | |||
| || (!selected_empty_slots.empty() | |||
| && (!selected_wire_cells.empty() | |||
| || !selected_gap_cells.empty() | |||
| || !selected_expressions.empty() || !selected_ids.empty())) | |||
| || (!selected_wire_cells.empty() | |||
| && (!selected_ids.empty() || selected_expressions.size() > 1U))) | |||
| && (!selected_gap_cells.empty() | |||
| || !selected_ids.empty() || selected_expressions.size() > 1U)) | |||
| || (!selected_gap_cells.empty() | |||
| && (!selected_ids.empty() || !selected_expressions.empty()))) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "插入条件时只能选择一个网格或条件对象", {}}; | |||
| @@ -1450,6 +1529,15 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | |||
| selected_wire_cells.front().second, | |||
| config); | |||
| } | |||
| else if (selected_gap_cells.size() == 1U) | |||
| { | |||
| result = editor_service_.replaceGapColumnWithCondition( | |||
| logic_id_, | |||
| currentRungId(), | |||
| selected_gap_cells.front().first, | |||
| selected_gap_cells.front().second, | |||
| config); | |||
| } | |||
| else if (selected_expressions.size() > 1U || selected_wires.size() > 1U) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| @@ -1493,6 +1581,8 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co | |||
| selectedEmptySlots(); | |||
| const std::vector<std::pair<std::string, int>> selected_wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::pair<std::string, int>> selected_gap_cells = | |||
| selectedGapCells(); | |||
| const std::vector<std::string> selected_ids = selectedNodeIds(); | |||
| std::vector<std::string> condition_ids; | |||
| for (const std::string &node_id : selected_ids) | |||
| @@ -1504,7 +1594,12 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co | |||
| } | |||
| } | |||
| LogicEditorResult result; | |||
| if (!selected_empty_slots.empty() | |||
| if (!selected_gap_cells.empty()) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "断路网格不能建立并联支路,请先补横线或插入触点", {}}; | |||
| } | |||
| else if (!selected_empty_slots.empty() | |||
| && (!selected_ids.empty() || !selected_wire_cells.empty())) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| @@ -1546,12 +1641,23 @@ LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &co | |||
| LogicEditorResult LogicEditorWidget::addHorizontalWire() | |||
| { | |||
| const std::vector<std::string> selected_ids = selectedExpressionIds(); | |||
| const std::vector<std::pair<std::string, int>> selected_gap_cells = | |||
| selectedGapCells(); | |||
| LogicEditorResult result; | |||
| if (selected_ids.size() > 1U) | |||
| if (selected_ids.size() > 1U || selected_gap_cells.size() > 1U | |||
| || (!selected_ids.empty() && !selected_gap_cells.empty())) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "插入横线时只能选择一个条件对象", {}}; | |||
| } | |||
| else if (selected_gap_cells.size() == 1U) | |||
| { | |||
| result = editor_service_.replaceGapColumnWithWire( | |||
| logic_id_, | |||
| currentRungId(), | |||
| selected_gap_cells.front().first, | |||
| selected_gap_cells.front().second); | |||
| } | |||
| else if (selected_ids.size() == 1U) | |||
| { | |||
| result = editor_service_.insertWireAfter( | |||
| @@ -1581,10 +1687,17 @@ LogicEditorResult LogicEditorWidget::addVerticalWire() | |||
| selectedEmptySlots(); | |||
| const std::vector<std::pair<std::string, int>> selected_wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::pair<std::string, int>> selected_gap_cells = | |||
| selectedGapCells(); | |||
| const std::vector<std::string> selected_nodes = selectedNodeIds(); | |||
| const std::vector<std::string> selected_ids = selectedExpressionIds(); | |||
| LogicEditorResult result; | |||
| if (!selected_empty_slots.empty() | |||
| if (!selected_gap_cells.empty()) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "断路网格不能建立竖线连接,请先补横线", {}}; | |||
| } | |||
| else if (!selected_empty_slots.empty() | |||
| && (!selected_wire_cells.empty() || !selected_nodes.empty())) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| @@ -1630,6 +1743,8 @@ LogicEditorResult LogicEditorWidget::addVerticalWire() | |||
| LogicEditorResult LogicEditorWidget::deleteHorizontalWire() | |||
| { | |||
| const std::vector<std::pair<std::string, int>> wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::string> wire_ids = selectedWireIds(); | |||
| const std::string rung_id = selectedRungId(); | |||
| if (wire_ids.empty() || rung_id.empty()) | |||
| @@ -1642,8 +1757,9 @@ LogicEditorResult LogicEditorWidget::deleteHorizontalWire() | |||
| reportFailure(result); | |||
| return result; | |||
| } | |||
| const LogicEditorResult result = editor_service_.removeExpressions( | |||
| logic_id_, rung_id, wire_ids); | |||
| const LogicEditorResult result = !wire_cells.empty() | |||
| ? editor_service_.disconnectWireCells(logic_id_, rung_id, wire_cells) | |||
| : editor_service_.disconnectWires(logic_id_, rung_id, wire_ids); | |||
| if (result.succeeded) | |||
| { | |||
| reloadLogic(); | |||
| @@ -1720,17 +1836,24 @@ LogicEditorResult LogicEditorWidget::pasteConditionNodes( | |||
| selectedEmptySlots(); | |||
| const std::vector<std::pair<std::string, int>> selected_wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::pair<std::string, int>> selected_gap_cells = | |||
| selectedGapCells(); | |||
| const std::vector<std::string> selected_expressions = selectedExpressionIds(); | |||
| const std::vector<std::string> selected_wires = selectedWireIds(); | |||
| const std::vector<std::string> selected_ids = selectedNodeIds(); | |||
| LogicEditorResult result; | |||
| LogicConditionPasteTarget target; | |||
| if (selected_empty_slots.size() > 1U || selected_wire_cells.size() > 1U | |||
| || selected_gap_cells.size() > 1U | |||
| || (!selected_empty_slots.empty() | |||
| && (!selected_wire_cells.empty() | |||
| || !selected_gap_cells.empty() | |||
| || !selected_expressions.empty() || !selected_ids.empty())) | |||
| || (!selected_wire_cells.empty() | |||
| && (!selected_ids.empty() || selected_expressions.size() > 1U)) | |||
| && (!selected_gap_cells.empty() | |||
| || !selected_ids.empty() || selected_expressions.size() > 1U)) | |||
| || (!selected_gap_cells.empty() | |||
| && (!selected_ids.empty() || !selected_expressions.empty())) | |||
| || selected_expressions.size() > 1U || selected_wires.size() > 1U | |||
| || selected_ids.size() > 1U) | |||
| { | |||
| @@ -1754,6 +1877,12 @@ LogicEditorResult LogicEditorWidget::pasteConditionNodes( | |||
| target.expressionId = selected_wire_cells.front().first; | |||
| target.column = selected_wire_cells.front().second; | |||
| } | |||
| else if (selected_gap_cells.size() == 1U) | |||
| { | |||
| target.kind = LogicConditionPasteTargetKind::ReplaceGapColumn; | |||
| target.expressionId = selected_gap_cells.front().first; | |||
| target.column = selected_gap_cells.front().second; | |||
| } | |||
| else if (selected_wires.size() == 1U) | |||
| { | |||
| target.kind = LogicConditionPasteTargetKind::ReplaceWire; | |||
| @@ -1787,12 +1916,20 @@ LogicEditorResult LogicEditorWidget::pasteConditionNodes( | |||
| LogicEditorResult LogicEditorWidget::deleteSelected() | |||
| { | |||
| const std::vector<std::string> expression_ids = selectedExpressionIds(); | |||
| const std::vector<std::string> wire_ids = selectedWireIds(); | |||
| const std::vector<std::pair<std::string, int>> wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::pair<std::string, int>> gap_cells = | |||
| selectedGapCells(); | |||
| const std::vector<std::string> branch_ids = selectedBranchIds(); | |||
| const std::vector<std::string> node_ids = selectedNodeIds(); | |||
| LogicEditorResult result; | |||
| if (!branch_ids.empty()) | |||
| if (!gap_cells.empty()) | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "所选网格已经是断路,不需要重复删除", {}}; | |||
| } | |||
| else if (!branch_ids.empty()) | |||
| { | |||
| result = editor_service_.removeExpressions( | |||
| logic_id_, selectedRungId(), branch_ids); | |||
| @@ -1814,8 +1951,11 @@ LogicEditorResult LogicEditorWidget::deleteSelected() | |||
| } | |||
| else | |||
| { | |||
| result = editor_service_.removeExpressions( | |||
| logic_id_, selectedRungId(), expression_ids); | |||
| result = !wire_cells.empty() | |||
| ? editor_service_.disconnectWireCells( | |||
| logic_id_, selectedRungId(), wire_cells) | |||
| : editor_service_.disconnectWires( | |||
| logic_id_, selectedRungId(), wire_ids); | |||
| } | |||
| } | |||
| else if (!node_ids.empty()) | |||
| @@ -31,6 +31,7 @@ public: | |||
| class NodeItem; | |||
| class WireItem; | |||
| class WireCellItem; | |||
| class GapCellItem; | |||
| class VerticalConnectorItem; | |||
| class RungItem; | |||
| class EmptySlotItem; | |||
| @@ -116,6 +117,8 @@ private: | |||
| std::vector<std::pair<std::string, int>> selectedEmptySlots() const; | |||
| /** 收集当前选中的横线网格位置 */ | |||
| std::vector<std::pair<std::string, int>> selectedWireCells() const; | |||
| /** 收集当前选中的显式断路网格位置 */ | |||
| std::vector<std::pair<std::string, int>> selectedGapCells() const; | |||
| /** 判断当前是否选中了网络内的图元 */ | |||
| bool hasSelectedRungItem() const; | |||
| @@ -9,6 +9,7 @@ | |||
| #include "domain/virtual_register_repository.h" | |||
| #include "support/test_support.h" | |||
| #include <algorithm> | |||
| #include <cstdint> | |||
| #include <exception> | |||
| #include <functional> | |||
| @@ -21,6 +22,30 @@ namespace { | |||
| using TestSupport::require; | |||
| int expressionColumns(const ConditionExpression &expression) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| { | |||
| return 1; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Wire) | |||
| { | |||
| return expression.wire->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return expression.gap->columnSpan; | |||
| } | |||
| int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| const int child_columns = expressionColumns(child); | |||
| columns = expression.kind == ConditionExpressionKind::Series | |||
| ? columns + child_columns : std::max(columns, child_columns); | |||
| } | |||
| return columns; | |||
| } | |||
| void testRegisterAddressBoundaries() | |||
| { | |||
| // 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径 | |||
| @@ -268,7 +293,13 @@ Project makeValidProject() | |||
| LadderRung rung; | |||
| rung.id = "rung-1"; | |||
| rung.name = "Network 1"; | |||
| rung.condition = ConditionExpression::fromNode(contact); | |||
| ConditionExpression condition; | |||
| condition.id = "start-series"; | |||
| condition.kind = ConditionExpressionKind::Series; | |||
| condition.children = { | |||
| ConditionExpression::fromNode(contact), | |||
| ConditionExpression::fromWire("start-wire", 9)}; | |||
| rung.condition = std::move(condition); | |||
| rung.output = coil; | |||
| logic.rungs.push_back(rung); | |||
| @@ -463,17 +494,30 @@ void testQuantityBoundaries() | |||
| true}); | |||
| } | |||
| const int address = (*next_address)++; | |||
| ConditionExpression nested = makeNested(depth - 1, next_address); | |||
| ConditionExpression sibling = ConditionExpression::fromNode({ | |||
| "depth-node-" + std::to_string(address), | |||
| ContactNodeConfig{RegisterAddress{RegisterArea::M, address}}, | |||
| true}); | |||
| ConditionExpression expression; | |||
| expression.id = "depth-expression-" + std::to_string(address); | |||
| expression.kind = depth % 2 == 0 | |||
| ? ConditionExpressionKind::Parallel | |||
| : ConditionExpressionKind::Series; | |||
| expression.children = { | |||
| makeNested(depth - 1, next_address), | |||
| ConditionExpression::fromNode({ | |||
| "depth-node-" + std::to_string(address), | |||
| ContactNodeConfig{RegisterAddress{RegisterArea::M, address}}, | |||
| true})}; | |||
| if (expression.kind == ConditionExpressionKind::Parallel | |||
| && expressionColumns(nested) > 1) | |||
| { | |||
| ConditionExpression padded_sibling; | |||
| padded_sibling.id = "depth-padding-" + std::to_string(address); | |||
| padded_sibling.kind = ConditionExpressionKind::Series; | |||
| padded_sibling.children = { | |||
| std::move(sibling), | |||
| ConditionExpression::fromWire( | |||
| "depth-wire-" + std::to_string(address), | |||
| expressionColumns(nested) - 1)}; | |||
| sibling = std::move(padded_sibling); | |||
| } | |||
| expression.children = {std::move(nested), std::move(sibling)}; | |||
| return expression; | |||
| }; | |||
| int next_address = 1; | |||
| @@ -625,7 +669,8 @@ void testLadderLogicBoundaries() | |||
| root.kind = ConditionExpressionKind::Series; | |||
| root.children = { | |||
| ConditionExpression::fromNode(stop), | |||
| start_parallel}; | |||
| start_parallel, | |||
| ConditionExpression::fromWire("hold-output-wire", 8)}; | |||
| LadderRung rung; | |||
| rung.id = "rung-1"; | |||
| rung.name = "Self hold"; | |||
| @@ -644,6 +689,10 @@ void testLadderLogicBoundaries() | |||
| "wire-too-wide", WireSegment::kMaximumColumnSpan + 1); | |||
| require(!invalid_wire.validate(), "an oversized horizontal wire must be rejected"); | |||
| ConditionExpression gap = ConditionExpression::fromGap("gap-1", 1); | |||
| require(gap.validate() && !gap.validateForRunning(), | |||
| "a gap must be a valid editing draft but must block runtime validation"); | |||
| ConditionExpression maximum_columns; | |||
| maximum_columns.id = "maximum-columns"; | |||
| maximum_columns.kind = ConditionExpressionKind::Series; | |||
| @@ -696,9 +745,14 @@ void testLadderLogicBoundaries() | |||
| require(empty_rung.validate(), "an empty editing network must be valid"); | |||
| empty_rung.output = coil; | |||
| require(empty_rung.validate(), "output-only network may remain in an editable draft"); | |||
| require(empty_rung.validateForRunning(), | |||
| "an output-only network must be valid as an unconditional rung"); | |||
| require(!empty_rung.validate(), | |||
| "an output without an explicit ten-column condition path must be rejected"); | |||
| empty_rung.condition = ConditionExpression::fromWire("unconditional-wire", 10); | |||
| require(empty_rung.validate() && empty_rung.validateForRunning(), | |||
| "a ten-column wire path must represent a runnable unconditional rung"); | |||
| empty_rung.condition = ConditionExpression::fromGap("unconditional-gap", 10); | |||
| require(empty_rung.validate() && !empty_rung.validateForRunning(), | |||
| "a full-width gap draft must remain saved but disconnected"); | |||
| logic.rungs.front().output = coil; | |||
| logic.rungs.front().condition = root; | |||
| @@ -711,8 +765,17 @@ void testLadderLogicBoundaries() | |||
| nested_parallel.children = { | |||
| ConditionExpression::fromNode(start), | |||
| root}; | |||
| require(!nested_parallel.validate(), | |||
| "parallel branches with unequal explicit widths must be rejected"); | |||
| ConditionExpression padded_start; | |||
| padded_start.id = "padded-start"; | |||
| padded_start.kind = ConditionExpressionKind::Series; | |||
| padded_start.children = { | |||
| ConditionExpression::fromNode(start), | |||
| ConditionExpression::fromWire("nested-parallel-wire", 9)}; | |||
| nested_parallel.children.front() = std::move(padded_start); | |||
| require(nested_parallel.validate(), | |||
| "nested series and parallel expressions must be valid"); | |||
| "parallel branches padded with explicit wires must be valid"); | |||
| } | |||
| void testModelsValidateBindingsAndIdentifiers() | |||
| @@ -36,6 +36,10 @@ int conditionColumns(const ConditionExpression &expression) | |||
| { | |||
| return expression.wire->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return expression.gap->columnSpan; | |||
| } | |||
| int columns = expression.kind == ConditionExpressionKind::Series ? 0 : 1; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| @@ -56,6 +60,10 @@ int wireColumns(const ConditionExpression &expression) | |||
| { | |||
| return expression.wire->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return 0; | |||
| } | |||
| int columns = 0; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| @@ -64,6 +72,25 @@ int wireColumns(const ConditionExpression &expression) | |||
| return columns; | |||
| } | |||
| int gapColumns(const ConditionExpression &expression) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return expression.gap->columnSpan; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Node | |||
| || expression.kind == ConditionExpressionKind::Wire) | |||
| { | |||
| return 0; | |||
| } | |||
| int columns = 0; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| columns += gapColumns(child); | |||
| } | |||
| return columns; | |||
| } | |||
| int conditionNodes(const ConditionExpression &expression) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| @@ -74,6 +101,10 @@ int conditionNodes(const ConditionExpression &expression) | |||
| { | |||
| return 0; | |||
| } | |||
| if (expression.kind == ConditionExpressionKind::Gap) | |||
| { | |||
| return 0; | |||
| } | |||
| int count = 0; | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| @@ -82,6 +113,23 @@ int conditionNodes(const ConditionExpression &expression) | |||
| return count; | |||
| } | |||
| const ConditionExpression *firstExpressionOfKind( | |||
| const ConditionExpression &expression, ConditionExpressionKind kind) | |||
| { | |||
| if (expression.kind == kind) | |||
| { | |||
| return &expression; | |||
| } | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| if (const ConditionExpression *found = firstExpressionOfKind(child, kind)) | |||
| { | |||
| return found; | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| void testEmptyLogicCreatesNetworksOnFirstEdit() | |||
| { | |||
| TestProjectStorage storage; | |||
| @@ -121,9 +169,13 @@ void testEmptyLogicCreatesNetworksOnFirstEdit() | |||
| CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}); | |||
| require(first_output.succeeded | |||
| && service.findLogic(logic_id)->rungs.size() == 1U | |||
| && !service.findLogic(logic_id)->rungs.front().condition.has_value() | |||
| && service.findLogic(logic_id)->rungs.front().condition.has_value() | |||
| && service.findLogic(logic_id)->rungs.front().condition->kind | |||
| == ConditionExpressionKind::Wire | |||
| && service.findLogic(logic_id)->rungs.front().condition->wire->columnSpan | |||
| == ProjectLimits::kMaximumConditionColumns | |||
| && service.findLogic(logic_id)->rungs.front().output.has_value(), | |||
| "the first output must create an unconditional network"); | |||
| "the first output must create a full-width explicit wire network"); | |||
| } | |||
| void testAppendingWireMovesToNextNetworkAfterTenColumns() | |||
| @@ -199,15 +251,20 @@ void testStructuredEditingAndNormalization() | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->condition->children.at(1).kind == ConditionExpressionKind::Parallel | |||
| && rung->condition->children.at(1).children.at(1).kind | |||
| == ConditionExpressionKind::Node, | |||
| "single-child series container must collapse after deletion"); | |||
| == ConditionExpressionKind::Series | |||
| && gapColumns(*rung->condition) == 1 | |||
| && conditionColumns(*rung->condition) == 3, | |||
| "deleting a nested node must preserve its column as an explicit gap"); | |||
| require(service.removeNode(logic_id, parallel.id).succeeded, | |||
| "parallel leaf deletion must succeed"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->condition->kind == ConditionExpressionKind::Series | |||
| && rung->condition->children.size() == 2U, | |||
| "single-child parallel container must collapse after deletion"); | |||
| "deleting a parallel leaf must preserve the surrounding topology"); | |||
| require(gapColumns(*rung->condition) == 2 | |||
| && conditionColumns(*rung->condition) == 3, | |||
| "each deleted condition must remain as a one-column gap"); | |||
| require(service.setOutput( | |||
| logic_id, | |||
| @@ -268,10 +325,21 @@ void testParallelBranchGridInsertion() | |||
| "parallel grid insertion setup must create a short lower branch"); | |||
| service.clearHistory(); | |||
| const LogicEditorResult inserted = service.insertConditionInBranchAtColumn( | |||
| logic_id, rung_id, branch.id, 2, contact(12)); | |||
| const LadderRung *padded_rung = service.findRung(logic_id, rung_id); | |||
| const ConditionExpression &padded_lower = padded_rung->condition->children.at(1); | |||
| require(padded_lower.kind == ConditionExpressionKind::Series | |||
| && padded_lower.children.size() == 2U | |||
| && padded_lower.children.back().kind == ConditionExpressionKind::Wire | |||
| && padded_lower.children.back().wire->columnSpan == 2, | |||
| "a short parallel branch must persist its two padding cells as a wire"); | |||
| const LogicEditorResult inserted = service.replaceWireColumnWithCondition( | |||
| logic_id, | |||
| rung_id, | |||
| padded_lower.children.back().id, | |||
| 1, | |||
| contact(12)); | |||
| require(inserted.succeeded, | |||
| "a visible parallel branch padding cell must accept a condition"); | |||
| "a persisted parallel branch wire cell must accept a condition"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| require(rung != nullptr && rung->condition.has_value() | |||
| && rung->condition->kind == ConditionExpressionKind::Parallel, | |||
| @@ -288,9 +356,12 @@ void testParallelBranchGridInsertion() | |||
| require(service.undo().succeeded, | |||
| "parallel branch grid insertion must be one undoable edit"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->condition->children.at(1).kind == ConditionExpressionKind::Node | |||
| && rung->condition->children.at(1).node->id == branch.id, | |||
| "undo must restore the original short parallel branch"); | |||
| require(rung->condition->children.at(1).kind == ConditionExpressionKind::Series | |||
| && rung->condition->children.at(1).children.front().node->id == branch.id | |||
| && rung->condition->children.at(1).children.back().kind | |||
| == ConditionExpressionKind::Wire | |||
| && rung->condition->children.at(1).children.back().wire->columnSpan == 2, | |||
| "undo must restore the original branch and its explicit padding wire"); | |||
| service.clearHistory(); | |||
| const bool modified_before = project_service.isModified(); | |||
| @@ -334,8 +405,8 @@ void testStructuredWireEditing() | |||
| "a selected wire must be replaceable by a configured node type"); | |||
| const LogicEditorResult extension = service.insertWireAfter( | |||
| logic_id, rung_id, replacement.id); | |||
| require(extension.succeeded && extension.id == "wire-1", | |||
| "a wire id must become reusable after its expression is replaced"); | |||
| require(extension.succeeded && extension.id == "wire-2", | |||
| "replacing one cell of a multi-cell wire must retain the original id on the remaining cell"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| const ConditionExpression *extended_branch = service.findExpression( | |||
| @@ -354,9 +425,12 @@ void testStructuredWireEditing() | |||
| logic_id, rung_id, {extended_branch_id}).succeeded, | |||
| "deleting a selected vertical connection branch must succeed atomically"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->condition->kind == ConditionExpressionKind::Series | |||
| && rung->condition->children.size() == 3U, | |||
| "removing a bypass branch must normalize back to the original series"); | |||
| require(rung->condition.has_value() | |||
| && conditionNodes(*rung->condition) == 3 | |||
| && conditionColumns(*rung->condition) == 4 | |||
| && wireColumns(*rung->condition) == 1 | |||
| && service.findNode(logic_id, replacement.id) == nullptr, | |||
| "removing a bypass branch must keep the materialized padding wire without reconnecting positions"); | |||
| require(service.undo().succeeded | |||
| && service.findExpression(logic_id, rung_id, extended_branch_id) != nullptr, | |||
| "wire branch deletion must participate in ladder undo history"); | |||
| @@ -487,8 +561,11 @@ void testBatchDeleteAllNodesInParallelBranch() | |||
| && rung->condition->children.at(0).kind | |||
| == ConditionExpressionKind::Node | |||
| && rung->condition->children.at(1).kind | |||
| == ConditionExpressionKind::Node, | |||
| "an empty parallel branch must collapse into the remaining branch"); | |||
| == ConditionExpressionKind::Parallel | |||
| && conditionNodes(*rung->condition) == 2 | |||
| && gapColumns(*rung->condition) == 3 | |||
| && conditionColumns(*rung->condition) == 4, | |||
| "deleted parallel conditions must remain as gaps without collapsing the branch"); | |||
| require(service.findNode(logic_id, second.id) == nullptr | |||
| && service.findNode(logic_id, third.id) == nullptr | |||
| && service.findNode(logic_id, fourth.id) == nullptr, | |||
| @@ -567,9 +644,131 @@ void testUnconditionalOutputEditing() | |||
| .succeeded, | |||
| "the editor must allow a coil before any condition is added"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| require(rung != nullptr && !rung->condition.has_value() | |||
| require(rung != nullptr && rung->condition.has_value() | |||
| && rung->condition->kind == ConditionExpressionKind::Wire | |||
| && rung->condition->wire->columnSpan | |||
| == ProjectLimits::kMaximumConditionColumns | |||
| && rung->output.has_value() && rung->validateForRunning(), | |||
| "an editor-created output-only network must be runnable as unconditional"); | |||
| "an editor-created unconditional network must use a full-width wire"); | |||
| const std::string contact_rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult contact_result = service.appendCondition( | |||
| logic_id, contact_rung_id, contact(0)); | |||
| require(contact_result.succeeded | |||
| && service.updateNodeConfig( | |||
| logic_id, contact_result.id, contact(0)).succeeded | |||
| && service.setOutput( | |||
| logic_id, | |||
| contact_rung_id, | |||
| CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 11}, CoilMode::Normal}, | |||
| true).succeeded, | |||
| "a contact followed by an output must be editable"); | |||
| const LadderRung *contact_rung = service.findRung(logic_id, contact_rung_id); | |||
| require(contact_rung != nullptr && contact_rung->condition.has_value() | |||
| && conditionNodes(*contact_rung->condition) == 1 | |||
| && wireColumns(*contact_rung->condition) == 9 | |||
| && conditionColumns(*contact_rung->condition) == 10 | |||
| && contact_rung->validateForRunning(), | |||
| "a contact followed by an output must persist the remaining nine wire cells"); | |||
| } | |||
| void testExplicitGapEditing() | |||
| { | |||
| 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); | |||
| require(service.setOutput( | |||
| logic_id, | |||
| rung_id, | |||
| CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 30}, CoilMode::Normal}, | |||
| true).succeeded, | |||
| "gap editing setup must create an explicit full-width wire"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| const std::string full_wire_id = rung->condition->id; | |||
| require(service.disconnectWireCells( | |||
| logic_id, rung_id, {{full_wire_id, 4}}).succeeded, | |||
| "deleting one wire cell must create a gap"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->validate() && !rung->validateForRunning() | |||
| && conditionColumns(*rung->condition) == 10 | |||
| && wireColumns(*rung->condition) == 9 | |||
| && gapColumns(*rung->condition) == 1, | |||
| "a disconnected wire cell must preserve width and block runtime"); | |||
| const ConditionExpression *gap = firstExpressionOfKind( | |||
| *rung->condition, ConditionExpressionKind::Gap); | |||
| require(gap != nullptr, "the disconnected cell must expose a gap expression"); | |||
| const std::string first_gap_id = gap->id; | |||
| require(service.replaceGapColumnWithWire( | |||
| logic_id, rung_id, first_gap_id, 0).succeeded, | |||
| "a gap cell must be repairable with a real wire"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(gapColumns(*rung->condition) == 0 | |||
| && wireColumns(*rung->condition) == 10 | |||
| && rung->validateForRunning(), | |||
| "repairing the gap with a wire must restore a runnable path"); | |||
| const ConditionExpression *wire = firstExpressionOfKind( | |||
| *rung->condition, ConditionExpressionKind::Wire); | |||
| require(wire != nullptr, "the repaired path must expose a wire expression"); | |||
| const std::string repaired_wire_id = wire->id; | |||
| require(service.disconnectWireCells( | |||
| logic_id, rung_id, {{repaired_wire_id, 0}}).succeeded, | |||
| "a repaired wire cell must be disconnectable again"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| gap = firstExpressionOfKind(*rung->condition, ConditionExpressionKind::Gap); | |||
| require(gap != nullptr, "the second disconnection must expose a gap expression"); | |||
| const std::string second_gap_id = gap->id; | |||
| const LogicEditorResult inserted_result = service.replaceGapColumnWithCondition( | |||
| logic_id, rung_id, second_gap_id, 0, contact(31)); | |||
| require(inserted_result.succeeded | |||
| && service.updateNodeConfig( | |||
| logic_id, inserted_result.id, contact(31)).succeeded, | |||
| "a gap cell must be replaceable with a contact"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| const ConditionExpression *inserted = firstExpressionOfKind( | |||
| *rung->condition, ConditionExpressionKind::Node); | |||
| require(inserted != nullptr && gapColumns(*rung->condition) == 0 | |||
| && conditionNodes(*rung->condition) == 1 | |||
| && conditionColumns(*rung->condition) == 10 | |||
| && rung->validateForRunning(), | |||
| "a contact inserted into a gap must preserve the ten-column path"); | |||
| const std::string inserted_node_id = inserted->node->id; | |||
| require(service.removeNode(logic_id, inserted_node_id).succeeded, | |||
| "the inserted contact must be removable"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(conditionNodes(*rung->condition) == 0 | |||
| && gapColumns(*rung->condition) == 1 | |||
| && conditionColumns(*rung->condition) == 10 | |||
| && !rung->validateForRunning(), | |||
| "deleting a contact must restore a gap instead of reconnecting the path"); | |||
| const std::string multi_rung_id = makeEmptyRung(service, logic_id); | |||
| require(service.setOutput( | |||
| logic_id, | |||
| multi_rung_id, | |||
| CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 32}, CoilMode::Normal}, | |||
| true).succeeded, | |||
| "multi-gap setup must create a full-width wire"); | |||
| const std::string multi_wire_id = service.findRung( | |||
| logic_id, multi_rung_id)->condition->id; | |||
| require(service.disconnectWireCells( | |||
| logic_id, | |||
| multi_rung_id, | |||
| {{multi_wire_id, 2}, {multi_wire_id, 7}}).succeeded, | |||
| "multiple cells in one wire must disconnect atomically"); | |||
| const LadderRung *multi_rung = service.findRung(logic_id, multi_rung_id); | |||
| require(conditionColumns(*multi_rung->condition) == 10 | |||
| && wireColumns(*multi_rung->condition) == 8 | |||
| && gapColumns(*multi_rung->condition) == 2 | |||
| && !multi_rung->validateForRunning(), | |||
| "multiple disconnected cells must retain both explicit gaps"); | |||
| } | |||
| void testColumnTargetedConditionInsertion() | |||
| @@ -627,12 +826,17 @@ void testColumnTargetedConditionInsertion() | |||
| true) | |||
| .succeeded, | |||
| "an output-only network must be configurable before adding a condition"); | |||
| require(service.insertConditionAtColumn( | |||
| logic_id, second_rung.id, 0, contact(0)).succeeded, | |||
| "a selected first grid slot must work on an output-only network"); | |||
| const std::string output_wire_id = service.findRung( | |||
| logic_id, second_rung.id)->condition->id; | |||
| require(service.replaceWireColumnWithCondition( | |||
| logic_id, second_rung.id, output_wire_id, 0, contact(0)).succeeded, | |||
| "the first wire cell must accept a contact on an unconditional network"); | |||
| const LadderRung *output_rung = service.findRung(logic_id, second_rung.id); | |||
| require(output_rung != nullptr && output_rung->condition.has_value() | |||
| && output_rung->condition->kind == ConditionExpressionKind::Node | |||
| && output_rung->condition->kind == ConditionExpressionKind::Series | |||
| && conditionNodes(*output_rung->condition) == 1 | |||
| && wireColumns(*output_rung->condition) == 9 | |||
| && conditionColumns(*output_rung->condition) == 10 | |||
| && output_rung->output.has_value() | |||
| && output_rung->validate(), | |||
| "condition insertion must keep the independent output slot intact"); | |||
| @@ -1149,13 +1353,18 @@ void testConditionPasteTargetsAndValidation() | |||
| {branch_source.id, branch_source_two.id, branch_source_three.id}, | |||
| contact(4)); | |||
| require(branch.succeeded, "branch paste setup must create a parallel branch"); | |||
| const ConditionExpression &paste_lower = service.findRung( | |||
| logic_id, branch_rung)->condition->children.at(1); | |||
| require(paste_lower.kind == ConditionExpressionKind::Series | |||
| && paste_lower.children.back().kind == ConditionExpressionKind::Wire, | |||
| "branch paste setup must persist the short branch padding wire"); | |||
| LogicConditionPasteTarget branch_target; | |||
| branch_target.kind = LogicConditionPasteTargetKind::BranchEmptyColumn; | |||
| branch_target.expressionId = branch.id; | |||
| branch_target.column = 2; | |||
| branch_target.kind = LogicConditionPasteTargetKind::ReplaceWireColumn; | |||
| branch_target.expressionId = paste_lower.children.back().id; | |||
| branch_target.column = 1; | |||
| require(service.pasteConditionNodes( | |||
| logic_id, branch_rung, {source}, branch_target).succeeded, | |||
| "condition paste must support a visible empty cell in a branch"); | |||
| "condition paste must support a persisted wire cell in a branch"); | |||
| const std::string after_node_rung = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult after_source = service.appendCondition( | |||
| @@ -1248,6 +1457,7 @@ int main() | |||
| testBatchDeleteAllNodesInParallelBranch(); | |||
| testConditionColumnLimit(); | |||
| testUnconditionalOutputEditing(); | |||
| testExplicitGapEditing(); | |||
| testColumnTargetedConditionInsertion(); | |||
| testWireColumnReplacement(); | |||
| testSequentialConditionInsertionConsumesFollowingWire(); | |||
| @@ -1,4 +1,5 @@ | |||
| #include "domain/hmi_model.h" | |||
| #include "domain/project_limits.h" | |||
| #include "domain/register_repository.h" | |||
| #include "domain/virtual_register_repository.h" | |||
| #include "services/hmi_runtime_service.h" | |||
| @@ -132,6 +133,26 @@ LadderRung rung(const std::string &id, | |||
| series.children = std::move(series_children); | |||
| result.condition = std::move(series); | |||
| } | |||
| const int occupied_columns = static_cast<int>(stages.size()); | |||
| if (occupied_columns < ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| ConditionExpression wire = ConditionExpression::fromWire( | |||
| id + "-output-wire", | |||
| ProjectLimits::kMaximumConditionColumns - occupied_columns); | |||
| if (result.condition->kind == ConditionExpressionKind::Series) | |||
| { | |||
| result.condition->children.push_back(std::move(wire)); | |||
| } | |||
| else | |||
| { | |||
| ConditionExpression series; | |||
| series.id = id + "-explicit-series"; | |||
| series.kind = ConditionExpressionKind::Series; | |||
| series.children.push_back(std::move(*result.condition)); | |||
| series.children.push_back(std::move(wire)); | |||
| result.condition = std::move(series); | |||
| } | |||
| } | |||
| result.output = output; | |||
| return result; | |||
| } | |||
| @@ -179,12 +200,22 @@ void testNestedSeriesParallelExpression() | |||
| nested_series.children = { | |||
| ConditionExpression::fromNode(contact("b", 1)), | |||
| ConditionExpression::fromNode(contact("c", 2))}; | |||
| ConditionExpression padded_a; | |||
| padded_a.id = "padded-a"; | |||
| padded_a.kind = ConditionExpressionKind::Series; | |||
| padded_a.children = { | |||
| ConditionExpression::fromNode(contact("a", 0)), | |||
| ConditionExpression::fromWire("a-branch-wire", 1)}; | |||
| ConditionExpression parallel; | |||
| parallel.id = "root-parallel"; | |||
| parallel.kind = ConditionExpressionKind::Parallel; | |||
| parallel.children = {std::move(padded_a), nested_series}; | |||
| ConditionExpression root; | |||
| root.id = "root-parallel"; | |||
| root.kind = ConditionExpressionKind::Parallel; | |||
| root.id = "nested-output-series"; | |||
| root.kind = ConditionExpressionKind::Series; | |||
| root.children = { | |||
| ConditionExpression::fromNode(contact("a", 0)), | |||
| nested_series}; | |||
| std::move(parallel), | |||
| ConditionExpression::fromWire("nested-output-wire", 8)}; | |||
| LadderRung nested_rung; | |||
| nested_rung.id = "nested-rung"; | |||
| nested_rung.name = "nested-rung"; | |||
| @@ -220,16 +251,18 @@ void testUnconditionalCoil() | |||
| LadderRung unconditional; | |||
| unconditional.id = "unconditional-rung"; | |||
| unconditional.name = "unconditional-rung"; | |||
| unconditional.condition = ConditionExpression::fromWire( | |||
| "unconditional-wire", ProjectLimits::kMaximumConditionColumns); | |||
| unconditional.output = coil("unconditional-coil", 10); | |||
| const ControlLogic program = logic({unconditional}); | |||
| LogicTraceSnapshot trace; | |||
| require(executor.validate({program}).succeeded, | |||
| "an output-only network must pass runtime validation"); | |||
| "a full-width wire network must pass runtime validation"); | |||
| require(executor.executeScan({program}, repository, &trace).succeeded, | |||
| "an output-only network scan must succeed"); | |||
| "a full-width wire network scan must succeed"); | |||
| require(readBit(repository, 10), | |||
| "an output-only network must energize its coil as a constant-true rung"); | |||
| "a full-width wire network must energize its coil as a constant-true rung"); | |||
| require(trace.rungValues.at("unconditional-rung") | |||
| && trace.nodePowerValues.at("unconditional-coil"), | |||
| "an unconditional rung must report energized power flow"); | |||
| @@ -245,7 +278,8 @@ void testWirePassThroughAndPowerTrace() | |||
| root.children = { | |||
| ConditionExpression::fromNode(contact("wire-input", 0)), | |||
| ConditionExpression::fromWire("wire-segment", 2), | |||
| ConditionExpression::fromNode(contact("wire-output", 1))}; | |||
| ConditionExpression::fromNode(contact("wire-output", 1)), | |||
| ConditionExpression::fromWire("wire-output-padding", 6)}; | |||
| LadderRung wired_rung; | |||
| wired_rung.id = "wired-rung"; | |||
| wired_rung.name = "wired-rung"; | |||
| @@ -3,9 +3,11 @@ | |||
| #include "domain/virtual_register_repository.h" | |||
| #include "services/software_logic_executor.h" | |||
| #include <QDebug> | |||
| #include <QTest> | |||
| #include <string> | |||
| #include <utility> | |||
| #include <vector> | |||
| namespace | |||
| @@ -34,8 +36,15 @@ ControlLogic makeLogic(int index) | |||
| LadderRung rung; | |||
| rung.id = "rung-" + std::to_string(index); | |||
| rung.name = rung.id; | |||
| rung.condition = ConditionExpression::fromNode( | |||
| contact("contact-" + std::to_string(index), index)); | |||
| ConditionExpression condition; | |||
| condition.id = "series-" + std::to_string(index); | |||
| condition.kind = ConditionExpressionKind::Series; | |||
| condition.children = { | |||
| ConditionExpression::fromNode( | |||
| contact("contact-" + std::to_string(index), index)), | |||
| ConditionExpression::fromWire( | |||
| "wire-" + std::to_string(index), 9)}; | |||
| rung.condition = std::move(condition); | |||
| rung.output = coil("coil-" + std::to_string(index), index + 100); | |||
| return {"logic-" + std::to_string(index), | |||
| "logic-" + std::to_string(index), | |||
| @@ -48,6 +57,16 @@ class PerformanceTests final : public QObject | |||
| Q_OBJECT | |||
| private slots: | |||
| void initTestCase() | |||
| { | |||
| qInfo().noquote() | |||
| << QStringLiteral("性能测试开始:下面两项 RESULT 的耗时均为平均每次执行时间"); | |||
| qInfo().noquote() | |||
| << QStringLiteral("测试项目 1:100 个控制逻辑的软件扫描"); | |||
| qInfo().noquote() | |||
| << QStringLiteral("测试项目 2:M0 到 M4000 共 4001 个虚拟 M 地址的完整写入和读取"); | |||
| } | |||
| void softwareExecutor_scansRepresentativeProject() | |||
| { | |||
| std::vector<ControlLogic> logics; | |||
| @@ -89,6 +108,12 @@ private slots: | |||
| } | |||
| } | |||
| } | |||
| void cleanupTestCase() | |||
| { | |||
| qInfo().noquote() | |||
| << QStringLiteral("性能测试结束:请结合上方两项 RESULT 和最后的 Totals 判断结果"); | |||
| } | |||
| }; | |||
| } // namespace | |||
| @@ -150,7 +150,8 @@ Project makeExampleProject() | |||
| series.children = { | |||
| std::move(parallel), | |||
| ConditionExpression::fromWire("start-wire", 2), | |||
| ConditionExpression::fromNode(compare)}; | |||
| ConditionExpression::fromNode(compare), | |||
| ConditionExpression::fromWire("start-output-wire", 6)}; | |||
| rung.condition = std::move(series); | |||
| rung.output = coil; | |||
| logic.rungs.push_back(rung); | |||
| @@ -167,7 +168,13 @@ Project makeExampleProject() | |||
| edge_rung.id = "edge-rung"; | |||
| edge_rung.name = "Edge network"; | |||
| edge_rung.comment = "上升沿输出"; | |||
| edge_rung.condition = ConditionExpression::fromNode(rising_edge); | |||
| ConditionExpression edge_series; | |||
| edge_series.id = "edge-series"; | |||
| edge_series.kind = ConditionExpressionKind::Series; | |||
| edge_series.children = { | |||
| ConditionExpression::fromNode(rising_edge), | |||
| ConditionExpression::fromWire("edge-output-wire", 9)}; | |||
| edge_rung.condition = std::move(edge_series); | |||
| edge_rung.output = edge_output; | |||
| logic.rungs.push_back(edge_rung); | |||
| @@ -185,7 +192,13 @@ Project makeExampleProject() | |||
| LadderRung data_rung; | |||
| data_rung.id = rung_id; | |||
| data_rung.name = rung_id; | |||
| data_rung.condition = ConditionExpression::fromNode(input); | |||
| ConditionExpression data_series; | |||
| data_series.id = rung_id + "-series"; | |||
| data_series.kind = ConditionExpressionKind::Series; | |||
| data_series.children = { | |||
| ConditionExpression::fromNode(input), | |||
| ConditionExpression::fromWire(rung_id + "-output-wire", 9)}; | |||
| data_rung.condition = std::move(data_series); | |||
| data_rung.output = std::move(output); | |||
| logic.rungs.push_back(std::move(data_rung)); | |||
| }; | |||
| @@ -328,11 +341,13 @@ void testUnconditionalOutputRoundTrip() | |||
| coil.id = "unconditional-coil"; | |||
| coil.config = CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}; | |||
| rung.condition = ConditionExpression::fromWire( | |||
| "unconditional-wire", ProjectLimits::kMaximumConditionColumns); | |||
| rung.output = coil; | |||
| service.editProject().controlLogics.push_back( | |||
| ControlLogic{"logic-1", "Logic 1", {rung}, true}); | |||
| require(service.project().validateForRunning(), | |||
| "an output-only project must be runnable before saving"); | |||
| "an explicit full-width wire project must be runnable before saving"); | |||
| const QString path = directory.filePath("unconditional-output.json"); | |||
| require(service.saveAs(path.toStdString()).succeeded, | |||
| @@ -340,9 +355,71 @@ void testUnconditionalOutputRoundTrip() | |||
| require(service.load(path.toStdString()).succeeded, | |||
| "an output-only project must load successfully"); | |||
| const LadderRung &loaded = service.project().controlLogics.front().rungs.front(); | |||
| require(!loaded.condition.has_value() && loaded.output.has_value() | |||
| require(loaded.condition.has_value() | |||
| && loaded.condition->kind == ConditionExpressionKind::Wire | |||
| && loaded.condition->wire->columnSpan | |||
| == ProjectLimits::kMaximumConditionColumns | |||
| && loaded.output.has_value() | |||
| && loaded.validateForRunning(), | |||
| "JSON round trip must preserve unconditional output semantics"); | |||
| "JSON round trip must preserve explicit unconditional wire semantics"); | |||
| QJsonObject implicit_root = QJsonDocument::fromJson(readBytes(path)).object(); | |||
| QJsonArray logics = implicit_root.value(QStringLiteral("controlLogics")).toArray(); | |||
| QJsonObject logic = logics.at(0).toObject(); | |||
| QJsonArray rungs = logic.value(QStringLiteral("rungs")).toArray(); | |||
| QJsonObject implicit_rung = rungs.at(0).toObject(); | |||
| implicit_rung.insert(QStringLiteral("condition"), QJsonValue::Null); | |||
| rungs.replace(0, implicit_rung); | |||
| logic.insert(QStringLiteral("rungs"), rungs); | |||
| logics.replace(0, logic); | |||
| implicit_root.insert(QStringLiteral("controlLogics"), logics); | |||
| const QString implicit_path = directory.filePath("implicit-output.json"); | |||
| writeText( | |||
| implicit_path, | |||
| QJsonDocument(implicit_root).toJson(QJsonDocument::Indented)); | |||
| require(!service.load(implicit_path.toStdString()).succeeded, | |||
| "version 1.0 implicit output wiring must not be migrated on load"); | |||
| } | |||
| void testGapRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| JsonProjectStorage storage; | |||
| ProjectService service(storage); | |||
| require(service.createNewProject("Gap draft").succeeded, | |||
| "gap draft project creation must succeed"); | |||
| LadderRung gap_rung; | |||
| gap_rung.id = "gap-rung"; | |||
| gap_rung.name = "Gap rung"; | |||
| gap_rung.condition = ConditionExpression::fromGap( | |||
| "gap-full-width", ProjectLimits::kMaximumConditionColumns); | |||
| gap_rung.output = LogicNode{ | |||
| "gap-output", | |||
| CoilNodeConfig{RegisterAddress{RegisterArea::M, 20}, CoilMode::Normal}, | |||
| true}; | |||
| service.editProject().controlLogics.push_back( | |||
| ControlLogic{"gap-logic", "Gap logic", {gap_rung}, true}); | |||
| require(service.project().validate() && !service.project().validateForRunning(), | |||
| "a gap network must be saveable but not runnable"); | |||
| const QString gap_path = directory.filePath("gap-project.json"); | |||
| require(service.saveAs(gap_path.toStdString()).succeeded, | |||
| "a gap draft must save successfully"); | |||
| require(readBytes(gap_path).contains("\"kind\": \"gap\""), | |||
| "version 1.0 JSON must persist gap expressions explicitly"); | |||
| require(service.load(gap_path.toStdString()).succeeded, | |||
| "a version 1.0 gap draft must load successfully"); | |||
| const LadderRung &loaded_gap = | |||
| service.project().controlLogics.front().rungs.front(); | |||
| require(service.project().metadata.formatVersion == "1.0" | |||
| && loaded_gap.condition.has_value() | |||
| && loaded_gap.condition->kind == ConditionExpressionKind::Gap | |||
| && loaded_gap.condition->gap->columnSpan | |||
| == ProjectLimits::kMaximumConditionColumns | |||
| && !loaded_gap.validateForRunning(), | |||
| "gap JSON round trip must preserve the disconnected runtime state"); | |||
| } | |||
| void testExampleProjectRoundTrip() | |||
| @@ -490,8 +567,9 @@ void testExampleProjectRoundTrip() | |||
| == RegisterAddress{RegisterArea::M, 6}, | |||
| "edge network output must survive round trip"); | |||
| require(std::holds_alternative<EdgeContactNodeConfig>( | |||
| edge_rung.condition->node->config) | |||
| && std::get<EdgeContactNodeConfig>(edge_rung.condition->node->config).mode | |||
| edge_rung.condition->children.front().node->config) | |||
| && std::get<EdgeContactNodeConfig>( | |||
| edge_rung.condition->children.front().node->config).mode | |||
| == EdgeMode::Rising, | |||
| "rising edge configuration must survive round trip"); | |||
| require(std::get<MoveNodeConfig>( | |||
| @@ -935,6 +1013,7 @@ int main() | |||
| // 工程服务和 JSON 存储在同一测试进程中验证完整闭环 | |||
| testEmptyProjectRoundTrip(); | |||
| testUnconditionalOutputRoundTrip(); | |||
| testGapRoundTrip(); | |||
| testExampleProjectRoundTrip(); | |||
| testRegisterCommentService(); | |||
| testInvalidFiles(); | |||
| @@ -89,6 +89,19 @@ private: | |||
| using TestSupport::require; | |||
| ConditionExpression conditionWithOutputWire( | |||
| LogicNode node, | |||
| const std::string &wire_id) | |||
| { | |||
| ConditionExpression series; | |||
| series.id = wire_id + "-series"; | |||
| series.kind = ConditionExpressionKind::Series; | |||
| series.children = { | |||
| ConditionExpression::fromNode(std::move(node)), | |||
| ConditionExpression::fromWire(wire_id, 9)}; | |||
| return series; | |||
| } | |||
| void testModeTransitions() | |||
| { | |||
| // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 | |||
| @@ -126,7 +139,8 @@ void testModeTransitions() | |||
| LadderRung edge_rung; | |||
| edge_rung.id = "poll-edge-rung"; | |||
| edge_rung.name = "Poll edge"; | |||
| edge_rung.condition = ConditionExpression::fromNode(edge); | |||
| edge_rung.condition = conditionWithOutputWire( | |||
| edge, "poll-edge-output-wire"); | |||
| edge_rung.output = edge_coil; | |||
| logic.rungs.push_back(edge_rung); | |||
| LogicNode comparison; | |||
| @@ -140,7 +154,8 @@ void testModeTransitions() | |||
| LadderRung comparison_rung; | |||
| comparison_rung.id = "poll-comparison-rung"; | |||
| comparison_rung.name = "Poll comparison"; | |||
| comparison_rung.condition = ConditionExpression::fromNode(comparison); | |||
| comparison_rung.condition = conditionWithOutputWire( | |||
| comparison, "poll-comparison-output-wire"); | |||
| comparison_rung.output = comparison_coil; | |||
| logic.rungs.push_back(comparison_rung); | |||
| LogicNode move_input; | |||
| @@ -158,7 +173,8 @@ void testModeTransitions() | |||
| LadderRung move_rung; | |||
| move_rung.id = "poll-move-rung"; | |||
| move_rung.name = "Poll MOVE"; | |||
| move_rung.condition = ConditionExpression::fromNode(move_input); | |||
| move_rung.condition = conditionWithOutputWire( | |||
| move_input, "poll-move-output-wire"); | |||
| move_rung.output = move_output; | |||
| logic.rungs.push_back(move_rung); | |||
| LogicNode add_input; | |||
| @@ -181,7 +197,8 @@ void testModeTransitions() | |||
| LadderRung add_rung; | |||
| add_rung.id = "poll-add-rung"; | |||
| add_rung.name = "Poll ADD"; | |||
| add_rung.condition = ConditionExpression::fromNode(add_input); | |||
| add_rung.condition = conditionWithOutputWire( | |||
| add_input, "poll-add-output-wire"); | |||
| add_rung.output = add_output; | |||
| logic.rungs.push_back(add_rung); | |||
| project.controlLogics.push_back(logic); | |||