feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect
Task 3 of the acdream UI Studio plan. The studio previously drew the panel straight to the window; it now renders the panel into an off-screen FBO (PanelFbo) and displays it in an ImGui Canvas pane alongside a Tree pane (recursive element hierarchy, click-to-select) and a Properties pane (EventId/type/rect/anchors/ZOrder/flags of the selected element). Click-to-inspect: a left-click inside the Canvas calls UiElement.HitTest on the panel root (same-assembly internal access) and selects the topmost element at the cursor, wiring the canvas directly to the tree selection. PanelFbo lifecycle mirrors PaperdollViewportRenderer (RGBA8 color + Depth24Stencil8 renderbuffer, GLStateScope-sealed, lazy resize on size change). V-flip in DrawCanvas (uv0=(0,1)/uv1=(1,0)) corrects GL's bottom-left FBO origin to ImGui's top-left convention so click Y maps directly to panel-local Y without inversion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
df6b5b3b39
commit
d2240974ec
3 changed files with 359 additions and 4 deletions
128
src/AcDream.App/Studio/PanelFbo.cs
Normal file
128
src/AcDream.App/Studio/PanelFbo.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.UI;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a <see cref="UiHost"/> into an off-screen FBO each frame and
|
||||
/// returns the color texture handle for display in ImGui.
|
||||
///
|
||||
/// <para>Pattern lifted verbatim from <see cref="AcDream.App.Rendering.PaperdollViewportRenderer"/>:
|
||||
/// RGBA8 color texture + Depth24Stencil8 renderbuffer, lazily (re)created on
|
||||
/// size change. The entire 2-D UI pass is sealed in a <see cref="GLStateScope"/>
|
||||
/// so it cannot disturb the surrounding ImGui GL state.</para>
|
||||
///
|
||||
/// <para>FBO origin is bottom-left (GL convention). The caller must flip V when
|
||||
/// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image)
|
||||
/// so the image appears right-side-up in ImGui's top-left coordinate system.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe class PanelFbo : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
|
||||
// Off-screen target — lazily (re)created when the requested size changes.
|
||||
private uint _fbo;
|
||||
private uint _colorTex;
|
||||
private uint _depthRbo;
|
||||
private int _fbW;
|
||||
private int _fbH;
|
||||
|
||||
public PanelFbo(GL gl)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render <paramref name="host"/> (a full <see cref="UiHost"/> draw pass) into a
|
||||
/// private FBO at <paramref name="width"/> × <paramref name="height"/> pixels.
|
||||
/// Returns the GL color texture handle (0 on failure). The texture is valid until
|
||||
/// the next call to <see cref="Render"/> with a different size, or until <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
public uint Render(int width, int height, UiHost host)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || host is null) return 0u;
|
||||
|
||||
EnsureFramebuffer(width, height);
|
||||
if (_fbo == 0) return 0u;
|
||||
|
||||
// Seal the entire pass: GLStateScope saves + restores every GL state the
|
||||
// UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state
|
||||
// — set up by BeginFrame and expected intact by Render — is untouched.
|
||||
using var scope = new GLStateScope(_gl);
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
||||
_gl.Viewport(0, 0, (uint)width, (uint)height);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.ClearColor(0.18f, 0.18f, 0.18f, 1f); // dark bg matching the old direct draw
|
||||
_gl.ClearDepth(1.0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
|
||||
host.Draw(new Vector2(width, height));
|
||||
|
||||
// FBO stays bound here; GLStateScope.Dispose() restores the previous binding.
|
||||
return _colorTex;
|
||||
}
|
||||
|
||||
// ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ──────
|
||||
|
||||
private void EnsureFramebuffer(int width, int height)
|
||||
{
|
||||
if (_fbo != 0 && width == _fbW && height == _fbH) return;
|
||||
DeleteFramebuffer();
|
||||
|
||||
_fbW = width;
|
||||
_fbH = height;
|
||||
|
||||
_colorTex = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, _colorTex);
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba8,
|
||||
(uint)width, (uint)height, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, (void*)0);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
|
||||
_depthRbo = _gl.GenRenderbuffer();
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo);
|
||||
_gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8, (uint)width, (uint)height);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
|
||||
|
||||
_fbo = _gl.GenFramebuffer();
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
||||
_gl.FramebufferTexture2D(FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0,
|
||||
TextureTarget.Texture2D, _colorTex, 0);
|
||||
_gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.DepthStencilAttachment,
|
||||
RenderbufferTarget.Renderbuffer, _depthRbo);
|
||||
|
||||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
if (status != GLEnum.FramebufferComplete)
|
||||
{
|
||||
Console.WriteLine($"[studio] PanelFbo incomplete: {status} ({width}x{height})");
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteFramebuffer()
|
||||
{
|
||||
if (_fbo != 0) { _gl.DeleteFramebuffer(_fbo); _fbo = 0; }
|
||||
if (_colorTex != 0) { _gl.DeleteTexture(_colorTex); _colorTex = 0; }
|
||||
if (_depthRbo != 0) { _gl.DeleteRenderbuffer(_depthRbo); _depthRbo = 0; }
|
||||
_fbW = _fbH = 0;
|
||||
}
|
||||
|
||||
public void Dispose() => DeleteFramebuffer();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue