# 泳道图（Swimlane）

适用于：跨角色/跨系统的端到端流程（用户/网关/服务/存储/回调）、多泳道协作流程、系统交互链路图。

支持两种方向：
- **水平泳道**：泳道为横向条带（自上而下排列），流程从左到右推进
- **垂直泳道**：泳道为纵向列（自左向右排列），流程从上到下推进

## Content 约束

- 泳道数（lanes）建议 3-7，超过 7 会显著降低可读性；如必须更多泳道，优先合并同类或拆成两张图
- 阶段数（stages）建议 4-8；超过 8 优先合并相邻阶段或改成“代表性阶段”
- 每个阶段在每条泳道中最多放 1 个“主步骤卡片”；如同一阶段需要多个步骤，放在同一格内做纵向堆叠（2-3 个为上限）
- 节点文本 1-2 行为主；长文本用 `\n` 手动换行，避免单行超长导致卡片过宽
- 仅画必要连线：泳道图的结构已经表达了“属于哪个角色/系统 + 发生顺序”，连线只用于表达跨泳道交互、关键因果关系或异步事件流

## Layout 选型

| 模式 | 适用条件 | 特征 |
|------|---------|------|
| **水平泳道** | 默认推荐；流程天然左→右推进 | lanes=行，stages=列；跨泳道同一阶段严格 x 对齐 |
| **垂直泳道** | 用户明确要求竖版、或画布更适合纵向滚动阅读 | lanes=列，stages=行；跨泳道同一阶段严格 y 对齐 |

## Layout 规则

### 通用规则（两种方向都适用）

1. **网格对齐是第一优先级**：跨泳道同一阶段必须严格对齐（水平对齐 x；垂直对齐 y）。对齐通过“共享阶段标尺（stage ruler / stage slots）”实现，不靠肉眼估算，也不靠逐节点随意手写坐标
2. **只生成真实节点**：为保证跨泳道阶段严格对齐，所有阶段统一保留透明的 **stage cell**；仅在真实阶段的 cell 内生成卡片节点，并按阶段索引映射到对应槽位
3. **泳道底色**：为了增强层级感同时保持界面整洁，**强烈建议所有泳道容器统一使用极浅灰色背景**（如 `fillColor: "#F8F9FA"` 或 `"#FCFCFC"`）。边框使用浅灰色细虚线（`borderDash: "dashed"`, `borderWidth: 1`, `borderColor: "#DEE0E3"`）以明确边界。
4. **步骤卡片**：使用 `rect`。为建立清晰的视觉层级，卡片**必须填充浅色背景**（参考 `elements/style.md` 中的浅色板，如极浅的主题色），边框使用对应的主题主色（`borderWidth: 1-2`），文字使用深色（如 `#1F2329`）以确保可读性。统一圆角；宽高以可读为先，避免过窄导致换行过多
5. **间距**：只要存在 connector 连线，卡片之间的主轴间距必须满足 `gap >= 40`

### 子节点对齐

- **同一阶段必须严格对齐**：所有泳道复用同一套 stage slots；不允许靠卡片自身宽度或肉眼估算来对齐
- **卡片宽度一致**：同一泳道中的步骤卡片应保持统一宽度；推荐使用统一固定宽度，或严格复用同一槽位宽度
- **统一使用 stack 容器**：有内容的阶段统一使用 `layout: "vertical"` 的 stack frame（纵向堆叠 1-3 张卡片）；空阶段不生成 stack/卡片，但保留透明 cell 保证对齐
- **垂直居中但不影响对齐**：stage cell 默认 `alignItems: "stretch"`，可用 `justifyContent: "center"` 让卡片在 cell 内居中，以确保左右边界严格对齐
- **不靠底色区分行/列**：阶段网格默认不需要背景色；如需“轻微”的行/列边界提示，优先给 stage cell 加 1px 细边框（`fillColor: "transparent"` 仍保持视觉透明）

### Flex 栅格模式（默认）

