437 lines
24 KiB
Markdown
437 lines
24 KiB
Markdown
# 固定粗路径案例使用实时 AMR 位姿 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 让全部 `粗路径规划-*` 固定 MovementTest 从一次冻结的当前 AMR 世界坐标和航向开始规划,并把案例地图、目标和障碍平移到 AMR 附近。
|
||
|
||
**Architecture:** `CoarsePathScenarioFactory` 保留确定性的基准案例入口,并新增接受 AMR mm/deg 位姿的入口。一个仅属于固定场景工厂的平移对象将基准案例的起点映射为 AMR 起点,平移地图和障碍,并以起终点基准航向差计算新的终点航向。MovementTest 运行器在前台读取一次 `DetourInterface.getCartLocation()`,校验并冻结快照后才提交已有后台规划流程。
|
||
|
||
**Tech Stack:** C# / .NET Standard 2.0、Clumsy `MovementTest` 与 Painter、现有 `CoarsePathPlanningService`、PowerShell 反射验证脚本。
|
||
|
||
## Global Constraints
|
||
|
||
- 保留 `CoarsePathScenarioFactory.Create(CoarsePathTestScenario)` 的现有确定性行为,自动化离线测试继续使用它。
|
||
- 新实时入口的 X/Y 单位为世界 mm、航向单位为 deg;核心 `Pose2D` 仍为 m/rad。
|
||
- 固定场景仅做位置平移;TwoLeg 的 `DetectionHeadingRadians` 不得因 AMR 航向改变。
|
||
- 起点航向必须等于冻结 AMR 航向;终点航向必须保持基准案例的起终点航向差,并规范化到 `[-pi, pi]`。
|
||
- 不改变 Hybrid A*、车辆参数、碰撞模型、地图缓存实现、手动 `粗路径规划` 的终点/障碍/超时输入流程,或任何底盘控制行为。
|
||
- 实时位姿为空、读取抛异常或 X/Y/航向不是有限数时,不得创建后台任务;必须向用户报告以 `AMR 位姿不可用` 开头的原因。
|
||
- 不暂存工作区中的无关改动;每次提交均明确列出文件路径。
|
||
|
||
## File Structure
|
||
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
|
||
- 固定场景的基准坐标定义、实时 AMR 工厂重载、平移与航向转换的唯一实现位置。
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
|
||
- 固定场景的 AMR 快照读取、输入失败提示,以及冻结位姿的 Painter 状态显示。
|
||
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
|
||
- 对新工厂重载、平移几何、航向规则、缓存和基准入口不回归的程序集级验证。
|
||
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
|
||
- 对运行器只读一次实时位姿、传入实时工厂入口及 AMR 失败/状态文案的源级验证。
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
|
||
- 说明六个固定案例的实时锚定规则、缓存命中条件和定位异常行为。
|
||
|
||
---
|
||
|
||
### Task 1: 为固定案例建立可验证的实时 AMR 平移工厂
|
||
|
||
**Files:**
|
||
|
||
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:303-399`
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs:115-284`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `CoarsePathTestScenario`、`PlanningMapRequest`、`MapBoundsMm`、`ManualObstacleSource`、`TwoLegObstacleSource` 和 `TwoLegProjectionInput`。
|
||
- Produces: `public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。
|
||
- Produces: 每个固定案例以 `FixedScenarioTransform` 统一转换的起点、终点、地图边界和来源快照;旧 `Create(scenario)` 仍创建原始基准几何。
|
||
|
||
- [ ] **Step 1: 在程序集级验证脚本中写出失败测试**
|
||
|
||
在 `$factoryCreate` 定义之后加入实时重载查找;在基准案例循环之后加入以下断言。测试锚点 `(12000, -3000, 90)` 对单矩形场景的偏移应为 `(+11000, -5000)` mm;多来源锚点 `(7000, 8000, 45)` 对该基准场景的偏移应为 `(+6000, +7000)` mm。
|
||
|
||
```powershell
|
||
$factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @(
|
||
$scenarioEnumType, [double], [double], [double])
|
||
Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).'
|
||
|
||
foreach ($scenarioName in $scenarioNames) {
|
||
$scenario = [Enum]::Parse($scenarioEnumType, $scenarioName)
|
||
$liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0))
|
||
Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X."
|
||
Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y."
|
||
Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading."
|
||
}
|
||
|
||
$rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour')
|
||
$liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0))
|
||
Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.'
|
||
Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.'
|
||
Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.'
|
||
Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.'
|
||
Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.'
|
||
Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.'
|
||
Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.'
|
||
Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.'
|
||
$rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld()
|
||
$rectangleObstacle = $rectangleProjection.Obstacles[0]
|
||
Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.'
|
||
Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.'
|
||
|
||
$multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg')
|
||
$baselineMulti = $factoryCreate.Invoke($null, @($multiScenario))
|
||
$liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0))
|
||
Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.'
|
||
Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.'
|
||
Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.'
|
||
$baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles
|
||
$liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles
|
||
Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.'
|
||
Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.'
|
||
|
||
$noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath')
|
||
$liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0))
|
||
Assert-Near 9000.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.'
|
||
Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.'
|
||
Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.'
|
||
|
||
$invalidLivePoseRejected = $false
|
||
try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) }
|
||
catch [Reflection.TargetInvocationException] {
|
||
$invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException]
|
||
}
|
||
Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.'
|
||
```
|
||
|
||
- [ ] **Step 2: 运行该测试,确认它因缺少实时工厂重载而失败**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
|
||
```
|
||
|
||
Expected: build succeeds; the script fails at `Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).`
|
||
|
||
- [ ] **Step 3: 以单一平移对象实现实时工厂重载**
|
||
|
||
在 `CoarsePathScenarioFactory` 中保留一参 `Create`,并用一个私有锚点和变换对象驱动同一个 switch。使用以下接口形状;`CreateCore` 的每个 case 都先以该案例的基准起点创建 `FixedScenarioTransform`,再传给对应的场景构造函数。
|
||
|
||
```csharp
|
||
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario)
|
||
{
|
||
return CreateCore(scenario, null);
|
||
}
|
||
|
||
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario,
|
||
double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)
|
||
{
|
||
return CreateCore(scenario,
|
||
new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees));
|
||
}
|
||
|
||
private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario,
|
||
FixedScenarioAnchor anchor)
|
||
{
|
||
switch (scenario)
|
||
{
|
||
case CoarsePathTestScenario.ExplicitEmpty:
|
||
return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
||
case CoarsePathTestScenario.RectangleDetour:
|
||
case CoarsePathTestScenario.CacheHit:
|
||
return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
||
case CoarsePathTestScenario.ManualAndTwoLeg:
|
||
return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor));
|
||
case CoarsePathTestScenario.ReverseGearSwitch:
|
||
return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
||
case CoarsePathTestScenario.NoFeasiblePath:
|
||
return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
||
default:
|
||
throw new ArgumentOutOfRangeException(nameof(scenario));
|
||
}
|
||
}
|
||
```
|
||
|
||
Implement the two private classes in the same file. They must validate all public live inputs with existing `EnsureFinite`, keep the identity transform when `anchor` is null, convert fixed mm to metres only when creating `Pose2D`, and normalize headings without changing a position.
|
||
|
||
```csharp
|
||
private sealed class FixedScenarioAnchor
|
||
{
|
||
public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees)
|
||
{
|
||
EnsureFinite(xMillimeters, nameof(xMillimeters));
|
||
EnsureFinite(yMillimeters, nameof(yMillimeters));
|
||
EnsureFinite(headingDegrees, nameof(headingDegrees));
|
||
XMillimeters = xMillimeters;
|
||
YMillimeters = yMillimeters;
|
||
HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians);
|
||
}
|
||
|
||
public double XMillimeters { get; }
|
||
public double YMillimeters { get; }
|
||
public double HeadingRadians { get; }
|
||
}
|
||
|
||
private sealed class FixedScenarioTransform
|
||
{
|
||
private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians)
|
||
{
|
||
DeltaXMillimeters = deltaXMillimeters;
|
||
DeltaYMillimeters = deltaYMillimeters;
|
||
HeadingDeltaRadians = headingDeltaRadians;
|
||
}
|
||
|
||
public double DeltaXMillimeters { get; }
|
||
public double DeltaYMillimeters { get; }
|
||
public double HeadingDeltaRadians { get; }
|
||
|
||
public static FixedScenarioTransform From(double baselineStartXMillimeters,
|
||
double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor)
|
||
{
|
||
if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d);
|
||
return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters,
|
||
anchor.YMillimeters - baselineStartYMillimeters,
|
||
NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians));
|
||
}
|
||
|
||
public float X(float value) { return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); }
|
||
public float Y(float value) { return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); }
|
||
public Pose2D Pose(double xMeters, double yMeters, double headingRadians)
|
||
{
|
||
return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter,
|
||
(yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter,
|
||
NormalizeRadians(headingRadians + HeadingDeltaRadians));
|
||
}
|
||
}
|
||
|
||
private static double NormalizeRadians(double angle)
|
||
{
|
||
double normalized = angle % (2d * Math.PI);
|
||
if (normalized <= -Math.PI) return normalized + 2d * Math.PI;
|
||
return normalized > Math.PI ? normalized - 2d * Math.PI : normalized;
|
||
}
|
||
```
|
||
|
||
Update every fixed builder to accept `FixedScenarioTransform transform` and use `transform.Pose`, `transform.X` and `transform.Y` for all coordinates:
|
||
|
||
```csharp
|
||
new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f),
|
||
transform.Y(1200f), transform.Y(2800f));
|
||
new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f);
|
||
new TwoLegProjectionInput(true, transform.X(3900f), transform.Y(2500f), 0d,
|
||
-180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.");
|
||
```
|
||
|
||
Change `CreateMap` to accept the transform and translate all four `MapBoundsMm` limits with the correct axis. It must not change resolution, source IDs, source versions, required flags, empty-map flags, vehicle parameters, direction constraints or existing timeout assignments.
|
||
|
||
- [ ] **Step 4: 运行工厂和回归验证,确认新旧入口都通过**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
|
||
```
|
||
|
||
Expected: build has zero errors; script ends with `Coarse path P1 scenario checks passed.` Existing assertions for the one-argument factory must continue to pass.
|
||
|
||
- [ ] **Step 5: 提交仅包含工厂与程序集级测试的可审查变更**
|
||
|
||
```powershell
|
||
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1
|
||
git commit -m "feat: anchor coarse path scenarios to AMR pose"
|
||
```
|
||
|
||
### Task 2: 让固定 MovementTest 冻结 AMR 快照并显示诊断
|
||
|
||
**Files:**
|
||
|
||
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-52`
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:221-371,512-540`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: 新的 `CoarsePathScenarioFactory.Create(scenario, xMm, yMm, headingDeg)`、`DetourInterface.getCartLocation()` 和现有 `Run(string, CoarsePathPlanningJob)`。
|
||
- Produces: `RunScenario` 只读取一次位姿,创建已冻结的实时 job;手动入口继续调用现有两参 `Run`。
|
||
- Produces: 私有 `AmrPoseSnapshot`,仅保存有限 X/Y/deg 和格式化后的状态文本,不向规划后台读取定位。
|
||
|
||
- [ ] **Step 1: 在 UI 源级验证中加入失败断言**
|
||
|
||
在现有 `$source` 断言后追加以下代码。使用 ASCII 的方法名和 `AMR` 文本,避免 Windows PowerShell 5 对中文脚本字符串的编码歧义。
|
||
|
||
```powershell
|
||
$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner')
|
||
Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.'
|
||
$runnerSource = $source.Substring($runnerStart)
|
||
Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.'
|
||
Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.'
|
||
Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.'
|
||
Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.'
|
||
Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.'
|
||
Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.'
|
||
```
|
||
|
||
- [ ] **Step 2: 运行 UI 脚本,确认新增实时位姿约束尚未满足**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
|
||
```
|
||
|
||
Expected: script fails at `Fixed scenarios must read the current AMR pose.` or `Fixed scenarios must use the AMR-aware factory overload.`
|
||
|
||
- [ ] **Step 3: 实现单次 AMR 读取、校验、冻结与绘制传递**
|
||
|
||
在 `CoarsePathPlanningTestRunner` 内加入以下私有快照类型和工厂调用路径。任何读取或校验异常都在前台转换为 `AMR 位姿不可用:...`,因此不会进入 `Task.Run`。
|
||
|
||
```csharp
|
||
private sealed class AmrPoseSnapshot
|
||
{
|
||
public AmrPoseSnapshot(double xMillimeters, double yMillimeters, double headingDegrees)
|
||
{
|
||
EnsureFiniteAmrValue(xMillimeters, "X");
|
||
EnsureFiniteAmrValue(yMillimeters, "Y");
|
||
EnsureFiniteAmrValue(headingDegrees, "航向");
|
||
XMillimeters = xMillimeters;
|
||
YMillimeters = yMillimeters;
|
||
HeadingDegrees = headingDegrees;
|
||
}
|
||
|
||
public double XMillimeters { get; }
|
||
public double YMillimeters { get; }
|
||
public double HeadingDegrees { get; }
|
||
|
||
public string DisplayText
|
||
{
|
||
get
|
||
{
|
||
return "AMR 起点:X=" + XMillimeters.ToString("F0", CultureInfo.InvariantCulture) +
|
||
" mm,Y=" + YMillimeters.ToString("F0", CultureInfo.InvariantCulture) +
|
||
" mm,航向=" + HeadingDegrees.ToString("F1", CultureInfo.InvariantCulture) + " deg";
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void EnsureFiniteAmrValue(double value, string name)
|
||
{
|
||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||
throw new ArgumentException(name + " 必须是有限数。");
|
||
}
|
||
|
||
internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName)
|
||
{
|
||
try
|
||
{
|
||
var pose = DetourInterface.getCartLocation();
|
||
var snapshot = new AmrPoseSnapshot(pose.x, pose.y, pose.th);
|
||
CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenario,
|
||
snapshot.XMillimeters, snapshot.YMillimeters, snapshot.HeadingDegrees);
|
||
Run(scenarioName, job, snapshot);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
ShowInputFailure(new ArgumentException("AMR 位姿不可用:" + exception.Message, exception));
|
||
}
|
||
}
|
||
```
|
||
|
||
Keep the existing `internal static void Run(string scenarioName, CoarsePathPlanningJob job)` as a thin compatibility overload that calls a new private `Run(string, CoarsePathPlanningJob, AmrPoseSnapshot)`. Thread the snapshot through `DrawPending`, the `ContinueWith` lambda, `Finish`, `DrawResult`, and `DrawStatus`. In `DrawStatus`, render `snapshot.DisplayText` after the scene line only when the snapshot is non-null; increase subsequent Y offsets consistently so vehicle and termination-reason text do not overlap. Manual `CoarsePathPlanningTest.Test()` must keep its present `Run(..., job)` call and user-entered timeout behavior.
|
||
|
||
- [ ] **Step 4: 编译并运行 UI 验证,确认后台边界未回归**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
|
||
```
|
||
|
||
Expected: build has zero errors; script ends with `Coarse path UI source checks passed.` Existing assertions still confirm one shared service, background `Task.Run`, cancellation and no blocking `Task.Result`/`Wait`.
|
||
|
||
- [ ] **Step 5: 提交仅包含运行器与 UI 验证的可审查变更**
|
||
|
||
```powershell
|
||
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/tests/verify_coarse_path_ui.ps1
|
||
git commit -m "feat: use live AMR pose in coarse path tests"
|
||
```
|
||
|
||
### Task 3: 记录实时固定场景语义并执行完整回归
|
||
|
||
**Files:**
|
||
|
||
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:289-347`
|
||
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:54-100`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: 已实现的实时工厂入口和 MovementTest 状态文本。
|
||
- Produces: 可供现场使用者理解的固定案例锚定、缓存和定位异常说明;文档验证继续只检查稳定 ASCII 标识符。
|
||
|
||
- [ ] **Step 1: 在 README 源级验证中加入失败断言**
|
||
|
||
在 `$readmeStructure` 数组中加入以下 ASCII 项,并在数组后加入 `Contains` 断言:
|
||
|
||
```powershell
|
||
foreach ($requiredLiveAmrText in @(
|
||
'Create(CoarsePathTestScenario scenario, double amrXMillimeters',
|
||
'AMR',
|
||
'Input',
|
||
'TwoLeg')) {
|
||
Assert-True ($readme.Contains($requiredLiveAmrText)) "P1 README must document live fixed scenarios: $requiredLiveAmrText."
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 运行 UI 文档验证,确认文档尚未描述实时固定案例**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
|
||
```
|
||
|
||
Expected: script fails because README does not yet contain the new `Create(CoarsePathTestScenario scenario, double amrXMillimeters` signature.
|
||
|
||
- [ ] **Step 3: 更新 P1 手动测试与可视化章节**
|
||
|
||
在固定案例表之前新增“固定案例的实时 AMR 锚点”小节,逐条说明:六个 `粗路径规划-*` 入口读取一次 `getCartLocation`;起点等于这份冻结的世界 mm/deg 位姿;地图边界、目标、圆形/轴对齐矩形和 TwoLeg 检测原点统一平移;终点航向保持和基准起点的航向差;TwoLeg 朝向不旋转;手动 `粗路径规划` 的输入流程不变。
|
||
|
||
在同一小节以单行 inline code 明确列出公开签名:`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。
|
||
|
||
加入公开签名示例:
|
||
|
||
```csharp
|
||
CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(
|
||
CoarsePathTestScenario.RectangleDetour,
|
||
amrXMillimeters, amrYMillimeters, amrHeadingDegrees);
|
||
```
|
||
|
||
将缓存命中表项改为“相同 AMR X/Y 下的重复完整地图输入”;明确 AMR 位置已移动时未命中是正常的 `None`/非 `Input` 缓存结果,且仅航向变化不改变地图输入、仍可命中。补充运行器在定位读取、空值访问或有限数校验失败时显示 `AMR 位姿不可用` 且不启动后台规划;状态区会显示该次冻结的 AMR 起点。保留关于 `getCartLocation` 可能阻塞和必须在定位准备完成后运行的既有警告。
|
||
|
||
- [ ] **Step 4: 运行完整粗路径与地图回归集**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
|
||
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
|
||
```
|
||
|
||
Expected: build reports zero warnings and zero errors. Every verification script exits 0; integration ends with `Coarse path P1 scenario checks passed.` and UI ends with `Coarse path UI source checks passed.`
|
||
|
||
- [ ] **Step 5: 提交文档与最后验证脚本变更**
|
||
|
||
```powershell
|
||
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1
|
||
git commit -m "docs: explain live AMR coarse path scenarios"
|
||
```
|