Files
ParkingRobot/docs/superpowers/plans/2026-07-27-p1-coarse-path-ui-integration.md
T

535 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<IMapObstacleSource>()),
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 @(
'## P1Clumsy 手动测试与可视化',
'粗路径-AMR起点手动终点(空图演示)',
'AMR 位姿输入:X/Y 使用世界 mmth 使用 deg',
'Pose2DX/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: ## P1Clumsy 手动测试与可视化` 失败。
- [ ] **Step 3: 在 README 增加 P1 专节**
在“第一版限制”之前增加 `## P1Clumsy 手动测试与可视化`,逐项写明:
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。