Added tests for editor
parent
4210ff1aba
commit
8252418db9
@ -0,0 +1,63 @@
|
||||
#ifndef EDITOR_H
|
||||
#define EDITOR_H
|
||||
|
||||
#include "engine.h"
|
||||
|
||||
typedef enum EditorModeEnum {
|
||||
MODE_NONE,
|
||||
MODE_VERTEX,
|
||||
MODE_NEIGHBOR,
|
||||
MODE_HEX,
|
||||
MODE_REGION,
|
||||
MODE_MAX_ENUM,
|
||||
} EditorMode;
|
||||
|
||||
typedef struct EditorDataStruct EditorData;
|
||||
typedef struct ModeKeyStruct ModeKey;
|
||||
|
||||
typedef void(*ModeKeyCallback)(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
|
||||
struct ModeKeyStruct {
|
||||
int key;
|
||||
ModeKeyCallback logic;
|
||||
int axis;
|
||||
float amount;
|
||||
};
|
||||
|
||||
struct EditorDataStruct {
|
||||
EditorMode mode;
|
||||
|
||||
ModeKey* mode_keys[MODE_MAX_ENUM];
|
||||
uint32_t mode_key_counts[MODE_MAX_ENUM];
|
||||
|
||||
uint32_t selected_max;
|
||||
uint32_t selected_count;
|
||||
|
||||
uint32_t* selected_regions;
|
||||
uint32_t* selected_hexes;
|
||||
uint32_t* selected_vertices;
|
||||
};
|
||||
|
||||
EditorData* create_editor_data(void);
|
||||
|
||||
void editor_startup(ClientContext* context);
|
||||
void editor_frame_callback(ClientContext* context);
|
||||
void editor_key_callback(ClientContext* context, int key, int action, int mods);
|
||||
void editor_scroll_callback(ClientContext* context, double x, double y);
|
||||
void editor_button_callback(ClientContext* context, float x, float y, int button, int action, int mods);
|
||||
|
||||
void clear_mode(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
void enter_vertex_mode(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
void enter_neighbor_mode(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
void enter_hex_mode(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
void enter_region_mode(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods);
|
||||
|
||||
void resize_selected(EditorData* data, unsigned int size);
|
||||
int32_t find_selected_hex(EditorData* data, uint32_t region, uint32_t hex);
|
||||
int32_t find_selected_vertex(EditorData* data, uint32_t region, uint32_t hex, uint32_t vertex);
|
||||
void add_selected(EditorData* data, uint32_t region, uint32_t hex, uint32_t vertex);
|
||||
void remove_selected_at(EditorData* data, uint32_t index);
|
||||
|
||||
bool editor_pick(ClientContext* context, double cursor[2], uint32_t* rid, uint32_t* hid, uint32_t* vertex, bool edge_only);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,6 @@
|
||||
#include "editor.h"
|
||||
|
||||
int main() {
|
||||
EditorData* data = create_editor_data();
|
||||
return run_app(data, editor_startup, editor_frame_callback, NULL, editor_key_callback, editor_button_callback, editor_scroll_callback, NULL);
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
#include "editor.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue