acdream/docs/superpowers/plans/2026-06-25-ui-studio-previewer.md
Erik f55650fdcd docs(studio): implementation plan — acdream UI Studio (8 staged tasks)
Bite-sized, gated build order: (1) RenderBootstrap [new code, no GameWindow
edit → zero game-regression risk], (2) StudioWindow + LayoutSource + ui-studio
subcommand, (3) ImGui inspector (canvas FBO + tree + props + click-inspect),
(4) FixtureProvider sample data, (5) live 3-D doll, (6) markup + hot-reload,
(7) editable props + write-back, (8) doll-camera sliders. Pure-logic units
(LayoutSource/FixtureProvider/MarkupWriteBack) unit-tested; GL/window tasks
visual-gated. Spec: docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:13:27 +02:00

26 KiB
Raw Blame History

acdream UI Studio 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: Build a standalone Silk.NET dev tool that renders any acdream UI panel through the production render stack, with an ImGui click-to-inspect inspector, sample-data fixtures, the live 3-D doll, markup hot-reload + write-back, and doll-camera sliders.

Architecture: A new StudioWindow (Silk GL app, launched via a ui-studio subcommand in Program.cs) builds its render stack through a new RenderBootstrap (a clean construction of just the subset the studio needs — GL, DatCollection, TextureCache, the WB mesh pipeline, UiHost — leaving GameWindow untouched). A LayoutSource loads a panel (dat LayoutDesc id or markup file) into a UiElement tree; a FixtureProvider populates it via the production controllers; a StudioInspector (ImGui) renders the tool chrome with the panel shown as an FBO image.

Tech Stack: C# .NET 10, Silk.NET (Windowing/Input/OpenGL), ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0 (via the existing AcDream.UI.ImGui project), the existing AcDream.App.UI retained-mode tree + LayoutImporter/DatWidgetFactory, DatReaderWriter.

Spec: docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md

Subagent note (project policy): implementers are Sonnet and INHERIT CLAUDE.md; they read the cited code themselves. Tasks give the contract, the key code, the files to read, the test, the gate, and the commit message — not every line. GL/window tasks verify via a visual gate (the studio is itself the visual tool); pure-logic tasks (LayoutSource infos, FixtureProvider, MarkupWriteBack) use real unit tests.

Branch: claude/hopeful-maxwell-214a12. Commit after every task. Full build (dotnet build src/AcDream.App/AcDream.App.csproj) green before any commit; relevant tests green.

Launch (for every visual gate):

$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug -- ui-studio "$env:USERPROFILE\Documents\Asheron's Call" --layout 0x2100006C

Do NOT auto-kill a running client; if a rebuild is locked, ask the user to close it (feedback_dont_kill_clients_before_launch).


File structure

