# 前端交互与状态机

本文档记录当前前端真实交互规则，重点覆盖那些不会直接体现在接口契约里的 UI 细节。测试以本文件、`doc/07-current-requirements-freeze.md` 和 `doc/09-test-plan.md` 为准。

## 全局状态

| 状态字段 | 所在文件 | 含义 |
|----------|----------|------|
| `activeModule` | `src/store/useStore.ts` | 当前页面模块；登录后默认 `dashboard`。 |
| `currentProject` / `frames` / `currentFrameIndex` | `src/store/useStore.ts` | 当前工作项目、帧序列和当前帧。 |
| `activeTool` | `src/store/useStore.ts` | 工作区当前工具。 |
| `selectedMaskIds` | `src/store/useStore.ts` | 当前选中的 mask id 列表；Canvas、本体面板和 AI 页共享。 |
| `activeTemplateId` / `activeClass` | `src/store/useStore.ts` | 当前模板和后续新建 mask 使用的语义类别。 |
| `maskHistory` / `maskFuture` | `src/store/useStore.ts` | 撤销/重做栈。 |

## 工作区工具自动机

| 状态 | 进入事件 | 可用动作 | 退出事件 | 测试 |
|------|----------|----------|----------|------|
| `idle/no-selection` | 初始、`Esc`、左侧“取消选中”、删除 mask、切帧无对应传播结果 | 右侧语义树只设置后续新建类别；清空遮罩作用于当前帧全部 mask | 点击 mask、AI 推送、创建新 mask | `CanvasArea.test.tsx`、`OntologyInspector.test.tsx` |
| `mask-selected` | `move/edit_polygon` 下点击 mask、新建 mask 完成、AI 候选选中 | 右侧语义树给已选 mask 换类；Delete/Backspace/DEL 删除；橡皮擦可扣除；顶点可编辑；创建工具的新几何会并入当前 mask | `Esc`、左侧“取消选中”、删除 mask、切帧无对应传播结果 | `CanvasArea.test.tsx` |
| `polygon-drawing` | `create_polygon` 下点击画布 | 继续加点；三点后 Enter 或点击首点闭合 | Enter/首点在有选中 mask 时并入选中 mask，无选中时创建新 mask 并显示边界点；`Esc` 放弃临时点并清选区 | `CanvasArea.test.tsx` |
| `shape-dragging` | `create_rectangle/create_circle` 下按下鼠标 | 拖拽预览形状 | 鼠标释放时有选中 mask 则并入选中 mask，无选中时创建新 mask 并显示边界点；切工具取消临时状态 | `CanvasArea.test.tsx` |
| `brush-stroking` | `brush` 且已有 `activeClass` 或当前选中 mask 时按下鼠标 | 采样图像范围内圆形笔触 | 鼠标释放时有选中 mask 则并入选中 mask，无选中时创建新的当前类别 mask；闭合成中空区域时保留内洞 ring；图外落笔不创建；`Esc` 取消笔触和选区 | `CanvasArea.test.tsx` |
| `eraser-stroking` | `eraser` 且已有选中 mask 时按下鼠标 | 采样图像范围内圆形笔触 | 鼠标释放从选中 mask 扣除；扣空则删除该 mask；`Esc` 取消笔触和选区 | `CanvasArea.test.tsx` |
| `boolean-selecting` | `area_merge/area_remove` | 选择多个 mask；主区域黄色实线，参与区域红色虚线 | 当前帧执行、所有传播帧、按帧范围、取消、切换工具 | `CanvasArea.test.tsx`、`VideoWorkspace.test.tsx` |

### 细节规则

