add paint logic

main
noah metz 2026-07-28 19:06:32 -06:00
parent 42332a7ffc
commit ef7b426cb6
12 changed files with 333 additions and 52 deletions

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

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

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

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

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

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

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

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

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

@ -1,4 +1,5 @@
#include "editor_lua.h"
#include "editor.h"
#include "ui_lua.h"
#include <stdlib.h>
@ -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");
}

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

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