New, all under src/AcDream.App/:

  • Rendering/RenderBootstrap.csRenderStack Create(GL, RenderBootstrapOptions); the shared render-stack builder + the RenderStack record.
  • Studio/StudioWindow.cs — the Silk window + frame loop + owns the stack/inspector/sources.
  • Studio/StudioOptions.cs — parsed ui-studio args (layout id | markup path | dat dir).
  • Studio/LayoutSource.cs — load/reload a panel from a dat id or a markup file.
  • Studio/FixtureProvider.cs — sample data + per-layout controller binding.
  • Studio/SampleData.cs — the canned items / vitals / creature fixture constants.
  • Studio/StudioInspector.cs — the ImGui chrome (canvas/tree/properties/render-config).
  • Studio/PanelFbo.cs — render-to-texture target for the previewed panel (mirrors PaperdollViewportRenderer's FBO).
  • Studio/MarkupWriteBack.cs — targeted in-place markup attribute edits.
  • Studio/HotReloadWatcher.cs — debounced FileSystemWatcher.
  • Modify: src/AcDream.App/Program.csui-studio subcommand dispatch.

Tests under tests/AcDream.App.Tests/Studio/:

  • LayoutSourceTests.cs, FixtureProviderTests.cs, MarkupWriteBackTests.cs.

Task 1: RenderBootstrap (shared render stack)

Files:

  • Create: src/AcDream.App/Rendering/RenderBootstrap.cs
  • Read first: src/AcDream.App/Rendering/GameWindow.cs:1026-1815,2313-2408 (the construction order), src/AcDream.App/UI/UiHost.cs, src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (ctor), src/AcDream.App/UI/Layout/UiDatFont.cs.

Contract:

namespace AcDream.App.Rendering;

public sealed record RenderStack(
    GL Gl,
    DatReaderWriter.DatCollection Dats,
    string ShaderDir,
    BindlessSupport Bindless,
    TextureCache TextureCache,
    Shader MeshShader,
    Wb.WbMeshAdapter MeshAdapter,
    Wb.EntitySpawnAdapter EntitySpawnAdapter,
    Wb.WbDrawDispatcher DrawDispatcher,
    SceneLightingUboBinding LightingUbo,
    AcDream.App.UI.UiHost UiHost,
    AcDream.App.UI.Layout.UiDatFont? VitalsDatFont)
{
    /// <summary>sprite id (0x06xxxxxx) → (GL handle, width, height) for the importer's resolve func.</summary>
    public (uint, int, int) ResolveChrome(uint spriteId)
        => TextureCache.GetOrUploadRenderSurface(spriteId, out var w, out var h) is var h2
           ? (h2, w, h) : (0u, 0, 0);   // match GameWindow.ResolveChrome — read its exact body first
}

public sealed record RenderBootstrapOptions(QualitySettings Quality);

public static class RenderBootstrap
{
    /// <summary>Build the subset of the production render stack the UI Studio needs — same classes,
    /// same construction order as GameWindow.OnLoad, minus terrain/sky/physics/streaming. Throws
    /// NotSupportedException if bindless/draw-params are missing (same as the game).</summary>
    public static RenderStack Create(GL gl, DatReaderWriter.DatCollection dats, RenderBootstrapOptions opts);
}
  • Step 1: Read the cited GameWindow construction lines and ResolveChrome (grep it) so the order + the exact GetOrUploadRenderSurface/sequencer-factory signatures are copied, not guessed.
  • Step 2: Write RenderBootstrap.Create constructing, in order: BindlessSupport.TryCreate (throw if absent/no draw-params, copy the GameWindow message), shadersDir = Path.Combine(AppContext.BaseDirectory,"Rendering","Shaders"), SceneLightingUboBinding(gl), MeshShader = new Shader(gl, mesh_modern.vert/.frag), TextureCache(gl, dats, bindless), WbMeshAdapter(gl, dats, NullLogger<WbMeshAdapter>.Instance), the SequencerFactory lambda (copy from GameWindow:2335-2361, capturing dats + an AnimLoader), EntitySpawnAdapter(textureCache, sequencerFactory, meshAdapter), WbDrawDispatcher(gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter, bindless, new <classificationCache>) with AlphaToCoverage = opts.Quality.AlphaToCoverage, the vitals dat font (copy GameWindow's load of 0x40000000 via UiDatFont), and UiHost(gl, shadersDir, defaultFont). Return the RenderStack.
  • Step 3: Build: dotnet build src/AcDream.App/AcDream.App.csproj -c Debug. Expected: green (no test yet — GL construction is verified by Task 2's boot gate).
  • Step 4: Commit.

Gate: Build green. (Functional gate is Task 2.)

Commit: feat(studio): RenderBootstrap — shared render stack for the UI previewer


Task 2: StudioWindow + LayoutSource (dat id) + Program dispatch

Files:

  • Create: src/AcDream.App/Studio/StudioOptions.cs, src/AcDream.App/Studio/LayoutSource.cs, src/AcDream.App/Studio/StudioWindow.cs
  • Modify: src/AcDream.App/Program.cs (before new GameWindow)
  • Test: tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs
  • Read first: src/AcDream.App/UI/Layout/LayoutImporter.cs:57-190, src/AcDream.App/Rendering/GameWindow.cs:972-1017 (window+Run), src/AcDream.App/UI/UiHost.cs:65-96 (Draw/WireMouse/WireKeyboard).

StudioOptions contract:

namespace AcDream.App.Studio;
public sealed record StudioOptions(string DatDir, uint? LayoutId, string? MarkupPath)
{
    public static StudioOptions Parse(string[] args);   // args AFTER the "ui-studio" token
}

Parse: positional dat dir (or ACDREAM_DAT_DIR), --layout 0xNNNN (hex), --markup <path>. Default layout when neither given: 0x2100006Cu (vitals).

LayoutSource contract:

public enum LayoutSourceKind { DatLayout, Markup }
public sealed class LayoutSource
{
    public LayoutSource(DatCollection dats, Func<uint,(uint,int,int)> resolve, UiDatFont? datFont);
    public LayoutSourceKind Kind { get; }
    public uint? LayoutId { get; }
    public string? MarkupPath { get; }
    public string? LastError { get; private set; }
    /// <summary>Imported tree, or null on error (LastError set). Dat → LayoutImporter.Import; markup → MarkupDocument.Build with an empty binding.</summary>
    public ImportedLayout? CurrentLayout { get; private set; }
    public UiElement? Load(StudioOptions opts);   // sets Kind/LayoutId/MarkupPath + CurrentLayout
    public UiElement? Reload();                    // re-run the current source; markup re-reads the file
}
  • Step 1 (test, GL-free): LayoutSourceTests.LoadsDatLayout_byId. Open real dats via ConformanceDats.ResolveDatDir() (skip if null), build a LayoutSource with a STUB resolve _ => (1u,1,1) + null font, Load(new StudioOptions(dir, 0x2100006Cu, null)), assert CurrentLayout non-null and FindElement(0x2100006Cu) != null and Kind == DatLayout. (Use LayoutImporter.ImportInfos is GL-free; Import's resolve is only called for sprites — the stub is fine for tree-shape.)
  • Step 2: Run it — expect FAIL (LayoutSource missing). dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter LayoutSourceTests.
  • Step 3: Implement StudioOptions.Parse + LayoutSource (dat path only; markup path returns LastError "markup unsupported" for now — wired in Task 6). Dat path = LayoutImporter.Import(dats, id, resolve, datFont).
  • Step 4: Run the test — expect PASS.
  • Step 5: Implement StudioWindow: mirror GameWindow.Run() window creation (1280×720, GL 4.3 Core, stencil 8). On Load: gl = GL.GetApi(window), input = window.CreateInput(), dats = new DatCollection(opts.DatDir, DatAccessType.Read), stack = RenderBootstrap.Create(gl, dats, new(QualitySettings...)), wire stack.UiHost.WireMouse/WireKeyboard for each mouse/keyboard, source = new LayoutSource(dats, stack.ResolveChrome, stack.VitalsDatFont), root = source.Load(opts), and if non-null add it to stack.UiHost.Root (use the same add-child API GameWindow uses — grep _uiHost.Root adds). On Render: gl.ClearColor mid-grey, Clear, stack.UiHost.Tick(dt) + stack.UiHost.Draw(new Vector2(window.Size.X, window.Size.Y)). On Closing: dispose.
  • Step 6: Program.cs: before new GameWindow, add if (args.Length >= 1 && args[0] == "ui-studio") { var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]); using var sw = new AcDream.App.Studio.StudioWindow(so); sw.Run(); return 0; }.
  • Step 7: Build green, then VISUAL GATE (launch command above). Expect: a window showing the vitals panel chrome (three empty bars + frame) rendered by the real UiHost.
  • Step 8: Commit.

Gate (visual): ui-studio --layout 0x2100006C opens a window showing the vitals panel.

Commit: feat(studio): StudioWindow + LayoutSource — render a dat panel standalone


Task 3: StudioInspector (ImGui) + canvas FBO + tree + props + click-to-inspect

Files:

  • Create: src/AcDream.App/Studio/PanelFbo.cs, src/AcDream.App/Studio/StudioInspector.cs
  • Modify: StudioWindow.cs (init ImGui, render panel→FBO, draw inspector)
  • Read first: src/AcDream.UI.ImGui/ImGuiBootstrapper.cs:35-46, src/AcDream.App/Rendering/PaperdollViewportRenderer.cs (the FBO RTT pattern + GLStateScope), src/AcDream.App/UI/UiRoot.cs (grep HitTestTopDown), src/AcDream.App/UI/UiElement.cs (Id/Type/Left/Top/Width/Height/Anchors/ZOrder/Children accessors).

PanelFbo: mirror PaperdollViewportRenderer's FBO (RGBA8 + Depth24Stencil8, lazy resize). uint Render(int w, int h, UiHost host) = bind FBO, viewport, clear transparent, host.Draw(new Vector2(w,h)) inside a GLStateScope, return color tex. Reuse the exact FBO setup from PaperdollViewportRenderer.EnsureFramebuffer.

StudioInspector: ImGui windows, drawn between BeginFrame/Render:

public sealed class StudioInspector
{
    public UiElement? Selected { get; set; }
    // Canvas window: ImGui.Image(panelTex) sized to the panel; capture click → return panel-local (x,y)
    public (int x,int y)? DrawCanvas(nint panelTex, int w, int h);
    public void DrawTree(UiElement root);          // recursive ImGui.TreeNode; click sets Selected
    public void DrawProperties();                  // Selected's Id/Type/Rect/Anchors/ZOrder/sprites (read-only here)
}

Click-to-inspect: when DrawCanvas returns a coord, call root HitTestTopDown(x,y) (grep its exact signature/return) → set Selected.

  • Step 1: Implement PanelFbo (copy PaperdollViewportRenderer FBO lifecycle; swap the doll draw for host.Draw).
  • Step 2: StudioWindow.Load: construct new ImGuiBootstrapper(gl, window, input) (no DevToolsEnabled gate — the studio is always devtools) + new PanelFbo(gl) + new StudioInspector().
  • Step 3: StudioWindow.Render: panelTex = panelFbo.Render(w,h, stack.UiHost); imgui.BeginFrame(dt); var click = inspector.DrawCanvas((nint)panelTex, w, h); if (click is {} c) inspector.Selected = root.HitTestTopDown(c.x, c.y); inspector.DrawTree(root); inspector.DrawProperties();; imgui.Render().
  • Step 4: Implement StudioInspector windows (ImGui.NET: ImGui.Begin, ImGui.Image, ImGui.TreeNodeEx, ImGui.Text). Tree recurses element.Children; each node label = $"{name} 0x{Id:X8} [{Type}]". Properties: Id, Type, Rect (Left,Top,Width,Height), Anchors, ZOrder, and each state sprite id (grep how UiDatElement/UiButton expose their sprite ids; if not trivially exposed, list "—" for v1 and note follow-up).
  • Step 5: Build green, VISUAL GATE: window shows a Canvas pane (the panel) + a Tree pane + a Properties pane; clicking the panel or a tree node selects an element and shows its id/rect.
  • Step 6: Commit.

Gate (visual): click an element in the canvas or tree → its id / Type / rect appear in Properties.

Commit: feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect


Task 4: FixtureProvider — populate 2-D panels with sample data

Files:

  • Create: src/AcDream.App/Studio/SampleData.cs, src/AcDream.App/Studio/FixtureProvider.cs
  • Modify: StudioWindow.cs (call FixtureProvider.Populate after Load)
  • Test: tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs
  • Read first: src/AcDream.Core/Items/ClientObjectTable.cs:41-420 (AddOrUpdate/Get/GetContents/ClientObject fields), the four controllers' Bind (signatures in the plan header research), src/AcDream.Core/Items/ItemType.cs + EquipMask.

SampleData: static factory building a ClientObjectTable seeded with: a player object (guid 0xPLAYER, ItemsCapacity=102, ContainersCapacity=7), ~6 loose items in the main pack (varied Type/IconId — pick real icon ids from existing tests or a known wcid; ContainerId = player, gapless ContainerSlot), ~2 side bags (Type=Container, ItemsCapacity>0), and a few equipped items (CurrentlyEquippedLocation set to Head/Chest/etc). Plus sample vitals (health=0.8f, stam=0.6f, mana=0.9f + text). Constants only — no wire.

FixtureProvider.Populate(uint layoutId, ImportedLayout layout, RenderStack stack) switch on layoutId:

  • 0x2100006CVitalsController.Bind(layout, ()=>0.8f, ()=>0.6f, ()=>0.9f, ()=>"80/100", ()=>"60/100", ()=>"90/100").

  • 0x21000016ToolbarController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, _=>{}, ...digits from GameWindow's load...).

  • 0x21000023InventoryController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, ()=>100, stack.VitalsDatFont, contents/sideBag/mainPack sprites, ...).

  • default → no-op (structural preview). IconIds = reuse GameWindow's iconIds lambda (grep it — IconComposer-backed); if heavy, a stub returning the passed base id is acceptable for v1 (note it).

  • Step 1 (test): FixtureProviderTests.SampleTable_hasPackContents. Build SampleData.BuildObjectTable(), assert table.GetContents(PLAYER).Count >= 6 and a known item's Type/IconId set.

  • Step 2: Run — FAIL (missing).

  • Step 3: Implement SampleData + the table seeding.

  • Step 4: Run — PASS.

  • Step 5: Implement FixtureProvider.Populate; call it from StudioWindow after source.Load.

  • Step 6: Build green, VISUAL GATE: --layout 0x21000023 shows the inventory with sample items in the grid + a non-zero burden bar; --layout 0x21000016 shows toolbar slots populated; --layout 0x2100006C shows filled vital bars.

  • Step 7: Commit.

Gate (visual): inventory / toolbar / vitals panels render POPULATED (sample items, filled bars).

Commit: feat(studio): FixtureProvider — sample data populates the 2-D panels


Task 5: Live doll in the paperdoll viewport

Files:

  • Modify: FixtureProvider.cs (build a sample creature entity for 0x21000023/paperdoll), StudioWindow.cs (wire PaperdollViewportRenderer + the pre-UI doll render, mirroring GameWindow:9148-9160)
  • Read first: src/AcDream.App/Rendering/GameWindow.cs:3786-3897 (RefreshPaperdollDoll/ApplyPaperdollPose), 9148-9160 (the pre-UI doll hook), src/AcDream.App/Rendering/DollEntityBuilder.cs (Build), src/AcDream.App/Rendering/PaperdollViewportRenderer.cs (ctor + SetDoll/Render), EntitySpawnAdapter.OnCreate.

Sample creature: a base human Setup id (grep the test character / a known humanoid Setup 0x0200....; or reuse the player Setup constant if one exists in tests). Build a WorldEntity { SourceGfxObjOrSetupId = setupId, MeshRefs = <resolved from Setup via WbMeshAdapter>, ... }, run it through stack.EntitySpawnAdapter.OnCreate to resolve MeshRefs (same path GameWindow uses), then DollEntityBuilder.Build + ApplyPaperdollPose-equivalent. Keep a small StudioDoll helper if this grows.

  • Step 1: In StudioWindow.Load, construct new PaperdollViewportRenderer(gl, stack.DrawDispatcher, stack.LightingUbo).
  • Step 2: When the layout is the inventory/paperdoll, have FixtureProvider build the sample creature, call RefreshPaperdollDoll-equivalent (clone → ApplyPaperdollPose) → paperdollRenderer.SetDoll(doll); expose the doll-viewport widget (FindElement 0x100001D5).
  • Step 3: In StudioWindow.Render, BEFORE the panel FBO draw, render the doll into the viewport widget's texture exactly as GameWindow:9156-9159 (widget.TextureHandle = paperdollRenderer.Render(w,h)), gated on the viewport widget being present + visible.
  • Step 4: Build green, VISUAL GATE: --layout 0x21000023 shows the paperdoll with a DRESSED 3-D doll in the held pose (reuses the camera/heading/pose shipped in 8fa66c2).
  • Step 5: Commit.

Gate (visual): the paperdoll viewport shows the sample creature as a posed 3-D doll.

Commit: feat(studio): live 3-D doll in the paperdoll preview


Task 6: Markup source + hot-reload

Files:

  • Create: src/AcDream.App/Studio/HotReloadWatcher.cs
  • Modify: LayoutSource.cs (markup path via MarkupDocument.Build), StudioWindow.cs (watch + reload), StudioOptions already parses --markup
  • Read first: src/AcDream.App/UI/MarkupDocument.cs:21-82, an existing first-party markup file (grep assets/*.xml, e.g. the retired vitals.xml in git history or a current plugin panel) for the schema.
  • Test: extend LayoutSourceTests.

LayoutSource markup path: Kind=Markup; Load reads the file → MarkupDocument.Build(xml, EmptyBinding, resolve, null) → wrap its UiNineSlicePanel as the root; Reload re-reads the file. EmptyBinding = new object() (no {Binding} resolution needed for preview; unresolved bindings render literally or empty — acceptable).

HotReloadWatcher:

public sealed class HotReloadWatcher : IDisposable
{
    public HotReloadWatcher(string filePath, Action onChanged);   // debounce ~150ms; marshal onChanged via a thread-safe flag the window polls
}

The watcher sets a volatile bool _dirty; StudioWindow.Render checks it and calls source.Reload() + FixtureProvider.Populate + re-add to the UiRoot on the GL thread (FileSystemWatcher fires on a worker thread — never touch GL off-thread).

  • Step 1 (test): LayoutSourceTests.LoadsMarkupFile. Write a temp .xml with a minimal UiNineSlicePanel, Load(new StudioOptions(dir, null, path)), assert non-null root + Kind==Markup.
  • Step 2: Run — FAIL.
  • Step 3: Implement the markup path in LayoutSource + HotReloadWatcher.
  • Step 4: Run — PASS.
  • Step 5: Wire the watcher in StudioWindow (poll _dirty in Render; reload on the GL thread).
  • Step 6: Build green, VISUAL GATE: launch --markup <path>; edit the file in an editor + save → the studio reloads the panel within ~1s.
  • Step 7: Commit.

Gate (visual): edit a markup file externally → studio re-renders it live.

Commit: feat(studio): markup source + hot-reload


Task 7: Editable markup props + write-back

Files:

  • Create: src/AcDream.App/Studio/MarkupWriteBack.cs
  • Modify: StudioInspector.cs (editable rect/anchor fields when Kind==Markup), StudioWindow.cs (apply edit live + persist)
  • Test: tests/AcDream.App.Tests/Studio/MarkupWriteBackTests.cs

MarkupWriteBack: targeted in-place attribute edits — NOT a full re-serialize (preserve formatting/comments):

public static class MarkupWriteBack
{
    /// <summary>In the markup text, find the element whose id attribute == elementId and set attr=value
    /// (add the attribute if absent). Returns the new text. Leaves all other bytes untouched.</summary>
    public static string SetAttribute(string markup, uint elementId, string attr, string value);
}

Implement by locating the element's opening tag (match the id attribute literal Id="0x...." — grep the markup schema for the exact id attribute name), then regex-replace just that attribute within that tag span.

  • Step 1 (test): MarkupWriteBackTests.SetAttribute_updatesInPlace. Given a 2-element markup string, SetAttribute(m, id, "Width", "200") → re-parse, assert the target's Width==200 AND the sibling element + any comment are byte-identical.
  • Step 2: Run — FAIL.
  • Step 3: Implement MarkupWriteBack.SetAttribute.
  • Step 4: Run — PASS (add an absent-attribute case + a not-found case returning the input unchanged).
  • Step 5: StudioInspector.DrawProperties: when Kind==Markup, render rect (Left/Top/Width/Height) + anchors as ImGui InputInt/combo; on change, set the live UiElement field AND call back to the window to MarkupWriteBack.SetAttribute(...) + write the file (the watcher then reloads, or skip the watcher for self-writes via a guard flag).
  • Step 6: Build green, VISUAL GATE: select an element in a markup panel, change its Width in Properties → the panel updates AND the file on disk shows the new value.
  • Step 7: Commit.

Gate (visual): edit a rect in the inspector → live panel updates + the markup file is rewritten.

Commit: feat(studio): editable markup props with in-place write-back


Task 8: Render-config sliders (doll camera)

Files:

  • Modify: src/AcDream.App/Rendering/DollCamera.cs (make Eye/Target/Fov instance-settable — currently static readonly), StudioInspector.cs (sliders), PaperdollViewportRenderer.cs (expose its DollCamera or accept overrides)
  • Read first: DollCamera.cs (current static fields), PaperdollViewportRenderer.cs:35 (private readonly DollCamera _camera = new();).

DollCamera change: convert the static readonly Eye/Target/Up/FovRadians to instance properties with the retail defaults as initializers (keep the decomp citations). This is a safe, behavior-preserving change (same defaults) — verify the existing DollCameraTests still pass (they read cam.View).

Inspector: a "Render config" ImGui window shown when a doll viewport is selected: SliderFloat3("eye", ...), SliderFloat("fov", ...), SliderFloat("heading", ...) bound to the PaperdollViewportRenderer's camera (expose it) + the doll's heading. A "Copy to clipboard" button emits the C# literals.

  • Step 1: Make DollCamera Eye/Target/Up/FovRadians instance properties (defaults = current retail values). Build + run DollCameraTests — expect PASS (defaults unchanged).
  • Step 2: Expose the camera from PaperdollViewportRenderer (a public DollCamera Camera => _camera;).
  • Step 3: StudioInspector: render the Render-config window with sliders mutating renderer.Camera.{Eye,FovRadians} + the doll heading; a copy button.
  • Step 4: Build green, VISUAL GATE: select the doll, drag the eye/FOV sliders → the doll re-frames live; copy emits the literals.
  • Step 5: Commit.

Gate (visual): dragging the camera sliders re-frames the doll in real time.

Commit: feat(studio): render-config sliders for the doll camera


Wrap-up (after Task 8)

  • Update claude-memory/project_d2b_retail_ui.md (or a new project_ui_studio.md) with the studio's architecture + the launch command + DO-NOT-RETRY notes; add a MEMORY.md pointer.
  • Update the roadmap: note the UI Studio tool shipped (it's tooling, not a milestone phase — a one-line ledger entry).
  • Full suite green; final commit.

Self-review notes

  • Spec coverage: both sources (Tasks 2 dat / 6 markup) ✓; full fixtures (Task 4 + 5) ✓; inspector read (3) + editable markup (7) + render sliders (8) ✓; hot-reload (6) ✓; live doll (5) ✓; RenderBootstrap (1) ✓; ImGui chrome (3) ✓.
  • GL-untestable tasks (1,2-window,3,5,6-window,8) use visual gates by design — the studio is a visual tool; pure-logic units (LayoutSource infos, FixtureProvider, MarkupWriteBack) have unit tests.
  • Type consistency: RenderStack/ImportedLayout/StudioOptions/LayoutSource/ClientObjectTable member names match the research reference; ResolveChrome body must be copied from GameWindow (flagged in Task 1 Step 1).
  • Risk: Task 1 builds NEW code (no GameWindow edit) → zero game-regression risk, the de-risk decision from the spec's open risk. Task 5's sample Setup id is the one unknown literal — Task 5 Step reads the test fixtures to find a real humanoid Setup before coding.