- lane body 使用 Flex 布局：水平泳道用 `layout: "horizontal"`，垂直泳道用 `layout: "vertical"`
- 为每个阶段生成一个 **stage cell**（占位单元格）；空阶段的 cell 透明但保留；cell 内用 `layout: "vertical"` 的 stack 承载 1-3 张卡片
- 统一参数：`slotWidth: 180-220`（水平泳道 cell 宽度）、`slotHeight: 64-104`（垂直泳道 cell 高度建议档）、`gap: 40-56`（有连线时必须 ≥40）、`stackGap: 8`、`lanePadding: 16`
- 对齐规则：所有泳道复用同一组 `slotWidth/slotHeight/gap`；同一阶段在各泳道上使用相同的 cell 索引保证严格对齐
- 尺寸语义：lane body `width/height` 用 `"fit-content"`（Yoga 自适应）；卡片 `height: "fit-content"`；Flex 容器内不写子节点 `x/y`
- 内容密度：卡片文字 1-2 行；同阶段堆叠上限 2-3；超过上限优先拆分到相邻阶段或缩短文本

### 跨泳道间距（lanesGap）

- 根容器承载所有泳道：水平泳道用 `layout: "vertical"`，垂直泳道用 `layout: "horizontal"`
- 缩减跨泳道主轴间距 `lanesGap`（建议 `16-24`），以保持整体图表的紧凑性。避免 `lanesGap` 设置为 `0` 导致边框重叠变粗，也避免间距过大导致视觉涣散。
- 每条泳道作为根容器的子 frame，内部再使用上述 Flex 栅格的 stage cell 布局
- `lanesGap` 与 `lanePadding/stackGap` 独立；lane 内容增减不应影响跨泳道间距
- 4px 基线对齐：`lanesGap`、`lanePadding`、cell 尺寸建议按 4 的倍数对齐

### 水平泳道（lanes=行，stages=列）

- 根容器：`layout: "vertical"`，`gap: lanesGap` 固定；`alignItems: "stretch"`，标题在最上方
- 每条泳道：一个可见 frame（分组容器），内部用 `layout: "horizontal"` 分成两块：
  - 左侧 lane label：固定宽度 text（如 100-140），垂直居中；左对齐（`textAlign: "left"`）；title 需要比步骤卡片更醒目，优先通过 `fontSize: 18-20` + `fontWeight: "bold"` + 与泳道边框一致的 `textColor` 实现
  - 右侧 lane body：`layout: "horizontal"`，包含完整的阶段 **stage cell** 数组；cell 宽度固定为 `slotWidth`，相邻 cell 间 `gap` 统一；空阶段 cell 透明但保留
- 步骤卡片：推荐统一卡片宽度（如 160-220），并在所有泳道复用同一组 `slotWidth / gap`，保证跨泳道阶段严格 x 对齐

### 垂直泳道（lanes=列，stages=行）

- 根容器：`layout: "horizontal"`，`gap: lanesGap` 固定；`alignItems: "stretch"`，标题在最上方
- 每条泳道：一个可见 frame（分组容器），内部 `layout: "vertical"`：
  - 顶部 lane label：必须放在单独的 `lane label frame` 中，label frame 使用 `width: "fill-container"`、`alignItems: "center"`、`justifyContent: "center"`，并通过 `paddingTop` 留出与泳道上边的 gap（推荐 `12-16`，按 4px 基线取值，如 `padding: [12, 8, 8, 8]`）；内部 text 使用 `width: "fill-container"` + `textAlign: "center"`，确保 title 在整条泳道顶部**水平居中**
  - lane body：`layout: "vertical"`，包含完整的阶段 **stage cell** 数组；cell 高度固定为 `slotHeight`，相邻 cell 间 `gap` 统一；空阶段 cell 透明但保留
  - 内容居中对齐：stage cell 建议 `alignItems: "center"` + `justifyContent: "center"`，让卡片在每个 cell 内水平/垂直居中；卡片宽度不超过 `slotWidth`（或固定宽度），避免被 `"fill-container"` 拉伸导致“看起来不居中”
