Added EventBus logic and moved camera to be drawn on Containers
parent
8252418db9
commit
1f2118f210
@ -0,0 +1,78 @@
|
|||||||
|
#ifndef CAMERA_H
|
||||||
|
#define CAMERA_H
|
||||||
|
|
||||||
|
#include <cglm/types.h>
|
||||||
|
#include "gpu.h"
|
||||||
|
|
||||||
|
// Forward declaration only (no #include "ui.h") - camera.h and ui.h are
|
||||||
|
// mutually referential (Container owns a Camera*, Camera targets a
|
||||||
|
// UIContext's texture pool) and neither needs more than a pointer to the
|
||||||
|
// other's type.
|
||||||
|
typedef struct UIContextStruct UIContext;
|
||||||
|
|
||||||
|
// GPU mirror of the proj/view a camera pushes to its render target's push
|
||||||
|
// constant; std430 layout must match shader/camera_common.glsl's Camera
|
||||||
|
// buffer_reference struct exactly.
|
||||||
|
typedef struct GPUCameraStruct {
|
||||||
|
mat4 proj;
|
||||||
|
mat4 view;
|
||||||
|
} GPUCamera;
|
||||||
|
|
||||||
|
// Orbital camera: looks at `position` from `distance` away, oriented by
|
||||||
|
// `rotation` (yaw, pitch). `view`/`proj` are derived state, stale until
|
||||||
|
// camera_update_view/camera_update_proj run - callers are responsible for
|
||||||
|
// calling them after changing position/rotation/distance, or after the
|
||||||
|
// camera's target size changes.
|
||||||
|
//
|
||||||
|
// A camera's on-screen placement isn't camera state - attach it to a
|
||||||
|
// Container (container_set_camera, ui.h) and that container's anchor/
|
||||||
|
// offset/size/z-order (container_order) becomes the camera's viewport and
|
||||||
|
// draw order, same as every other container. A camera can separately (or
|
||||||
|
// instead) render into an offscreen texture via camera_init_texture_target,
|
||||||
|
// for other pipelines/containers to sample.
|
||||||
|
typedef struct CameraStruct {
|
||||||
|
vec3 position;
|
||||||
|
vec2 rotation;
|
||||||
|
double distance;
|
||||||
|
mat4 view;
|
||||||
|
mat4 proj;
|
||||||
|
|
||||||
|
bool has_texture_target;
|
||||||
|
struct {
|
||||||
|
uint32_t width, height;
|
||||||
|
// Double-buffered per frame-in-flight: two separate vkQueueSubmits
|
||||||
|
// aren't hazard-free against each other without extra sync, so this
|
||||||
|
// mirrors the existing pattern (HexContext/Container GPU buffers)
|
||||||
|
// rather than adding a new semaphore.
|
||||||
|
uint32_t texture_slot[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
VkImage depth_image[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
VkImageView depth_image_view[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
VmaAllocation depth_image_memory[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
} texture;
|
||||||
|
|
||||||
|
VkBuffer gpu_buffer[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
VmaAllocation gpu_buffer_memory[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
VkDeviceAddress gpu_address[MAX_FRAMES_IN_FLIGHT];
|
||||||
|
} Camera;
|
||||||
|
|
||||||
|
VkResult create_camera(RenderContext* gpu, Camera* camera);
|
||||||
|
|
||||||
|
void camera_update_view(Camera* camera);
|
||||||
|
|
||||||
|
void camera_update_proj(Camera* camera, float aspect);
|
||||||
|
|
||||||
|
VkResult camera_sync_gpu(Camera* camera, RenderContext* gpu);
|
||||||
|
|
||||||
|
VkResult camera_init_texture_target(
|
||||||
|
RenderContext* gpu,
|
||||||
|
UIContext* ui,
|
||||||
|
Camera* camera,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height);
|
||||||
|
|
||||||
|
void camera_destroy_texture_target(
|
||||||
|
RenderContext* gpu,
|
||||||
|
UIContext* ui,
|
||||||
|
Camera* camera);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
#ifndef EDITOR_LUA_H
|
||||||
|
#define EDITOR_LUA_H
|
||||||
|
|
||||||
|
#include "engine.h"
|
||||||
|
|
||||||
|
// Registers the `camera` global table (editor-specific, needs ClientContext,
|
||||||
|
// so it can't live in the engine-generic ui_lua.c)
|
||||||
|
void editor_lua_register(lua_State* L, ClientContext* context);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
#ifndef EVENTS_H
|
||||||
|
#define EVENTS_H
|
||||||
|
|
||||||
|
#include "ui.h"
|
||||||
|
|
||||||
|
// Source id for events/writes originating from C. Container ids are nonzero
|
||||||
|
// (load_container rejects 0), so 0 is free to mean "the engine".
|
||||||
|
#define EVENT_SOURCE_ENGINE 0
|
||||||
|
|
||||||
|
// Names under "engine." can only be emitted by C; app.emit rejects them.
|
||||||
|
// Property writes announce themselves as "engine.changed.<property>".
|
||||||
|
#define EVENT_ENGINE_PREFIX "engine."
|
||||||
|
#define EVENT_CHANGED_PREFIX "engine.changed."
|
||||||
|
|
||||||
|
// Dispatched directly (never queued) once per drain after the queue is
|
||||||
|
// exhausted, so handlers reliably run after every change event of the
|
||||||
|
// frame — the coalescing point for dirty-flag subscribers. Events emitted
|
||||||
|
// by its handlers queue for the next frame.
|
||||||
|
#define EVENT_FRAME "engine.frame"
|
||||||
|
|
||||||
|
// Max events processed per drain; hitting it means a handler cycle is
|
||||||
|
// endlessly re-emitting, so the remainder is dumped to stderr and dropped.
|
||||||
|
#define EVENT_DRAIN_MAX 1024
|
||||||
|
|
||||||
|
typedef enum PropertyTypeEnum {
|
||||||
|
PROPERTY_NUMBER,
|
||||||
|
PROPERTY_STRING,
|
||||||
|
PROPERTY_BOOL,
|
||||||
|
} PropertyType;
|
||||||
|
|
||||||
|
typedef struct PropertyStruct {
|
||||||
|
char* name;
|
||||||
|
PropertyType type;
|
||||||
|
union {
|
||||||
|
double number;
|
||||||
|
char* string; // owned; NULL until first set
|
||||||
|
bool boolean;
|
||||||
|
} value;
|
||||||
|
} Property;
|
||||||
|
|
||||||
|
// C subscriber. The event's packed args table is on the Lua stack at
|
||||||
|
// args_index (t[1..t.n]); it must still be there when the handler returns.
|
||||||
|
typedef void (*EventHandler)(
|
||||||
|
void* userdata,
|
||||||
|
const char* event,
|
||||||
|
uint32_t source,
|
||||||
|
lua_State* L,
|
||||||
|
int args_index);
|
||||||
|
|
||||||
|
typedef struct EventSubscriptionStruct {
|
||||||
|
char* event;
|
||||||
|
// Registry ref of the script this subscription was made from, restored as
|
||||||
|
// the ambient "current script" before invoking a Lua handler — so
|
||||||
|
// ui.create_overlay called from inside it attributes correctly. Not used
|
||||||
|
// for lifecycle: subscriptions currently live for the process, since
|
||||||
|
// nothing unloads a script (only overlays, which are a separate
|
||||||
|
// lifecycle — see events-design memory).
|
||||||
|
int script_env;
|
||||||
|
int lua_ref; // handler function ref, LUA_NOREF for C handlers
|
||||||
|
EventHandler handler; // NULL for Lua handlers
|
||||||
|
void* userdata;
|
||||||
|
// Removal during a drain marks instead of compacting so in-flight
|
||||||
|
// iteration stays valid; compaction happens when the drain finishes
|
||||||
|
uint8_t dead;
|
||||||
|
} EventSubscription;
|
||||||
|
|
||||||
|
typedef struct QueuedEventStruct {
|
||||||
|
char* name;
|
||||||
|
uint32_t source;
|
||||||
|
int args_ref; // registry ref to the packed args table
|
||||||
|
} QueuedEvent;
|
||||||
|
|
||||||
|
struct EventBusStruct {
|
||||||
|
lua_State* L;
|
||||||
|
|
||||||
|
EventSubscription* subs;
|
||||||
|
uint32_t sub_count;
|
||||||
|
uint32_t sub_cap;
|
||||||
|
|
||||||
|
QueuedEvent* queue;
|
||||||
|
uint32_t queue_count;
|
||||||
|
uint32_t queue_cap;
|
||||||
|
|
||||||
|
Property* properties;
|
||||||
|
uint32_t property_count;
|
||||||
|
uint32_t property_cap;
|
||||||
|
|
||||||
|
bool draining;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Registers the `app` global (emit/subscribe/get/set) in the Lua state
|
||||||
|
VkResult event_bus_init(EventBus* bus, lua_State* L);
|
||||||
|
|
||||||
|
// Pops nargs values off the top of the Lua stack into the event's packed
|
||||||
|
// args and appends to the queue. Never dispatches; the queue drains once
|
||||||
|
// per frame from the top level.
|
||||||
|
VkResult event_emit(EventBus* bus, const char* name, uint32_t source, int nargs);
|
||||||
|
|
||||||
|
// Subscribes a C handler; Lua handlers subscribe via app.subscribe
|
||||||
|
VkResult event_subscribe(EventBus* bus, const char* name, EventHandler handler, void* userdata);
|
||||||
|
|
||||||
|
// Dispatches queued events in emit order, including events emitted by
|
||||||
|
// handlers during the drain, up to EVENT_DRAIN_MAX
|
||||||
|
void event_bus_drain(EventBus* bus, UIContext* ui, RenderContext* gpu, double delta_time);
|
||||||
|
|
||||||
|
VkResult event_property_register(EventBus* bus, const char* name, PropertyType type);
|
||||||
|
|
||||||
|
// Direct read access; NULL if unregistered
|
||||||
|
Property* event_property(EventBus* bus, const char* name);
|
||||||
|
|
||||||
|
// Setters no-op when the value is unchanged; otherwise they store the value
|
||||||
|
// and emit engine.changed.<name> with the new value as the single argument.
|
||||||
|
// Emission is unconditional on real changes regardless of who wrote.
|
||||||
|
VkResult event_property_set_number(EventBus* bus, const char* name, double value, uint32_t source);
|
||||||
|
VkResult event_property_set_string(EventBus* bus, const char* name, const char* value, uint32_t source);
|
||||||
|
VkResult event_property_set_bool(EventBus* bus, const char* name, bool value, uint32_t source);
|
||||||
|
|
||||||
|
// Arg accessors for C EventHandler bodies
|
||||||
|
int event_arg_count(lua_State* L, int args_index);
|
||||||
|
double event_arg_number(lua_State* L, int args_index, int i);
|
||||||
|
bool event_arg_bool(lua_State* L, int args_index, int i);
|
||||||
|
// Returned pointer is anchored by the args table; valid until the drain
|
||||||
|
// releases the event
|
||||||
|
const char* event_arg_string(lua_State* L, int args_index, int i);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -1,265 +0,0 @@
|
|||||||
-- Color picker: element declarations and interaction logic.
|
|
||||||
-- z controls stacking within the container; equal z means don't overlap.
|
|
||||||
|
|
||||||
local bg = ui.rect{
|
|
||||||
pos = {0, 0},
|
|
||||||
size = {190, 150},
|
|
||||||
color = {0.4, 0.4, 0.4, 0.8},
|
|
||||||
z = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
local sv_square = ui.rect{
|
|
||||||
type = RECT_HSV,
|
|
||||||
pos = {2, 2},
|
|
||||||
size = {130, 130},
|
|
||||||
colors = {{0, 0, 1, 1}, {0, 1, 1, 1}, {0, 0, 0, 1}, {0, 1, 0, 1}},
|
|
||||||
events = EVENT_BUTTON,
|
|
||||||
z = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
local hue_bar = ui.rect{
|
|
||||||
type = RECT_HSV,
|
|
||||||
pos = {134, 2},
|
|
||||||
size = {10, 130},
|
|
||||||
colors = {{0, 1, 1, 1}, {0, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}},
|
|
||||||
events = EVENT_BUTTON + EVENT_SCROLL,
|
|
||||||
z = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
local sv_outline = ui.rect{
|
|
||||||
pos = {130-4, 130-4},
|
|
||||||
size = {7, 7},
|
|
||||||
color = {0, 0, 0, 1},
|
|
||||||
z = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
local sv_select = ui.rect{
|
|
||||||
type = RECT_HSV,
|
|
||||||
pos = {130-3, 130-3},
|
|
||||||
size = {5, 5},
|
|
||||||
color = {1, 0, 0, 1},
|
|
||||||
z = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
local hex_area = ui.rect{
|
|
||||||
pos = {20, 134},
|
|
||||||
size = {95, 15},
|
|
||||||
events = EVENT_BUTTON + EVENT_CURSOR,
|
|
||||||
z = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
local hue_select = ui.rect{
|
|
||||||
pos = {134, 2},
|
|
||||||
size = {10, 1},
|
|
||||||
color = {0, 0, 0, 1},
|
|
||||||
z = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
local hex_text = ui.text{
|
|
||||||
pos = {2, 150},
|
|
||||||
size = 16,
|
|
||||||
color = {1, 1, 1, 1},
|
|
||||||
font = 0,
|
|
||||||
max_length = 16,
|
|
||||||
z = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- 12 saved color slots in a 2-wide grid
|
|
||||||
local slots = {}
|
|
||||||
local slot_index = {}
|
|
||||||
for i = 1, 12 do
|
|
||||||
local slot = ui.rect{
|
|
||||||
pos = {146 + ((i-1) % 2)*22, 2 + math.floor((i-1)/2)*22},
|
|
||||||
size = {20, 20},
|
|
||||||
color = {0, 0, 0, 1},
|
|
||||||
events = EVENT_BUTTON,
|
|
||||||
z = 1,
|
|
||||||
}
|
|
||||||
slots[i] = slot
|
|
||||||
slot_index[slot] = i
|
|
||||||
end
|
|
||||||
|
|
||||||
local state = {
|
|
||||||
hsv = {0, 0, 0},
|
|
||||||
rgb = {0, 0, 0, 1},
|
|
||||||
saved = {},
|
|
||||||
hex_string = "#000000FF",
|
|
||||||
}
|
|
||||||
for i = 1, 12 do state.saved[i] = {0, 0, 0, 0} end
|
|
||||||
|
|
||||||
local function clamp01(x) return math.max(0, math.min(1, x)) end
|
|
||||||
|
|
||||||
local function update_hex_string()
|
|
||||||
state.hex_string = string.format("#%02X%02X%02X%02X",
|
|
||||||
math.floor(state.rgb[1]*255 + 0.5),
|
|
||||||
math.floor(state.rgb[2]*255 + 0.5),
|
|
||||||
math.floor(state.rgb[3]*255 + 0.5),
|
|
||||||
math.floor(state.rgb[4]*255 + 0.5))
|
|
||||||
hex_text:set_text(state.hex_string)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sync_rgb_from_hsv()
|
|
||||||
state.rgb[1], state.rgb[2], state.rgb[3] =
|
|
||||||
ui.hsv_to_rgb(state.hsv[1], state.hsv[2], state.hsv[3])
|
|
||||||
update_hex_string()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sv_pick(s, v)
|
|
||||||
s = clamp01(s)
|
|
||||||
v = clamp01(v)
|
|
||||||
state.hsv[2] = s
|
|
||||||
state.hsv[3] = v
|
|
||||||
|
|
||||||
sv_select:set_pos(s*130 - 2, 130 - v*130 - 2)
|
|
||||||
for corner = 0, 3 do
|
|
||||||
sv_select:set_corner_color(corner, state.hsv[1], s, v, 1)
|
|
||||||
end
|
|
||||||
sv_outline:set_pos(s*130 - 3, 130 - v*130 - 3)
|
|
||||||
|
|
||||||
sync_rgb_from_hsv()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hue_set(h)
|
|
||||||
h = clamp01(h)
|
|
||||||
state.hsv[1] = h
|
|
||||||
|
|
||||||
for _, element in ipairs({sv_square, sv_select}) do
|
|
||||||
for corner = 0, 3 do
|
|
||||||
local _, s, v, a = element:corner_color(corner)
|
|
||||||
element:set_corner_color(corner, h, s, v, a)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
hue_select:set_pos(134, 2 + h*130)
|
|
||||||
|
|
||||||
sync_rgb_from_hsv()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hex_string_highlight(value)
|
|
||||||
for corner = 0, 3 do
|
|
||||||
local r, g, _, _ = hex_area:corner_color(corner)
|
|
||||||
hex_area:set_corner_color(corner, r, g, value, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function set_saved(index, color)
|
|
||||||
state.saved[index] = {color[1], color[2], color[3], color[4]}
|
|
||||||
local slot = slots[index]
|
|
||||||
for corner = 0, 3 do
|
|
||||||
local _, _, _, a = slot:corner_color(corner)
|
|
||||||
slot:set_corner_color(corner, color[1], color[2], color[3], a)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sync HSV+visuals from state.rgb; used after setting state.rgb directly.
|
|
||||||
local function apply_state_rgb()
|
|
||||||
state.hsv[1], state.hsv[2], state.hsv[3] =
|
|
||||||
ui.rgb_to_hsv(state.rgb[1], state.rgb[2], state.rgb[3])
|
|
||||||
hue_set(state.hsv[1])
|
|
||||||
sv_pick(state.hsv[2], state.hsv[3])
|
|
||||||
end
|
|
||||||
|
|
||||||
local drag = {
|
|
||||||
[sv_square] = function(x, y) sv_pick(x, 1 - y) end,
|
|
||||||
[hue_bar] = function(x, y) hue_set(y) end,
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_button(element, x, y, button, action, mods)
|
|
||||||
if drag[element] then
|
|
||||||
if action == PRESS and button == MOUSE_LEFT then
|
|
||||||
element:focus()
|
|
||||||
drag[element](x, y)
|
|
||||||
elseif action == RELEASE and button == MOUSE_LEFT then
|
|
||||||
ui.blur()
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
|
|
||||||
elseif element == hex_area then
|
|
||||||
if action == PRESS and button == MOUSE_LEFT then
|
|
||||||
element:focus()
|
|
||||||
hex_string_highlight(1)
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
|
|
||||||
elseif slot_index[element] then
|
|
||||||
local index = slot_index[element]
|
|
||||||
if action == PRESS then
|
|
||||||
if button == MOUSE_LEFT then
|
|
||||||
local saved = state.saved[index]
|
|
||||||
state.rgb = {saved[1], saved[2], saved[3], saved[4]}
|
|
||||||
apply_state_rgb()
|
|
||||||
elseif button == MOUSE_RIGHT then
|
|
||||||
set_saved(index, state.rgb)
|
|
||||||
elseif button == MOUSE_MIDDLE then
|
|
||||||
set_saved(index, {0, 0, 0, 0})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function on_cursor(element, x, y)
|
|
||||||
for el, fn in pairs(drag) do
|
|
||||||
if el:is_focused() then
|
|
||||||
fn(x, y)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function on_scroll(element, x, y)
|
|
||||||
if element == hue_bar then
|
|
||||||
hue_set(state.hsv[1] + y*0.01)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function on_key(element, key, action, mods)
|
|
||||||
if element ~= hex_area then return false end
|
|
||||||
|
|
||||||
if action == PRESS then
|
|
||||||
if key == KEY_ESCAPE then
|
|
||||||
sync_rgb_from_hsv()
|
|
||||||
ui.blur()
|
|
||||||
elseif key == KEY_ENTER then
|
|
||||||
local s = state.hex_string
|
|
||||||
for i = 1, 4 do
|
|
||||||
state.rgb[i] = (tonumber(s:sub(2*i, 2*i + 1), 16) or 0) / 255
|
|
||||||
end
|
|
||||||
apply_state_rgb()
|
|
||||||
ui.blur()
|
|
||||||
elseif key == KEY_BACKSPACE then
|
|
||||||
if #state.hex_string > 1 then
|
|
||||||
state.hex_string = state.hex_string:sub(1, -2)
|
|
||||||
hex_text:set_text(state.hex_string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function on_text(element, codepoint)
|
|
||||||
if element ~= hex_area then return false end
|
|
||||||
|
|
||||||
if codepoint < 128 then
|
|
||||||
local ch = string.char(codepoint):upper()
|
|
||||||
if ch:match("[0-9A-F]") and #state.hex_string < 9 then
|
|
||||||
state.hex_string = state.hex_string .. ch
|
|
||||||
hex_text:set_text(state.hex_string)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function on_deselect(element)
|
|
||||||
if element == hex_area then
|
|
||||||
hex_string_highlight(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- runs once at container load
|
|
||||||
hex_text:set_text(state.hex_string)
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
-- Mode-switch input handling: turns V/N/H/R/Escape presses into writes of
|
||||||
|
-- the editor.mode property. Separate from editor_ui.lua, which only reacts
|
||||||
|
-- to that property and doesn't care what triggers it.
|
||||||
|
|
||||||
|
local key_to_mode = {
|
||||||
|
[KEY_V] = "Vertex",
|
||||||
|
[KEY_N] = "Neighbor",
|
||||||
|
[KEY_H] = "Hex",
|
||||||
|
[KEY_R] = "Region",
|
||||||
|
[KEY_ESCAPE] = "None",
|
||||||
|
}
|
||||||
|
|
||||||
|
app.subscribe("engine.key", function(source, key, action, mods)
|
||||||
|
if action ~= PRESS then return end
|
||||||
|
local mode = key_to_mode[key]
|
||||||
|
if mode then app.set("editor.mode", mode) end
|
||||||
|
end)
|
||||||
@ -0,0 +1,337 @@
|
|||||||
|
-- Editor UI controller. Loaded once at startup; owns the mode label
|
||||||
|
-- (always present) and the color picker (created/destroyed with the mode),
|
||||||
|
-- both driven by the editor.mode property. z controls stacking within an
|
||||||
|
-- overlay; equal z means don't overlap.
|
||||||
|
|
||||||
|
-- ============================== mode label ==============================
|
||||||
|
|
||||||
|
local label_overlay = ui.create_overlay{anchor = ANCHOR_TOP_LEFT, size = {160, 40}}
|
||||||
|
local label = ui.text{
|
||||||
|
pos = {0, 32},
|
||||||
|
size = 32,
|
||||||
|
color = {1, 1, 1, 1},
|
||||||
|
font = 0,
|
||||||
|
max_length = 8,
|
||||||
|
z = 0,
|
||||||
|
}
|
||||||
|
label:set_text(app.get("editor.mode") or "None")
|
||||||
|
|
||||||
|
-- ============================== color picker =============================
|
||||||
|
-- Elements/state are outer locals, assigned (not re-declared) each time
|
||||||
|
-- open_picker() runs, so the shared on_button etc. below always see
|
||||||
|
-- whichever picker is currently open.
|
||||||
|
|
||||||
|
local picker_overlay = nil
|
||||||
|
local bg, sv_square, hue_bar, sv_outline, sv_select
|
||||||
|
local hex_area, hue_select, hex_text, slots, slot_index, drag
|
||||||
|
local state
|
||||||
|
|
||||||
|
local function clamp01(x) return math.max(0, math.min(1, x)) end
|
||||||
|
|
||||||
|
local function update_hex_string()
|
||||||
|
state.hex_string = string.format("#%02X%02X%02X%02X",
|
||||||
|
math.floor(state.rgb[1]*255 + 0.5),
|
||||||
|
math.floor(state.rgb[2]*255 + 0.5),
|
||||||
|
math.floor(state.rgb[3]*255 + 0.5),
|
||||||
|
math.floor(state.rgb[4]*255 + 0.5))
|
||||||
|
hex_text:set_text(state.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)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sync_rgb_from_hsv()
|
||||||
|
state.rgb[1], state.rgb[2], state.rgb[3] =
|
||||||
|
ui.hsv_to_rgb(state.hsv[1], state.hsv[2], state.hsv[3])
|
||||||
|
update_hex_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sv_pick(s, v)
|
||||||
|
s = clamp01(s)
|
||||||
|
v = clamp01(v)
|
||||||
|
state.hsv[2] = s
|
||||||
|
state.hsv[3] = v
|
||||||
|
|
||||||
|
sv_select:set_pos(s*130 - 2, 130 - v*130 - 2)
|
||||||
|
for corner = 0, 3 do
|
||||||
|
sv_select:set_corner_color(corner, state.hsv[1], s, v, 1)
|
||||||
|
end
|
||||||
|
sv_outline:set_pos(s*130 - 3, 130 - v*130 - 3)
|
||||||
|
|
||||||
|
sync_rgb_from_hsv()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hue_set(h)
|
||||||
|
h = clamp01(h)
|
||||||
|
state.hsv[1] = h
|
||||||
|
|
||||||
|
for _, element in ipairs({sv_square, sv_select}) do
|
||||||
|
for corner = 0, 3 do
|
||||||
|
local _, s, v, a = element:corner_color(corner)
|
||||||
|
element:set_corner_color(corner, h, s, v, a)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
hue_select:set_pos(134, 2 + h*130)
|
||||||
|
|
||||||
|
sync_rgb_from_hsv()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hex_string_highlight(value)
|
||||||
|
for corner = 0, 3 do
|
||||||
|
local r, g, _, _ = hex_area:corner_color(corner)
|
||||||
|
hex_area:set_corner_color(corner, r, g, value, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_saved(index, color)
|
||||||
|
state.saved[index] = {color[1], color[2], color[3], color[4]}
|
||||||
|
local slot = slots[index]
|
||||||
|
for corner = 0, 3 do
|
||||||
|
local _, _, _, a = slot:corner_color(corner)
|
||||||
|
slot:set_corner_color(corner, color[1], color[2], color[3], a)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sync HSV+visuals from state.rgb; used after setting state.rgb directly.
|
||||||
|
local function apply_state_rgb()
|
||||||
|
state.hsv[1], state.hsv[2], state.hsv[3] =
|
||||||
|
ui.rgb_to_hsv(state.rgb[1], state.rgb[2], state.rgb[3])
|
||||||
|
hue_set(state.hsv[1])
|
||||||
|
sv_pick(state.hsv[2], state.hsv[3])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function open_picker()
|
||||||
|
if picker_overlay then return end
|
||||||
|
picker_overlay = ui.create_overlay{anchor = ANCHOR_BOTTOM_LEFT, size = {190, 150}}
|
||||||
|
|
||||||
|
bg = ui.rect{
|
||||||
|
pos = {0, 0},
|
||||||
|
size = {190, 150},
|
||||||
|
color = {0.4, 0.4, 0.4, 0.8},
|
||||||
|
z = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sv_square = ui.rect{
|
||||||
|
type = RECT_HSV,
|
||||||
|
pos = {2, 2},
|
||||||
|
size = {130, 130},
|
||||||
|
colors = {{0, 0, 1, 1}, {0, 1, 1, 1}, {0, 0, 0, 1}, {0, 1, 0, 1}},
|
||||||
|
events = EVENT_BUTTON,
|
||||||
|
z = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
hue_bar = ui.rect{
|
||||||
|
type = RECT_HSV,
|
||||||
|
pos = {134, 2},
|
||||||
|
size = {10, 130},
|
||||||
|
colors = {{0, 1, 1, 1}, {0, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}},
|
||||||
|
events = EVENT_BUTTON + EVENT_SCROLL,
|
||||||
|
z = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
sv_outline = ui.rect{
|
||||||
|
pos = {130-4, 130-4},
|
||||||
|
size = {7, 7},
|
||||||
|
color = {0, 0, 0, 1},
|
||||||
|
z = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
sv_select = ui.rect{
|
||||||
|
type = RECT_HSV,
|
||||||
|
pos = {130-3, 130-3},
|
||||||
|
size = {5, 5},
|
||||||
|
color = {1, 0, 0, 1},
|
||||||
|
z = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
hex_area = ui.rect{
|
||||||
|
pos = {20, 134},
|
||||||
|
size = {95, 15},
|
||||||
|
events = EVENT_BUTTON + EVENT_CURSOR,
|
||||||
|
z = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
hue_select = ui.rect{
|
||||||
|
pos = {134, 2},
|
||||||
|
size = {10, 1},
|
||||||
|
color = {0, 0, 0, 1},
|
||||||
|
z = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
hex_text = ui.text{
|
||||||
|
pos = {2, 150},
|
||||||
|
size = 16,
|
||||||
|
color = {1, 1, 1, 1},
|
||||||
|
font = 0,
|
||||||
|
max_length = 16,
|
||||||
|
z = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 12 saved color slots in a 2-wide grid
|
||||||
|
slots = {}
|
||||||
|
slot_index = {}
|
||||||
|
for i = 1, 12 do
|
||||||
|
local slot = ui.rect{
|
||||||
|
pos = {146 + ((i-1) % 2)*22, 2 + math.floor((i-1)/2)*22},
|
||||||
|
size = {20, 20},
|
||||||
|
color = {0, 0, 0, 1},
|
||||||
|
events = EVENT_BUTTON,
|
||||||
|
z = 1,
|
||||||
|
}
|
||||||
|
slots[i] = slot
|
||||||
|
slot_index[slot] = i
|
||||||
|
end
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hsv = {0, 0, 0},
|
||||||
|
rgb = {0, 0, 0, 1},
|
||||||
|
saved = {},
|
||||||
|
hex_string = "#000000FF",
|
||||||
|
}
|
||||||
|
for i = 1, 12 do state.saved[i] = {0, 0, 0, 0} end
|
||||||
|
|
||||||
|
drag = {
|
||||||
|
[sv_square] = function(x, y) sv_pick(x, 1 - y) end,
|
||||||
|
[hue_bar] = function(x, y) hue_set(y) end,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- adopt the current published color so reopening the picker doesn't
|
||||||
|
-- reset the selection
|
||||||
|
local published = app.get("editor.color")
|
||||||
|
if published then
|
||||||
|
for i = 1, 4 do
|
||||||
|
state.rgb[i] = (tonumber(published:sub(2*i, 2*i + 1), 16) or 0) / 255
|
||||||
|
end
|
||||||
|
end
|
||||||
|
apply_state_rgb()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function close_picker()
|
||||||
|
if not picker_overlay then return end
|
||||||
|
picker_overlay:destroy()
|
||||||
|
picker_overlay = nil
|
||||||
|
bg, sv_square, hue_bar, sv_outline, sv_select = nil, nil, nil, nil, nil
|
||||||
|
hex_area, hue_select, hex_text, slots, slot_index, drag = nil, nil, nil, nil, nil, nil
|
||||||
|
state = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================== dispatch ================================
|
||||||
|
-- label_overlay has no handlers; every handler below only ever fires for
|
||||||
|
-- picker_overlay, but still checks since a shared script_env would call the
|
||||||
|
-- same on_button etc. for any future overlay this script owns.
|
||||||
|
|
||||||
|
function on_button(overlay, element, x, y, button, action, mods)
|
||||||
|
if overlay ~= picker_overlay then return false end
|
||||||
|
|
||||||
|
if drag[element] then
|
||||||
|
if action == PRESS and button == MOUSE_LEFT then
|
||||||
|
element:focus()
|
||||||
|
drag[element](x, y)
|
||||||
|
elseif action == RELEASE and button == MOUSE_LEFT then
|
||||||
|
ui.blur()
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif element == hex_area then
|
||||||
|
if action == PRESS and button == MOUSE_LEFT then
|
||||||
|
element:focus()
|
||||||
|
hex_string_highlight(1)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif slot_index[element] then
|
||||||
|
local index = slot_index[element]
|
||||||
|
if action == PRESS then
|
||||||
|
if button == MOUSE_LEFT then
|
||||||
|
local saved = state.saved[index]
|
||||||
|
state.rgb = {saved[1], saved[2], saved[3], saved[4]}
|
||||||
|
apply_state_rgb()
|
||||||
|
elseif button == MOUSE_RIGHT then
|
||||||
|
set_saved(index, state.rgb)
|
||||||
|
elseif button == MOUSE_MIDDLE then
|
||||||
|
set_saved(index, {0, 0, 0, 0})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_cursor(overlay, element, x, y)
|
||||||
|
if overlay ~= picker_overlay then return false end
|
||||||
|
|
||||||
|
for el, fn in pairs(drag) do
|
||||||
|
if el:is_focused() then
|
||||||
|
fn(x, y)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_scroll(overlay, element, x, y)
|
||||||
|
if overlay ~= picker_overlay then return false end
|
||||||
|
|
||||||
|
if element == hue_bar then
|
||||||
|
hue_set(state.hsv[1] + y*0.01)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_key(overlay, element, key, action, mods)
|
||||||
|
if overlay ~= picker_overlay or element ~= hex_area then return false end
|
||||||
|
|
||||||
|
if action == PRESS then
|
||||||
|
if key == KEY_ESCAPE then
|
||||||
|
sync_rgb_from_hsv()
|
||||||
|
ui.blur()
|
||||||
|
elseif key == KEY_ENTER then
|
||||||
|
local s = state.hex_string
|
||||||
|
for i = 1, 4 do
|
||||||
|
state.rgb[i] = (tonumber(s:sub(2*i, 2*i + 1), 16) or 0) / 255
|
||||||
|
end
|
||||||
|
apply_state_rgb()
|
||||||
|
ui.blur()
|
||||||
|
elseif key == KEY_BACKSPACE then
|
||||||
|
if #state.hex_string > 1 then
|
||||||
|
state.hex_string = state.hex_string:sub(1, -2)
|
||||||
|
hex_text:set_text(state.hex_string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_text(overlay, element, codepoint)
|
||||||
|
if overlay ~= picker_overlay or element ~= hex_area then return false end
|
||||||
|
|
||||||
|
if codepoint < 128 then
|
||||||
|
local ch = string.char(codepoint):upper()
|
||||||
|
if ch:match("[0-9A-F]") and #state.hex_string < 9 then
|
||||||
|
state.hex_string = state.hex_string .. ch
|
||||||
|
hex_text:set_text(state.hex_string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_deselect(overlay, element)
|
||||||
|
if overlay ~= picker_overlay then return end
|
||||||
|
if element == hex_area then
|
||||||
|
hex_string_highlight(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================== mode tracking ============================
|
||||||
|
|
||||||
|
app.subscribe("engine.changed.editor.mode", function(source, mode)
|
||||||
|
label:set_text(mode)
|
||||||
|
if mode == "Vertex" or mode == "Neighbor" or mode == "Hex" then
|
||||||
|
open_picker()
|
||||||
|
else
|
||||||
|
close_picker()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@ -1,16 +0,0 @@
|
|||||||
-- Mode indicator label; the editor calls set_mode() on mode changes
|
|
||||||
|
|
||||||
local label = ui.text{
|
|
||||||
pos = {0, 32},
|
|
||||||
size = 32,
|
|
||||||
color = {1, 1, 1, 1},
|
|
||||||
font = 0,
|
|
||||||
max_length = 8,
|
|
||||||
z = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
function set_mode(name)
|
|
||||||
label:set_text(name)
|
|
||||||
end
|
|
||||||
|
|
||||||
set_mode("None")
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
layout(std430, buffer_reference) readonly buffer Camera {
|
||||||
|
mat4 proj;
|
||||||
|
mat4 view;
|
||||||
|
};
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
#include "camera.h"
|
||||||
|
#include "ui.h"
|
||||||
|
|
||||||
|
#include <cglm/cam.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
static vec3 up = {0, 1, 0};
|
||||||
|
|
||||||
|
VkResult create_camera(RenderContext* gpu, Camera* camera) {
|
||||||
|
VkResult result;
|
||||||
|
for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
|
||||||
|
VK_RESULT(create_storage_buffer(
|
||||||
|
gpu->allocator,
|
||||||
|
0,
|
||||||
|
sizeof(GPUCamera),
|
||||||
|
&camera->gpu_buffer[i],
|
||||||
|
&camera->gpu_buffer_memory[i]));
|
||||||
|
camera->gpu_address[i] = buffer_address(gpu->device, camera->gpu_buffer[i]);
|
||||||
|
}
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void camera_update_view(Camera* camera) {
|
||||||
|
vec3 eye = {};
|
||||||
|
eye[0] = camera->position[0] + camera->distance*cos(camera->rotation[1])*cos(camera->rotation[0]);
|
||||||
|
eye[1] = camera->position[1] + camera->distance*sin(camera->rotation[1]);
|
||||||
|
eye[2] = camera->position[2] + camera->distance*cos(camera->rotation[1])*sin(camera->rotation[0]);
|
||||||
|
|
||||||
|
glm_lookat(eye, camera->position, up, camera->view);
|
||||||
|
}
|
||||||
|
|
||||||
|
void camera_update_proj(Camera* camera, float aspect) {
|
||||||
|
glm_perspective(
|
||||||
|
PERSPECTIVE_FOVY,
|
||||||
|
aspect,
|
||||||
|
PERSPECTIVE_NEARZ,
|
||||||
|
PERSPECTIVE_FARZ,
|
||||||
|
camera->proj);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult camera_sync_gpu(Camera* camera, RenderContext* gpu) {
|
||||||
|
GPUCamera data;
|
||||||
|
glm_mat4_copy(camera->proj, data.proj);
|
||||||
|
glm_mat4_copy(camera->view, data.view);
|
||||||
|
return add_transfers(&data, camera->gpu_buffer, 0, sizeof(GPUCamera), gpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult camera_init_texture_target(
|
||||||
|
RenderContext* gpu,
|
||||||
|
UIContext* ui,
|
||||||
|
Camera* camera,
|
||||||
|
uint32_t width,
|
||||||
|
uint32_t height) {
|
||||||
|
VkResult result;
|
||||||
|
|
||||||
|
camera->texture.width = width;
|
||||||
|
camera->texture.height = height;
|
||||||
|
|
||||||
|
VkExtent2D extent = {width, height};
|
||||||
|
for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
|
||||||
|
VK_RESULT(create_render_target_texture(gpu, ui, width, height, &camera->texture.texture_slot[i]));
|
||||||
|
VK_RESULT(create_depth_image(
|
||||||
|
gpu->device,
|
||||||
|
gpu->depth_format,
|
||||||
|
extent,
|
||||||
|
gpu->allocator,
|
||||||
|
gpu->extra_graphics_pool,
|
||||||
|
gpu->graphics_queue,
|
||||||
|
&camera->texture.depth_image[i],
|
||||||
|
&camera->texture.depth_image_memory[i],
|
||||||
|
&camera->texture.depth_image_view[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
camera->has_texture_target = true;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void camera_destroy_texture_target(RenderContext* gpu, UIContext* ui, Camera* camera) {
|
||||||
|
(void)ui;
|
||||||
|
for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
|
||||||
|
vkDestroyImageView(gpu->device, camera->texture.depth_image_view[i], NULL);
|
||||||
|
vmaDestroyImage(gpu->allocator, camera->texture.depth_image[i], camera->texture.depth_image_memory[i]);
|
||||||
|
}
|
||||||
|
camera->has_texture_target = false;
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
#include "editor_lua.h"
|
||||||
|
|
||||||
|
#define EDITOR_LUA_CONTEXT "editor_context"
|
||||||
|
|
||||||
|
// ClientContext lives once, heap-allocated for the process, never moves —
|
||||||
|
// unlike ui_lua.c's per-dispatch container/gpu pointers, one registry slot
|
||||||
|
// is enough here.
|
||||||
|
static ClientContext* current_context(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT);
|
||||||
|
ClientContext* context = lua_touserdata(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recomputes the camera's view and pushes it to the currently-rendered hex
|
||||||
|
// scene. Not gated behind any per-frame check - a script-driven camera move
|
||||||
|
// with no held input must be visible immediately.
|
||||||
|
static void refresh_camera(ClientContext* context) {
|
||||||
|
camera_update_view(&context->camera);
|
||||||
|
camera_sync_gpu(&context->camera, &context->render);
|
||||||
|
update_hex_picking_inverse(&context->camera, &context->hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.set_position(x, y, z)
|
||||||
|
static int lua_camera_set_position(lua_State* L) {
|
||||||
|
ClientContext* context = current_context(L);
|
||||||
|
context->camera.position[0] = luaL_checknumber(L, 1);
|
||||||
|
context->camera.position[1] = luaL_checknumber(L, 2);
|
||||||
|
context->camera.position[2] = luaL_checknumber(L, 3);
|
||||||
|
refresh_camera(context);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.get_position() -> x, y, z
|
||||||
|
static int lua_camera_get_position(lua_State* L) {
|
||||||
|
Camera* camera = ¤t_context(L)->camera;
|
||||||
|
lua_pushnumber(L, camera->position[0]);
|
||||||
|
lua_pushnumber(L, camera->position[1]);
|
||||||
|
lua_pushnumber(L, camera->position[2]);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.set_rotation(yaw, pitch)
|
||||||
|
static int lua_camera_set_rotation(lua_State* L) {
|
||||||
|
ClientContext* context = current_context(L);
|
||||||
|
context->camera.rotation[0] = luaL_checknumber(L, 1);
|
||||||
|
context->camera.rotation[1] = luaL_checknumber(L, 2);
|
||||||
|
refresh_camera(context);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.get_rotation() -> yaw, pitch
|
||||||
|
static int lua_camera_get_rotation(lua_State* L) {
|
||||||
|
Camera* camera = ¤t_context(L)->camera;
|
||||||
|
lua_pushnumber(L, camera->rotation[0]);
|
||||||
|
lua_pushnumber(L, camera->rotation[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.set_distance(d)
|
||||||
|
static int lua_camera_set_distance(lua_State* L) {
|
||||||
|
ClientContext* context = current_context(L);
|
||||||
|
context->camera.distance = luaL_checknumber(L, 1);
|
||||||
|
refresh_camera(context);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera.get_distance() -> d
|
||||||
|
static int lua_camera_get_distance(lua_State* L) {
|
||||||
|
lua_pushnumber(L, current_context(L)->camera.distance);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void editor_lua_register(lua_State* L, ClientContext* context) {
|
||||||
|
lua_pushlightuserdata(L, context);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT);
|
||||||
|
|
||||||
|
static const luaL_Reg camera_funcs[] = {
|
||||||
|
{"set_position", lua_camera_set_position},
|
||||||
|
{"get_position", lua_camera_get_position},
|
||||||
|
{"set_rotation", lua_camera_set_rotation},
|
||||||
|
{"get_rotation", lua_camera_get_rotation},
|
||||||
|
{"set_distance", lua_camera_set_distance},
|
||||||
|
{"get_distance", lua_camera_get_distance},
|
||||||
|
{NULL, NULL},
|
||||||
|
};
|
||||||
|
luaL_newlib(L, camera_funcs);
|
||||||
|
lua_setglobal(L, "camera");
|
||||||
|
}
|
||||||
@ -0,0 +1,424 @@
|
|||||||
|
#include "events.h"
|
||||||
|
#include "ui_lua.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define EVENT_LUA_BUS "event_bus"
|
||||||
|
|
||||||
|
static EventBus* lua_bus(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, EVENT_LUA_BUS);
|
||||||
|
EventBus* bus = lua_touserdata(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VkResult grow(void** array, uint32_t count, uint32_t* cap, size_t stride) {
|
||||||
|
if(count < *cap) return VK_SUCCESS;
|
||||||
|
uint32_t new_cap = (*cap == 0) ? 8 : (*cap)*2;
|
||||||
|
void* grown = realloc(*array, new_cap*stride);
|
||||||
|
if(grown == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||||
|
*array = grown;
|
||||||
|
*cap = new_cap;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- queue ----------
|
||||||
|
|
||||||
|
VkResult event_emit(EventBus* bus, const char* name, uint32_t source, int nargs) {
|
||||||
|
lua_State* L = bus->L;
|
||||||
|
|
||||||
|
VkResult result = grow((void**)&bus->queue, bus->queue_count, &bus->queue_cap, sizeof(QueuedEvent));
|
||||||
|
if(result != VK_SUCCESS) {
|
||||||
|
lua_pop(L, nargs);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack the nargs stack values into a table.pack-style {1..n, n=n} table
|
||||||
|
lua_createtable(L, nargs, 1);
|
||||||
|
lua_insert(L, -(nargs + 1));
|
||||||
|
for(int i = nargs; i >= 1; i--) {
|
||||||
|
lua_rawseti(L, -(i + 1), i);
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, nargs);
|
||||||
|
lua_setfield(L, -2, "n");
|
||||||
|
|
||||||
|
QueuedEvent* ev = &bus->queue[bus->queue_count];
|
||||||
|
ev->args_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
ev->source = source;
|
||||||
|
ev->name = strdup(name);
|
||||||
|
if(ev->name == NULL) {
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, ev->args_ref);
|
||||||
|
return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||||
|
}
|
||||||
|
bus->queue_count += 1;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- subscriptions ----------
|
||||||
|
|
||||||
|
static VkResult add_subscription(
|
||||||
|
EventBus* bus,
|
||||||
|
const char* name,
|
||||||
|
int script_env,
|
||||||
|
int lua_ref,
|
||||||
|
EventHandler handler,
|
||||||
|
void* userdata) {
|
||||||
|
VkResult result = grow((void**)&bus->subs, bus->sub_count, &bus->sub_cap, sizeof(EventSubscription));
|
||||||
|
if(result != VK_SUCCESS) return result;
|
||||||
|
|
||||||
|
EventSubscription* sub = &bus->subs[bus->sub_count];
|
||||||
|
sub->event = strdup(name);
|
||||||
|
if(sub->event == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||||
|
sub->script_env = script_env;
|
||||||
|
sub->lua_ref = lua_ref;
|
||||||
|
sub->handler = handler;
|
||||||
|
sub->userdata = userdata;
|
||||||
|
sub->dead = 0;
|
||||||
|
bus->sub_count += 1;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_subscribe(EventBus* bus, const char* name, EventHandler handler, void* userdata) {
|
||||||
|
return add_subscription(bus, name, LUA_NOREF, LUA_NOREF, handler, userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void compact_subscriptions(EventBus* bus) {
|
||||||
|
uint32_t out = 0;
|
||||||
|
for(uint32_t i = 0; i < bus->sub_count; i++) {
|
||||||
|
if(bus->subs[i].dead) continue;
|
||||||
|
bus->subs[out] = bus->subs[i];
|
||||||
|
out += 1;
|
||||||
|
}
|
||||||
|
bus->sub_count = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- drain ----------
|
||||||
|
|
||||||
|
static void dispatch_to_lua(
|
||||||
|
EventBus* bus,
|
||||||
|
QueuedEvent* ev,
|
||||||
|
EventSubscription* sub,
|
||||||
|
UIContext* ui,
|
||||||
|
RenderContext* gpu) {
|
||||||
|
lua_State* L = bus->L;
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ev->args_ref);
|
||||||
|
int args_index = lua_gettop(L);
|
||||||
|
lua_getfield(L, args_index, "n");
|
||||||
|
int n = (int)lua_tointeger(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
if(!lua_checkstack(L, n + 4)) {
|
||||||
|
fprintf(stderr, "event %s: args overflow lua stack\n", ev->name);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No specific overlay is "current" for a bus-fired handler; ui.rect/text
|
||||||
|
// require an explicit ui.create_overlay call first if one runs from here
|
||||||
|
ui_lua_set_current_script(L, sub->script_env);
|
||||||
|
ui_lua_set_current(L, NULL, ui, gpu);
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, sub->lua_ref);
|
||||||
|
lua_pushinteger(L, ev->source);
|
||||||
|
for(int i = 1; i <= n; i++) {
|
||||||
|
lua_rawgeti(L, args_index, i);
|
||||||
|
}
|
||||||
|
lua_remove(L, args_index);
|
||||||
|
|
||||||
|
if(lua_pcall(L, 1 + n, 0, 0) != LUA_OK) {
|
||||||
|
fprintf(stderr, "event %s: %s\n", ev->name, lua_tostring(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sub_count is re-read every iteration: handlers may subscribe during the
|
||||||
|
// dispatch, and late subscribers still receive the in-flight event
|
||||||
|
static void dispatch_event(EventBus* bus, QueuedEvent* ev, UIContext* ui, RenderContext* gpu) {
|
||||||
|
lua_State* L = bus->L;
|
||||||
|
for(uint32_t i = 0; i < bus->sub_count; i++) {
|
||||||
|
if(bus->subs[i].dead || strcmp(bus->subs[i].event, ev->name) != 0) continue;
|
||||||
|
|
||||||
|
if(bus->subs[i].handler != NULL) {
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ev->args_ref);
|
||||||
|
bus->subs[i].handler(bus->subs[i].userdata, ev->name, ev->source, L, lua_gettop(L));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
} else {
|
||||||
|
dispatch_to_lua(bus, ev, &bus->subs[i], ui, gpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void event_bus_drain(EventBus* bus, UIContext* ui, RenderContext* gpu, double delta_time) {
|
||||||
|
lua_State* L = bus->L;
|
||||||
|
bus->draining = true;
|
||||||
|
|
||||||
|
uint32_t head = 0;
|
||||||
|
uint32_t processed = 0;
|
||||||
|
while(head < bus->queue_count && processed < EVENT_DRAIN_MAX) {
|
||||||
|
// Copy: handlers can emit, which may realloc the queue
|
||||||
|
QueuedEvent ev = bus->queue[head];
|
||||||
|
head += 1;
|
||||||
|
processed += 1;
|
||||||
|
|
||||||
|
dispatch_event(bus, &ev, ui, gpu);
|
||||||
|
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, ev.args_ref);
|
||||||
|
free(ev.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(head < bus->queue_count) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"event_bus_drain: cap of %d hit, dropping %d events (handler cycle?)\n",
|
||||||
|
EVENT_DRAIN_MAX,
|
||||||
|
bus->queue_count - head);
|
||||||
|
for(uint32_t i = head; i < bus->queue_count; i++) {
|
||||||
|
fprintf(stderr, " dropped: %s (source %u)\n", bus->queue[i].name, bus->queue[i].source);
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, bus->queue[i].args_ref);
|
||||||
|
free(bus->queue[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bus->queue_count = 0;
|
||||||
|
|
||||||
|
// engine.frame runs after the queue is empty so its handlers see the frame
|
||||||
|
// fully settled; anything they emit lands in next frame's drain (the queue
|
||||||
|
// was just reset). Skipped entirely when nothing subscribes, to avoid
|
||||||
|
// per-frame table garbage.
|
||||||
|
bool frame_wanted = false;
|
||||||
|
for(uint32_t i = 0; i < bus->sub_count; i++) {
|
||||||
|
if(!bus->subs[i].dead && strcmp(bus->subs[i].event, EVENT_FRAME) == 0) {
|
||||||
|
frame_wanted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(frame_wanted) {
|
||||||
|
lua_createtable(L, 1, 1);
|
||||||
|
lua_pushnumber(L, delta_time);
|
||||||
|
lua_rawseti(L, -2, 1);
|
||||||
|
lua_pushinteger(L, 1);
|
||||||
|
lua_setfield(L, -2, "n");
|
||||||
|
QueuedEvent frame_ev = {
|
||||||
|
.name = (char*)EVENT_FRAME,
|
||||||
|
.source = EVENT_SOURCE_ENGINE,
|
||||||
|
.args_ref = luaL_ref(L, LUA_REGISTRYINDEX),
|
||||||
|
};
|
||||||
|
dispatch_event(bus, &frame_ev, ui, gpu);
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, frame_ev.args_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
bus->draining = false;
|
||||||
|
compact_subscriptions(bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- property registry ----------
|
||||||
|
|
||||||
|
Property* event_property(EventBus* bus, const char* name) {
|
||||||
|
for(uint32_t i = 0; i < bus->property_count; i++) {
|
||||||
|
if(strcmp(bus->properties[i].name, name) == 0) return &bus->properties[i];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_property_register(EventBus* bus, const char* name, PropertyType type) {
|
||||||
|
if(event_property(bus, name) != NULL) return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||||
|
|
||||||
|
VkResult result = grow((void**)&bus->properties, bus->property_count, &bus->property_cap, sizeof(Property));
|
||||||
|
if(result != VK_SUCCESS) return result;
|
||||||
|
|
||||||
|
Property* p = &bus->properties[bus->property_count];
|
||||||
|
memset(p, 0, sizeof(Property));
|
||||||
|
p->name = strdup(name);
|
||||||
|
if(p->name == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||||
|
p->type = type;
|
||||||
|
bus->property_count += 1;
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The changed value is already on top of the Lua stack
|
||||||
|
static VkResult emit_changed(EventBus* bus, Property* p, uint32_t source) {
|
||||||
|
char name[256];
|
||||||
|
int written = snprintf(name, sizeof(name), EVENT_CHANGED_PREFIX "%s", p->name);
|
||||||
|
if(written < 0 || (size_t)written >= sizeof(name)) {
|
||||||
|
lua_pop(bus->L, 1);
|
||||||
|
return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||||
|
}
|
||||||
|
return event_emit(bus, name, source, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_property_set_number(EventBus* bus, const char* name, double value, uint32_t source) {
|
||||||
|
Property* p = event_property(bus, name);
|
||||||
|
if(p == NULL || p->type != PROPERTY_NUMBER) return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||||
|
if(p->value.number == value) return VK_SUCCESS;
|
||||||
|
|
||||||
|
p->value.number = value;
|
||||||
|
lua_pushnumber(bus->L, value);
|
||||||
|
return emit_changed(bus, p, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_property_set_string(EventBus* bus, const char* name, const char* value, uint32_t source) {
|
||||||
|
Property* p = event_property(bus, name);
|
||||||
|
if(p == NULL || p->type != PROPERTY_STRING || value == NULL) return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||||
|
if(p->value.string != NULL && strcmp(p->value.string, value) == 0) return VK_SUCCESS;
|
||||||
|
|
||||||
|
char* copy = strdup(value);
|
||||||
|
if(copy == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||||
|
free(p->value.string);
|
||||||
|
p->value.string = copy;
|
||||||
|
lua_pushstring(bus->L, value);
|
||||||
|
return emit_changed(bus, p, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_property_set_bool(EventBus* bus, const char* name, bool value, uint32_t source) {
|
||||||
|
Property* p = event_property(bus, name);
|
||||||
|
if(p == NULL || p->type != PROPERTY_BOOL) return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||||
|
if(p->value.boolean == value) return VK_SUCCESS;
|
||||||
|
|
||||||
|
p->value.boolean = value;
|
||||||
|
lua_pushboolean(bus->L, value);
|
||||||
|
return emit_changed(bus, p, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- C handler arg accessors ----------
|
||||||
|
|
||||||
|
int event_arg_count(lua_State* L, int args_index) {
|
||||||
|
lua_getfield(L, args_index, "n");
|
||||||
|
int n = (int)lua_tointeger(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
double event_arg_number(lua_State* L, int args_index, int i) {
|
||||||
|
lua_rawgeti(L, args_index, i);
|
||||||
|
double v = lua_tonumber(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool event_arg_bool(lua_State* L, int args_index, int i) {
|
||||||
|
lua_rawgeti(L, args_index, i);
|
||||||
|
bool v = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* event_arg_string(lua_State* L, int args_index, int i) {
|
||||||
|
lua_rawgeti(L, args_index, i);
|
||||||
|
const char* v = lua_tostring(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- lua bindings ----------
|
||||||
|
|
||||||
|
// app.emit(name, ...) — queues a custom event from the calling container
|
||||||
|
static int lua_app_emit(lua_State* L) {
|
||||||
|
EventBus* bus = lua_bus(L);
|
||||||
|
const char* name = luaL_checkstring(L, 1);
|
||||||
|
if(strncmp(name, EVENT_ENGINE_PREFIX, strlen(EVENT_ENGINE_PREFIX)) == 0) {
|
||||||
|
return luaL_error(L, "emit: the %s* namespace is reserved for the engine", EVENT_ENGINE_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
Container* c = ui_lua_current_container(L);
|
||||||
|
uint32_t source = (c != NULL) ? c->id : EVENT_SOURCE_ENGINE;
|
||||||
|
|
||||||
|
// Everything above the name is payload; emit consumes it off the stack
|
||||||
|
int nargs = lua_gettop(L) - 1;
|
||||||
|
if(event_emit(bus, name, source, nargs) != VK_SUCCESS) {
|
||||||
|
return luaL_error(L, "emit: failed to queue %s", name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.subscribe(name, fn) — fn is called as fn(source, ...) during drains
|
||||||
|
static int lua_app_subscribe(lua_State* L) {
|
||||||
|
EventBus* bus = lua_bus(L);
|
||||||
|
const char* name = luaL_checkstring(L, 1);
|
||||||
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
||||||
|
|
||||||
|
int script_env = ui_lua_current_script(L);
|
||||||
|
if(script_env == LUA_NOREF) {
|
||||||
|
return luaL_error(L, "subscribe: no script context");
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pushvalue(L, 2);
|
||||||
|
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
if(add_subscription(bus, name, script_env, ref, NULL, NULL) != VK_SUCCESS) {
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, ref);
|
||||||
|
return luaL_error(L, "subscribe: failed to subscribe to %s", name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.get(name) -> value or nil if the property is unregistered/unset
|
||||||
|
static int lua_app_get(lua_State* L) {
|
||||||
|
EventBus* bus = lua_bus(L);
|
||||||
|
Property* p = event_property(bus, luaL_checkstring(L, 1));
|
||||||
|
if(p == NULL) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(p->type) {
|
||||||
|
case PROPERTY_NUMBER: lua_pushnumber(L, p->value.number); break;
|
||||||
|
case PROPERTY_BOOL: lua_pushboolean(L, p->value.boolean); break;
|
||||||
|
case PROPERTY_STRING:
|
||||||
|
if(p->value.string == NULL) lua_pushnil(L);
|
||||||
|
else lua_pushstring(L, p->value.string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.set(name, value) — writes a registered property; a real change emits
|
||||||
|
// engine.changed.<name> with the calling container as the source
|
||||||
|
static int lua_app_set(lua_State* L) {
|
||||||
|
EventBus* bus = lua_bus(L);
|
||||||
|
const char* name = luaL_checkstring(L, 1);
|
||||||
|
Property* p = event_property(bus, name);
|
||||||
|
if(p == NULL) {
|
||||||
|
return luaL_error(L, "set: unknown property %s", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Container* c = ui_lua_current_container(L);
|
||||||
|
uint32_t source = (c != NULL) ? c->id : EVENT_SOURCE_ENGINE;
|
||||||
|
|
||||||
|
VkResult result = VK_SUCCESS;
|
||||||
|
switch(p->type) {
|
||||||
|
case PROPERTY_NUMBER:
|
||||||
|
result = event_property_set_number(bus, name, luaL_checknumber(L, 2), source);
|
||||||
|
break;
|
||||||
|
case PROPERTY_STRING:
|
||||||
|
result = event_property_set_string(bus, name, luaL_checkstring(L, 2), source);
|
||||||
|
break;
|
||||||
|
case PROPERTY_BOOL:
|
||||||
|
luaL_checktype(L, 2, LUA_TBOOLEAN);
|
||||||
|
result = event_property_set_bool(bus, name, lua_toboolean(L, 2), source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(result != VK_SUCCESS) {
|
||||||
|
return luaL_error(L, "set: failed to set %s", name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkResult event_bus_init(EventBus* bus, lua_State* L) {
|
||||||
|
memset(bus, 0, sizeof(EventBus));
|
||||||
|
bus->L = L;
|
||||||
|
|
||||||
|
lua_pushlightuserdata(L, bus);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, EVENT_LUA_BUS);
|
||||||
|
|
||||||
|
static const luaL_Reg app_funcs[] = {
|
||||||
|
{"emit", lua_app_emit},
|
||||||
|
{"subscribe", lua_app_subscribe},
|
||||||
|
{"get", lua_app_get},
|
||||||
|
{"set", lua_app_set},
|
||||||
|
{NULL, NULL},
|
||||||
|
};
|
||||||
|
luaL_newlib(L, app_funcs);
|
||||||
|
lua_setglobal(L, "app");
|
||||||
|
|
||||||
|
return VK_SUCCESS;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue