acdream/docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md
Erik d607b55e61 docs(studio): design spec — acdream UI Studio (live previewer + inspector)
A standalone dev tool that renders any acdream UI panel through the
PRODUCTION renderer (UiHost/LayoutImporter/dat-sprite path + the WB mesh
pipeline for the 3-D doll), with an ImGui click-to-inspect inspector,
sample-data fixtures, markup hot-reload + write-back, and render-config
sliders. Collapses the edit→build→login→F12→eyeball loop into edit→glance.

Full v1 scope (user pre-approved): both sources (dat LayoutDesc id +
markup file), full fixtures, editable inspector + doll-camera sliders,
live doll. Seven isolated units; the highest-risk step (extracting a
shared RenderBootstrap from GameWindow.OnLoad) is sequenced first behind
a "game still renders" gate, with a studio-local-duplicate fallback. The
drag-drop designer is deferred to phase 2.

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

12 KiB
Raw Blame History

acdream UI Studio — design (live previewer + inspector)

  • Date: 2026-06-25
  • Status: approved (user pre-approved full v1 scope 2026-06-25)
  • Branch: claude/hopeful-maxwell-214a12
  • Supersedes/foundation for: the deferred phase-2 drag-drop designer

Goal

A standalone, fast-booting dev tool that renders any acdream UI panel through the production renderer, with a click-to-inspect element inspector, sample-data fixtures, markup hot-reload + write-back editing, and render-config sliders.

It collapses the panel-iteration loop — today edit → build → login to ACE → wait ~8s → F12 → eyeball vs retail → close → repeat (~3060 s/iteration) — into edit → glance (~1 s). Everything we did on the paperdoll this session (confirming the pose, framing the camera, catching the mirrored heading) would have been a few clicks here instead of a dozen relaunches.

Fidelity is the whole point. The previewed panel is drawn by the SAME UiHost / LayoutImporter / DatWidgetFactory / dat-sprite path that ships, so what you see is what the game renders. This is the decisive reason NOT to build the tool in a separate stack (Godot/HTML) and NOT to rely on the existing render-vitals-mockup (a separate CPU SurfaceDecoder composite that can drift from the GL path).

Scope — v1 is the maximum capability

IN:

  • Load a panel from EITHER a dat LayoutDesc id OR a markup file (MarkupDocument).
  • Render via the production GL UiHost (2-D panels) AND the full WB mesh pipeline (the 3-D paperdoll doll).
  • Sample-data fixtures so data-bound panels look populated: canned inventory items, sample vital values, a dressed sample creature for the doll.
  • Inspector (ImGui): element tree, click-to-inspect, read-only props (id / Type / rect / anchors / state-sprites / ZOrder), editable markup props (write back to the file), and render-config sliders (the doll camera eye / FOV / heading — values that live in code, not markup).
  • Hot-reload: file-watch the markup source; reload-on-demand (id picker / reload button) for dat layouts.

OUT (phase 2): the drag-drop visual designer (compose new layouts on a canvas and emit markup). The previewer is the foundation that phase sits on. Its natural home is the plugin-UI story, not the retail-panel work.

Not faithfulness-gated: this is a DEV tool. The previewed panels are faithful (same renderer); the tool chrome (the ImGui inspector) is dev-only. No retail-divergence-register rows are created by this work.

Architecture

Seven isolated, single-purpose units. The previewed panel is drawn by the production renderer; the tool chrome is ImGui (already in AcDream.App for the ACDREAM_DEVTOOLS overlay — ideal for a tree / sliders / editable fields at near-zero cost).

