feat(studio): headless --screenshot mode (render panel to PNG + exit)

Add `--screenshot <path>` to the UI Studio: when set the window is
created hidden (WindowOptions.IsVisible = false), the panel is loaded
and rendered into PanelFbo on the first OnRender tick, pixels are read
back with the new PanelFbo.ReadColorRgba(), rows are flipped (GL
bottom-left → PNG top-left), and the result is saved via ImageSharp
SaveAsPng before calling _window.Close().

Render size is derived from the loaded root's Width/Height (clamped
256–2048 px); falls back to 1280×720 when the root has no explicit
size.  In headless mode ImGui, the inspector, and input wiring are
all skipped — only PanelFbo is created.  Interactive path is
unchanged.  SixLabors.ImageSharp 3.1.12 was already a direct dep of
AcDream.App (Phase O-T7); no new PackageReference needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 16:36:10 +02:00
parent e3de7f0dab
commit f778b100b7
3 changed files with 133 additions and 24 deletions

View file

@ -9,8 +9,9 @@ public sealed record StudioOptions(
string DatDir,
uint? LayoutId,
string? MarkupPath,
string? DumpSlug = null,
string? DumpFile = null)
string? DumpSlug = null,
string? DumpFile = null,
string? ScreenshotPath = null)
{
/// <summary>
/// Parse studio options from the args that come AFTER the <c>ui-studio</c> token.
@ -24,16 +25,20 @@ public sealed record StudioOptions(
/// <para><c>--dump-file &lt;path&gt;</c>: override the default dump file path
/// (<c>docs/research/2026-06-25-retail-ui-layout-dump.json</c> from the solution root).
/// Only meaningful when <c>--dump</c> is also given.</para>
/// <para><c>--screenshot &lt;path&gt;</c>: headless mode — render the loaded panel to a PNG
/// at <paramref name="path"/> and exit without showing an interactive window.
/// Combines with <c>--dump</c> or <c>--layout</c>.</para>
/// <para>When neither <c>--layout</c>, <c>--markup</c>, nor <c>--dump</c> is given the
/// default layout <c>0x2100006C</c> (vitals) is used.</para>
/// </summary>
public static StudioOptions Parse(string[] args)
{
string? datDir = null;
uint? layoutId = null;
string? markupPath = null;
string? dumpSlug = null;
string? dumpFile = null;
string? datDir = null;
uint? layoutId = null;
string? markupPath = null;
string? dumpSlug = null;
string? dumpFile = null;
string? screenshotPath = null;
for (int i = 0; i < args.Length; i++)
{
@ -58,6 +63,10 @@ public sealed record StudioOptions(
{
dumpFile = args[++i];
}
else if (args[i] == "--screenshot" && i + 1 < args.Length)
{
screenshotPath = args[++i];
}
else if (!args[i].StartsWith('-'))
{
datDir ??= args[i];
@ -75,7 +84,7 @@ public sealed record StudioOptions(
if (layoutId is null && markupPath is null && dumpSlug is null)
layoutId = 0x2100006Cu;
return new StudioOptions(datDir, layoutId, markupPath, dumpSlug, dumpFile);
return new StudioOptions(datDir, layoutId, markupPath, dumpSlug, dumpFile, screenshotPath);
}
/// <summary>