EdgeView 与边类型
`EdgeView` 是边的渲染视图对象。它把一条 `Edge`(数据)+ 源/目标节点的几何信息,转化成屏幕上的一条带箭头折线。本节聚焦于它的结构、边类型分派,以及「逻辑坐标算路径、整体变换到屏幕」的核心策略。
EdgeView 的构成
`EdgeView` 不持有独立状态,而是渲染时根据 `Edge` + 节点几何临时构造:
pub struct EdgeView<'a> {
pub edge: &'a Edge,
pub from_pos: PointF, // 源端口中心(逻辑坐标)
pub to_pos: PointF, // 目标端口中心(逻辑坐标)
pub from_side: PortSide, // 源端口朝向
pub to_side: PortSide, // 目标端口朝向
pub edge_type: EdgeType, // 路径算法
pub theme: &'a Theme,
pub scale: f32, // 视口缩放
pub offset: PointF, // 视口偏移
}
`from_side` / `to_side` 由 `FlowEditorView::port_sides()` 给出,与布局方向一致。横向布局时 `(Right, Left)`,纵向时 `(Bottom, Top)`。
EdgeType 路径算法
pub enum EdgeType {
Straight, // 直线
SmoothStep, // 折线 + 圆角(默认)
Bezier, // 三次贝塞尔
Step, // 直角折线(无圆角)
}
各类型的路径生成在 `edge_geometry.rs` 中实现,全部在逻辑坐标运算:
| 类型 | 几何 | 适用场景 | |------|------|----------| | `Straight` | 两点直线 | 简单关系图 | | `Step` | 直角折线,中点转折 | 工程示意图 | | `SmoothStep` | 折线 + 圆角 + step gap | 默认,平衡美观与清晰 | | `Bezier` | 三次贝塞尔曲线 | 自由流动视觉 |
`default_edge_type` 默认 `SmoothStep`,可在 `FlowEditorView` 构造后用 `set_edge_type` 修改(影响所有边)。
SmoothStep 的几何细节
SmoothStep 是最常用的类型,它的路径由「水平/垂直段 + 圆角」组成:
源端口 ●──────┐
│
└──────● 目标端口
关键参数:
| 参数 | 含义 | |------|------| | `step_gap` | 水平段的最小长度,避免圆角挤在一起 | | `corner_radius` | 圆角半径 | | `from_side` / `to_side` | 决定第一段与最后一段的走向 |
算法骨架(逻辑坐标):
fn smooth_step_points(from: PointF, from_side: PortSide,
to: PointF, to_side: PortSide) -> Vec<PointF> {
let mut pts = vec![from];
let mid = midpoint_with_gap(from, to, from_side, to_side, STEP_GAP);
// 根据 from_side / to_side 决定转折点
pts.push(/* 第一转折点 */);
pts.push(/* 第二转折点 */);
pts.push(to);
// 圆角在 PathBuilder 阶段用 arc/quadratic 加入
pts
}
圆角不是「点」,而是在 `PathBuilder` 画线时对每个转折点用二次贝塞尔或弧线过渡。这保证了缩放后圆角依然光滑。
渲染流程:逻辑路径 → 屏幕路径
impl<'a> EdgeView<'a> {
pub fn render(&self) -> impl IntoElement {
// 1. 逻辑坐标下计算路径点
let logical_pts = match self.edge_type {
EdgeType::Straight => vec![self.from_pos, self.to_pos],
EdgeType::Step => step_points(/* ... */),
EdgeType::SmoothStep => smooth_step_points(/* ... */),
EdgeType::Bezier => bezier_control_points(/* ... */),
};
// 2. PathBuilder 在逻辑坐标构建路径(含圆角)
let mut path = PathBuilder::new();
path.move_to(logical_pts[0]);
// ... line_to / quad_to / arc_to ...
// 3. 整体变换到屏幕空间
path.scale(self.scale, self.scale);
path.translate(self.offset.x, self.offset.y);
// 4. 线宽手动乘 scale
let stroke = BASE_STROKE_WIDTH * self.scale;
// 5. 画线 + 箭头 marker
svg()
.path(path.build())
.stroke(self.theme.edge_color)
.stroke_width(stroke)
.marker_end(arrow_marker(self.theme, self.scale))
}
}
为什么是「先算后变换」而不是「逐点乘 scale」?因为圆角、贝塞尔的控制点关系在缩放下会变形——逐点乘 scale 会让圆角变椭圆。整体 `PathBuilder::scale` 是均匀缩放,保持形状。
箭头 marker
箭头画在 `to_pos` 处,方向由 `to_side` 决定(指向端口):
to_side = Left: ──►●
to_side = Right: ●◄──
to_side = Top: ▲
to_side = Bottom: ▼
箭头大小也乘 scale,保证放大后成比例。箭头颜色取 `theme.edge_color` 或 `theme.edge_arrow`。
边的颜色
边的颜色由几个因素决定:
| 条件 | 颜色 | |------|------| | 普通边 | `theme.edge_color` | | `EdgeKind::LoopBack` | `theme.edge_loop_back` | | `hovered_plus == Some(edge_id)` | `theme.edge_hover` | | `DrawingEdge` 中的临时边 | `theme.edge_drawing`(虚线) |
着色在 `EdgeView::render` 里根据传入的 `theme` 与边元信息决定。
step gap 的作用
`step_gap` 解决「源端口与目标端口距离太近时圆角挤变形」的问题。当两点水平距离小于 `2 * step_gap`,算法会让中段折线退化为更简单的形状,避免画出「Z 字形」毛刺。
距离足够: 距离过近(无 gap 处理):
●──────┐ ●─┐
│ │ ┌──● ← 毛刺
└──────● └─┘
完整渲染顺序
EdgeView::render
│
├─ 1. 逻辑坐标算路径点(smooth_step_points 等)
├─ 2. PathBuilder 构建路径(含圆角)
├─ 3. path.scale(s, s) + path.translate(offset)
├─ 4. stroke_width *= s
├─ 5. 选颜色(edge_color / loop_back / hover / drawing)
└─ 6. svg().path().stroke().marker_end(arrow)
小结
`EdgeView` 把一条 `Edge` + 节点几何 + 视口参数,转化成屏幕上的折线。路径算法在逻辑坐标完成(SmoothStep 用 step gap + 圆角保证美观),再用 `PathBuilder::scale + translate` 整体变换到屏幕,线宽与箭头单独乘 scale。颜色按边类型与 hover 态选取。下一节讲一种特殊的边——循环回环,它有自己的路由算法。
下一节:[循环回环边](loop-back.md)