Condition 条件分支
结构化布局的首次登场
Condition 是第一个**结构化节点**——节点内部有按数据动态变化的行结构。它打破了「标题栏 + 单一主体」的二段式,引入「标题栏 + N 条件行 + 1 else 行」的多段式。
横向布局(左进右出):
┌───────────────────────────────┐
[In]│ ◆ Condition │ 标题栏 h=TITLE_H=36
├───────────────────────────────┤
│ If amount > 100 [if_0]→│ 条件行 h=ITEM_H=36
│ If user.is_admin [if_1]→│ 条件行 h=ITEM_H=36
│ Else [else]→│ 兜底行 h=ITEM_H=36
└───────────────────────────────┘
纵向布局(上进下出):
In
↓
┌─Condition──┐
│ if amount │
│ if user │
│ else │
└─┬──┬──┬────┘
↓ ↓ ↓ 底部均布出口(if_0 最左,else 最右)
关键常量与数据格式
| 常量 | 值 | 用途 | |------|-----|------| | `TITLE_H` | 36 | 标题栏高度 | | `ITEM_H` | 36 | 每个条件/else 行高度 |
数据格式:
{
"label": "Check",
"conditions": [
{ "id": "if_0", "label": "amount > 100" },
{ "id": "if_1", "label": "user.is_admin" }
],
"collapsed": false
}
`conditions` 数组长度决定 `if_i` 端口数量;`collapsed` 控制收起态。schema 默认 2 条件 + 1 else,默认高度 `TITLE_H + ITEM_H * 3 = 144`。
四个覆写方法
Condition 是覆写 `IFlowNode` 可选方法最多的内置节点:
| 方法 | 覆写 | 原因 | |------|------|------| | `ports_for_node` | 是 | if_i 端口随 conditions 数组动态生成 | | `port_position` | 是 | if_i 端口需对齐条件行,else 对齐兜底行 | | `content_size` | 是 | 高度 = `TITLE_H + ITEM_H * n_branches`,随条件数变化 | | `plus_button_at_target` | 否 | 出口虽多但端口在节点右侧分散,按钮聚集不严重 |
ports_for_node:动态端口
fn ports_for_node(&self, node: &Node) -> Vec<PortSpec> {
let conditions = get_conditions(node);
let mut ports = vec![
PortSpec::new("in", PortDirection::In, PortSide::Auto),
PortSpec::new("else", PortDirection::Out, PortSide::Auto),
];
for (id, _) in &conditions {
ports.push(PortSpec::new(id.as_str(), PortDirection::Out, PortSide::Auto));
}
ports
}
端口列表 = `in` + `else` + 每个条件一个 `if_i`。这是 Condition 区别于 Action 的核心——端口拓扑随数据变化。
content_size:高度随条件数推导
fn content_size(&self, node: &Node) -> SizeF {
let h = if is_collapsed(node) {
TITLE_H + ITEM_H // 收起:标题栏 + 主体(提示条件数)
} else {
TITLE_H + ITEM_H * n_branches(node) as f32 // 展开:标题栏 + N 条件行 + 1 else 行
};
SizeF::new(node.size.w, h)
}
`n_branches = conditions.len() + 1`(else 占一行)。收起态高度固定为 `TITLE_H + ITEM_H`(与 Action 同规格),展开态高度随条件数线性增长。
port_position:精确对齐视觉行
横向布局下,每个 `if_i` 端口必须对齐第 i 行条件项的垂直中心:
"if_0" => {
let idx = 0;
let y = node.position.y + TITLE_H + ITEM_H * (idx as f32 + 0.5); // 第 0 行中心
Some(PointF::new(right, y)) // 节点右边缘
}
"else" => {
let y = node.position.y + TITLE_H + ITEM_H * (n_cond as f32 + 0.5); // 最后一行中心
Some(PointF::new(right, y))
}
纵向布局下,所有出口(if_i + else)沿底部均匀分布,不重叠:
// if_i 在底部,按 (i + 0.5) / n_branches 比例定位
let t = (idx as f32 + 0.5) / n_br as f32;
let x = left + node.size.w * t;
Some(PointF::new(x, bottom))
// else 在底部最右,按 (n_cond + 0.5) / n_branches 比例定位
let t = (n_cond as f32 + 0.5) / n_br as f32;
let x = left + node.size.w * t;
Some(PointF::new(x, bottom))
`(i + 0.5) / n` 的 0.5 偏移确保端口在「格子的中心」而非边缘——n 个端口将节点宽度等分为 n 段,每段中心放一个端口。
收起态的特殊处理
收起时 Condition 退化为「标题栏 + 单一主体」结构,与 Action 同规格:
收起态:
┌───────────────────────────────┐
[In]│ ◆ Condition [▷] │ 标题栏
├───────────────────────────────┤
│ 2 条件 │ 主体显示条件数
└────────────────[else]→────┘ 单一出口(用 else 位置)
`port_position` 在收起态把所有 out 端口(if_0/if_1/.../else)合并到节点边缘垂直中心:
if collapsed {
return match port_id.as_str() {
"in" => /* 左中心 / 顶中心 */,
_ => /* 右中心 / 底中心(所有 out 合并) */,
};
}
这样收起时连到 if_0 的边和连到 else 的边端点重合——视觉上 Condition 退化为单出口节点,但数据上边的 `source_port` 仍保留,展开后恢复原端点。
toggle 按钮的 kind 感知
`handle_node_action` 对 `ToggleCollapse` 的处理:
let key = if node.kind == "loop" { "body_collapsed" } else { "collapsed" };
Condition 用 `collapsed` 字段,Loop 用 `body_collapsed`——因为 Loop 收起的是外部循环体节点,不是自身内容。这是编辑器少数感知 kind 的地方。
`collapsed` 字段写入后,`sync_node_sizes` + `relayout` 重排——因为收起/展开改变了节点高度,dagre 需要重新计算周围节点位置。
get_conditions 的容错
fn get_conditions(node: &Node) -> Vec<(String, String)> {
node.data.get("conditions")
.and_then(|v| v.as_array())
.map(|arr| /* 解析 (id, label) */)
.unwrap_or_else(|| vec![
("if_0".to_string(), String::new()),
("if_1".to_string(), String::new()),
])
}
数据缺失或格式错误时回退到默认 2 条件——保证节点始终可渲染。这是内置节点的防御性编程范式:**永远假设 `node.data` 可能不完整**。
端口颜色语义
| 端口 | ring/dot 颜色 | 含义 | |------|--------------|------| | `in` | `node_in_ring`/`node_in_dot` | 靛蓝(输入) | | `if_i` | `node_out_ring`/`node_out_dot` | 橙色(输出,条件命中) | | `else` | `node_out_ring`/`node_out_dot` | 橙色(输出,兜底) |
Condition 的 if_i 和 else 用相同颜色——它们语义上都是「出口」,只是触发条件不同。颜色区分方向(in/out),不区分分支类型。
小结
Condition 是首个结构化节点:标题栏 + N 条件行 + 1 else 行的多段式布局。它覆写了 `ports_for_node`(动态端口)、`port_position`(行对齐)、`content_size`(高度随条件数)三个方法。收起态退化为单出口节点,所有 out 端口合并到边缘中心。纵向布局下出口沿底部 `(i+0.5)/n` 均匀分布,避免重叠。`get_conditions` 的容错回退保证节点始终可渲染。
下一节:[Loop 循环迭代](loop-iteration.md)