- 步骤卡片：推荐统一卡片高度或统一 `slotHeight / gap`，保证跨泳道阶段严格 y 对齐
- 泳道外层容器必须显式写 `fillColor: "#F8F9FA"`（极浅灰）、`borderDash: "dashed"`、`borderWidth: 1`、`borderColor: "#DEE0E3"`（统一浅灰色），否则会被编译为虚拟 frame 导致不渲染
- 统一高度（Flex 自适应，可选）：根容器使用 `alignItems: "stretch"`，每个泳道外层 frame 使用 `height: "fill-container"`；泳道内部仍保持 lane label + lane body 的结构

示例：

```json
{
  "version": 2,
  "nodes": [
    {
      "type": "frame",
      "id": "lanes-root",
      "x": 40, "y": 40,
      "layout": "horizontal",
      "gap": 16,
      "alignItems": "stretch",
      "children": [
        {
          "type": "frame",
          "id": "lane-left",
          "layout": "vertical",
          "width": "fit-content",
          "height": "fill-container",
          "fillColor": "#F8F9FA",
          "borderDash": "dashed",
          "borderWidth": 1,
          "borderColor": "#DEE0E3",
          "children": [
            { "type": "frame", "id": "lane-left-label-wrap", "layout": "vertical", "width": "fill-container", "height": "fit-content",
              "alignItems": "center", "justifyContent": "center", "padding": [12, 8, 8, 8], "children": [
                { "type": "text", "id": "lane-left-label", "text": "Lane Left", "width": "fill-container", "height": "fit-content",
                  "textAlign": "center", "verticalAlign": "middle", "fontSize": 18, "fontWeight": "bold", "textColor": "#5178C6" }
              ] },
            { "type": "frame", "id": "lane-left-body", "layout": "vertical",
              "gap": 40, "padding": 16,
              "children": [
                { "type": "frame", "id": "stage-1-cell-left", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center",
                  "children": [{ "type": "rect", "id": "c-s1", "width": 200, "height": "fit-content", "fillColor": "#E1EAFA", "borderColor": "#5178C6", "borderWidth": 2, "borderRadius": 8 }] },
                { "type": "frame", "id": "stage-2-cell-left", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center", "children": [] }
              ] }
          ]
        },
        {
          "type": "frame",
          "id": "lane-right",
          "layout": "vertical",
          "width": "fit-content",
          "height": "fill-container",
          "fillColor": "#F8F9FA",
          "borderDash": "dashed",
          "borderWidth": 1,
          "borderColor": "#DEE0E3",
          "children": [
            { "type": "frame", "id": "lane-right-label-wrap", "layout": "vertical", "width": "fill-container", "height": "fit-content",
              "alignItems": "center", "justifyContent": "center", "padding": [12, 8, 8, 8], "children": [
                { "type": "text", "id": "lane-right-label", "text": "Lane Right", "width": "fill-container", "height": "fit-content",
                  "textAlign": "center", "verticalAlign": "middle", "fontSize": 18, "fontWeight": "bold", "textColor": "#8569CB" }
              ] },
            { "type": "frame", "id": "lane-right-body", "layout": "vertical",
              "gap": 40, "padding": 16,
              "children": [
                { "type": "frame", "id": "stage-1-cell-right", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center", "children": [] },
                { "type": "frame", "id": "stage-2-cell-right", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center",
                  "children": [{ "type": "rect", "id": "d-s2", "width": 200, "height": "fit-content", "fillColor": "#EAE6F3", "borderColor": "#8569CB", "borderWidth": 2, "borderRadius": 8 }] }
              ] }
          ]
        }
      ]
    },
    { "type": "connector", "connector": { "from": "c-s1", "to": "d-s2",
          "lineShape": "polyline", "lineColor": "#BBBFC4", "lineWidth": 2, "endArrow": "arrow" } }
  ]
}
```

### 泳道配色（默认色板）

