# 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 文件与脚本。