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>
118 lines
5.1 KiB
C#
118 lines
5.1 KiB
C#
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);
|
||
}
|
||
}
|