# TrapMap Image Export and Console Logging 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:** Add optional 300 DPI full-map PNG export with hard pixel/file limits and an independent switch for TrapMap terminal diagnostics.

**Architecture:** A new `TrapMapImageExporter` renders the completed in-memory grid without depending on CycleGUI/Painter state. `TrapMapTest` owns the two manual switches and calls the exporter only after successful map construction. A shared `TrapMapLog` always writes `DLog` and conditionally mirrors the same message to `Console.WriteLine`.

**Tech Stack:** C# 10, .NET Standard 2.0, internal pure-C# RGBA rasterizer, exact `StbImageWriteSharp` 1.16.7 managed PNG encoder, BCL-only PowerShell PNG parser, existing TrapMap tests.

## Global Constraints

- Do not inspect or modify `TrajPlanner`.
- Do not commit or stage any file.
- Keep `UI.GetPainter("TrapMapTest")` as the world-coordinate Painter; do not reintroduce `false`.
- Keep `UI.GetPainter("MultiWheelTwoLegDetect.Filter", false)` as the car-coordinate ROI Painter.
- Defaults: `_saveFullMapImage = true`, `_enableTerminalDebugLog = true`.
- PNG: 300 DPI, 4 pixels per cell, maximum edge 4000 pixels, maximum final size `50 * 1024 * 1024` bytes.
- Output: `<Environment.CurrentDirectory>\TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png`.
- Export failure never changes `TrapMapBuilder.Succeeded` or clears `GridMap`.
- Do not capture the Clumsy viewport or add point-cloud/motion behavior.

---

### Task 1: Export contract, dependency, and pre-allocation limits

**Files:**
- Modify: `ClumsyPilot/ClumsyPilot.csproj`
- Create: `ClumsyPilot/TrapMapImageExporter.cs`
- Create: `ClumsyPilot/tests/verify_trapmap_image.ps1`

**Interfaces:**
- Consumes: `GridMapData`, `TrapMapVehiclePose`, workstation `Vector2`, tire metadata, output root.
- Produces: `TrapMapImageExportRequest`, `TrapMapImageExportResult`, and `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)`.

- [ ] **Step 1: Add a failing image-export reflection test**

Create `verify_trapmap_image.ps1`. Load `ClumsyPilot/bin/Debug/netstandard2.0/ClumsyPilot.dll`; require types `MultiWheelC.TrapMapImageExporter`, `TrapMapImageExportRequest`, and `TrapMapImageExportResult`, and assert the assembly/output no longer contains a platform drawing dependency. Create a temporary directory under `$env:TEMP`, invoke the disabled path with an output directory that does not exist, and assert `Saved=false`, `Skipped=true`, and that no directory was created. Invoke an oversized request using a `1000×1` grid at 50mm so the four-pixels-per-cell canvas plus padding exceeds 4000, and assert rejection before any PNG/temp file exists.

Use these assertion helpers and cleanup guard:

```powershell
function Assert-Equal($expected, $actual, [string]$message) {
    if ($expected -ne $actual) { throw "$message Expected=$expected Actual=$actual" }
}
$testRoot = Join-Path $env:TEMP ("trapmap-image-test-" + [guid]::NewGuid().ToString('N'))
try {
    # reflection setup and assertions
} finally {
    if (Test-Path -LiteralPath $testRoot) {
        Remove-Item -LiteralPath $testRoot -Recurse -Force
    }
}
```

- [ ] **Step 2: Run RED verification**

```powershell
dotnet restore ClumsyPilot\ClumsyPilot.csproj
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
```

Expected: compilation succeeds and the image script fails because `TrapMapImageExporter` is absent.

- [ ] **Step 3: Add the managed PNG dependency**

Add the exact managed encoder package inside the package `ItemGroup` in `ClumsyPilot.csproj`. Keep the target framework unchanged, expose the package path, and use one explicit build target that copies only its single managed `netstandard2.0` runtime asset. No drawing-runtime or native asset is required:

```xml
<ItemGroup>
  <PackageReference Include="StbImageWriteSharp" Version="1.16.7"
                    GeneratePathProperty="true" />
</ItemGroup>
<Target Name="DeployManagedPngRuntime" AfterTargets="Build">
  <Copy SourceFiles="$(PkgStbImageWriteSharp)\lib\netstandard2.0\StbImageWriteSharp.dll"
        DestinationFiles="$(TargetDir)StbImageWriteSharp.dll" />
</Target>
```

Do not change the target framework.

- [ ] **Step 4: Implement request/result types and dimension validation**

Create `TrapMapImageExporter.cs` in namespace `MultiWheelC`. Use exact constants:

