渲染生命周期

从数据变更到画面刷新

完整的渲染链路:

数据变更(增删节点/编辑属性)
  ↓
sync_node_sizes()       // 同步节点尺寸到 schema 渲染尺寸
  ↓
relayout()              // dagre 重新排版
  ├─ 更新 cached_body_groups    // 循环体分组
  ├─ 更新 cached_all_body_nodes // 扁平化
  └─ 更新 cached_hidden_nodes   // 收起的节点
  ↓
cx.notify()             // 标记实体为脏
  ↓
render()                // GPUI 调度重绘
  ├─ ensure_panel_view()        // 确保面板实体存在
  ├─ render_edges()             // 边(Canvas 层)
  ├─ render_nodes()             // 节点(Content 层)
  ├─ render_edge_plus_buttons() // 「+」按钮层
  ├─ render_toolbar()           // 工具栏
  └─ panel_view.render()        // 属性面板

relayout 详解

`relayout` 是布局的核心入口,在图结构变化时调用:

fn relayout(&mut self) {
    // 1. 同步节点尺寸
    self.sync_node_sizes();

    // 2. 运行 dagre 布局
    let result = DagreLayout::new().layout(&self.graph, dir);
    for (node_id, pos) in &result.positions {
        self.graph.node_mut(*node_id).position = *pos;
    }

    // 3. 更新缓存
    self.cached_body_groups = self.graph.loop_body_groups();
    self.cached_all_body_nodes = /* 扁平化 */;
    self.cached_hidden_nodes = /* 收起的循环体 */;
}

**为什么需要 sync_node_sizes**:结构化节点(如 Condition)的渲染高度随条件项数量变化,但 `node.size.h` 可能在创建后未更新。布局前同步确保 dagre、命中测试、回环边边界计算使用正确尺寸。

sync_node_sizes 的优化

每次按键都调用 `sync_node_sizes`(遍历所有节点)+ `relayout`(运行 dagre)代价过高。`update_node_size_if_changed` 是**单节点优化**路径:

fn update_node_size_if_changed(&mut self, node_id: NodeId) -> bool {
    // 只检查并更新单个节点的渲染尺寸
    // 返回尺寸是否变化 → 仅变化时才触发 relayout
}

`SetData` 路径(属性面板编辑)用此方法:只有结构化节点的尺寸真正变化时才重排,普通节点(Action)的文本编辑不会触发 dagre。

render 的分层组装

`FlowEditorView::render` 将画布分为多个图层:

外层 flex 容器(画布 + 分隔条 + 面板)
└─ canvas(flex-1)
   ├─ edges 层(Canvas,直接渲染 SVG path)
   ├─ content 层(绝对定位 div,含所有节点)
   ├─ edge_plus_buttons 层(「+」按钮,交互时隐藏)
   ├─ plus_tooltip 层
   ├─ toolbar 层
   └─ node_picker 层(AddingNodeFromEdge 浮层)

**分层原因**:

- 边在节点**下方**(先渲染边再渲染节点,z-order 自然正确) - 「+」按钮在节点**上方**(可点击) - 工具栏与浮层在最顶层

ensure_panel_view 的生命周期

属性面板是独立实体,生命周期跟随选中状态:

fn ensure_panel_view(&mut self, entity, window, cx) -> Option<...> {
    if self.selected.is_none() {
        self.panel_view = None;  // 取消选中时销毁
        return None;
    }
    if self.panel_view.is_none() {
        // 创建面板(按节点 kind 分发 Generic/Start)
        self.panel_view = Some(create_panel(...));
    }
    // 同步节点数据到面板(防回环)
    panel.sync_from_node(node, window, cx);
}

面板创建后每帧调用 `sync_from_node`——但内部有**快速路径**:若 `node.data` 未变化直接返回,避免每帧更新所有 InputState。

缓存的更新时机

| 缓存 | 更新时机 | 用途 | |------|----------|------| | `cached_body_groups` | relayout 末尾 | 渲染/命中测试循环体 | | `cached_all_body_nodes` | relayout 末尾 | 判断节点是否在循环体内 | | `cached_hidden_nodes` | relayout 末尾 | 跳过收起节点的渲染与连线 |

拖动/平移等**不改变图结构**的交互不会触发 relayout,缓存保持有效——避免每帧 O(V+E) 遍历。

触发 relayout 的场景

| 场景 | 触发方式 | |------|----------| | `auto_layout()` | 直接调用 relayout | | `set_graph()` | 替换图后 relayout | | `insert_node_at_edge()` | 拆边插入后 relayout | | `delete_node()` | 删除+桥接后 relayout | | `set_layout_direction()` | 切换方向后 relayout | | 属性面板编辑结构化字段 | `update_node_size_if_changed` 返回 true 时 relayout |

小结

渲染生命周期是 `sync_node_sizes → relayout → notify → render` 的循环。relayout 统一更新布局与缓存,render 分层组装画面。`update_node_size_if_changed` 提供单节点优化路径,避免每次按键都全量重排。面板实体随选中状态创建/销毁,sync_from_node 有快速路径防抖。

下一节:[命中测试交互模型](hit-test-interaction.md)