From ef7b426cb6e7424604fe8acf04c86587ee5d0bc7 Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Tue, 28 Jul 2026 19:06:32 -0600 Subject: [PATCH] add paint logic --- client/Makefile | 15 +- client/include/editor.h | 22 ++- client/include/hex.h | 18 ++- client/script/editor_paint.lua | 17 +++ client/script/editor_ui.lua | 2 + client/shader/hex.vert | 11 +- client/shader/hex_common.glsl | 9 +- client/shader/hex_highlight.vert | 10 ++ client/src/editor.c | 229 +++++++++++++++++++++++++------ client/src/editor_lua.c | 37 +++++ client/src/hex.c | 12 +- client/src/ui_lua.c | 3 + 12 files changed, 333 insertions(+), 52 deletions(-) create mode 100644 client/script/editor_paint.lua diff --git a/client/Makefile b/client/Makefile index d3ca372..5d05a41 100644 --- a/client/Makefile +++ b/client/Makefile @@ -24,6 +24,15 @@ FRAG_SPV = $(addsuffix .frag.spv, $(basename $(wildcard shader/*.frag))) COMP_SPV = $(addsuffix .comp.spv, $(basename $(wildcard shader/*.comp))) SPV_FILES = $(VERT_SPV) $(FRAG_SPV) $(COMP_SPV) +# glslc doesn't take part in the dependency graph the way CFLAGS's -MMD does +# for C, so #include'd shared shader headers (hex_common.glsl etc.) aren't +# otherwise tracked - editing one wouldn't rebuild any .spv that includes it, +# leaving the GPU code compiled against a stale struct layout while the C +# structs it reads have already moved on. Treating every .glsl as a +# dependency of every shader is coarser than necessary but correct, and the +# shader directory is small enough that it's not worth being precise. +GLSL_HEADERS = $(wildcard shader/*.glsl) + EXTRA_DEBUG_REQUIREMENTS := UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) @@ -103,13 +112,13 @@ roleplay.dSYM: roleplay debug: roleplay $(EXTRA_DEBUG_REQUIREMENTS) $(SPV_FILES) $(GDB) roleplay -%.vert.spv: %.vert +%.vert.spv: %.vert $(GLSL_HEADERS) glslc -o $@ $< -%.frag.spv: %.frag +%.frag.spv: %.frag $(GLSL_HEADERS) glslc -o $@ $< -%.comp.spv: %.comp +%.comp.spv: %.comp $(GLSL_HEADERS) glslc -o $@ $< -include $(DEP_FILES) diff --git a/client/include/editor.h b/client/include/editor.h index f11800a..4e95550 100644 --- a/client/include/editor.h +++ b/client/include/editor.h @@ -36,6 +36,12 @@ struct EditorDataStruct { uint32_t selected_max; uint32_t selected_count; + // selected_vertices[i] (and hover_vertex below) is mode-dependent: a real + // vertex index (1-6) in Vertex mode, a wedge/triangle index (0-5) in Hex + // mode - same "hex mode used to just hardcode this to a placeholder" + // shape as before (that placeholder was always 0, now it's whichever + // wedge was actually picked). Always homogeneous within a frame, since + // on_mode_changed clears the selection on every mode switch. uint32_t* selected_regions; uint32_t* selected_hexes; uint32_t* selected_vertices; @@ -74,11 +80,23 @@ EditorMode current_mode(ClientContext* context); void editor_set_mode(ClientContext* context, EditorMode mode); 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); +// triangle (0-5) is always populated on a hit, independent of edge_only - +// it's the wedge the ray landed in, used by Hex-mode selection. +bool editor_pick(ClientContext* context, double cursor[2], uint32_t* rid, uint32_t* hid, uint32_t* vertex, uint32_t* triangle, bool edge_only); + +// Declared here since set_vertex_color/set_vertex_height are defined in +// editor.c but need to be callable from editor_lua.c's Lua bindings. +VkResult set_vertex_height(float height, uint32_t region, uint32_t hex, uint32_t vertex, ClientContext* context); +VkResult set_vertex_color(vec4 color, uint32_t region, uint32_t hex, uint32_t vertex, ClientContext* context); +VkResult set_hex_triangle_color(vec4 color, uint32_t region, uint32_t hex, uint32_t triangle, ClientContext* context); +VkResult set_hex_flat(bool flat, uint32_t region, uint32_t hex, uint32_t triangle, ClientContext* context); + +void editor_selection_set_color(ClientContext* context, vec4 color); +void editor_selection_nudge_height(ClientContext* context, float delta); +void editor_selection_toggle_flat(ClientContext* context); #endif diff --git a/client/include/hex.h b/client/include/hex.h index 5beeee1..a8d0e27 100644 --- a/client/include/hex.h +++ b/client/include/hex.h @@ -37,7 +37,16 @@ extern int hex_indices[]; typedef struct GPUHexStruct { float height[6]; - uint32_t color[7]; + // inner_colors[wedge] feeds the center-role vertex of triangle `wedge` + // (see hex.vert's `wedge = gl_VertexIndex / 3`) and, when flat_mask bit + // `wedge` is set, *all three* of that triangle's vertices - overriding + // the shared outer_colors for a solid fill local to that one wedge. + // outer_colors[i] is corner i+1 (matches the existing height[] and + // set_vertex_color vertex-1 indexing), shared in meaning (not storage) + // with the up-to-2 other hexes touching that same world corner. + uint32_t inner_colors[6]; + uint32_t outer_colors[6]; + uint32_t flat_mask; } GPUHex; typedef struct GPUHexRegionStruct { @@ -67,6 +76,9 @@ typedef struct GPUHighlightStruct { uint32_t region; uint32_t hex; float offset; + // 0xFFFFFFFF (the default/existing behavior) highlights the whole hex; + // any other value (0-5) highlights just that one wedge/triangle. + uint32_t triangle; } GPUHighlight; typedef struct GPUPointStruct { @@ -157,6 +169,10 @@ void cursor_to_world_ray( bool ray_world_intersect( float* distance, uint32_t* vertex, + // Wedge/triangle index (0-5) at the hit point - see hex.vert's `wedge` + // for the matching GPU-side meaning. Always populated on a hit, + // independent of edge_only/vertex resolution. + uint32_t* triangle, uint32_t* rid, uint32_t* hid, vec4 ray_start, diff --git a/client/script/editor_paint.lua b/client/script/editor_paint.lua new file mode 100644 index 0000000..846a41f --- /dev/null +++ b/client/script/editor_paint.lua @@ -0,0 +1,17 @@ +-- Quick-editing keys for the current selection: height nudging (Vertex +-- mode) and flat/gradient toggling (Hex mode). Same app.subscribe shape as +-- editor_mode.lua; editor.nudge_height/toggle_flat each no-op in the wrong +-- mode, so this doesn't need to check current mode itself. + +local HEIGHT_STEP = 0.1 + +app.subscribe("engine.key", function(source, key, action, mods) + if action ~= PRESS then return end + if key == KEY_EQUAL then + editor.nudge_height(HEIGHT_STEP) + elseif key == KEY_MINUS then + editor.nudge_height(-HEIGHT_STEP) + elseif key == KEY_F then + editor.toggle_flat() + end +end) diff --git a/client/script/editor_ui.lua b/client/script/editor_ui.lua index 1878616..fa47faa 100644 --- a/client/script/editor_ui.lua +++ b/client/script/editor_ui.lua @@ -38,6 +38,8 @@ 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() diff --git a/client/shader/hex.vert b/client/shader/hex.vert index 08c7d8e..82bc537 100644 --- a/client/shader/hex.vert +++ b/client/shader/hex.vert @@ -54,6 +54,15 @@ void main() { position.y = region.hexes[hex_index].heights[indices[gl_VertexIndex]-1] + region.y; } - color = int2color(region.hexes[hex_index].colors[indices[gl_VertexIndex]]); + // Wedge index (0-5) - matches ray_hex_intersect's `triangle` loop + // variable on the C side exactly (same 3-per-triangle indices[] layout). + // Not the same thing as `side` above, which is this hex's own + // ring-position within its region. + uint wedge = gl_VertexIndex / 3; + bool is_flat = ((region.hexes[hex_index].flat_mask >> wedge) & 1u) != 0; + uint color_value = (gl_VertexIndex % 3 == 0 || is_flat) + ? region.hexes[hex_index].inner_colors[wedge] + : region.hexes[hex_index].outer_colors[indices[gl_VertexIndex] - 1]; + color = int2color(color_value); gl_Position = pc.camera.proj * pc.camera.view * position; } diff --git a/client/shader/hex_common.glsl b/client/shader/hex_common.glsl index 9918571..3132ab0 100644 --- a/client/shader/hex_common.glsl +++ b/client/shader/hex_common.glsl @@ -2,7 +2,12 @@ struct Hex { float heights[6]; - uint colors[7]; + // inner_colors[wedge] is triangle `wedge`'s center-role color, and (when + // flat_mask bit `wedge` is set) its solid fill color for all 3 vertices. + // outer_colors[i] is corner i+1 (matches heights[] indexing). + uint inner_colors[6]; + uint outer_colors[6]; + uint flat_mask; }; layout(std430, buffer_reference) readonly buffer Region { @@ -18,6 +23,8 @@ struct Highlight { uint region; uint hex; float offset; + // 0xFFFFFFFF highlights the whole hex; 0-5 highlights just that wedge. + uint triangle; }; layout(std430, buffer_reference) readonly buffer HighlightList { diff --git a/client/shader/hex_highlight.vert b/client/shader/hex_highlight.vert index 2d2ebe8..1e1eee9 100644 --- a/client/shader/hex_highlight.vert +++ b/client/shader/hex_highlight.vert @@ -7,6 +7,16 @@ layout(location = 0) flat out vec4 color; void main() { uint hex_index = pc.context.highlights.h[gl_InstanceIndex].hex; + uint highlight_triangle = pc.context.highlights.h[gl_InstanceIndex].triangle; + // Wedge index (0-5) for this vertex, same meaning as hex.vert's `wedge`. + // A highlight targeting one specific wedge degenerates every vertex + // outside it; 0xFFFFFFFF (the default) means "whole hex", unchanged. + uint wedge = gl_VertexIndex / 3; + if(hex_index != 0xFFFFFFFF && highlight_triangle != 0xFFFFFFFF && wedge != highlight_triangle) { + gl_Position = vec4(0, 0, 0, 0); + color = vec4(0, 0, 0, 0); + return; + } if(hex_index != 0xFFFFFFFF) { Region region = pc.context.regions[pc.context.highlights.h[gl_InstanceIndex].region]; color = pc.context.highlights.h[gl_InstanceIndex].color; diff --git a/client/src/editor.c b/client/src/editor.c index 4184963..f42ea00 100644 --- a/client/src/editor.c +++ b/client/src/editor.c @@ -55,15 +55,49 @@ VkResult set_vertex_height(float height, uint32_t region, uint32_t hex, uint32_t &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].color[vertex] = hex_color(color); + context->hex.regions[region]->data.hexes[hex].outer_colors[vertex-1] = hex_color(color); return add_transfer( - &context->hex.regions[region]->data.hexes[hex].color[vertex], + &context->hex.regions[region]->data.hexes[hex].outer_colors[vertex-1], context->hex.regions[region]->region, offsetof(GPUHexRegion, hexes) + sizeof(GPUHex)*hex - + offsetof(GPUHex, color) - + sizeof(uint32_t)*vertex, + + 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); @@ -75,13 +109,19 @@ uint32_t add_hex_region(ClientContext* context) { 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].color[0] = 0xFFFFFFFF; - region->data.hexes[hex].color[1] = 0xFFFFFFFF; - region->data.hexes[hex].color[2] = 0xFFFFFFFF; - region->data.hexes[hex].color[3] = 0xFFFFFFFF; - region->data.hexes[hex].color[4] = 0xFFFFFFFF; - region->data.hexes[hex].color[5] = 0xFFFFFFFF; - region->data.hexes[hex].color[6] = 0xFFFFFFFF; + 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; @@ -261,15 +301,6 @@ void resize_selected(EditorData* data, unsigned int size) { data->selected_max = size; } -int32_t find_selected_hex(EditorData* data, uint32_t region, uint32_t hex) { - for(uint32_t i = 0; i < data->selected_count; i++) { - if(data->selected_regions[i] == region && data->selected_hexes[i] == hex) { - return (int32_t)i; - } - } - return -1; -} - 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 @@ -301,23 +332,129 @@ void remove_selected_at(EditorData* data, uint32_t index) { data->selected_count--; } -bool editor_pick(ClientContext* context, double cursor[2], uint32_t* rid, uint32_t* hid, uint32_t* vertex, bool edge_only) { +// 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, rid, hid, start, end, edge_only, &context->hex); + 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); for(uint32_t i = 0; i < MAX_HIGHLIGHTS - 1; i++) { if(i < data->selected_count) { GPUHighlight temp = { - .color = {1.0f, 0.85f, 0.05f, 1.0f}, + // 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 { @@ -328,10 +465,14 @@ 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++) { - if(i < data->selected_count) { + // 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, 1.0f}, + .color = {1.0f, 0.85f, 0.05f, 0.25f}, .region = data->selected_regions[i], .hex = data->selected_hexes[i], .vertex = data->selected_vertices[i], @@ -352,19 +493,18 @@ void sync_vertex_points(EditorData* data, ClientContext* context) { void refresh_hover_visuals(EditorData* data, ClientContext* context) { EditorMode mode = current_mode(context); uint32_t highlight_slot = MAX_HIGHLIGHTS - 1; - // In vertex mode the hex highlight is just spatial context for the - // hovered vertex, so it mirrors the vertex's own add/remove color rather - // than tracking hex selection (vertex mode doesn't select whole hexes). + // 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 = (mode == MODE_HEX) - ? find_selected_hex(data, data->hover_region, data->hover_hex) != -1 - : find_selected_vertex(data, data->hover_region, data->hover_hex, data->hover_vertex) != -1; + 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; @@ -446,17 +586,18 @@ void editor_button_callback(ClientContext* context, float x, float y, int button v = data->hover_vertex; } else { double cursor[2] = {x, y}; - uint32_t vertex; + 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, mode == MODE_VERTEX)) return; - v = (mode == MODE_VERTEX) ? vertex : 0; + if(!editor_pick(context, cursor, &rid, &hid, &vertex, &triangle, mode == MODE_VERTEX)) return; + v = (mode == MODE_VERTEX) ? vertex : triangle; } - int32_t idx = (mode == MODE_VERTEX) - ? find_selected_vertex(data, rid, hid, v) - : find_selected_hex(data, rid, hid); + // 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); // Left click adds to the selection; Shift is reserved for camera movement // (GLFW_KEY_LEFT_SHIFT) so it carries no selection meaning here. Ctrl @@ -483,15 +624,17 @@ void editor_cursor_callback(ClientContext* context, float x, float y) { } double cursor[2] = {x, y}; - uint32_t rid, hid, vertex; - bool hit = editor_pick(context, cursor, &rid, &hid, &vertex, mode == MODE_VERTEX); - uint32_t v = (mode == MODE_VERTEX) ? vertex : 0; + 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. + // 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 - || (mode == MODE_VERTEX && data->hover_vertex != v))); + || data->hover_vertex != v)); if(!changed) return; data->hover_valid = hit; @@ -524,6 +667,8 @@ void editor_startup(ClientContext* context) { 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- diff --git a/client/src/editor_lua.c b/client/src/editor_lua.c index 231cca2..53325eb 100644 --- a/client/src/editor_lua.c +++ b/client/src/editor_lua.c @@ -1,4 +1,5 @@ #include "editor_lua.h" +#include "editor.h" #include "ui_lua.h" #include @@ -215,6 +216,33 @@ static int lua_camera_set_aspect(lua_State* L) { return 0; } +// editor.set_color(r, g, b, a) - applies to the current selection (Vertex +// or Hex mode); a no-op if nothing is selected. See +// editor_selection_set_color (editor.c) for the per-mode behavior. +static int lua_editor_set_color(lua_State* L) { + vec4 color = { + (float)luaL_checknumber(L, 1), + (float)luaL_checknumber(L, 2), + (float)luaL_checknumber(L, 3), + (float)luaL_checknumber(L, 4), + }; + editor_selection_set_color(current_context(L), color); + return 0; +} + +// editor.nudge_height(delta) - Vertex mode only, no-ops otherwise. +static int lua_editor_nudge_height(lua_State* L) { + float delta = (float)luaL_checknumber(L, 1); + editor_selection_nudge_height(current_context(L), delta); + return 0; +} + +// editor.toggle_flat() - Hex mode only, no-ops otherwise. +static int lua_editor_toggle_flat(lua_State* L) { + editor_selection_toggle_flat(current_context(L)); + return 0; +} + void editor_lua_register(lua_State* L, ClientContext* context) { lua_pushlightuserdata(L, context); lua_setfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT); @@ -246,4 +274,13 @@ void editor_lua_register(lua_State* L, ClientContext* context) { lua_pushcfunction(L, lua_camera_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); + + static const luaL_Reg editor_funcs[] = { + {"set_color", lua_editor_set_color}, + {"nudge_height", lua_editor_nudge_height}, + {"toggle_flat", lua_editor_toggle_flat}, + {NULL, NULL}, + }; + luaL_newlib(L, editor_funcs); + lua_setglobal(L, "editor"); } diff --git a/client/src/hex.c b/client/src/hex.c index 47b15b1..92e1c4b 100644 --- a/client/src/hex.c +++ b/client/src/hex.c @@ -958,6 +958,7 @@ VkResult allocate_hex_region( bool ray_hex_intersect( float* distance, uint32_t* vertex, + uint32_t* triangle_out, vec3 start, vec3 dir, vec3 region_offset, @@ -1041,6 +1042,7 @@ bool ray_hex_intersect( float intersect_distance = glm_vec3_dot(v0v2, q) / det; if(intersect_distance < *distance) { *distance = intersect_distance; + *triangle_out = triangle; if(edge_only) { // Never report the hex center (vertex 0) - always snap to whichever // of the triangle's two edge vertices is closer, so vertex-mode @@ -1069,6 +1071,7 @@ bool ray_hex_intersect( bool ray_region_intersect( float* distance, uint32_t* vertex, + uint32_t* triangle_out, uint32_t* hid, vec3 start, vec3 dir, @@ -1077,6 +1080,7 @@ bool ray_region_intersect( bool intersect = false; float intersect_distance = INFINITY; uint32_t intersection_vertex = 0; + uint32_t intersection_triangle = 0; vec3 region_offset = { ((float)region->data.position.q + (float)region->data.position.r/2)*REGION_WIDTH - region->data.position.r*HEX_X/2, 0, @@ -1084,12 +1088,13 @@ bool ray_region_intersect( }; for(uint32_t intersect_hid = 0; intersect_hid < REGION_HEX_COUNT; intersect_hid++) { - if(ray_hex_intersect(&intersect_distance, &intersection_vertex, start, dir, region_offset, intersect_hid, edge_only, region)) { + if(ray_hex_intersect(&intersect_distance, &intersection_vertex, &intersection_triangle, start, dir, region_offset, intersect_hid, edge_only, region)) { if(intersect_distance < *distance) { intersect = true; *hid = intersect_hid; *distance = intersect_distance; *vertex = intersection_vertex; + *triangle_out = intersection_triangle; } } } @@ -1100,6 +1105,7 @@ bool ray_region_intersect( bool ray_world_intersect( float* distance, uint32_t* vertex, + uint32_t* triangle_out, uint32_t* rid, uint32_t* hid, vec4 ray_start, @@ -1126,6 +1132,7 @@ bool ray_world_intersect( bool intersect = false; float intersect_distance = INFINITY; uint32_t intersection_vertex = 0; + uint32_t intersection_triangle = 0; uint32_t intersect_hid; *distance = INFINITY; @@ -1138,12 +1145,13 @@ bool ray_world_intersect( continue; } - if(ray_region_intersect(&intersect_distance, &intersection_vertex, &intersect_hid, start, dir, edge_only, region)) { + if(ray_region_intersect(&intersect_distance, &intersection_vertex, &intersection_triangle, &intersect_hid, start, dir, edge_only, region)) { if(intersect_distance < *distance) { intersect = true; *hid = intersect_hid; *rid = intersect_rid; *vertex = intersection_vertex; + *triangle_out = intersection_triangle; *distance = intersect_distance; } } diff --git a/client/src/ui_lua.c b/client/src/ui_lua.c index f77a6ac..145ecf7 100644 --- a/client/src/ui_lua.c +++ b/client/src/ui_lua.c @@ -602,6 +602,9 @@ void ui_lua_register(lua_State* L) { lua_pushinteger(L, GLFW_KEY_N); lua_setglobal(L, "KEY_N"); lua_pushinteger(L, GLFW_KEY_H); lua_setglobal(L, "KEY_H"); lua_pushinteger(L, GLFW_KEY_R); lua_setglobal(L, "KEY_R"); + lua_pushinteger(L, GLFW_KEY_F); lua_setglobal(L, "KEY_F"); + lua_pushinteger(L, GLFW_KEY_EQUAL); lua_setglobal(L, "KEY_EQUAL"); + lua_pushinteger(L, GLFW_KEY_MINUS); lua_setglobal(L, "KEY_MINUS"); lua_pushinteger(L, DRAWABLE_TYPE_RECT); lua_setglobal(L, "RECT"); lua_pushinteger(L, DRAWABLE_TYPE_RECT_HSV); lua_setglobal(L, "RECT_HSV");