799 lines
33 KiB
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, ®ion, &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);
|
|
}
|