roleplay/client/src/editor.c

799 lines
33 KiB
C

#include <stdio.h>
#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 <math.h>
#include <stdlib.h>
#include <string.h>
#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, &region, &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);
}