#include "editor.h" #include #include static int failures = 0; #define CHECK(cond, msg) do { \ if(!(cond)) { \ fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ failures++; \ } \ } while(0) // Inverse of cursor_to_world_ray (src/hex.c): forward-projects a world point // through the current proj*view and solves for the pixel coordinate that // would ray-cast back to it, so tests can compute exactly which (x, y) will // land a synthetic click on a given hex vertex/center. void world_to_cursor(ClientContext* context, vec3 world_point, double cursor[2]) { mat4 view_proj; glm_mat4_mul(context->hex.data.proj, context->hex.data.view, view_proj); vec4 point = {world_point[0], world_point[1], world_point[2], 1.0f}; vec4 clip; glm_mat4_mulv(view_proj, point, clip); double ndc[2] = {clip[0]/clip[3], clip[1]/clip[3]}; cursor[0] = (ndc[0]/2 + 0.5) * context->render.swapchain_extent.width / context->render.window_scale[0]; cursor[1] = (ndc[1]/2 + 0.5) * context->render.swapchain_extent.height / context->render.window_scale[1]; } // Positions the camera at a fixed, known, non-degenerate angle above hex 0 // (loaded at the origin by editor_startup) and pushes it through the same // matrices the real per-frame update would - required up front since a // one-shot app_frame callback runs before app_main's own camera-update step. void setup_camera(ClientContext* context) { context->position[0] = 0; context->position[1] = 0; context->position[2] = 0; // rotation[1] intentionally avoids M_PI/2 (straight down), which is // parallel to the {0,1,0} up vector and singular for glm_lookat. context->rotation[0] = 0; context->rotation[1] = 1.3f; context->distance = 10; update_hex_proj(&context->render, &context->hex); update_hex_view(context->position, context->rotation, context->distance, &context->render, &context->hex); } void click(ClientContext* context, vec3 world_point, int mods) { double cursor[2]; world_to_cursor(context, world_point, cursor); editor_button_callback(context, (float)cursor[0], (float)cursor[1], GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, mods); } void test_hex_mode_click_adds(ClientContext* context) { EditorData* data = context->app_data; clear_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); enter_hex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); vec3 hex0_center = {0, 0, 0}; click(context, hex0_center, 0); CHECK(data->selected_count == 1, "clicking hex 0 should select it"); CHECK(data->selected_count != 1 || (data->selected_regions[0] == 0 && data->selected_hexes[0] == 0), "selection should record region 0, hex 0"); // hex_starts[0] is the world-space offset of hex index 1 (radius 1, ring // 0, side 0 in the hex-ring indexing ray_hex_intersect uses). vec3 hex1_center = {hex_starts[0][0], hex_starts[0][1], hex_starts[0][2]}; click(context, hex1_center, 0); CHECK(data->selected_count == 2, "clicking a second hex should add to, not replace, the selection"); } void test_hex_mode_ctrl_click_removes(ClientContext* context) { EditorData* data = context->app_data; clear_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); enter_hex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); vec3 hex0_center = {0, 0, 0}; vec3 hex1_center = {hex_starts[0][0], hex_starts[0][1], hex_starts[0][2]}; click(context, hex0_center, 0); click(context, hex1_center, 0); CHECK(data->selected_count == 2, "setup: both hexes should be selected"); click(context, hex0_center, GLFW_MOD_CONTROL); CHECK(data->selected_count == 1, "ctrl-click should remove the clicked hex"); CHECK(data->selected_count != 1 || data->selected_hexes[0] != 0, "the remaining selection entry should not be the removed hex"); } void test_vertex_mode_edge_only_picking(ClientContext* context) { EditorData* data = context->app_data; clear_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); enter_vertex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); // Clicking dead center of the hex would, without edge-only picking, hit // vertex 0 (the center) and select nothing. vec3 hex0_center = {0, 0, 0}; click(context, hex0_center, 0); CHECK(data->selected_count == 1, "a center click in vertex mode should still select something"); CHECK(data->selected_count != 1 || (data->selected_vertices[0] >= 1 && data->selected_vertices[0] <= 6), "vertex mode picking must never resolve to vertex 0"); } void test_vertex_mode_click_adds_and_ctrl_removes(ClientContext* context) { EditorData* data = context->app_data; clear_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); enter_vertex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); vec3 v1 = {hex_vertices[0][0], hex_vertices[0][1], hex_vertices[0][2]}; vec3 v4 = {hex_vertices[3][0], hex_vertices[3][1], hex_vertices[3][2]}; click(context, v1, 0); click(context, v4, 0); CHECK(data->selected_count == 2, "clicking two different vertices should add both"); click(context, v1, GLFW_MOD_CONTROL); CHECK(data->selected_count == 1, "ctrl-click should remove one vertex from the selection"); } void test_mode_switch_resets_selection(ClientContext* context) { EditorData* data = context->app_data; enter_hex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); click(context, (vec3){0, 0, 0}, 0); CHECK(data->selected_count == 1, "setup: a hex should be selected before the mode switch"); enter_vertex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); CHECK(data->selected_count == 0, "switching modes should reset the selection"); } void test_empty_space_click_is_noop(ClientContext* context) { EditorData* data = context->app_data; clear_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); enter_hex_mode(data, context, &(ModeKey){0}, GLFW_PRESS, 0); // A cursor coordinate this far outside the window unprojects to a ray // angled far off the view axis - guaranteed to miss the small hex region // clustered near the origin regardless of camera specifics. editor_button_callback(context, 1e6f, 1e6f, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); CHECK(data->selected_count == 0, "clicking empty space should not select anything"); } void test_frame_callback(ClientContext* context) { static bool ran = false; if(ran) return; ran = true; setup_camera(context); test_hex_mode_click_adds(context); test_hex_mode_ctrl_click_removes(context); test_vertex_mode_edge_only_picking(context); test_vertex_mode_click_adds_and_ctrl_removes(context); test_mode_switch_resets_selection(context); test_empty_space_click_is_noop(context); glfwSetWindowShouldClose(context->window, GLFW_TRUE); } int main() { setenv("ROLEPLAY_HEADLESS_TEST", "1", 1); EditorData* data = create_editor_data(); int result = run_app(data, editor_startup, test_frame_callback, NULL, editor_key_callback, editor_button_callback, editor_scroll_callback, NULL); if(failures > 0) { fprintf(stderr, "%d test(s) failed\n", failures); return 1; } return result; }