```csharp
public const int PixelsPerCell = 4;
public const int MaximumImageEdgePixels = 4000;
public const long MaximumFileSizeBytes = 50L * 1024L * 1024L;
public const float OutputDpi = 300f;
public const int OuterPaddingPixels = 24;
public const int HeaderHeightPixels = 140;
```

Request properties must include `GridMap`, `VehiclePose`, `WorkstationWorld`, `TireLayerStatus`, `TireLayerMessage`, `InputSource`, and `OutputRootDirectory`. Result properties must include `Saved`, `Skipped`, `FilePath`, `Message`, `FileSizeBytes`, `PixelWidth`, and `PixelHeight`.

`ExportIfEnabled(false, request)` returns a skipped result before validating the request or touching the filesystem. Enabled export validates non-null map/pose, finite workstation, then computes with `long`:

```csharp
long pixelWidth = 2L * OuterPaddingPixels + (long)grid.Cols * PixelsPerCell;
long pixelHeight = HeaderHeightPixels + 2L * OuterPaddingPixels
    + (long)grid.Rows * PixelsPerCell;
```

Reject non-positive or over-4000 dimensions before allocating the RGBA surface. Add `public static bool IsFileSizeAllowed(long byteCount)` returning `byteCount >= 0 && byteCount <= MaximumFileSizeBytes`; the actual save path must call this same function.

- [ ] **Step 5: Run GREEN contract checks**

Run the Task 1 commands. Expected: disabled/oversized assertions pass, no output directory exists for disabled export, and no RGBA buffer is allocated for oversized export.

### Task 2: Complete 300 DPI PNG rendering and atomic 50MB save

**Files:**
- Modify: `ClumsyPilot/TrapMapImageExporter.cs`
- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1`

**Interfaces:**
- Consumes: validated Task 1 request.
- Produces: a complete PNG or a failure result with no final/temporary file.

- [ ] **Step 1: Add failing PNG behavior assertions**

Extend the test with a `2×2` 50mm grid, mark one known occupied cell, set a finite vehicle/workstation, and export twice. Assert:

```powershell
Assert-Equal $true $result.Saved 'Small map must save.'
Assert-Equal $false $result.Skipped 'Enabled successful export is not skipped.'
if (-not (Test-Path -LiteralPath $result.FilePath)) { throw 'PNG file missing.' }
if ((Get-Item -LiteralPath $result.FilePath).Length -gt 50MB) { throw 'PNG exceeds 50MB.' }
if ([IO.Path]::GetExtension($result.FilePath) -ne '.png') { throw 'Output is not PNG.' }
if ((Split-Path $result.FilePath -Leaf) -notmatch '^TrapMap_\d{8}_\d{6}_\d{3}(_\d+)?\.png$') {
    throw 'Timestamp filename is invalid.'
}
$image = Read-PngRgba $result.FilePath
Assert-Equal $result.PixelWidth $image.Width 'PNG width mismatch.'
Assert-Equal $result.PixelHeight $image.Height 'PNG height mismatch.'
Assert-Equal 11811 $image.PixelsPerMetreX 'PNG horizontal pHYs mismatch.'
Assert-Equal 11811 $image.PixelsPerMetreY 'PNG vertical pHYs mismatch.'
```

Assert the two file paths differ. Assert `IsFileSizeAllowed(50MB)` is true and `IsFileSizeAllowed(50MB + 1)` is false. Assert no `*.tmp` remains.

- [ ] **Step 2: Run RED behavior test**

Run the image test. Expected: it fails because enabled rendering/save is not implemented.

- [ ] **Step 3: Render the full grid**

After validation, create an internal RGBA8 surface and draw cells, grid, overlays, and 5×7 bitmap text with clipped integer primitives. Encode the buffer with `StbImageWriteSharp`, then insert a CRC-protected `pHYs` chunk containing `11811,11811,1` immediately after `IHDR`.

Exact layout and colors:

```text
Canvas background: White
Map top-left: (OuterPaddingPixels, HeaderHeightPixels + OuterPaddingPixels)
Unmarked cell interior: White
Grid lines: LightGray, 1px
Occupied cell interior: Red
Map border: Black, 2px
Vehicle outline/center/heading: Blue
Workstation circle/cross/text: LimeGreen
Header text: Black
```

For cell `(col,row)`, invert Y:

```csharp
int imageCol = col;
int imageRow = grid.Rows - 1 - row;
int x = mapLeft + imageCol * PixelsPerCell;
int y = mapTop + imageRow * PixelsPerCell;
```

Fill occupied interiors before drawing all vertical/horizontal grid lines. Convert vehicle rectangle corners and workstation through a shared world-to-pixel helper using `(worldX - XMin) / ResolutionMm` for X and `((YMin + Rows * ResolutionMm) - worldY) / ResolutionMm` for Y. The Y expression uses the discrete raster's actual upper edge, so overlays stay aligned when the requested world bounds are not an exact multiple of the resolution. Draw title strings for bounds, resolution, rows/cols, occupancy, obstacle count, tire status/message, and input source.

- [ ] **Step 4: Implement collision-safe atomic save and cleanup**

Create `<OutputRootDirectory>\TrapMapExports` only after all request/dimension validation. Select the millisecond timestamp name; if it exists, append `_1`, `_2`, etc. Encode into the exclusively reserved `finalPath + ".tmp"` stream, read `FileInfo.Length`, call `IsFileSizeAllowed`, delete the temp on rejection, then `File.Move(tempPath, finalPath)`.

Wrap rendering/saving in `try/catch/finally`; `finally` deletes only the current temp path if present. Never delete an existing final PNG. Return failure messages instead of throwing into the test runner.

- [ ] **Step 5: Run image and existing behavior tests**

```powershell
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1
```

Expected: PNG assertions pass and existing behavior scripts retain their passing messages.

### Task 3: Terminal switch and successful-map export integration

**Files:**
- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs`
- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1`
- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1`