- **泳道背景**：所有泳道容器统一使用极浅灰色（如 `fillColor: "#F8F9FA"` 或 `"#FCFCFC"`），以增强物理容器的层级感，并突出内部的彩色卡片。
- **泳道边框**：所有泳道外层容器统一使用浅灰色细虚线（`borderColor: "#DEE0E3"`, `borderWidth: 1`, `borderDash: "dashed"`）。
- **泳道标题**：按 `elements/style.md` 经典色板为每条泳道分配不同的主题色，泳道 title 的 `textColor` 使用该主题色。
- **内容节点（rect）**：采用“浅色底 + 主题色边框”策略。`fillColor` 使用与该泳道主题色对应的极浅色（如浅蓝、浅紫等），`borderColor` 使用对应的主题色，文字 `textColor` 统一使用深色 `#1F2329`。
- **连线（connector）**：连线颜色固定为灰色 `#BBBFC4`，不随泳道颜色变化。当连线带有文字（`label`）时，为防止文字压在边框上难以阅读，必须为连线文字设置纯白背景（`labelFillColor: "#FFFFFF"`）遮挡底纹。

提醒：避免创建“虚拟 frame”（见 `elements/schema.md` 的说明）。lane 外层必须具有可见属性以避免在编译时被跳过。


## 连线规则（强制参考 connectors.md）

泳道图中所有连线的选择与写法必须严格遵循 `elements/connectors.md`，尤其是：
- `connector` 必须放在 `WBDocument.nodes` 顶层，不能嵌套在 `children`
- 默认优先使用自动绕线：`lineShape: "polyline"` / `"rightAngle"`，且不写 `waypoints`
- 未指定 `lineShape` 时默认使用 `"rightAngle"`
- 只有在必要时才强制锚点方向；锚点选择必须与节点相对位置一致
- 有连线时卡片间距必须满足 `gap >= 40`；如果连线包含文字（`label`），主轴间距必须 `gap >= 64`
- 带文字的连线必须设置 `labelFillColor: "#FFFFFF"` 遮挡底纹

泳道图语境下的落地约束：
- **默认不写锚点**，交给引擎自动推断；只有需要强制“左→右推进 / 上→下推进”时才写
- 需要表达“异步/事件流/推送”（如 SSE/Chunk）时：使用 `lineStyle: "dashed"` 并配合 `label` 说明语义；其他参数仍按 connectors.md
- 避免连接“仅用于布局且可能被优化掉的虚拟 frame”，尽量连接具体步骤卡片的节点 id（参考 `elements/schema.md` 的虚拟 frame 陷阱）

## 骨架示例

> 示例展示布局的结构与对齐方法；实际节点的样式满足当前布局规则的前提下参考 `elements/style.md`

- 水平泳道示例：