- `Esc` 是取消当前交互状态，不是删除：清空 `selectedMaskIds`、临时多边形点、矩形/圆拖拽状态、画笔/橡皮擦笔触和顶点选择；保留已有 mask、当前 `activeClass` 和当前工具。
- 切换到 `create_polygon`、`create_rectangle`、`create_circle` 会保留旧 mask 选区；用户若想新建独立 mask，需要先按 `Esc` 或点击“取消选中”。
- 多边形、矩形、圆和画笔创建完成后，有选中 mask 时会并入选中 mask，无选中 mask 时会自动选中新创建的 mask。
- 画笔和形状创建遵循同一规则：有选中 mask 时并入选中 mask，没有选中 mask 时才新建独立 mask。
- 橡皮擦只作用于当前选中 mask，不会在无选区时启动。
- 绘制类工具点击已有 mask 时继续绘制，不触发 mask 选择。

## 右侧语义分类树自动机

| 状态 | 点击分类结果 | 后续效果 | 测试 |
|------|--------------|----------|------|
| 无选中 mask | 仅更新 `activeClass` | 后续新建 mask 使用该类别；已有 mask 不变 | `OntologyInspector.test.tsx` |
| 有选中 mask | 更新已选 mask 的 class/label/color；同传播链对应 mask 同步更新 | 已保存 mask 标记为 dirty；已选 mask 移到前端渲染数组末尾 | `OntologyInspector.test.tsx` |
| 当前 mask 的类别被删除 | 工作区回显时降级为 `maskid:0` “待分类” | 保留几何并等待用户重新分类保存 | `VideoWorkspace.test.tsx` |

## 键盘交互

| 按键 | 前置状态 | 行为 | 测试 |
|------|----------|------|------|
| `Esc` / 左侧“取消选中” | 任意 Canvas 工具 | 取消选中 mask 和临时绘制状态，不删除 mask，不清 active class | `CanvasArea.test.tsx`、`ToolsPalette.test.tsx` |
| `Enter` | 多边形已有至少 3 点 | 闭合并创建新 mask | `CanvasArea.test.tsx` |
| `Delete/Backspace` | 选中顶点 | 删除该顶点，保持 polygon 至少 3 点 | `CanvasArea.test.tsx` |
| `Delete/Backspace` | 选中整块 mask | 删除 mask；传播链 mask 走范围确认；人工/AI 帧按确认策略处理 | `CanvasArea.test.tsx`、`VideoWorkspace.test.tsx` |
| `Ctrl/Cmd+Z` | 工作区且非输入控件聚焦 | 撤销 mask 历史 | `VideoWorkspace.test.tsx`、`keyboardShortcuts.test.ts` |
| `Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` | 工作区且非输入控件聚焦 | 重做 mask 历史 | `VideoWorkspace.test.tsx`、`keyboardShortcuts.test.ts` |
| 左/右方向键 | 工作区时间轴 | 切换上一帧/下一帧 | `VideoWorkspace.test.tsx` |

## 工作区范围选择自动机

`VideoWorkspace` 用 `rangeSelectionMode` 区分四类范围选择：`propagation`、`export`、`boolean`、`clear`。

| 模式 | 进入事件 | 顶栏状态 | 时间轴行为 | 确认行为 | 测试 |
|------|----------|----------|------------|----------|------|
| `propagation` | 左侧“自动传播” | 显示传播权重、向前/向后帧数和“开始传播” | 拖拽/点击设置传播起止帧 | 保存参考帧 draft/dirty seed，提交 Celery 传播任务 | `VideoWorkspace.test.tsx` |
| `export` | 打开导出菜单并选择“特定范围帧” | 导出菜单保持打开 | 拖拽/点击设置导出起止帧 | “开始导出”保存待归档 mask 后下载 ZIP | `VideoWorkspace.test.tsx` |
| `boolean` | 区域合并/去除选择“按帧范围选择” | 显示“确认区域合并/确认重叠区域去除” | 拖拽/点击设置布尔操作范围 | 弹最终确认，只同步范围内对应传播帧，保留传播 metadata | `CanvasArea.test.tsx`、`VideoWorkspace.test.tsx` |
| `clear` | 清空/DEL 选择“按帧范围选择” | 显示“确认清空” | 拖拽/点击设置清空范围 | 弹最终确认；如范围含人工/AI 帧，再询问是否删除这些帧 | `VideoWorkspace.test.tsx` |

