# P1 粗路径 Clumsy UI 集成设计

## 目标

在不改变 P0 地图、Hybrid A* 与碰撞安全语义的前提下，为 Clumsy 增加可手动运行的粗路径场景测试。使用者能够从 MovementTest 列表启动固定场景，或传入 AMR 当前世界位姿并手动输入终点；界面显示栅格地图、起点、终点、连续路径、换向点和扩大车辆矩形检查点，并可随时停止正在进行的规划。

本设计只覆盖 P1 的首个交付：场景工厂、后台 MovementTest、Painter 可视化、自动化集成检查和模块 README。Release 性能基准属于 P1 的下一项交付；旧 TrapMap 迁移、旧验证脚本和旧入口的清理按已确认范围排除。

## 既有边界

- 业务入口仍唯一为 `CoarsePathPlanningService.Plan(job, cancellationToken)`；MovementTest 不自行拼接 `PlanningMapFactory`、`HybridAStarPlanner`、栅格化器、碰撞器、原语或搜索节点。
- `PlanningMapRequest` 的边界、分辨率和障碍物几何使用 mm；`CoarsePathPlanningJob` 位姿、车辆尺寸和路径点使用 m，航向使用 rad。
- 项目传入的 AMR 位姿采用世界 `X/Y(mm)` 与航向 `th(deg)`。P1 只在 UI 边界将其一次性转换为 `Pose2D(X / 1000, Y / 1000, th × pi / 180)`；规划核心不接受度或 mm 位姿。当前车队路径代码将来自 `getCartLocation().th` 的姿态与度制角相加，并在调用三角函数前显式除以 180 再乘 pi，因此 P1 不沿用旧 `Movements.cs` 直接对 `.th` 调用 `Math.Cos/Sin` 的不一致写法。
- AMR 起点必须表示车辆几何中心。若上游定位的参考点是雷达、天线或其他安装点，上游必须先按外参转换到车辆几何中心；安全余量仍只由 `VehicleParameters` 表达。
- `PlanningResult` 只有 `Success` 才能携带完整路径；取消、超时、无解和失败不得在 UI 上表现为部分路径。
- `Painter` 在现有后台多车线程中已被调用，因此本设计允许规划任务完成后的后台回调操作该图层；规划核心本身始终不依赖 UI。
- 注释延续 P0 风格：公开类型与成员使用中文 XML 文档，说明单位、并发/停止语义和返回行为；会影响竞态的内部代码保留简短中文行注释。

## 方案选择

采用“六个薄 MovementTest 入口 + 共享后台执行器”的方案。

每个入口对应一个已命名的固定场景，便于在 Clumsy 的测试列表中直接运行；它们共用同一个静态 `CoarsePathPlanningService`，因此既能复用地图缓存，也不会让测试代码绕开门面。一个位于同一源文件内的执行器负责互斥会话、`Task` 生命周期、取消和绘制，避免六个入口复制并发逻辑。

不采用单一测试入口配合代码常量切换，因为手动验证需要反复改代码；也不采用运行时弹窗选项，因为这会增加 UI 输入状态和无法直接观察每个场景的可发现性。

## 文件与职责

| 文件 | 职责 |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 创建不读取 UI、传感器、定位或时钟的固定 `CoarsePathPlanningJob` 场景。每次创建均返回新请求对象。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 声明七个 MovementTest 入口，以及共享服务、后台执行、取消、结果日志、AMR 位姿/手动终点输入与 Painter 绘制。 |
| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 通过真实程序集反射验证场景、门面调用约束、缓存、换向、无解、取消和测试代码结构。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 补充 P1 手动测试方法、颜色图例、单位转换、停止语义和非目标。 |

所有 UI 辅助类型保留在 `CoarsePath/Test` 内；不会向 `Map`、`Search`、`Vehicle` 或 `Facade` 增加 UI 依赖。

## 场景工厂

`CoarsePathScenarioFactory` 公开一个场景枚举和按枚举创建请求的方法。工厂的职责仅是构造纯输入；它不持有服务、缓存、Painter 或取消源。每个请求采用同一组可验证的车辆和搜索默认值，再按场景覆盖障碍物、起终点和方向约束。