```json
{
  "version": 2,
  "nodes": [
    {
      "type": "frame",
      "id": "lanes-root",
      "x": 40,
      "y": 40,
      "layout": "vertical",
      "gap": 16,
      "alignItems": "stretch",
      "padding": 0,
      "width": "fit-content",
      "height": "fit-content",
      "children": [
        {
          "type": "frame",
          "id": "lane-a",
          "layout": "horizontal",
          "gap": 40,
          "padding": 16,
          "width": "fit-content",
          "height": "fill-container",
      "fillColor": "#F8F9FA",
      "borderDash": "dashed",
      "borderWidth": 1,
      "borderColor": "#DEE0E3",
          "children": [
        {
          "type": "text",
          "id": "lane-a-label",
          "text": "Lane A",
          "width": 120,
          "height": "fit-content",
          "textAlign": "left",
          "verticalAlign": "middle",
          "fontSize": 18,
          "fontWeight": "bold",
          "textColor": "#5178C6"
        },
        {
          "type": "frame",
          "id": "stage-1-cell-a",
          "layout": "vertical",
          "gap": 8,
          "padding": 0,
          "width": 200,
          "height": "fit-content",
          "fillColor": "transparent",
          "alignItems": "stretch",
          "justifyContent": "center",
          "children": [
                {
                  "type": "rect",
                  "id": "a-s1",
                  "width": "fill-container",
                  "height": "fit-content",
                  "fillColor": "#E1EAFA",
                  "borderColor": "#5178C6",
                  "borderWidth": 2,
                  "borderRadius": 8,
                  "text": "[阶段 1 节点]",
                  "fontSize": 14,
                  "textColor": "#1F2329",
                  "textAlign": "center",
                  "verticalAlign": "middle"
                }
              ]
            },
        {
          "type": "frame",
          "id": "stage-2-cell-a",
          "layout": "vertical",
          "gap": 8,
          "padding": 0,
          "width": 200,
          "height": "fit-content",
          "fillColor": "transparent",
          "alignItems": "stretch",
          "justifyContent": "center",
          "children": []
        }
          ]
        },
        {
          "type": "frame",
          "id": "lane-b",
          "layout": "horizontal",
          "gap": 40,
          "padding": 16,
          "width": "fit-content",
          "height": "fill-container",
      "fillColor": "#F8F9FA",
      "borderDash": "dashed",
      "borderWidth": 1,
      "borderColor": "#DEE0E3",
          "children": [
        {
          "type": "text",
          "id": "lane-b-label",
          "text": "Lane B",
          "width": 120,
          "height": "fit-content",
          "textAlign": "left",
          "verticalAlign": "middle",
          "fontSize": 18,
          "fontWeight": "bold",
          "textColor": "#8569CB"
        },
        {
          "type": "frame",
          "id": "stage-1-cell-b",
          "layout": "vertical",
          "gap": 8,
          "padding": 0,
          "width": 200,
          "height": "fit-content",
          "fillColor": "transparent",
          "alignItems": "stretch",
          "justifyContent": "center",
          "children": []
        },
        {
          "type": "frame",
          "id": "stage-2-cell-b",
          "layout": "vertical",
          "gap": 8,
          "padding": 0,
          "width": 200,
          "height": "fit-content",
          "fillColor": "transparent",
          "alignItems": "stretch",
          "justifyContent": "center",
          "children": [
                {
                  "type": "rect",
                  "id": "b-s2",
                  "width": "fill-container",
                  "height": "fit-content",
                  "fillColor": "#EAE6F3",
                  "borderColor": "#8569CB",
                  "borderWidth": 2,
                  "borderRadius": 8,
                  "text": "[阶段 2 节点]",
                  "fontSize": 14,
                  "textColor": "#1F2329",
                  "textAlign": "center",
                  "verticalAlign": "middle"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "type": "connector",
      "connector": {
        "from": "a-s1",
        "to": "b-s2",
        "lineShape": "polyline",
        "lineColor": "#BBBFC4",
        "lineWidth": 2,
        "endArrow": "arrow",
        "label": "[跨泳道交互]",
        "labelFillColor": "#FFFFFF"
      }
    }
  ]
}
```

- 垂直泳道示例：见上文“垂直泳道”

- 全泳道统一 `slotWidth/slotHeight/gap`，并为每个阶段生成占位 **stage cell**（空阶段 cell 透明但保留）
- Flex 容器内不写子节点 `x/y`；对齐通过 cell 索引与统一尺寸实现
- 只有真实阶段才在对应 cell 内生成卡片；空阶段不生成卡片但保留 cell 保证网格完整
- 连线必须放在 `nodes` 顶层，并连接具体步骤卡片 id，不要连接 `lane-*-body` 这类布局容器
- **水平泳道**：根容器用 `layout: "vertical"` 固定 `lanesGap`；lane body 用 `layout: "horizontal"`；cell 固定宽度 `slotWidth`；主轴 `gap` 统一
- **垂直泳道**：根容器用 `layout: "horizontal"` 固定 `lanesGap`；lane body 用 `layout: "vertical"`；cell 固定高度 `slotHeight`；主轴 `gap` 统一
- **泳道 title**：title 比步骤卡片更醒目，但仍只用字号、字重、文字色强调；不要给泳道 title 额外加背景条

## 陷阱

- **各泳道复用的 stage slots 不一致**：会导致同阶段错位；`slotWidth / slotHeight / gap` 必须全泳道统一
- **把 connector 放进 children**：会导致 schema 报错或无法连线（见 connectors.md）
- **把辅助容器画成可见元素**：lane body 或其他支撑 frame 必须保持 `fillColor: "transparent"`，除泳道分组容器外不要额外加边框
- **手写 waypoints 过早**：先让引擎自动绕线；只有在必要时才通过 waypoints 接管
- **连线过多**：按 connectors.md 的连线数量策略降采样，否则跨泳道线会互相遮挡导致不可读
