feat(studio): forward canvas mouse to the previewed panel (interactive preview)
Canvas clicks now reach the panel UiHost so buttons, tabs, and slots respond to user interaction. Previously only UiRoot.Pick (inspector selection) received click events; the panel itself was inert. Key changes: - StudioInspector.DrawCanvas now returns a CanvasInputEvent struct (was nullable click tuple) — carries isHovered, move position, leftDown/Up, and scroll delta, all in panel-local pixels. - Coordinate mapping: panel_local = mouse_screen - GetItemRectMin() (1:1, no scale factor). V-flip (uv0.Y=1, uv1.Y=0) makes screen top = panel Y=0, so NO extra Y inversion. Documented in comments. - StudioWindow.OnLoad: removed WireMouse — raw Silk window coords are offset by the canvas sub-window position and land in the wrong place. WireKeyboard kept (keyboard input needs no spatial remapping). - StudioWindow.OnRender: forwards OnMouseMove always (hover states), plus OnMouseDown/Up/OnScroll in Interact mode. Console.WriteLine on each forwarded left-click for live verification. - Interact/Inspect toggle: checkbox in the Studio toolbar (default Interact). Inspect mode restores old click-to-select-element behavior while still forwarding OnMouseMove for hover states. - CanvasCoordMappingTests: 7 pure-math unit tests covering the origin, interior points, corner, chrome OOB, and the no-extra-flip invariant (no GL required). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eccacc59de
commit
21d8485053
3 changed files with 280 additions and 62 deletions
118
tests/AcDream.App.Tests/Studio/CanvasCoordMappingTests.cs
Normal file
118
tests/AcDream.App.Tests/Studio/CanvasCoordMappingTests.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using AcDream.App.Studio;
|
||||
|
||||
namespace AcDream.App.Tests.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-math tests for the canvas → panel-local coordinate mapping used by
|
||||
/// <see cref="StudioInspector.DrawCanvas"/>.
|
||||
///
|
||||
/// <para>No GL context required — we're just verifying the formula:
|
||||
/// panel_local = (raw_mouse_screen) - (image_screen_top_left)</para>
|
||||
///
|
||||
/// <para>The image_screen_top_left is what ImGui.GetItemRectMin() returns after
|
||||
/// ImGui.Image: the sub-window top-left + title-bar height + inner padding + any
|
||||
/// scrolling. We model it as a constant offset in these tests.</para>
|
||||
///
|
||||
/// <para>V-flip: the image is drawn with uv0=(0,1) / uv1=(1,0) so GL's bottom-left
|
||||
/// origin is flipped to top-left on screen. After the flip, screen Y=0 (top of image)
|
||||
/// = panel Y=0 (top of the UI), so NO additional Y inversion is applied.</para>
|
||||
/// </summary>
|
||||
public class CanvasCoordMappingTests
|
||||
{
|
||||
// Simulates the mapping DrawCanvas performs:
|
||||
// panel pixel = mouse_screen - image_rectMin
|
||||
// Returns null when the result falls outside [0, width) x [0, height).
|
||||
private static (int px, int py)? Map(
|
||||
float mouseScreenX, float mouseScreenY,
|
||||
float imageOriginX, float imageOriginY,
|
||||
int panelWidth, int panelHeight)
|
||||
{
|
||||
int ix = (int)(mouseScreenX - imageOriginX);
|
||||
int iy = (int)(mouseScreenY - imageOriginY);
|
||||
if (ix < 0 || ix >= panelWidth || iy < 0 || iy >= panelHeight)
|
||||
return null;
|
||||
return (ix, iy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TopLeft_of_image_maps_to_panel_origin()
|
||||
{
|
||||
// The canvas image starts at screen (300, 50) (after sub-window chrome).
|
||||
// A click exactly at the image's screen top-left → panel (0, 0).
|
||||
var result = Map(mouseScreenX: 300f, mouseScreenY: 50f,
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Equal((0, 0), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interior_point_maps_correctly()
|
||||
{
|
||||
// Image origin at screen (300, 50). Mouse at screen (780, 230).
|
||||
// Expected panel coord: (780-300, 230-50) = (480, 180).
|
||||
var result = Map(mouseScreenX: 780f, mouseScreenY: 230f,
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Equal((480, 180), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bottom_right_corner_maps_to_last_valid_pixel()
|
||||
{
|
||||
// Image is 1280×720, origin at screen (300, 50).
|
||||
// Last pixel in bottom-right is panel (1279, 719) → screen (1579, 769).
|
||||
var result = Map(mouseScreenX: 1579f, mouseScreenY: 769f,
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Equal((1279, 719), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mouse_on_ImGui_chrome_above_image_returns_null()
|
||||
{
|
||||
// The ImGui window title-bar / padding is above rectMin, i.e. at screenY < 50.
|
||||
// The mouse there should NOT produce a panel event.
|
||||
var result = Map(mouseScreenX: 400f, mouseScreenY: 40f, // 10px above image origin
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mouse_below_image_returns_null()
|
||||
{
|
||||
// screenY = 50 + 720 = 770 → iy = 720 which is >= panelHeight (720).
|
||||
var result = Map(mouseScreenX: 400f, mouseScreenY: 770f,
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Y_is_not_inverted_after_vflip()
|
||||
{
|
||||
// Confirm the "no extra Y inversion" contract:
|
||||
// The image is V-flipped in ImGui (uv0.Y=1, uv1.Y=0), so screen top row = panel Y=0.
|
||||
// A click near the TOP of the image should give a SMALL panel Y, not a large one.
|
||||
// Image origin at (300, 50). Click at (400, 55) → panel (100, 5). Y is small (near top).
|
||||
var result = Map(mouseScreenX: 400f, mouseScreenY: 55f,
|
||||
imageOriginX: 300f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Equal((100, 5), result);
|
||||
// NOT (100, 715) — which would be the result if Y were incorrectly inverted.
|
||||
Assert.True(result!.Value.py < 720 / 2, "Y near screen top should map to small panel Y, not near bottom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeChrome_offset_is_fully_absorbed()
|
||||
{
|
||||
// Simulate a canvas sub-window with large chrome: ImGui title (20px) + padding (8px)
|
||||
// puts the image origin at screenY = menuBar(22) + titleBar(20) + padding(8) = 50.
|
||||
// Also a wide tree pane puts imageOriginX = 280 + padding.
|
||||
// A click at screen (400, 110) with origin (290, 50) → panel (110, 60).
|
||||
var result = Map(mouseScreenX: 400f, mouseScreenY: 110f,
|
||||
imageOriginX: 290f, imageOriginY: 50f,
|
||||
panelWidth: 1280, panelHeight: 720);
|
||||
Assert.Equal((110, 60), result);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue