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

195 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.Reload`
`FixtureProvider.Populate` → re-select the previously-selected element by id when possible.
## Data flow
- **Boot:** `StudioWindow``RenderBootstrap.Create``LayoutSource.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 → `Reload``Populate` → 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 doll**`PaperdollViewportRenderer` 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).