场景固定使用世界 mm 地图边界和分辨率，向 `Pose2D` 写入对应的 m 坐标。障碍物只通过 `ManualObstacleSource` 和 `TwoLegObstacleSource` 进入 `PlanningMapRequest`，并为内容变化提供固定且正确的 `SourceVersion`。

| 场景 | 地图与预期 |
| --- | --- |
| 显式空图 | `AllowExplicitEmptyMap=true`，直达前进路径成功，用于检查最短调用链。 |
| 单矩形绕行 | 中央矩形阻断直线，路径成功且必须绕障。 |
| 手工圆、矩形与 TwoLeg | 同时使用手工圆形、手工矩形和有效 TwoLeg 快照，路径成功，证明多来源经过同一门面。 |
| 缓存命中 | 连续以新建但完全相同的输入调用同一服务两次；第二次 `MapResult.CacheHit` 必须为 `Input`。 |
| 倒车换向 | 起步方向限制为前进、终点进入方向限制为倒车；成功路径必须出现标记的换向点。 |
| 无解 | 完全贯穿地图的障碍带隔开起点和终点，返回 `NoFeasiblePath` 且无路径。 |
| AMR 位姿与手动终点 | 起点使用上层传入并冻结的 AMR 世界位姿；操作者输入同一世界系的终点 X/Y/航向。该入口仅使用明确提供的障碍物快照，显式空图只能作为演示，不能代表现场无障碍。 |

实现期间先用自动化断言固定每个场景的状态；若需为当前 P0 运动原语调整数值，只能调整场景几何或请求参数，不能放宽碰撞、目标或失败语义。

## 后台会话与取消

共享执行器持有一个静态、长期存活的 `CoarsePathPlanningService`。任一入口启动时会创建新的会话：运行编号、专用 `CancellationTokenSource`、场景描述和后台 `Task`。启动新会话前取消旧会话，以确保同时最多只有一个可绘制的规划结果。

AMR 位姿和手动终点在创建任务前被转换、有限值校验并冻结，随后只作为 `CoarsePathPlanningJob` 数据传给后台。MovementTest 不在规划后台持续读取定位；若未来接入可能阻塞的 `DetourInterface.getCartLocation()`，它必须位于独立的上游快照提供者，不能阻塞 UI 或绕过本设计的输入契约。

```text
MovementTest.Test
  -> 生成场景的全新 CoarsePathPlanningJob
  -> 创建运行编号和 CancellationTokenSource
  -> Task.Run(() => service.Plan(job, token))
  -> 完成回调：仅当运行编号仍为当前会话时记录并绘制结果

MovementTest.TestStop
  -> 取消当前 CancellationTokenSource
  -> 使当前运行编号失效并解绑 Task 引用
  -> 清空专用 Painter 图层
  -> 旧任务完成后只释放其 CancellationTokenSource，不再绘制
```

`Test` 绝不等待 `Task`、不读取 `Task.Result`，因此不会阻塞 Clumsy 界面。`TestStop` 不等待规划任务退出；P0 的共享预算会将令牌传递至建图、EDT、Dijkstra 和 Hybrid A*，任务在其检查点返回 `Cancelled`。运行编号检查可防止已取消的旧任务在新任务结果之后覆盖画面。

任务异常只记录清晰的测试诊断并释放资源，不伪造 `PlanningResult`。正常停止、超时、无解与输入失败均使用门面实际返回的状态。

所有七个入口只创建规划请求、任务和绘制；不得引用 `BasicPilotBase.Chassis`、`SendMotion`、`DriveTask` 或任何底盘控制 API。

## 绘制规则

使用独立的全局世界坐标 Painter 图层，例如 `CoarsePathPlanningV1`。Painter 输入为 mm，因此所有来自 `Pose2D` 与 `CoarsePathPoint` 的 X/Y 必须乘以 1000；航向仍以 rad 计算旋转矩形。不得混用 Map 的 mm 和 CoarsePath 的 m。所有地图输入、AMR 起点和手动终点均处于同一个世界坐标系。

