diff --git a/client/include/editor.h b/client/include/editor.h index 4e95550..9a028a5 100644 --- a/client/include/editor.h +++ b/client/include/editor.h @@ -51,6 +51,15 @@ struct EditorDataStruct { uint32_t hover_hex; uint32_t hover_vertex; + // Drag-to-multi-select: set on left-button press (Vertex/Hex mode), + // cleared on release. While true, editor_cursor_callback applies the same + // add/remove toggle a click would to whatever becomes newly hovered. + // drag_remove captures the initiating click's Ctrl state, since + // editor_cursor_callback (GLFW's cursor-pos callback) doesn't receive + // modifier keys itself. + bool dragging; + bool drag_remove; + // Camera control scheme: WASD/scroll accumulate into these, which // editor_frame_callback integrates into context->camera each frame. The // camera itself (position/rotation/distance/view) is engine-owned diff --git a/client/include/hex.h b/client/include/hex.h index a8d0e27..32824db 100644 --- a/client/include/hex.h +++ b/client/include/hex.h @@ -6,9 +6,14 @@ #include "vulkan/vulkan_core.h" #define MAX_RAYS 10 -// Last slot of each is reserved for the hover preview (see editor.c). -#define MAX_HIGHLIGHTS 11 -#define MAX_POINTS 11 +// Initial capacity, not a hard cap - ensure_highlight_capacity/ +// ensure_point_capacity (hex.c) grow HexContext.highlights/points on +// demand, same "start small, double when a fixed pool fills up" pattern as +// ui.h's CONTAINER_MIN_DRAWABLES/CONTAINER_MIN_STRINGS. The last slot of +// whatever the current capacity is stays reserved for the hover preview +// (see editor.c). +#define MIN_HIGHLIGHTS 16 +#define MIN_POINTS 16 #define MAX_LOADED_REGIONS 2500 #define REGION_SIZE 10 @@ -110,9 +115,13 @@ typedef struct HexContextStruct { VmaAllocation points_memory[MAX_FRAMES_IN_FLIGHT]; VmaAllocation highlights_memory[MAX_FRAMES_IN_FLIGHT]; + // Current allocated size of points[]/highlights[] (in elements, same for + // both frames-in-flight) - grows via ensure_point_capacity/ + // ensure_highlight_capacity, independent of each other. + uint32_t cap_points; + uint32_t cap_highlights; + GPURay rays_buffer[MAX_RAYS]; - GPUHighlight highlights_buffer[MAX_HIGHLIGHTS]; - GPUPoint points_buffer[MAX_POINTS]; mat4 inverse; @@ -142,6 +151,16 @@ void destroy_hex_context( RenderContext* gpu, HexContext* context); +// Grows highlights[]/points[] (both frames-in-flight) to at least `needed` +// elements if the current capacity falls short - doubling from whatever +// it's at, same pattern as ui.c's container_realloc_drawable_pool/ +// container_realloc_string_pool. A no-op if already sufficient. Old +// contents aren't preserved (nothing needs them to be - both buffers are +// fully rewritten every call to sync_hex_highlights/sync_vertex_points, +// see editor.c), so this is just retire-old/create-new/patch-address. +VkResult ensure_highlight_capacity(HexContext* context, RenderContext* gpu, uint32_t needed); +VkResult ensure_point_capacity(HexContext* context, RenderContext* gpu, uint32_t needed); + VkResult set_hex_region( HexRegion* region, HexContext* hex, diff --git a/client/script/editor_ui.lua b/client/script/editor_ui.lua index fa47faa..627bdd3 100644 --- a/client/script/editor_ui.lua +++ b/client/script/editor_ui.lua @@ -28,6 +28,14 @@ local state local function clamp01(x) return math.max(0, math.min(1, x)) end +-- Applies the picker's current color to the selection on Enter, rather +-- than live on every drag/hue/slot change - a no-op if the picker isn't +-- open (state == nil) or nothing is selected. +local function paint_selection() + if not state then return end + editor.set_color(state.rgb[1], state.rgb[2], state.rgb[3], state.rgb[4]) +end + local function update_hex_string() state.hex_string = string.format("#%02X%02X%02X%02X", math.floor(state.rgb[1]*255 + 0.5), @@ -38,8 +46,6 @@ local function update_hex_string() -- Publish for C and other overlays; only ever called with a complete -- color, so editor.color never holds a half-typed hex string app.set("editor.color", state.hex_string) - -- Live-apply to the current selection (a no-op if nothing is selected) - editor.set_color(state.rgb[1], state.rgb[2], state.rgb[3], state.rgb[4]) end local function sync_rgb_from_hsv() @@ -295,6 +301,7 @@ function on_key(overlay, element, key, action, mods) state.rgb[i] = (tonumber(s:sub(2*i, 2*i + 1), 16) or 0) / 255 end apply_state_rgb() + paint_selection() ui.blur() elseif key == KEY_BACKSPACE then if #state.hex_string > 1 then @@ -337,3 +344,12 @@ app.subscribe("engine.changed.editor.mode", function(source, mode) close_picker() end end) + +-- Enter commits the picker's current color to the selection. Doesn't fire +-- while hex_area is focused (its own on_key above consumes that Enter and +-- calls paint_selection() itself after parsing the typed value). +app.subscribe("engine.key", function(source, key, action, mods) + if action == PRESS and key == KEY_ENTER then + paint_selection() + end +end) diff --git a/client/src/draw.c b/client/src/draw.c index 4adac63..92aeda3 100644 --- a/client/src/draw.c +++ b/client/src/draw.c @@ -15,10 +15,10 @@ void record_hex_draw(VkCommandBuffer command_buffer, HexContext* hex, VkDeviceAd vkCmdDraw(command_buffer, 18, REGION_HEX_COUNT*MAX_LOADED_REGIONS, 0, 0); vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, hex->point_pipeline.pipeline); - vkCmdDraw(command_buffer, 1, MAX_POINTS, 0, 0); + vkCmdDraw(command_buffer, 1, hex->cap_points, 0, 0); vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, hex->highlight_pipeline.pipeline); - vkCmdDraw(command_buffer, 18, MAX_HIGHLIGHTS, 0, 0); + vkCmdDraw(command_buffer, 18, hex->cap_highlights, 0, 0); vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, hex->ray_pipeline.pipeline); vkCmdDraw(command_buffer, 2, 2, 0, 0); diff --git a/client/src/editor.c b/client/src/editor.c index f42ea00..9f783ed 100644 --- a/client/src/editor.c +++ b/client/src/editor.c @@ -442,7 +442,9 @@ bool editor_pick(ClientContext* context, double cursor[2], uint32_t* rid, uint32 // reserved for the hover preview (see refresh_hover_visuals). void sync_hex_highlights(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); - for(uint32_t i = 0; i < MAX_HIGHLIGHTS - 1; i++) { + // +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 @@ -466,7 +468,13 @@ void sync_hex_highlights(EditorData* data, ClientContext* context) { void sync_vertex_points(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); - for(uint32_t i = 0; i < MAX_POINTS - 1; i++) { + // 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). @@ -492,7 +500,14 @@ void sync_vertex_points(EditorData* data, ClientContext* context) { // remove) is visible before the user commits to it. void refresh_hover_visuals(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); - uint32_t highlight_slot = MAX_HIGHLIGHTS - 1; + // 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. @@ -511,7 +526,7 @@ void refresh_hover_visuals(EditorData* data, ClientContext* context) { add_transfers(&disabled, context->hex.highlights, sizeof(GPUHighlight)*highlight_slot + offsetof(GPUHighlight, hex), sizeof(uint32_t), &context->render); } - uint32_t point_slot = MAX_POINTS - 1; + 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; @@ -566,14 +581,24 @@ static void on_mode_changed(void* userdata, const char* event, uint32_t source, 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; - EditorMode mode = current_mode(context); - if(button != GLFW_MOUSE_BUTTON_LEFT || action != GLFW_PRESS) return; + 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; @@ -599,6 +624,12 @@ void editor_button_callback(ClientContext* context, float x, float y, int button // 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. @@ -643,7 +674,22 @@ void editor_cursor_callback(ClientContext* context, float x, float y) { data->hover_hex = hid; data->hover_vertex = v; } - refresh_hover_visuals(data, context); + + // 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) { diff --git a/client/src/hex.c b/client/src/hex.c index 92e1c4b..817230c 100644 --- a/client/src/hex.c +++ b/client/src/hex.c @@ -764,19 +764,21 @@ VkResult create_hex_context( VK_RESULT(create_storage_buffer( gpu->allocator, 0, - sizeof(GPUPoint)*MAX_POINTS, + sizeof(GPUPoint)*MIN_POINTS, &context->points[i], &context->points_memory[i])); VK_RESULT(create_storage_buffer( gpu->allocator, 0, - sizeof(GPUHighlight)*MAX_HIGHLIGHTS, + sizeof(GPUHighlight)*MIN_HIGHLIGHTS, &context->highlights[i], &context->highlights_memory[i])); } + context->cap_points = MIN_POINTS; + context->cap_highlights = MIN_HIGHLIGHTS; - for(uint32_t i = 0; i < MAX_HIGHLIGHTS; i++) { + for(uint32_t i = 0; i < MIN_HIGHLIGHTS; i++) { uint32_t temp = 0xFFFFFFFF; VK_RESULT(add_transfers( &temp, @@ -786,7 +788,7 @@ VkResult create_hex_context( gpu)); } - for(uint32_t i = 0; i < MAX_POINTS; i++) { + for(uint32_t i = 0; i < MIN_POINTS; i++) { uint32_t temp = 0xFFFFFFFF; VK_RESULT(add_transfers( &temp, @@ -837,6 +839,42 @@ VkResult create_hex_context( return VK_SUCCESS; } +VkResult ensure_highlight_capacity(HexContext* context, RenderContext* gpu, uint32_t needed) { + VkResult result; + if(needed <= context->cap_highlights) return VK_SUCCESS; + + uint32_t new_cap = context->cap_highlights; + while(new_cap < needed) new_cap *= 2; + + for(uint32_t f = 0; f < MAX_FRAMES_IN_FLIGHT; f++) { + retire_buffer(context->highlights[f], context->highlights_memory[f], gpu); + VK_RESULT(create_storage_buffer(gpu->allocator, 0, sizeof(GPUHighlight)*new_cap, &context->highlights[f], &context->highlights_memory[f])); + VkDeviceAddress address = buffer_address(gpu->device, context->highlights[f]); + VK_RESULT(add_transfer(&address, context->context[f], offsetof(GPUHexContext, highlights), sizeof(VkDeviceAddress), f, gpu)); + } + + context->cap_highlights = new_cap; + return VK_SUCCESS; +} + +VkResult ensure_point_capacity(HexContext* context, RenderContext* gpu, uint32_t needed) { + VkResult result; + if(needed <= context->cap_points) return VK_SUCCESS; + + uint32_t new_cap = context->cap_points; + while(new_cap < needed) new_cap *= 2; + + for(uint32_t f = 0; f < MAX_FRAMES_IN_FLIGHT; f++) { + retire_buffer(context->points[f], context->points_memory[f], gpu); + VK_RESULT(create_storage_buffer(gpu->allocator, 0, sizeof(GPUPoint)*new_cap, &context->points[f], &context->points_memory[f])); + VkDeviceAddress address = buffer_address(gpu->device, context->points[f]); + VK_RESULT(add_transfer(&address, context->context[f], offsetof(GPUHexContext, points), sizeof(VkDeviceAddress), f, gpu)); + } + + context->cap_points = new_cap; + return VK_SUCCESS; +} + void destroy_hex_context(RenderContext* gpu, HexContext* context) { vkDestroyPipeline(gpu->device, context->graphics.pipeline, NULL); vkDestroyPipelineLayout(gpu->device, context->graphics.layout, NULL);