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>
116 lines
4.8 KiB
C#
116 lines
4.8 KiB
C#
namespace AcDream.App.Studio;
|
|
|
|
/// <summary>
|
|
/// Parsed options for the acdream UI Studio standalone tool.
|
|
/// Constructed by <see cref="Parse"/> from the command-line tokens that follow
|
|
/// the <c>ui-studio</c> dispatch token.
|
|
/// </summary>
|
|
public sealed record StudioOptions(
|
|
string DatDir,
|
|
uint? LayoutId,
|
|
string? MarkupPath,
|
|
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.
|
|
///
|
|
/// <para>Positional (first non-flag arg): dat directory. Falls back to
|
|
/// <c>ACDREAM_DAT_DIR</c> when omitted.</para>
|
|
/// <para><c>--layout 0xNNNN</c>: hex LayoutDesc dat id to preview.</para>
|
|
/// <para><c>--markup <path></c>: path to a KSML markup file (Task 6, unsupported for now).</para>
|
|
/// <para><c>--dump <slug></c>: load a panel from the retail UI dump JSON by slug
|
|
/// (e.g. <c>inventory</c>, <c>radar</c>, <c>toolbar</c>). Static mockup — no controllers.</para>
|
|
/// <para><c>--dump-file <path></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 <path></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? screenshotPath = null;
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--layout" && i + 1 < args.Length)
|
|
{
|
|
var raw = args[++i];
|
|
// Accept 0xNNNN or plain hex.
|
|
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
raw = raw[2..];
|
|
if (uint.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out var id))
|
|
layoutId = id;
|
|
}
|
|
else if (args[i] == "--markup" && i + 1 < args.Length)
|
|
{
|
|
markupPath = args[++i];
|
|
}
|
|
else if (args[i] == "--dump" && i + 1 < args.Length)
|
|
{
|
|
dumpSlug = args[++i];
|
|
}
|
|
else if (args[i] == "--dump-file" && i + 1 < args.Length)
|
|
{
|
|
dumpFile = args[++i];
|
|
}
|
|
else if (args[i] == "--screenshot" && i + 1 < args.Length)
|
|
{
|
|
screenshotPath = args[++i];
|
|
}
|
|
else if (!args[i].StartsWith('-'))
|
|
{
|
|
datDir ??= args[i];
|
|
}
|
|
}
|
|
|
|
// Fall back to ACDREAM_DAT_DIR when no positional dat dir was given.
|
|
datDir ??= Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
|
|
|
if (string.IsNullOrWhiteSpace(datDir))
|
|
throw new InvalidOperationException(
|
|
"ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
|
|
|
|
// Default layout: vitals (0x2100006C), unless a dump slug or markup is requested.
|
|
if (layoutId is null && markupPath is null && dumpSlug is null)
|
|
layoutId = 0x2100006Cu;
|
|
|
|
return new StudioOptions(datDir, layoutId, markupPath, dumpSlug, dumpFile, screenshotPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve the dump file path for this session:
|
|
/// <list type="bullet">
|
|
/// <item><see cref="DumpFile"/> if explicitly set.</item>
|
|
/// <item>Otherwise <c><solutionRoot>/docs/research/2026-06-25-retail-ui-layout-dump.json</c>.</item>
|
|
/// </list>
|
|
/// Returns null when neither the override nor the default file exists.
|
|
/// </summary>
|
|
public string? ResolveDumpFile()
|
|
{
|
|
if (!string.IsNullOrEmpty(DumpFile))
|
|
return DumpFile;
|
|
|
|
// Walk up from the App binary to the solution root (same approach as
|
|
// ConformanceDats.SolutionRoot in the test project).
|
|
var dir = AppContext.BaseDirectory;
|
|
while (!string.IsNullOrEmpty(dir))
|
|
{
|
|
var candidate = Path.Combine(dir, "docs", "research",
|
|
"2026-06-25-retail-ui-layout-dump.json");
|
|
if (File.Exists(candidate))
|
|
return candidate;
|
|
dir = Path.GetDirectoryName(dir);
|
|
}
|
|
return null;
|
|
}
|
|
}
|