取消规则：

- 关闭导出菜单会退出 `export` 范围选择。
- 在布尔范围确认前重新点击“合并选中/从主区域去除”，会取消旧的顶栏范围请求。
- 清空/删除传播链时选择“取消”不会删除任何 mask。

## AI 智能分割自动机

| 状态 | 进入事件 | 主要行为 | 退出事件 | 测试 |
|------|----------|----------|----------|------|
| `no-prompt` | 打开 AI 页或清空提示 | 等待正点/负点/框选 | 放置提示或框选 | `AISegmentation.test.tsx` |
| `box-prompt` | 框选完成 | 仅框选时发送 `box` prompt | 加正/负点后转 interactive | `AISegmentation.test.tsx` |
| `interactive-prompt` | 框选后加点或直接点选 | 发送累计正/负点；负点启用背景过滤 | 空结果移除旧候选 | `AISegmentation.test.tsx` |
| `candidate-selected` | 推理返回 mask 或点击候选 | 可通过语义树换标签；可删除候选 | 推送工作区、删除候选、重新推理 | `AISegmentation.test.tsx` |
| `send-blocked` | 候选缺少语义分类时点击推送 | 显示 error toast，不切模块、不改工具 | 选择语义分类 | `AISegmentation.test.tsx` |

## 模板与项目确认流

| 交互 | 状态机 | 测试 |
|------|--------|------|
| 切换激活模板 | 无 mask 直接切换；有任意 mask 时弹确认；确认后删除项目所有本地/后端标注再切换；取消则保持原模板 | `OntologyInspector.test.tsx` |
| 删除模板 | 站内确认后删除；系统默认模板可由演示恢复出厂设置恢复 | `TemplateRegistry.test.tsx`、后端模板/管理员测试 |
| 复制模板 | 鼠标点击复制入口，生成当前用户私有副本并保留分类颜色、maskid 和层级 | `TemplateRegistry.test.tsx` |
| 项目复制 | 项目删除按钮旁复制入口；可选“新项目重置”或“全内容复制” | `ProjectLibrary.test.tsx` |
| 演示恢复出厂设置 | 管理员危险区二次确认并要求输入 `RESET_DEMO_FACTORY`；后端也校验 confirmation | `UserAdmin.test.tsx`、`backend/tests/test_admin.py` |

## 文件与导入导出交互

| 交互 | 状态机 | 测试 |
|------|--------|------|
| 视频/DICOM 上传 | 选择文件后显示上传进度；DICOM 显示有效文件数量；上传后继续轮询解析任务进度 | `ProjectLibrary.test.tsx` |
| 显式生成帧 | 只对视频项目显示；项目名称编辑状态不显示；DICOM 项目不显示；入队后轮询解析任务，成功后刷新项目列表并立即显示新封面 | `ProjectLibrary.test.tsx` |
| GT Mask 导入 | 选择文件后预览并选择未知 maskid 策略；非法格式返回错误；尺寸不一致最近邻拉伸；导入结果与普通 mask 同体验 | `VideoWorkspace.test.tsx`、后端 AI 测试 |
| 分割结果导出 | 默认当前帧；可选整体/范围；范围可用时间轴；导出前保存待归档 mask；按钮带导出图标和绿色强调背景 | `VideoWorkspace.test.tsx`、`api.test.ts`、后端导出测试 |

## 维护要求

新增或修改前端交互时，应同步做三件事：

1. 更新本文件中对应状态机或规则。
2. 在 `doc/09-test-plan.md` 的覆盖矩阵中写明测试归属。
3. 添加或更新组件测试，至少覆盖状态转移的进入条件、退出条件和副作用。
