Phase 7: cockpit overlay generator, region extraction, and profile editor
Overlay generator (pure, tested) — RioJoy.Core/Overlay:
- FontFitter ports the legacy calc-fontsize auto-fit search (validated against a
brute-force oracle); OverlayLayoutEngine ports create-data-layer's fit + h/v
justification and adds per-region 90 deg CCW rotation; OverlayTemplate/Region +
OverlayTemplateStore hold cell geometry/colour/rotation (regions.json).
- GoobieDataImporter parses the legacy .data label sheet into label rows.
- src/RioJoy.Overlay: SkiaSharp rasterizer (SkiaTextMeasurer shared with the engine
so measured layout == drawn output; SkiaOverlayRenderer -> PNG;
ProfileWallpaperGenerator). Verified end-to-end on the real cockpit art
(regions.json + riojoy.png + TEST.data) by OverlayRenderIntegrationTests.
- RioJoy.Tray/WallpaperApplier applies via SystemParametersInfo; RioCoordinator
generates+applies on profile activation (opt-in via AppConfig.OverlayTemplatePath).
Region geometry — tools/XcfRegionExtract parses riojoy.xcf (a GIMP-format binary
reader, no GIMP needed) into the 119-cell regions.json: per-layer offsets/size/
font/colour, with 90 deg CCW rotation on a-00 + b-10..b-1F. Base image riojoy.png.
NOTE: the wallpaper positions are a VGA chroma-split display artifact (6 displays),
not the cockpit's logical layout.
Mapping editor (first cut) — RioJoy.Tray/Editor/ProfileEditorForm renders the
config sheet's logical grid (SheetLayout parses the sheet CSV export). The iRIO
word is edited via ButtonBinding <-> RioMapEntry.Create (no hex bit-twiddling), with
a context-sensitive picker: keyboard keys by name (KeyCatalog VK names), joystick
Button N, hat direction, mouse/RIO-command enum names; modifiers only for keyboard.
Opened from the tray ("Edit profile..."). The sheet-grid layout is a starting point
that needs rework from a better-formatted source.
~220 xUnit tests green. Docs (PLAN.md, README) updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using RioJoy.Core.Overlay;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace RioJoy.Overlay;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a profile's cockpit wallpaper: renders a label set onto the overlay
|
||||
/// template's base image and writes a PNG. Resolves the template's
|
||||
/// <see cref="OverlayTemplate.BaseImagePath"/> relative to the template file's
|
||||
/// directory (so a <c>regions.json</c> + sibling <c>riojoy.png</c> work as a unit).
|
||||
/// The runtime calls this on profile activation; an editor can call it for preview.
|
||||
/// </summary>
|
||||
public sealed class ProfileWallpaperGenerator
|
||||
{
|
||||
private readonly SkiaOverlayRenderer _renderer;
|
||||
|
||||
public ProfileWallpaperGenerator(SkiaOverlayRenderer? renderer = null) =>
|
||||
_renderer = renderer ?? new SkiaOverlayRenderer();
|
||||
|
||||
/// <summary>
|
||||
/// Render <paramref name="labels"/> onto the template's base image and write a
|
||||
/// PNG to <paramref name="outputPath"/>; returns the output path.
|
||||
/// </summary>
|
||||
/// <param name="templateDirectory">
|
||||
/// Directory the template's relative <see cref="OverlayTemplate.BaseImagePath"/>
|
||||
/// is resolved against (normally the folder holding the regions.json).
|
||||
/// </param>
|
||||
public string Generate(
|
||||
OverlayTemplate template,
|
||||
string templateDirectory,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
string outputPath,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(template);
|
||||
ArgumentNullException.ThrowIfNull(labels);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(outputPath);
|
||||
if (string.IsNullOrWhiteSpace(template.BaseImagePath))
|
||||
throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set.");
|
||||
|
||||
string basePath = Path.IsPathRooted(template.BaseImagePath)
|
||||
? template.BaseImagePath
|
||||
: Path.Combine(templateDirectory, template.BaseImagePath);
|
||||
|
||||
using SKBitmap baseImage = SKBitmap.Decode(basePath)
|
||||
?? throw new InvalidOperationException($"Could not decode base image: {basePath}");
|
||||
|
||||
byte[] png = _renderer.RenderToPng(template, labels, baseImage, options);
|
||||
|
||||
string? dir = Path.GetDirectoryName(Path.GetFullPath(outputPath));
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllBytes(outputPath, png);
|
||||
return outputPath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user