**Interfaces:**
- Consumes: Task 2 exporter.
- Produces: `_saveFullMapImage`, `_enableTerminalDebugLog`, shared dual-channel logging, and post-success export.

- [ ] **Step 1: Add failing source/integration contracts**

Require exact defaults, `EnableTerminalDebugLog` builder wiring, `TrapMapLog.Write`, and `TrapMapImageExporter.ExportIfEnabled`. Assert source still contains both world-Painter calls without `false`, retains the filter Painter with `false`, and export invocation occurs only after `_builder.Succeeded` is checked.

Add source assertions:

```powershell
if ($source -notmatch '_saveFullMapImage\s*=\s*true') { $failures.Add('Image switch default missing.') }
if ($source -notmatch '_enableTerminalDebugLog\s*=\s*true') { $failures.Add('Terminal switch default missing.') }
if ($source -notmatch 'TrapMapLog\.Write\(') { $failures.Add('Shared TrapMap logger missing.') }
if ($source -notmatch 'Console\.WriteLine\(') { $failures.Add('Terminal mirror missing.') }
if ($source -match 'GetPainter\("TrapMapTest",\s*false\)') { $failures.Add('TrapMap Painter regressed to local coordinates.') }
if ($source -notmatch 'GetPainter\("MultiWheelTwoLegDetect\.Filter",\s*false\)') { $failures.Add('Filter Painter lost local coordinates.') }
```

- [ ] **Step 2: Run RED contract test**

Run `verify_trapmap_inputs.ps1`. Expected: new switch/logger/export assertions fail.

- [ ] **Step 3: Implement the shared logger and switches**

Add a `TrapMapLog` static class with:

```csharp
public static void Write(string message, bool enableTerminal)
{
    DLog.Log(message, "TrapMapTest");
    if (enableTerminal)
        Console.WriteLine($"[TrapMapTest] {message}");
}
```

Add `public bool EnableTerminalDebugLog = true` to the builder. Replace each TrapMap-owned two-argument `DLog.Log` call whose category is exactly `"TrapMapTest"` with `TrapMapLog.Write(message, EnableTerminalDebugLog)` in the builder and with the const switch in `TrapMapTest`. Do not replace unrelated log categories or `Hedingben.ToastText`.

Add the exact two constants to the test manual-edit section and pass terminal configuration into the builder.

- [ ] **Step 4: Invoke image export after successful map construction**

After the `_builder.Succeeded` failure return and after retrieving `grid`, construct the request from `_builder.GridMap`, `VehiclePose`, `WorkstationWorld`, tire status/message/input source, and `Environment.CurrentDirectory`. Call:

```csharp
var export = TrapMapImageExporter.ExportIfEnabled(_saveFullMapImage, request);
TrapMapLog.Write(export.Message, _enableTerminalDebugLog);
if (export.Saved)
    Hedingben.ToastText($"栅格图片已保存: {export.FilePath}", "TrapMapTest");
```

Do not alter builder success when export fails/skips.

- [ ] **Step 5: Run full fresh verification**

```powershell
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
git diff --check
$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" }
```

Expected: source, compile, grid, lifecycle, and image tests pass; no whitespace errors; no staged files. Confirm an exporter-generated test PNG reports 300 DPI and never exceeds 50MB before test cleanup.