- 先绘制地图 `[XMin, XMax) × [YMin, YMax)` 的粗外边界、世界 X/Y 参考和栅格网络。格线遵循真实 `ResolutionMm`；当格线数量超过显示上限时，按整数格距抽稀，并在状态文本中保留真实分辨率与显示步距。
- 从 `PlanningGridMap.IsOccupied(row, col)` 绘制占据格，而不是重新绘制原始障碍物几何；因此显示内容与实际规划快照一致。空闲格使用背景，不为每个空格增加填充。
- 起点为绿色圆、方向短线和“起点”标签；终点为橙色圆、方向短线、“终点”标签及目标位置容差圈。
- 成功路径逐段连接：前进与倒车使用不同颜色，并以固定间距绘制方向箭头；路径不成功时不绘制任何路径段。
- `IsGearSwitchPoint=true` 的点使用紫色标记和“换向”标签。
- 对首点、末点、每个换向点和固定间隔点绘制旋转矩形。矩形半长/半宽为 `Vehicle.LengthMeters / 2 + SafetyMarginMeters` 和 `Vehicle.WidthMeters / 2 + SafetyMarginMeters`，仅用于显示 P0 已采用的扩大车体，不参与碰撞判断。
- 在地图角落绘制固定图例：边界、占据格、起点、终点、前进、倒车、换向与扩大车体检查框的颜色含义。无论成功与否，绘制文本状态：场景名称、地图快照 ID、地图构建状态、缓存层级、规划状态、耗时和终止原因。停止或新会话开始时先清空旧图层。

绘制只消费 `CoarsePathPlanningJobResult` 的只读结果；不修改 `PlanningMapRequest`、地图快照、路径、调试开关或服务缓存。

## 测试与验收

按 Red-Green-Refactor 顺序扩展 `verify_coarse_path_integration.ps1`：先增加以下会失败的反射/行为断言，再实现最小代码，最后运行相同脚本。

1. 断言 `CoarsePath/Test` 中的场景工厂和 MovementTest 文件存在；工厂提供六类固定场景与一个 AMR 位姿/手动终点入口，且每次创建返回独立请求。
2. 使用同一 `CoarsePathPlanningService` 运行工厂场景：空图、矩形、多来源和倒车换向均成功；倒车换向路径含 `IsGearSwitchPoint`；无解结果为 `NoFeasiblePath` 且路径为空。
3. 对缓存场景连续调用两次，断言第二个地图结果为 `Input` 命中，且路径状态和点数不因缓存改变。
4. 断言 AMR `0 deg`、`90 deg` 的 UI 输入分别转换为 `0 rad`、`pi/2 rad`，同时 X/Y 由 mm 转为 m；手动目标与起点都使用相同转换与有限值校验。
5. 对预先取消的后台调用断言门面映射为 `Cancelled`、地图或路径不发布部分结果；结构检查确认 MovementTest 使用 `Task.Run` 和 `CancellationTokenSource`，且没有等待任务。
6. 结构检查确认测试入口只通过 `CoarsePathPlanningService` 进行规划，且不引用底盘命令、栅格化器、碰撞器、原语生成器或搜索节点；并检查绘制代码消费 `PlanningGridMap` 的边界、分辨率与占据状态，包含图例和成功路径保护。
7. 更新 README 断言，确认 P1 的 UI、AMR 位姿单位、手动终点、取消和“无底盘命令”边界可被调用方查阅。

完成后运行 Debug 构建及现有 P0 Map/CoarsePath 验证脚本（不恢复或改动已被排除的旧 TrapMap 验证脚本）。

## 非目标

- 不实现路径平滑、速度规划、跟踪控制、底盘命令、实时重规划或传感器采集。
- 不改变地图指纹、缓存键、障碍物栅格化、车辆碰撞、终点判定、搜索代价或资源上限。
- 不在本交付中实现 Release 性能基准，也不清理、迁移或恢复任何 TrapMap 文件与脚本。
