Dynamically grow highlights, and drag to multi-select

main
noah metz 2026-07-28 19:29:11 -06:00
parent ef7b426cb6
commit d3691bd146
6 changed files with 148 additions and 20 deletions

@ -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

@ -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,

@ -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)

@ -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);

@ -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,8 +674,23 @@ void editor_cursor_callback(ClientContext* context, float x, float y) {
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;

@ -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);