#include #include "hex.h" #include "engine.h" #include "editor.h" #include "events.h" #include "ui_lua.h" #include "editor_lua.h" #include "vulkan/vulkan_core.h" #include #include #include #define SELECTION_HIGHLIGHT_OFFSET 0.05f #define SELECTION_POINT_SIZE 12.0f #define SELECTION_POINT_OFFSET 0.05f // Hover preview uses the last (reserved) highlight/point slot, a slightly // higher offset so it never z-fights with a selected element underneath, // and a translucent color that switches between "would add" and "would // remove" depending on whether the hovered element is already selected. #define HOVER_HIGHLIGHT_OFFSET 0.07f #define HOVER_POINT_SIZE 16.0f #define HOVER_POINT_OFFSET 0.07f #define HOVER_ADD_COLOR {0.2f, 0.85f, 1.0f, 0.45f} #define HOVER_REMOVE_COLOR {1.0f, 0.2f, 0.2f, 0.55f} const char* ModeStrings[] = { "None", "Vertex", "Neighbor", "Hex", "Region", }; uint32_t hex_color(vec4 color) { return ((lrint(color[0]*255.0) & 0xFF) << 24) + ((lrint(color[1]*255.0) & 0xFF) << 16) + ((lrint(color[2]*255.0) & 0xFF) << 8) + ((lrint(color[3]*255.0) & 0xFF) << 0); } VkResult set_vertex_height(float height, uint32_t region, uint32_t hex, uint32_t vertex, ClientContext* context) { if(vertex == 0) return VK_ERROR_VALIDATION_FAILED_EXT; context->hex.regions[region]->data.hexes[hex].height[vertex-1] = height; return add_transfer( &context->hex.regions[region]->data.hexes[hex].height[vertex-1], context->hex.regions[region]->region, offsetof(GPUHexRegion, hexes) + sizeof(GPUHex)*hex + offsetof(GPUHex, height) + sizeof(float)*(vertex-1), sizeof(float), context->render.current_frame, &context->render); } // vertex is 1-6 (the hex's 6 outer corners - matches height[]'s indexing). VkResult set_vertex_color(vec4 color, uint32_t region, uint32_t hex, uint32_t vertex, ClientContext* context) { context->hex.regions[region]->data.hexes[hex].outer_colors[vertex-1] = hex_color(color); return add_transfer( &context->hex.regions[region]->data.hexes[hex].outer_colors[vertex-1], context->hex.regions[region]->region, offsetof(GPUHexRegion, hexes) + sizeof(GPUHex)*hex + offsetof(GPUHex, outer_colors) + sizeof(uint32_t)*(vertex-1), sizeof(uint32_t), context->render.current_frame, &context->render); } // triangle is 0-5 (one of the hex's 6 wedges - see hex.vert's `wedge`). VkResult set_hex_triangle_color(vec4 color, uint32_t region, uint32_t hex, uint32_t triangle, ClientContext* context) { context->hex.regions[region]->data.hexes[hex].inner_colors[triangle] = hex_color(color); return add_transfer( &context->hex.regions[region]->data.hexes[hex].inner_colors[triangle], context->hex.regions[region]->region, offsetof(GPUHexRegion, hexes) + sizeof(GPUHex)*hex + offsetof(GPUHex, inner_colors) + sizeof(uint32_t)*triangle, sizeof(uint32_t), context->render.current_frame, &context->render); } // Sets (rather than toggles) wedge `triangle`'s flat bit, so callers that // already know the target state (editor_selection_toggle_flat) don't need // a read-modify-write of their own on top of this one. VkResult set_hex_flat(bool flat, uint32_t region, uint32_t hex, uint32_t triangle, ClientContext* context) { uint32_t* mask = &context->hex.regions[region]->data.hexes[hex].flat_mask; if(flat) *mask |= (1u << triangle); else *mask &= ~(1u << triangle); return add_transfer( mask, context->hex.regions[region]->region, offsetof(GPUHexRegion, hexes) + sizeof(GPUHex)*hex + offsetof(GPUHex, flat_mask), sizeof(uint32_t), context->render.current_frame, &context->render); } uint32_t add_hex_region(ClientContext* context) { HexRegion* region; allocate_hex_region(0, 0, 0, context->hex.data.current_map, ®ion, &context->hex, &context->render); for(uint32_t hex = 0; hex < REGION_HEX_COUNT; hex++) { region->data.hexes[hex].inner_colors[0] = 0xFFFFFFFF; region->data.hexes[hex].inner_colors[1] = 0xFFFFFFFF; region->data.hexes[hex].inner_colors[2] = 0xFFFFFFFF; region->data.hexes[hex].inner_colors[3] = 0xFFFFFFFF; region->data.hexes[hex].inner_colors[4] = 0xFFFFFFFF; region->data.hexes[hex].inner_colors[5] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[0] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[1] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[2] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[3] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[4] = 0xFFFFFFFF; region->data.hexes[hex].outer_colors[5] = 0xFFFFFFFF; region->data.hexes[hex].flat_mask = 0; region->data.hexes[hex].height[0] = 0; region->data.hexes[hex].height[1] = 0; region->data.hexes[hex].height[2] = 0; region->data.hexes[hex].height[3] = 0; region->data.hexes[hex].height[4] = 0; region->data.hexes[hex].height[5] = 0; } set_hex_region(region, &context->hex, &context->render); uint32_t i = 0; for(; i < MAX_LOADED_REGIONS; i++) { if(region == context->hex.regions[i]) { break; } } return i; } // Movement/spin keys are only ever seen by editor.c when no UI element has // focus (engine.c routes key events to the focused element first). If focus // is gained while a key is held, its release never reaches us and the // accumulator in move_cam/spin_cam is left stuck. Zeroing here every frame a // UI element is focused keeps the camera from drifting forever in that case. // // Integrates the editor's WASD/scroll control scheme into the engine-owned // context->camera. The orbital parameterization itself lives on Camera; this // is just the policy of how held input maps to changes in it, so a scripted // sequence or a different app could drive the same camera without any of // this accumulator machinery. void editor_frame_callback(ClientContext* context, double delta_time) { EditorData* data = context->app_data; Camera* camera = &context->camera; // Which cameras/containers track the window size is app policy (engine.c // only handles UI's own screen-space scale) - the main view is the one // built-in camera, kept full-window in this pass (see // cursor_to_world_ray's full-window assumption in hex.c). if(context->render.framebuffer_recreated) { data->main_container->data.size[0] = context->render.swapchain_extent.width / context->render.window_scale[0]; data->main_container->data.size[1] = context->render.swapchain_extent.height / context->render.window_scale[1]; add_transfers( &data->main_container->data.size, data->main_container->container, offsetof(GPUContainer, size), sizeof(vec2), &context->render); camera_update_proj(camera, (float)context->render.swapchain_extent.width/(float)context->render.swapchain_extent.height); camera_sync_gpu(camera, &context->render); update_hex_picking_inverse(camera, &context->hex); } if(context->ui.active_container != NULL) { data->velocity[0] = 0; data->velocity[1] = 0; data->velocity[2] = 0; data->spin[0] = 0; data->spin[1] = 0; } if(data->spin[0] != 0 || data->spin[1] != 0 || data->velocity[0] != 0 || data->velocity[1] != 0 || data->velocity[2] != 0 || data->zoom != 0) { camera->rotation[0] += (float)data->spin[0]*delta_time*data->spin_speed; if(camera->rotation[0] > 2*M_PI) { camera->rotation[0] -= 2*M_PI; } else if(camera->rotation[0] < 0) { camera->rotation[0] += 2*M_PI; } camera->rotation[1] += (float)data->spin[1]*delta_time*data->spin_speed; if(camera->rotation[1] > (M_PI/2 - 0.1)) { camera->rotation[1] = (M_PI/2 - 0.1); } else if(camera->rotation[1] < 0) { camera->rotation[1] = 0; } float move_x = data->velocity[0]; float move_z = data->velocity[2]; float move_mag = sqrt(move_x*move_x + move_z*move_z); if(move_mag > 1) { move_x /= move_mag; move_z /= move_mag; } camera->position[0] += - move_z*data->move_speed*cos(camera->rotation[0]) - move_x*data->move_speed*sin(camera->rotation[0]); camera->position[2] += move_x*data->move_speed*cos(camera->rotation[0]) - move_z*data->move_speed*sin(camera->rotation[0]); camera->position[1] += data->velocity[1]*data->move_speed; // data->zoom is a one-frame scroll impulse, not a held input like // spin/velocity, so it isn't scaled by delta_time. camera->distance += data->zoom*data->zoom_speed; if(camera->distance < 1) { camera->distance = 1; } camera_update_view(camera); camera_sync_gpu(camera, &context->render); update_hex_picking_inverse(camera, &context->hex); } // Consumed above; reset after (not at the top, where it would wipe this // frame's scroll_callback input, which fires during glfwPollEvents, // before this callback runs). data->zoom = 0; } void editor_key_callback(ClientContext* context, int key, int action, int mods) { EditorData* data = context->app_data; EditorMode mode = current_mode(context); for(uint32_t i = 0; i < data->mode_key_counts[mode]; i++) { if(data->mode_keys[mode][i].key == key) { data->mode_keys[mode][i].logic(data, context, &data->mode_keys[mode][i], action, mods); return; } } for(uint32_t i = 0; i < data->mode_key_counts[MODE_NONE]; i++) { if(data->mode_keys[MODE_NONE][i].key == key) { data->mode_keys[MODE_NONE][i].logic(data, context, &data->mode_keys[MODE_NONE][i], action, mods); return; } } } void spin_cam_key(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods) { (void)context; (void)mods; if(action == GLFW_PRESS) data->spin[binding->axis] += binding->amount; else if(action == GLFW_RELEASE) data->spin[binding->axis] -= binding->amount; } void move_cam_key(EditorData* data, ClientContext* context, ModeKey* binding, int action, int mods) { (void)context; (void)mods; if(action == GLFW_PRESS) data->velocity[binding->axis] += binding->amount; else if(action == GLFW_RELEASE) data->velocity[binding->axis] -= binding->amount; } void editor_scroll_callback(ClientContext* context, double x, double y) { (void)x; EditorData* data = context->app_data; data->zoom = (int32_t)y; } void resize_selected(EditorData* data, unsigned int size) { uint32_t* new_regions = malloc(sizeof(uint32_t)*size); uint32_t* new_hexes = malloc(sizeof(uint32_t)*size); uint32_t* new_vertices = malloc(sizeof(uint32_t)*size); if(data->selected_regions != NULL) { memcpy(new_regions, data->selected_regions, data->selected_count*sizeof(uint32_t)); free(data->selected_regions); } if(data->selected_hexes != NULL) { memcpy(new_hexes, data->selected_hexes, data->selected_count*sizeof(uint32_t)); free(data->selected_hexes); } if(data->selected_vertices != NULL) { memcpy(new_vertices, data->selected_vertices, data->selected_count*sizeof(uint32_t)); free(data->selected_vertices); } data->selected_regions = new_regions; data->selected_hexes = new_hexes; data->selected_vertices = new_vertices; data->selected_max = size; } int32_t find_selected_vertex(EditorData* data, uint32_t region, uint32_t hex, uint32_t vertex) { for(uint32_t i = 0; i < data->selected_count; i++) { if(data->selected_regions[i] == region && data->selected_hexes[i] == hex && data->selected_vertices[i] == vertex) { return (int32_t)i; } } return -1; } void add_selected(EditorData* data, uint32_t region, uint32_t hex, uint32_t vertex) { if(data->selected_count >= data->selected_max) { resize_selected(data, data->selected_max == 0 ? 1 : data->selected_max*2); } data->selected_regions[data->selected_count] = region; data->selected_hexes[data->selected_count] = hex; data->selected_vertices[data->selected_count] = vertex; data->selected_count++; } void remove_selected_at(EditorData* data, uint32_t index) { for(uint32_t i = index+1; i < data->selected_count; i++) { data->selected_regions[i-1] = data->selected_regions[i]; data->selected_hexes[i-1] = data->selected_hexes[i]; data->selected_vertices[i-1] = data->selected_vertices[i]; } data->selected_count--; } // Resolves (region, hex, vertex) plus up to 2 other hexes sharing that same // world corner (a hex corner touches up to 3 hexes): hex_qr/region_qr/ // hex_add build the hex's absolute world coordinate, hex_vertex_neighbors // finds the corner's other hexes in world coordinates, and // first_matching_region resolves each back to a loaded region index (a // neighbor that isn't loaded is skipped). Writes up to 3 entries (self // first, then any loaded neighbors) into the out arrays and returns the // count. Only meaningful for real vertex indices (1-6) - Hex-mode wedge // selections are hex-local, so nothing calls this for those. static uint32_t resolve_vertex_group( ClientContext* context, uint32_t region, uint32_t hex, uint32_t vertex, uint32_t out_region[3], uint32_t out_hex[3], uint32_t out_vertex[3]) { out_region[0] = region; out_hex[0] = hex; out_vertex[0] = vertex; uint32_t count = 1; HexRegion* r = context->hex.regions[region]; HexCoord world; region_qr(r->data.position, &world); HexCoord hex_local; hex_qr(hex, &hex_local); hex_add(hex_local, &world); uint32_t n_vertex[2]; HexCoord n_region_coord[2]; uint32_t n_hex[2]; hex_vertex_neighbors(vertex, world, n_vertex, n_region_coord, n_hex); for(uint32_t i = 0; i < 2; i++) { uint32_t actual_region; first_matching_region(n_region_coord[i], r->data.y, &actual_region, &context->hex); if(actual_region == MAX_LOADED_REGIONS) continue; out_region[count] = actual_region; out_hex[count] = n_hex[i]; out_vertex[count] = n_vertex[i]; count++; } return count; } // editor.set_color (editor_lua.c) - Vertex mode paints the selected corner // and any loaded neighbors sharing it (so adjacent hexes stay seamless); // Hex mode paints just the selected wedge's own inner color. void editor_selection_set_color(ClientContext* context, vec4 color) { EditorData* data = context->app_data; EditorMode mode = current_mode(context); for(uint32_t i = 0; i < data->selected_count; i++) { if(mode == MODE_VERTEX) { uint32_t g_region[3], g_hex[3], g_vertex[3]; uint32_t count = resolve_vertex_group( context, data->selected_regions[i], data->selected_hexes[i], data->selected_vertices[i], g_region, g_hex, g_vertex); for(uint32_t g = 0; g < count; g++) { set_vertex_color(color, g_region[g], g_hex[g], g_vertex[g], context); } } else if(mode == MODE_HEX) { set_hex_triangle_color(color, data->selected_regions[i], data->selected_hexes[i], data->selected_vertices[i], context); } } } // editor.nudge_height (editor_lua.c) - Vertex mode only; a wedge index // (Hex mode) isn't a valid height-vertex index, so this no-ops there. void editor_selection_nudge_height(ClientContext* context, float delta) { EditorData* data = context->app_data; if(current_mode(context) != MODE_VERTEX) return; for(uint32_t i = 0; i < data->selected_count; i++) { uint32_t g_region[3], g_hex[3], g_vertex[3]; uint32_t count = resolve_vertex_group( context, data->selected_regions[i], data->selected_hexes[i], data->selected_vertices[i], g_region, g_hex, g_vertex); for(uint32_t g = 0; g < count; g++) { float height = context->hex.regions[g_region[g]]->data.hexes[g_hex[g]].height[g_vertex[g]-1]; set_vertex_height(height + delta, g_region[g], g_hex[g], g_vertex[g], context); } } } // editor.toggle_flat (editor_lua.c) - Hex mode only; flips each selected // wedge's own flat bit independently (no neighbor concept - inner colors // and flat_mask are hex-local, unlike outer_colors/height). void editor_selection_toggle_flat(ClientContext* context) { EditorData* data = context->app_data; if(current_mode(context) != MODE_HEX) return; for(uint32_t i = 0; i < data->selected_count; i++) { uint32_t region = data->selected_regions[i]; uint32_t hex = data->selected_hexes[i]; uint32_t triangle = data->selected_vertices[i]; uint32_t mask = context->hex.regions[region]->data.hexes[hex].flat_mask; bool currently_flat = (mask >> triangle) & 1u; set_hex_flat(!currently_flat, region, hex, triangle, context); } } bool editor_pick(ClientContext* context, double cursor[2], uint32_t* rid, uint32_t* hid, uint32_t* vertex, uint32_t* triangle, bool edge_only) { vec4 start, end; float distance; cursor_to_world_ray(&context->render, context->hex.inverse, cursor, start, end); return ray_world_intersect(&distance, vertex, triangle, rid, hid, start, end, edge_only, &context->hex); } // Selection uses all highlight/point slots except the last, which is // reserved for the hover preview (see refresh_hover_visuals). void sync_hex_highlights(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); // +1 for the reserved hover slot (see refresh_hover_visuals). ensure_highlight_capacity(&context->hex, &context->render, data->selected_count + 1); for(uint32_t i = 0; i < context->hex.cap_highlights - 1; i++) { if(i < data->selected_count) { GPUHighlight temp = { // 0.25 alpha (was 1.0) so a color applied under the selection // overlay is actually visible while still selected. .color = {1.0f, 0.85f, 0.05f, 0.25f}, .region = data->selected_regions[i], .hex = data->selected_hexes[i], .offset = SELECTION_HIGHLIGHT_OFFSET, // Hex mode selects one wedge at a time - highlight just that // triangle so it's clear which one a color/flat edit will affect. // Vertex mode keeps highlighting the whole hex as spatial context. .triangle = (mode == MODE_HEX) ? data->selected_vertices[i] : 0xFFFFFFFF, }; add_transfers(&temp, context->hex.highlights, sizeof(GPUHighlight)*i, sizeof(GPUHighlight), &context->render); } else { uint32_t disabled = 0xFFFFFFFF; add_transfers(&disabled, context->hex.highlights, sizeof(GPUHighlight)*i + offsetof(GPUHighlight, hex), sizeof(uint32_t), &context->render); } } } void sync_vertex_points(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); // Only grow for demand that will actually be drawn (Hex-mode selections // never populate points - see below), so switching to Hex mode with a // huge selection doesn't grow a buffer nothing will read. if(mode == MODE_VERTEX) { ensure_point_capacity(&context->hex, &context->render, data->selected_count + 1); } for(uint32_t i = 0; i < context->hex.cap_points - 1; i++) { // Hex-mode selections are wedges, not vertices - selected_vertices[i] // isn't a real vertex index there, so there's nothing for a point // marker to draw (the wedge highlight above is the only visual). if(mode == MODE_VERTEX && i < data->selected_count) { GPUPoint temp = { .color = {1.0f, 0.85f, 0.05f, 0.25f}, .region = data->selected_regions[i], .hex = data->selected_hexes[i], .vertex = data->selected_vertices[i], .size = SELECTION_POINT_SIZE, .offset = SELECTION_POINT_OFFSET, }; add_transfers(&temp, context->hex.points, sizeof(GPUPoint)*i, sizeof(GPUPoint), &context->render); } else { uint32_t disabled = 0xFFFFFFFF; add_transfers(&disabled, context->hex.points, sizeof(GPUPoint)*i + offsetof(GPUPoint, hex), sizeof(uint32_t), &context->render); } } } // Writes (or disables) the reserved hover slot. Color depends on whether // the hovered element is already selected, so a click's effect (add vs. // remove) is visible before the user commits to it. void refresh_hover_visuals(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); // No capacity check needed here: cap_highlights/cap_points start at // MIN_HIGHLIGHTS/MIN_POINTS (create_hex_context) and only grow when // selection does, via sync_hex_highlights/sync_vertex_points - so by the // time this runs (whether after those, via refresh_selection_visuals, or // on its own for a hover-only update) capacity already covers whatever // the selection currently needs, and the hover slot just rides along at // whatever the last index of that capacity is. uint32_t highlight_slot = context->hex.cap_highlights - 1; // would_remove and the hover highlight's shape (whole hex vs. one wedge) // both key off the same mode-dependent hover_vertex meaning that // find_selected_vertex/sync_hex_highlights use. if((mode == MODE_HEX || mode == MODE_VERTEX) && data->hover_valid) { bool would_remove = find_selected_vertex(data, data->hover_region, data->hover_hex, data->hover_vertex) != -1; GPUHighlight temp; if(would_remove) temp = (GPUHighlight){.color = HOVER_REMOVE_COLOR}; else temp = (GPUHighlight){.color = HOVER_ADD_COLOR}; temp.region = data->hover_region; temp.hex = data->hover_hex; temp.offset = HOVER_HIGHLIGHT_OFFSET; temp.triangle = (mode == MODE_HEX) ? data->hover_vertex : 0xFFFFFFFF; add_transfers(&temp, context->hex.highlights, sizeof(GPUHighlight)*highlight_slot, sizeof(GPUHighlight), &context->render); } else { uint32_t disabled = 0xFFFFFFFF; add_transfers(&disabled, context->hex.highlights, sizeof(GPUHighlight)*highlight_slot + offsetof(GPUHighlight, hex), sizeof(uint32_t), &context->render); } uint32_t point_slot = context->hex.cap_points - 1; if(mode == MODE_VERTEX && data->hover_valid) { bool would_remove = find_selected_vertex(data, data->hover_region, data->hover_hex, data->hover_vertex) != -1; GPUPoint temp; if(would_remove) temp = (GPUPoint){.color = HOVER_REMOVE_COLOR}; else temp = (GPUPoint){.color = HOVER_ADD_COLOR}; temp.region = data->hover_region; temp.hex = data->hover_hex; temp.vertex = data->hover_vertex; temp.size = HOVER_POINT_SIZE; temp.offset = HOVER_POINT_OFFSET; add_transfers(&temp, context->hex.points, sizeof(GPUPoint)*point_slot, sizeof(GPUPoint), &context->render); } else { uint32_t disabled = 0xFFFFFFFF; add_transfers(&disabled, context->hex.points, sizeof(GPUPoint)*point_slot + offsetof(GPUPoint, hex), sizeof(uint32_t), &context->render); } } void refresh_selection_visuals(EditorData* data, ClientContext* context) { sync_hex_highlights(data, context); sync_vertex_points(data, context); refresh_hover_visuals(data, context); } // editor.mode is the single source of truth (a property on the event bus); // nothing stores a separate copy. Reads are synchronous and cheap (a strcmp // loop over MODE_MAX_ENUM short strings) so there's nothing worth caching. EditorMode current_mode(ClientContext* context) { const char* name = event_property(&context->events, "editor.mode")->value.string; for(int i = 0; i < MODE_MAX_ENUM; i++) { if(strcmp(ModeStrings[i], name) == 0) return (EditorMode)i; } return MODE_NONE; } // The only C-side way to change modes; script/editor_mode.lua changes modes // by writing the same property directly via app.set instead. Either path // queues engine.changed.editor.mode, which on_mode_changed below reacts to // uniformly regardless of which path triggered it. void editor_set_mode(ClientContext* context, EditorMode mode) { event_property_set_string(&context->events, "editor.mode", ModeStrings[mode], EVENT_SOURCE_ENGINE); } // Selection/hover don't carry across modes; this is the only place that // resets them, so it runs the same whether the mode change came from C or // from a script's app.set. static void on_mode_changed(void* userdata, const char* event, uint32_t source, lua_State* L, int args) { (void)event; (void)source; (void)L; (void)args; ClientContext* context = userdata; EditorData* data = context->app_data; data->selected_count = 0; data->hover_valid = false; data->dragging = false; refresh_selection_visuals(data, context); } void editor_button_callback(ClientContext* context, float x, float y, int button, int action, int mods) { EditorData* data = context->app_data; if(button != GLFW_MOUSE_BUTTON_LEFT) return; // Checked ahead of the mode gate below so a release always clears the // drag, even if the mode somehow changed while the button was still held. if(action == GLFW_RELEASE) { data->dragging = false; return; } if(action != GLFW_PRESS) return; EditorMode mode = current_mode(context); if(mode != MODE_VERTEX && mode != MODE_HEX) return; uint32_t rid, hid, v; if(data->hover_valid) { // editor_cursor_callback already raycast this exact spot for the hover // preview; the cursor can't have moved between that and this click, so // reuse it instead of raycasting again. rid = data->hover_region; hid = data->hover_hex; v = data->hover_vertex; } else { double cursor[2] = {x, y}; uint32_t vertex, triangle; // Vertex mode requests edge-only picking so a click always resolves to a // real, editable vertex instead of frequently landing on the hex center // (vertex 0) and giving no feedback. if(!editor_pick(context, cursor, &rid, &hid, &vertex, &triangle, mode == MODE_VERTEX)) return; v = (mode == MODE_VERTEX) ? vertex : triangle; } // find_selected_vertex is a plain 3-tuple match despite its name - used // here for Hex mode too now that the third field is a wedge index, so two // different wedges of the same hex are distinct selections. int32_t idx = find_selected_vertex(data, rid, hid, v); // Starts a drag-select session: editor_cursor_callback extends this same // add/remove toggle to whatever becomes newly hovered while the button // stays down (see the "dragging" field's comment in editor.h). data->dragging = true; data->drag_remove = (mods & GLFW_MOD_CONTROL) != 0; // Left click adds to the selection; Shift is reserved for camera movement // (GLFW_KEY_LEFT_SHIFT) so it carries no selection meaning here. Ctrl // removes. Mode changes remain the only way to reset the selection. if(mods & GLFW_MOD_CONTROL) { if(idx != -1) remove_selected_at(data, (uint32_t)idx); } else if(idx == -1) { add_selected(data, rid, hid, v); } refresh_selection_visuals(data, context); } void editor_cursor_callback(ClientContext* context, float x, float y) { EditorData* data = context->app_data; EditorMode mode = current_mode(context); if(mode != MODE_VERTEX && mode != MODE_HEX) { if(data->hover_valid) { data->hover_valid = false; refresh_hover_visuals(data, context); } return; } double cursor[2] = {x, y}; uint32_t rid, hid, vertex, triangle; bool hit = editor_pick(context, cursor, &rid, &hid, &vertex, &triangle, mode == MODE_VERTEX); uint32_t v = (mode == MODE_VERTEX) ? vertex : triangle; // Skip the GPU write unless the hovered element actually changed; cursor // motion fires far more often than the hover target changes. v is now a // meaningful varying value in both modes (a real vertex in Vertex mode, a // wedge in Hex mode), so it's always part of the comparison. bool changed = hit != data->hover_valid || (hit && (data->hover_region != rid || data->hover_hex != hid || data->hover_vertex != v)); if(!changed) return; data->hover_valid = hit; if(hit) { data->hover_region = rid; data->hover_hex = hid; data->hover_vertex = v; } // Drag-select: extend the initiating click's add/remove toggle to // whatever just became hovered, same idx check as editor_button_callback // uses for a plain click - so gliding back over an already-selected item // during a plain (non-Ctrl) drag is a no-op, not a re-toggle. if(data->dragging && hit) { int32_t idx = find_selected_vertex(data, rid, hid, v); if(data->drag_remove) { if(idx != -1) remove_selected_at(data, (uint32_t)idx); } else if(idx == -1) { add_selected(data, rid, hid, v); } refresh_selection_visuals(data, context); } else { refresh_hover_visuals(data, context); } } void editor_startup(ClientContext* context) { EditorData* data = context->app_data; editor_lua_register(context->ui.lua, context); // Application state surface visible to scripts; registered before any // container loads so app.get sees the initial values event_property_register(&context->events, "editor.mode", PROPERTY_STRING); event_property_register(&context->events, "editor.color", PROPERTY_STRING); event_property_set_string(&context->events, "editor.mode", ModeStrings[MODE_NONE], EVENT_SOURCE_ENGINE); event_property_set_string(&context->events, "editor.color", "#000000FF", EVENT_SOURCE_ENGINE); // Registered before any script loads so it's guaranteed to see mode // changes editor_mode.lua triggers, ahead of editor_ui.lua's own reaction event_subscribe(&context->events, "engine.changed.editor.mode", on_mode_changed, context); // Controller script: owns the mode label and the color picker overlay, // both driven entirely by the editor.mode property from here on ui_lua_run_script(context->ui.lua, &context->ui, &context->render, "script/editor_ui.lua"); // Input->state-machine trigger: writes editor.mode on V/N/H/R/Escape ui_lua_run_script(context->ui.lua, &context->ui, &context->render, "script/editor_mode.lua"); // Height nudge (=/-) and flat toggle (F) for the current selection ui_lua_run_script(context->ui.lua, &context->ui, &context->render, "script/editor_paint.lua"); // Main 3D view: a full-window container hosting the interactive camera, // same as every other overlay - see container_set_camera (ui.h). Screen- // region targeting lives on the container, not the camera, so this is // the only place that needs to know the camera renders full-window. ContainerInput main_view = { .id = ui_alloc_container_id(&context->ui), .anchor = ANCHOR_TOP_LEFT, .offset = {0, 0}, .size = { context->render.swapchain_extent.width / context->render.window_scale[0], context->render.swapchain_extent.height / context->render.window_scale[1], }, }; load_container(&main_view, &context->render, &context->ui); data->main_container = context_container(main_view.id, &context->ui); container_set_camera(data->main_container, &context->camera); // The editor sets the camera's initial values and computes its first // view/projection here; editor_frame_callback only recomputes them when // spin/velocity/zoom (or a resize) happen, so without this the initial // view stays whatever ClientContext's zero-init left it as until the // camera is first moved. context->camera.rotation[0] = 3*M_PI/2; context->camera.rotation[1] = M_PI/4; context->camera.distance = 25; camera_update_view(&context->camera); camera_update_proj(&context->camera, (float)context->render.swapchain_extent.width/(float)context->render.swapchain_extent.height); camera_sync_gpu(&context->camera, &context->render); update_hex_picking_inverse(&context->camera, &context->hex); // Top-down minimap: a second, script-owned camera - proves camera // creation/attachment isn't C-only. See script/editor_topdown.lua. ui_lua_run_script(context->ui.lua, &context->ui, &context->render, "script/editor_topdown.lua"); // TODO: Remove when region mode is implemented add_hex_region(context); } EditorData* create_editor_data(void) { EditorData* data = malloc(sizeof(EditorData)); memset(data, 0, sizeof(EditorData)); data->selected_count = 0; resize_selected(data, 1); data->spin_speed = 1.0; data->zoom_speed = 0.5; data->move_speed = 0.1; // Mode-switch keys (Escape/V/N/H/R) moved to script/editor_mode.lua, // triggered by the engine.key event instead of this table data->mode_key_counts[MODE_NONE] = 10; for(int i = 0; i < MODE_MAX_ENUM; i++) { data->mode_keys[i] = malloc(sizeof(ModeKey)*data->mode_key_counts[i]); } // Camera Movement (axis: 0 = strafe x, 1 = up/down, 2 = forward/back) data->mode_keys[MODE_NONE][0] = (ModeKey){GLFW_KEY_SPACE, move_cam_key, 1, 1}; data->mode_keys[MODE_NONE][1] = (ModeKey){GLFW_KEY_LEFT_SHIFT, move_cam_key, 1, -1}; data->mode_keys[MODE_NONE][2] = (ModeKey){GLFW_KEY_LEFT, move_cam_key, 0, -1}; data->mode_keys[MODE_NONE][3] = (ModeKey){GLFW_KEY_RIGHT, move_cam_key, 0, 1}; data->mode_keys[MODE_NONE][4] = (ModeKey){GLFW_KEY_UP, move_cam_key, 2, 1}; data->mode_keys[MODE_NONE][5] = (ModeKey){GLFW_KEY_DOWN, move_cam_key, 2, -1}; // Camera Spin (axis: 0 = yaw, 1 = pitch) data->mode_keys[MODE_NONE][6] = (ModeKey){GLFW_KEY_A, spin_cam_key, 0, -1}; data->mode_keys[MODE_NONE][7] = (ModeKey){GLFW_KEY_D, spin_cam_key, 0, 1}; data->mode_keys[MODE_NONE][8] = (ModeKey){GLFW_KEY_W, spin_cam_key, 1, 1}; data->mode_keys[MODE_NONE][9] = (ModeKey){GLFW_KEY_S, spin_cam_key, 1, -1}; return data; } void destroy_editor_data(EditorData* data) { for(int i = 0; i < MODE_MAX_ENUM; i++) { free(data->mode_keys[i]); } free(data->selected_regions); free(data->selected_hexes); free(data->selected_vertices); free(data); }