Program.cs ──"ui-studio"──▶ StudioWindow
                              │  owns: Silk window, ImGui ctx, frame loop
                              ▼
                         RenderBootstrap.Create(gl, opts) ──▶ RenderStack
                              │   (GL + DatCollection + TextureCache + UiHost
                              │    + ObjectMeshManager + WbDrawDispatcher + lighting UBO)
                              │   ALSO consumed by GameWindow (shared = de-tangles OnLoad)
        ┌─────────────────────┼───────────────────────────────┐
        ▼                     ▼                                ▼
   LayoutSource         FixtureProvider                  StudioInspector (ImGui)
   dat id | markup      sample item table / vitals /     tree · props(read+edit) ·
   → UiElement tree     dressed creature → controllers   render sliders · canvas(FBO)
        │                                                      │ click → HitTestTopDown
        └────────── HotReloadWatcher (markup) ─────────────────┘
                    MarkupWriteBack (edited props → file)

1. RenderBootstrap — NEW, src/AcDream.App/Rendering/

Extracts the render-stack construction currently tangled inside GameWindow.OnLoad (the GL api, dat-dir → DatCollection, TextureCache, the WB mesh pipeline — ObjectMeshManager / WbDrawDispatcher / SceneLightingUboBinding — and UiHost) into a reusable unit that BOTH GameWindow and StudioWindow build through.

  • Interface: RenderStack Create(GL gl, RenderBootstrapOptions opts); RenderStack is a record of the constructed pieces (dats, textureCache, meshManager, drawDispatcher, lightingUbo, uiHost, shaderDir).
  • GameWindow.OnLoad is refactored to consume it — behavior-preserving, locked by the existing App/Core suites + a visual gate that the GAME still renders.
  • Highest-risk step → extract the SMALLEST cohesive slice only; do it first, gate it, build the studio on it second. Fallback if extraction is too entangled: StudioWindow builds a studio-local duplicate of the mesh-pipeline setup (more drift, lower regression risk) — to be decided at implementation, not assumed away.

2. StudioWindow — NEW, src/AcDream.App/Studio/

A Silk.NET windowed GL app — the studio entry. Owns the window, the RenderStack, the ImGui context, the inspector, the hot-reload watcher, and the per-frame loop. No ACE / world-streaming / physics / movement input. Launched from Program.cs: AcDream.App ui-studio <datdir> [--layout 0x21000023 | --markup path.xml]. Config flows through RuntimeOptions (one env/arg read), per the code-structure rules.

3. LayoutSource — NEW, src/AcDream.App/Studio/

Loads a panel into a UiElement subtree from a dat id (LayoutImporter.Import) or a markup file (MarkupDocument.Build). Owns the current source + Reload().

  • Interface: UiElement Load(), void Reload(), SourceKind Kind, string? MarkupPath, uint? LayoutId, plus a string? LastError for the inspector to surface parse failures.

4. FixtureProvider — NEW, src/AcDream.App/Studio/

Supplies sample data so data-bound panels render populated. A fake ClientObjectTable seeded with a handful of canned items (icon ids + types); sample vital values; a sample creature (Setup + ObjDesc) for the doll. Maps a known layoutId → its real controller (VitalsController / InventoryController / ToolbarController / PaperdollController) bound to the fixtures — REUSING the production controllers, not re-implementing them.

  • Interface: void Populate(uint layoutId, UiElement root, RenderStack stack). Unknown layout id → no fixture (structural preview only), not an error.

5. StudioInspector — NEW, src/AcDream.App/Studio/

The ImGui tool chrome, four docked windows:

  • Canvas — the panel rendered to an FBO (RTT, the PaperdollViewportRenderer technique applied to the whole panel) shown via ImGui.Image. Click → map canvas-local coords → UiRoot.HitTestTopDown → select. The FBO route gives a clean dockable IDE layout + accurate click mapping, and isolates the panel's GL state from ImGui's.
  • Tree — the live UiElement hierarchy with each node's registered Type; select syncs with the canvas highlight.
  • Properties — the selected element's id / Type / rect / anchors / state-sprites (with swatches) / ZOrder. Rect/anchor fields are editable (markup source) → applied live + queued to MarkupWriteBack.
  • Render config — sliders for the doll's DollCamera eye / FOV / heading, mutating the live camera so the doll updates next frame; a "copy values" button emits the literals to paste into code.

6. MarkupWriteBack — NEW, src/AcDream.App/Studio/

