# P1 粗路径 Clumsy UI 集成 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 交付可在 Clumsy 中后台运行的七个粗路径测试入口,显示真实规划栅格快照和完整路径信息,并支持传入 AMR 世界位姿与手动目标位姿。 **Architecture:** `CoarsePathScenarioFactory` 保持无 UI 的纯输入构造职责,提供六个可重复的回归场景及一个显式标注为空图演示的“AMR 位姿 + 手动终点”请求创建入口。`MovementTest.CoarsePathTest.cs` 只作为 UI 适配层:把 AMR/目标 mm+deg 转为核心所需的 m+rad,使用一个共享门面在 `Task.Run` 后台运行,并从不可变 `CoarsePathPlanningJobResult` 绘制地图快照和路径。 **Tech Stack:** C# / `netstandard2.0`、现有 Clumsy `MovementTest`/`Painter`、`CoarsePathPlanningService`、PowerShell 反射验证脚本。 ## Global Constraints - `PlanningMapRequest` 的地图、障碍物、AMR 输入和手动目标 X/Y 均为世界 mm;`Pose2D` 和路径 X/Y 为世界 m;核心航向为 rad。 - 项目上游的 AMR `th` 输入按 deg 适配为 `th * Math.PI / 180d`;不得沿用直接将该值传给 `Math.Cos/Sin` 的旧写法。 - AMR 起点必须是车辆几何中心;传感器安装点必须由上游先按外参转换。 - UI 和测试只能调用 `CoarsePathPlanningService.Plan(job, token)`;不得直接实例化 `PlanningMapFactory`、`HybridAStarPlanner`、栅格化器、碰撞器、原语生成器或搜索节点。 - 所有七个 MovementTest 都不得引用 `BasicPilotBase.Chassis`、`SendMotion`、`DriveTask` 或任何底盘控制 API。 - `Test` 不得等待后台任务或读取 `Task.Result`;`TestStop` 先取消令牌,再使运行编号失效、解绑任务并清空 Painter。 - 只在 `PlanningStatus.Success` 绘制路径、方向箭头、换向点和扩大车体检查框;失败、取消和超时只显示地图、起点、终点和状态。 - 代码兼容 `netstandard2.0`,不引入新 NuGet 包;公开类型/成员写中文 XML 文档,复杂单位与并发逻辑写简短中文行注释。 - 不改动 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。 --- ## 文件结构 | 文件 | 变更职责 | | --- | --- | | `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新建纯场景工厂、六场景枚举、AMR/目标位姿 mm+deg 到核心 `Pose2D` 的转换,以及空图演示手动目标请求。 | | `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 新建七个 UI 入口、共享会话执行器、任务取消、Painter 地图/路径/图例绘制与手动输入解析。 | | `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 为工厂行为、单位转换、缓存/换向/无解、UI 源码边界与 README 内容新增真实程序集和文本断言。 | | `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 记录 P1 测试入口、输入单位、空图演示限制、图例、停止语义及无底盘命令边界。 | ### 固定接口 ```csharp namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test; public enum CoarsePathTestScenario { ExplicitEmpty, RectangleDetour, ManualAndTwoLeg, CacheHit, ReverseGearSwitch, NoFeasiblePath, } public static class CoarsePathScenarioFactory { public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario); public static CoarsePathPlanningJob CreateManualGoalDemo( double startXMillimeters, double startYMillimeters, double startHeadingDegrees, double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees); } ``` `CreateManualGoalDemo` 只构造带 2,000 mm 边缘留白的显式空图演示请求,并在 README/测试名称中明确其不代表真实环境安全。未来现场入口必须提供真实 `IMapObstacleSource` 快照,而不是修改此方法的语义。 ### Task 1: 工厂契约与失败测试 **Files:** - Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - Create later in Task 2: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` **Consumes:** 已有 `CoarsePathPlanningService.Plan(CoarsePathPlanningJob, CancellationToken)`、`Find-Method`、`Assert-True`、`Assert-Equal` 与程序集加载逻辑。 **Produces:** 对 `CoarsePathTestScenario` 和 `CoarsePathScenarioFactory` 的反射行为约束;Task 2 的最小实现必须使这些断言通过。 - [ ] **Step 1: 在集成脚本加入工厂反射测试** 在现有 facade 检查后、最终输出前插入以下 PowerShell。它要求类型和两个公开方法都存在,因此在工厂未创建时失败。 ```powershell $testNamespace = $coarsePath + 'Test.' $scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false) $scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false) Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.' Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.' $factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType) $factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @( [double], [double], [double], [double], [double], [double]) Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).' Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.' $scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath') foreach ($scenarioName in $scenarioNames) { $scenario = [Enum]::Parse($scenarioEnumType, $scenarioName) $jobA = $factoryCreate.Invoke($null, @($scenario)) $jobB = $factoryCreate.Invoke($null, @($scenario)) Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job." Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call." } $manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0)) Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.' Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.' Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.' Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.' Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.' Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.' ``` - [ ] **Step 2: 运行脚本确认失败** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 构建成功;脚本因 `P1 scenario enum must exist.` 失败。 ### Task 2: 实现纯场景工厂并通过行为测试 **Files:** - Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` - Test: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` **Consumes:** Task 1 的枚举和两个公开工厂方法;`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource`、`Pose2D`、`VehicleParameters`、`HybridAStarConfiguration`。 **Produces:** 六个可重复 job 与一个空图演示手动 job,供 UI 入口和后续脚本行为断言共同使用。 - [ ] **Step 1: 先建立最小的公共类型和转换辅助函数** 创建工厂文件并定义以下枚举、转换函数和公共入口。所有输入先做有限值检查;非有限输入抛出 `ArgumentOutOfRangeException`,避免伪造核心请求。 ```csharp public enum CoarsePathTestScenario { ExplicitEmpty, RectangleDetour, ManualAndTwoLeg, CacheHit, ReverseGearSwitch, NoFeasiblePath, } public static class CoarsePathScenarioFactory { private const double MillimetersPerMeter = 1000d; private const double DegreesToRadians = Math.PI / 180d; private const float ResolutionMillimeters = 50f; private const double ManualMapPaddingMillimeters = 2000d; public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario) { switch (scenario) { case CoarsePathTestScenario.ExplicitEmpty: return CreateExplicitEmpty(); case CoarsePathTestScenario.RectangleDetour: return CreateRectangleDetour(); case CoarsePathTestScenario.ManualAndTwoLeg: return CreateManualAndTwoLeg(); case CoarsePathTestScenario.CacheHit: return CreateRectangleDetour(); case CoarsePathTestScenario.ReverseGearSwitch: return CreateReverseGearSwitch(); case CoarsePathTestScenario.NoFeasiblePath: return CreateNoFeasiblePath(); default: throw new ArgumentOutOfRangeException(nameof(scenario)); } } public static CoarsePathPlanningJob CreateManualGoalDemo( double startXMillimeters, double startYMillimeters, double startHeadingDegrees, double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees) { EnsureFinite(startXMillimeters, nameof(startXMillimeters)); EnsureFinite(startYMillimeters, nameof(startYMillimeters)); EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees)); EnsureFinite(goalXMillimeters, nameof(goalXMillimeters)); EnsureFinite(goalYMillimeters, nameof(goalYMillimeters)); EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees)); return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters), ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees), ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any); } private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees) => new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter, headingDegrees * DegreesToRadians); } ``` - [ ] **Step 2: 实现统一请求模板和六个固定场景** 使用统一的车辆和配置,避免场景间无意改变安全或搜索语义。模板必须是新对象: ```csharp private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal, TravelDirection? startDirection, GoalDirectionConstraint goalDirection) { return new CoarsePathPlanningJob { MapRequest = mapRequest, Start = start, Goal = goal, Vehicle = new VehicleParameters { LengthMeters = 0.80d, WidthMeters = 0.60d, SafetyMarginMeters = 0.05d, MaximumCurvaturePerMeter = 1d / 1.20d, }, Configuration = new HybridAStarConfiguration(), StartDirection = startDirection, GoalDirection = goalDirection, }; } ``` 固定地图均使用 `new MapBoundsMm(0f, 6000f, 0f, 4000f)`、50 mm 分辨率。以下代码固定各场景的障碍来源和世界位姿,所有 `ManualObstacleSource` 版本为 `1L`、所有必需来源为 `true`: ```csharp private static CoarsePathPlanningJob CreateExplicitEmpty() => CreateJob(CreateMap(true, Array.Empty()), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); private static CoarsePathPlanningJob CreateRectangleDetour() => CreateJob(CreateMap(false, new IMapObstacleSource[] { new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] { new AxisAlignedRectangleObstacle(2700f, 3300f, 1200f, 2800f) }), }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); private static CoarsePathPlanningJob CreateManualAndTwoLeg() => CreateJob(CreateMap(false, new IMapObstacleSource[] { new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] { new CircleObstacle(2400f, 1300f, 220f), new AxisAlignedRectangleObstacle(3000f, 3600f, 2000f, 2600f), }), new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true, 3900f, 2500f, 0d, -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")), }), new Pose2D(1d, 1d, 0d), new Pose2D(5d, 3d, 0d), null, GoalDirectionConstraint.Forward); private static CoarsePathPlanningJob CreateNoFeasiblePath() => CreateJob(CreateMap(false, new IMapObstacleSource[] { new ManualObstacleSource("manual", 1L, true, new IMapObstacle[] { new AxisAlignedRectangleObstacle(2900f, 3100f, 0f, 4000f) }), }), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward); ``` `CreateMap` 返回新的 `PlanningMapRequest`,固定写入地图边界、分辨率、给定来源和 `AllowExplicitEmptyMap`。倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。 倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)`、`StartDirection=Forward`、`GoalDirection=Reverse`,使路径必须以至少一次换向结束。若 P0 的离散搜索在此几何下无法稳定得到成功,只允许调整此场景的目标距离或障碍布局,且测试必须继续要求 `IsGearSwitchPoint=true`。 `CreateManualDemoMap` 用起终点 X/Y 的最小/最大值各扩展 `ManualMapPaddingMillimeters`,按 50 mm 向外取整,并明确设置 `AllowExplicitEmptyMap=true` 和空的 `ObstacleSources`。 - [ ] **Step 3: 扩展行为断言以覆盖所有场景的实际状态** 在 Task 1 的反射代码之后增加服务执行测试。它不引用任何 Painter 或 MovementTest: ```powershell $scenarioService = [Activator]::CreateInstance($serviceType) foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) { $job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName))) $result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None)) Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed." } $reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch'))) $reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None)) Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.' $noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath'))) $noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None)) Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.' Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.' $cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) $cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit'))) $cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None)) $cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None)) Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.' Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.' ``` - [ ] **Step 4: 运行测试并固定数值场景** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 工厂、单位转换、六个固定场景、缓存与无解断言通过;此时尚未加入 MovementTest 源码检查,因此脚本整体通过。 ### Task 3: MovementTest 后台会话与完整 Painter 绘制 **Files:** - Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` - Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` **Consumes:** Task 2 的 `CoarsePathScenarioFactory.Create` 和 `CreateManualGoalDemo`;`CoarsePathPlanningService`、`CoarsePathPlanningJobResult`、`PlanningGridMap`、`Painter`。 **Produces:** 六个固定场景入口与一个“AMR 位姿 + 手动终点(空图演示)”入口;所有入口通过同一个后台执行器运行和绘制。 - [ ] **Step 1: 写入失败的 UI 源码结构断言** 在 PowerShell 脚本中加入下列纯文本检查,避免在自动化测试中实例化外部 UI: ```powershell $movementTestPath = Join-Path $plannerRoot 'CoarsePath\Test\MovementTest.CoarsePathTest.cs' if (-not (Test-Path -LiteralPath $movementTestPath -PathType Leaf)) { throw 'P1 coarse-path MovementTest source file must exist.' } $movementTestContent = Get-Content -LiteralPath $movementTestPath -Raw foreach ($required in @( '[MovementTest(name = "粗路径-显式空图")]', '[MovementTest(name = "粗路径-矩形绕行")]', '[MovementTest(name = "粗路径-多来源障碍")]', '[MovementTest(name = "粗路径-缓存命中")]', '[MovementTest(name = "粗路径-倒车换向")]', '[MovementTest(name = "粗路径-无解")]', '[MovementTest(name = "粗路径-AMR起点手动终点(空图演示)")]', 'Task.Run', 'CancellationTokenSource', 'CoarsePathPlanningService', 'PlanningGridMap', 'IsOccupied', 'ResolutionMm', 'SnapshotId', '图例', 'IsGearSwitchPoint')) { Assert-True $movementTestContent.Contains($required) "MovementTest must contain: $required" } foreach ($forbidden in @('PlanningMapFactory', 'HybridAStarPlanner', 'MapObstacleRasterizer', 'FootprintCollisionChecker', 'MotionPrimitiveGenerator', 'HybridAStarSearch', 'BasicPilotBase.Chassis', 'SendMotion', 'DriveTask', '.Wait()', '.Result')) { Assert-False $movementTestContent.Contains($forbidden) "MovementTest must not depend on: $forbidden" } ``` - [ ] **Step 2: 运行脚本确认 UI 结构检查失败** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 在工厂断言通过后,因 `P1 coarse-path MovementTest source file must exist.` 失败。 - [ ] **Step 3: 实现共享会话执行器与七个薄入口** 在新文件中使用 `namespace MultiWheelC;`,引用 `System.Threading`、`System.Threading.Tasks`、`System.Drawing`、`System.Numerics`、`ClumsyCore`、`MDCSToolBox.Clumsy.Movements` 和 Map/CoarsePath 命名空间。定义一个内部静态执行器,核心形状如下: ```csharp internal static class CoarsePathMovementTestRunner { private static readonly object SyncRoot = new object(); private static readonly CoarsePathPlanningService Service = new CoarsePathPlanningService(); private static readonly Painter Painter = UI.GetPainter("CoarsePathPlanningV1", true); private static long _nextRunId; private static long _activeRunId; private static CancellationTokenSource _activeCancellation; private static Task _activeTask; internal static void Start(string displayName, CoarsePathPlanningJob job) { CancellationTokenSource previous; long runId; var cancellation = new CancellationTokenSource(); lock (SyncRoot) { previous = _activeCancellation; runId = ++_nextRunId; _activeRunId = runId; _activeCancellation = cancellation; Painter.Clear(); _activeTask = Task.Run(() => Service.Plan(job, cancellation.Token)); _activeTask.ContinueWith(task => Complete(runId, displayName, job, cancellation, task), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); } previous?.Cancel(); } internal static void Stop() { CancellationTokenSource cancellation; lock (SyncRoot) { cancellation = _activeCancellation; } cancellation?.Cancel(); lock (SyncRoot) { if (!ReferenceEquals(_activeCancellation, cancellation)) return; _activeCancellation = null; _activeTask = null; _activeRunId = ++_nextRunId; } Painter.Clear(); } } ``` `Complete` 必须捕获 `task.Exception`,但正常情况下只接受 `CoarsePathPlanningJobResult`。在锁内确认 `runId == _activeRunId`、任务未取消并且 `task.Status == TaskStatus.RanToCompletion` 后才绘制;无论绘制与否都在 finally 中释放该任务专用 `CancellationTokenSource`。不可在锁内等待任务。 添加一个抽象 `CoarsePathScenarioMovementTest`,其 `Test` 调用 `CoarsePathMovementTestRunner.Start(DisplayName, CoarsePathScenarioFactory.Create(Scenario))`,其 `TestStop` 调用 `Stop()`。实现六个带固定属性名称的密封子类。第七个类在 `Test` 中只读取一次六个 UI 输入:AMR 起点 X/Y/航向和目标 X/Y/航向(分别为 mm/mm/deg),调用 `CreateManualGoalDemo` 后启动;输入解析失败时仅记录错误并不启动任务。 - [ ] **Step 4: 实现确定的地图和结果绘制辅助方法** 在同一执行器内只消费 `job` 与 `CoarsePathPlanningJobResult`,按固定顺序调用以下辅助方法: ```csharp private static void DrawMap(PlanningGridMap map); private static void DrawPose(Color color, string label, Pose2D pose); private static void DrawGoalTolerance(Pose2D goal, HybridAStarConfiguration configuration); private static void DrawSuccessfulPath(PlanningResult result, VehicleParameters vehicle); private static void DrawLegendAndStatus(string displayName, CoarsePathPlanningJobResult result, int gridStride); ``` `DrawMap` 画 `[Bounds.XMin, Bounds.XMax) × [Bounds.YMin, Bounds.YMax)` 的粗边界和 X/Y 参考。`gridStride = Max(1, Ceiling(Max(Rows, Cols) / 100d))`;每 `gridStride` 个真实栅格画一条线,状态文字写入 `分辨率=...mm,显示每...格`。遍历 `row/col`,仅对 `map.IsOccupied(row,col)` 为 true 的单元以四条边线画深色格框,确保显示的是最终快照而非原始几何。 `DrawPose` 将 m 转 mm,以圆、朝向短线和标签分别绘制绿色起点、橙色终点。`DrawGoalTolerance` 将位置容差 m 转 mm,绘制橙色容差圆。`DrawSuccessfulPath` 仅在 `result.Status == PlanningStatus.Success` 时运行:相邻路径点按当前点 `Direction` 使用青色(前进)或蓝色(倒车)连线;每隔 10 点画短箭头;`IsGearSwitchPoint` 画紫色圆与“换向”;首、末、换向和每 20 点调用旋转矩形绘制,半长/半宽严格按车辆长宽加安全余量。`DrawLegendAndStatus` 在边界左上方显示边界、占据格、起点、终点、前进、倒车、换向和扩大车体颜色说明,另显示 `SnapshotId`、`MapResult.Status`、`CacheHit`、`PlanningResult.Status`、`Elapsed` 和终止原因。 - [ ] **Step 5: 运行构建与集成脚本** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 构建成功;工厂行为、七入口结构、后台取消约束和 Painter 数据来源检查全部通过。 ### Task 4: README 与文档断言 **Files:** - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` **Consumes:** Task 2 的 `CreateManualGoalDemo` 单位契约和 Task 3 的七个入口名称、图例颜色和停止行为。 **Produces:** 可独立使用的 P1 UI 说明,以及对其关键安全声明的自动化保护。 - [ ] **Step 1: 为 README 写失败断言** 在现有 README 检查后加入: ```powershell foreach ($requiredReadmeText in @( '## P1:Clumsy 手动测试与可视化', '粗路径-AMR起点手动终点(空图演示)', 'AMR 位姿输入:X/Y 使用世界 mm,th 使用 deg', 'Pose2D:X/Y 使用 m,航向使用 rad', '显式空图只能用于演示', '不会发送底盘运动命令', 'TestStop', '栅格边界', '占据格', '换向')) { Assert-True $coarsePathReadmeContent.Contains($requiredReadmeText) "CoarsePath README must document: $requiredReadmeText" } ``` - [ ] **Step 2: 运行脚本确认 README 检查失败** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 所有代码检查通过;脚本因 `CoarsePath README must document: ## P1:Clumsy 手动测试与可视化` 失败。 - [ ] **Step 3: 在 README 增加 P1 专节** 在“第一版限制”之前增加 `## P1:Clumsy 手动测试与可视化`,逐项写明: 1. 七个 MovementTest 名称及对应场景;缓存测试连续运行两次,第二次展示 `Input` 命中。 2. AMR/手动目标输入契约:世界 `X/Y(mm)`、`th(deg)`,转换成 `Pose2D` 的 m/rad;起点是车辆几何中心。 3. 空图手动目标入口只能演示坐标、路径和取消流程;现场必须提供真实障碍物快照。 4. 可视化图例:边界、抽稀格线、占据格、起点、终点及容差、前进、倒车、换向和扩大车体检查框;失败不显示部分路径。 5. `Test` 在后台规划,`TestStop` 取消令牌并清空图层;测试只显示结果,绝不发送底盘运动命令或执行路径跟踪。 - [ ] **Step 4: 运行 README 与集成检查** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 构建成功;README 和全部 P1 集成检查通过。 ### Task 5: 全量回归与手动核验说明 **Files:** - Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` - Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` - Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` **Consumes:** Tasks 1–4 的代码、文档和脚本。 **Produces:** 通过 Debug 回归的 P1 UI 集成首个交付;不进入 Release 性能基准。 - [ ] **Step 1: 执行 Debug 构建和所有现存 P0/P1 功能脚本** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 构建 0 errors;每个存在的脚本返回 0 并输出其 `passed` 消息。 - [ ] **Step 2: 手动 Clumsy 验收** 在 Clumsy 的 MovementTest 列表依次运行“粗路径-矩形绕行”和“粗路径-AMR起点手动终点(空图演示)”。检查:测试启动后界面仍可操作;图层拥有边界、格线、占据格、图例、起终点与状态;成功案例有方向区分路径和扩大车体框;点击停止后图层清空且没有任何底盘运动命令。 - [ ] **Step 3: 记录交付边界** 在完成报告中明确:P1 UI 集成已完成;下一 P1 子项目是 Release 性能、资源和确定性基准;TrapMap 迁移/清理继续排除;没有执行 Git 操作。 ## 自检 - 覆盖性:Task 2 交付纯场景与单位转换;Task 3 交付后台七入口和完整视觉要素;Task 4 交付 README;Task 5 交付自动化与手动验收。规格中的空图限制、实际占据快照、停止语义、无底盘命令和不显示部分路径均有对应任务。 - 占位符:已检查任务不含未决占位、延后实现或泛化错误处理类措辞;每个实现任务均给出文件、接口、测试、命令和具体代码形状。 - 类型一致性:所有任务统一使用 `CoarsePathTestScenario`、`CoarsePathScenarioFactory.Create`、`CreateManualGoalDemo`、`CoarsePathPlanningJob`、`CoarsePathPlanningJobResult` 和 `PlanningGridMap`;AMR 输入始终是 mm+deg,核心位姿始终是 m+rad。