14 KiB
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 reintroducefalse. - 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 * 1024bytes. - Output:
<Environment.CurrentDirectory>\TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png. - Export failure never changes
TrapMapBuilder.Succeededor clearsGridMap. - 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, workstationVector2, tire metadata, output root. -
Produces:
TrapMapImageExportRequest,TrapMapImageExportResult, andTrapMapImageExporter.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:
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
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:
<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:
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:
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:
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:
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:
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
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:
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:
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:
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 -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.