Serializes an edited element's changed props back to the markup XML file (markup source only; dat layouts are read-only). v1 = targeted in-place attribute updates (find the element by id, rewrite the changed attribute values), NOT a full tree re-serialize — so hand-authored formatting/comments survive.

7. HotReloadWatcher — NEW, src/AcDream.App/Studio/

FileSystemWatcher on the markup file → debounced (≈150 ms) reload event → LayoutSource.ReloadFixtureProvider.Populate → re-select the previously-selected element by id when possible.

Data flow

  • Boot: StudioWindowRenderBootstrap.CreateLayoutSource.Load(initial)FixtureProvider.Populate → loop.
  • Frame: bind panel FBO → UiHost.Tick + Draw (panel, incl. the doll's own nested RTT) → unbind → ImGui frame (Canvas image + Tree + Properties + Render-config) → present.
  • Click canvas: map coords → HitTestTopDown → select → Properties shows props.
  • Edit prop: apply to the live UiElement (instant) + (markup) queue write-back.
  • File change: watcher → ReloadPopulate → re-select by id.
  • Render slider: mutate DollCamera → next frame the doll re-renders.

Error handling

  • Bad layout id / unparseable markup → LayoutSource.LastError shown in the inspector; the previous panel stays (or an empty canvas), no crash.
  • Missing dats at boot → clear error + exit non-zero.
  • Panel build/draw wrapped in try/catch → a thrown panel surfaces its exception text in the inspector; the tool stays alive (you're often previewing half-broken markup).
  • Write-back failure (file locked) → non-fatal inline notice, edit stays applied in-memory.

Testing

  • LayoutSource: load a known dat id + a markup string → assert the UiElement tree shape (reuse the existing golden-fixture pattern, e.g. vitals_2100006C.json).
  • FixtureProvider: assert the seeded ClientObjectTable + vitals shapes.
  • MarkupWriteBack: round-trip — parse → edit a rect → serialize → re-parse → assert the changed attribute + that siblings/formatting are untouched.
  • RenderBootstrap: the existing suites stay green + a manual game visual gate (the refactor is behavior-preserving).
  • Window / ImGui / doll render: manual visual gate — the studio is itself the visual tool.
  • Tests live in tests/AcDream.App.Tests/Studio/.

Build order (staged; each ≈ one subagent chunk + a gate)

  1. RenderBootstrap extraction — behavior-preserving. GATE: game renders unchanged; suites green.
  2. StudioWindow skeleton + LayoutSource(dat id) — render a 2-D panel to the window. GATE: ui-studio --layout 0x2100006C shows vitals.
  3. StudioInspector — ImGui + canvas-FBO + tree + read-only props + click-to-inspect. GATE: click an element → see its id/rect/sprites.
  4. FixtureProvider — 2-D panels populated (vitals / toolbar / inventory). GATE: inventory shows sample items.
  5. Live dollPaperdollViewportRenderer in the studio + sample creature. GATE: the paperdoll shows a dressed doll.
  6. Markup source + HotReloadWatcher — GATE: edit a markup file → studio reloads.
  7. Editable markup props + MarkupWriteBack — GATE: edit a rect in the inspector → file updates → reloads.
  8. Render-config sliders — GATE: drag the eye/FOV → doll updates live.

Steps 14 deliver the high-value 2-D previewer + inspector + fixtures fast; 58 add the doll and the editing power. The end-state is the full maximum-capability tool.

Open risks

  • RenderBootstrap extraction from the tangled GameWindow.OnLoad is the riskiest step. Mitigation: smallest cohesive slice, behavior-preserving, gated by the game visual + the suites BEFORE the studio depends on it; documented fallback (studio-local duplicate).
  • MarkupWriteBack formatting preservation — keep v1 to in-place attribute edits, never a full re-serialize, so hand-authored markup isn't clobbered.
  • ImGui + UiHost GL-state interplay — the panel renders to its own FBO and ImGui owns the default framebuffer, so their GL state can't fight (the same seal the paperdoll RTT uses).