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

29 KiB
Raw Blame History

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/PainterCoarsePathPlanningService、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);不得直接实例化 PlanningMapFactoryHybridAStarPlanner、栅格化器、碰撞器、原语生成器或搜索节点。
  • 所有七个 MovementTest 都不得引用 BasicPilotBase.ChassisSendMotionDriveTask 或任何底盘控制 API。
  • Test 不得等待后台任务或读取 Task.ResultTestStop 先取消令牌,再使运行编号失效、解绑任务并清空 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 测试入口、输入单位、空图演示限制、图例、停止语义及无底盘命令边界。

固定接口

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-MethodAssert-TrueAssert-Equal 与程序集加载逻辑。

Produces:CoarsePathTestScenarioCoarsePathScenarioFactory 的反射行为约束;Task 2 的最小实现必须使这些断言通过。

  • Step 1: 在集成脚本加入工厂反射测试

在现有 facade 检查后、最终输出前插入以下 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:

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 的枚举和两个公开工厂方法;MapBoundsMmManualObstacleSourceTwoLegObstacleSourcePose2DVehicleParametersHybridAStarConfiguration

Produces: 六个可重复 job 与一个空图演示手动 job,供 UI 入口和后续脚本行为断言共同使用。

  • Step 1: 先建立最小的公共类型和转换辅助函数

创建工厂文件并定义以下枚举、转换函数和公共入口。所有输入先做有限值检查;非有限输入抛出 ArgumentOutOfRangeException,避免伪造核心请求。

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: 实现统一请求模板和六个固定场景

使用统一的车辆和配置,避免场景间无意改变安全或搜索语义。模板必须是新对象:

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

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=ForwardGoalDirection=Reverse,使路径必须以至少一次换向结束。

倒车换向场景使用空图、起点 (1,2,0)、终点 (4,2,0)StartDirection=ForwardGoalDirection=Reverse,使路径必须以至少一次换向结束。若 P0 的离散搜索在此几何下无法稳定得到成功,只允许调整此场景的目标距离或障碍布局,且测试必须继续要求 IsGearSwitchPoint=true

CreateManualDemoMap 用起终点 X/Y 的最小/最大值各扩展 ManualMapPaddingMillimeters,按 50 mm 向外取整,并明确设置 AllowExplicitEmptyMap=true 和空的 ObstacleSources

  • Step 3: 扩展行为断言以覆盖所有场景的实际状态

在 Task 1 的反射代码之后增加服务执行测试。它不引用任何 Painter 或 MovementTest

$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:

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.CreateCreateManualGoalDemoCoarsePathPlanningServiceCoarsePathPlanningJobResultPlanningGridMapPainter

Produces: 六个固定场景入口与一个“AMR 位姿 + 手动终点(空图演示)”入口;所有入口通过同一个后台执行器运行和绘制。

  • Step 1: 写入失败的 UI 源码结构断言

在 PowerShell 脚本中加入下列纯文本检查,避免在自动化测试中实例化外部 UI:

$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 -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.ThreadingSystem.Threading.TasksSystem.DrawingSystem.NumericsClumsyCoreMDCSToolBox.Clumsy.Movements 和 Map/CoarsePath 命名空间。定义一个内部静态执行器,核心形状如下:

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: 实现确定的地图和结果绘制辅助方法

在同一执行器内只消费 jobCoarsePathPlanningJobResult,按固定顺序调用以下辅助方法:

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 在边界左上方显示边界、占据格、起点、终点、前进、倒车、换向和扩大车体颜色说明,另显示 SnapshotIdMapResult.StatusCacheHitPlanningResult.StatusElapsed 和终止原因。

  • Step 5: 运行构建与集成脚本

Run:

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 检查后加入:

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 -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:

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:

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 交付自动化与手动验收。规格中的空图限制、实际占据快照、停止语义、无底盘命令和不显示部分路径均有对应任务。
  • 占位符:已检查任务不含未决占位、延后实现或泛化错误处理类措辞;每个实现任务均给出文件、接口、测试、命令和具体代码形状。
  • 类型一致性:所有任务统一使用 CoarsePathTestScenarioCoarsePathScenarioFactory.CreateCreateManualGoalDemoCoarsePathPlanningJobCoarsePathPlanningJobResultPlanningGridMap;AMR 输入始终是 mm+deg,核心位姿始终是 m+rad。