751 lines
28 KiB
C
751 lines
28 KiB
C
// ## Editor long-term plan
|
|
// - editor = tree of nodes. levels and objects are nodes, and their widgets are also nodes
|
|
// - you can perform actions on nodes, with or without descendants, top-bottom or bottom-top
|
|
// - these operations include load/save, undo/redo, reset, play/render, ddraw, etc
|
|
// - nodes are saved to disk as a filesystem layout: parents are folders, and leafs are files
|
|
// - network replication can be done by external tools by comparing the filesystems and by sending the resulting diff zipped
|
|
//
|
|
// ## Editor roadmap
|
|
// - Gizmos✱, scene tree, property editor✱, load/save✱, undo/redo✱, copy/paste, on/off (vis,tick,ddraw,log), vcs.
|
|
// - Scenenode pass: node singleton display, node console, node labels, node outlines✱.<!-- node == gameobj ? -->
|
|
// - Render pass: billboards✱, materials, un/lit, cast shadows, wireframe, skybox✱/mie✱, fog/atmosphere
|
|
// - Level pass: volumes, triggers, platforms, level streaming, collide✱, physics
|
|
// - Edit pass: Procedural content, brushes, noise and CSG.
|
|
// - GUI pass: timeline and data tracks, node graphs. <!-- worthy: will be reused into materials, animgraphs and blueprints -->
|
|
|
|
// ## Alt plan
|
|
// editor is a database + window/tile manager + ui toolkit; all network driven.
|
|
// to be precise, editor is a dumb app and ...
|
|
// - does not know a thing about what it stores.
|
|
// - does not know how to render the game graphics.
|
|
// - does not know how to run the game logic.
|
|
//
|
|
// the editor will create a canvas for your game to render.
|
|
// your game will be responsible to tick the logic and render the window inside the editor.
|
|
//
|
|
// that being said, editor...
|
|
// - can store datas hierarchically.
|
|
// - can perform diffs and merges, and version the datas into repositories.
|
|
// - can be instructed to render UI on top of game and window views.
|
|
// - can download new .natvis and plugins quickly.
|
|
// - can dump whole project in a filesystem form (zip).
|
|
|
|
// - editor reflects database contents up-to-date.
|
|
// - database can be queried and modified via OSC(UDP) commands.
|
|
|
|
// editor database uses one table, and stores two kind of payload types:
|
|
// - classes: defines typename and dna. class names are prefixed by '@'
|
|
// - instances: defines typename and datas. instance names are as-is, not prefixed.
|
|
//
|
|
// every save contains 5Ws: what, who, when, where, how,
|
|
// every save can be diffed/merged.
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#define EDITOR_VERSION "2023.10"
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
typedef struct editor_bind_t {
|
|
const char *command;
|
|
const char *bindings;
|
|
void (*fn)();
|
|
} editor_bind_t;
|
|
|
|
array(editor_bind_t) editor_binds;
|
|
|
|
#define EDITOR_BIND(CMD,KEYS,...) void macro(editor_bind_##CMD##_fn_)() { __VA_ARGS__ }; AUTORUN { array_push(editor_binds, ((editor_bind_t){#CMD,KEYS,macro(editor_bind_##CMD##_fn_)}) ); }
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
typedef void (*editor_no_property)(void *);
|
|
array(void*) editor_persist_kv;
|
|
array(editor_no_property) editor_no_properties;
|
|
|
|
#define EDITOR_PROPERTY(property_name,T,defaults) \
|
|
typedef map(void*,T) editor_##property_name##_map_t; \
|
|
editor_##property_name##_map_t *editor_##property_name##_map() { \
|
|
static editor_##property_name##_map_t map = 0; do_once map_init_ptr(map); \
|
|
return ↦ \
|
|
} \
|
|
T editor_##property_name(const void *obj) { \
|
|
return *map_find_or_add(*editor_##property_name##_map(), (void*)obj, ((T) defaults)); \
|
|
} \
|
|
void editor_set##property_name(const void *obj, T value) { \
|
|
*map_find_or_add(*editor_##property_name##_map(), (void*)obj, ((T) value)) = ((T) value); \
|
|
} \
|
|
void editor_alt##property_name(const void *obj) { \
|
|
T* found = map_find_or_add(*editor_##property_name##_map(), (void*)obj, ((T) defaults)); \
|
|
*found = (T)(uintptr_t)!(*found); \
|
|
} \
|
|
void editor_no##property_name(void *obj) { \
|
|
T* found = map_find_or_add(*editor_##property_name##_map(), (void*)obj, ((T) defaults)); \
|
|
map_erase(*editor_##property_name##_map(), (void*)obj); \
|
|
} \
|
|
AUTORUN { array_push(editor_persist_kv, #T); array_push(editor_persist_kv, editor_##property_name##_map()); array_push(editor_no_properties, editor_no##property_name); }
|
|
|
|
EDITOR_PROPERTY(open, int, 0); // whether object is tree opened in tree editor
|
|
EDITOR_PROPERTY(selected, int, 0); // whether object is displaying a contextual popup or not
|
|
EDITOR_PROPERTY(changed, int, 0); // whether object is displaying a contextual popup or not
|
|
EDITOR_PROPERTY(popup, int, 0); // whether object is displaying a contextual popup or not
|
|
EDITOR_PROPERTY(visible, int, 0);
|
|
EDITOR_PROPERTY(script, int, 0);
|
|
EDITOR_PROPERTY(event, int, 0);
|
|
EDITOR_PROPERTY(iconinstance, char*, 0);
|
|
EDITOR_PROPERTY(iconclass, char*, 0);
|
|
EDITOR_PROPERTY(treeoffsety, int, 0);
|
|
// new prop: breakpoint: request to break on any given node
|
|
// new prop: persist: objects with this property will be saved on disk
|
|
|
|
void editor_destroy_properties(void *o) {
|
|
for each_array(editor_no_properties,editor_no_property,fn) {
|
|
fn(o);
|
|
}
|
|
}
|
|
|
|
void editor_load_on_boot(void) {
|
|
puts("@todo: load editor");
|
|
}
|
|
void editor_save_on_quit(void) {
|
|
puts("@todo: save editor");
|
|
}
|
|
AUTORUN {
|
|
editor_load_on_boot();
|
|
(atexit)(editor_save_on_quit);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
typedef int(*subeditor)(int mode);
|
|
|
|
struct editor_t {
|
|
// time
|
|
unsigned frame;
|
|
double t, dt, slomo;
|
|
// controls
|
|
int transparent;
|
|
int attached;
|
|
int active; // focus? does_grabinput instead?
|
|
int key;
|
|
vec2 mouse; // 2d coord for ray/picking
|
|
bool gamepad; // mask instead? |1|2|4|8
|
|
int hz_high, hz_medium, hz_low;
|
|
int filter;
|
|
bool battery; // battery mode: low fps
|
|
bool unlit;
|
|
bool ddraw;
|
|
// event root nodes
|
|
obj* root;
|
|
obj* on_init;
|
|
obj* on_tick;
|
|
obj* on_draw;
|
|
obj* on_edit;
|
|
obj* on_quit;
|
|
// all of them (hierarchical)
|
|
array(obj*) objs; // @todo:set() world?
|
|
// all of them (flat)
|
|
set(obj*) world;
|
|
//
|
|
array(char*) cmds;
|
|
// subeditors
|
|
array(subeditor) subeditors;
|
|
} editor = {
|
|
.active = 1,
|
|
.gamepad = 1,
|
|
.hz_high = 60, .hz_medium = 18, .hz_low = 5,
|
|
};
|
|
|
|
enum {
|
|
EDITOR_PANEL,
|
|
EDITOR_WINDOW,
|
|
EDITOR_WINDOW_NK,
|
|
EDITOR_WINDOW_NK_SMALL,
|
|
};
|
|
|
|
int editor_begin(const char *title, int mode) {
|
|
if( mode == 0 ) return ui_panel(title, PANEL_OPEN);
|
|
if( mode == 1 ) return ui_window(title, 0);
|
|
|
|
int ww = window_width(), w = ww * 0.66;
|
|
int hh = window_height(), h = hh * 0.66;
|
|
|
|
struct nk_rect position = { (ww-w)/2,(hh-h)/2, w,h };
|
|
nk_flags win_flags = NK_WINDOW_TITLE | NK_WINDOW_BORDER |
|
|
NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
|
|
NK_WINDOW_CLOSABLE | NK_WINDOW_MINIMIZABLE |
|
|
// NK_WINDOW_SCALE_LEFT|NK_WINDOW_SCALE_TOP| //< @fixme: move this logic into nuklear
|
|
// NK_WINDOW_MAXIMIZABLE | NK_WINDOW_PINNABLE |
|
|
0; // NK_WINDOW_SCROLL_AUTO_HIDE;
|
|
|
|
if( mode == 3 ) {
|
|
mode = 2, position.x = input(MOUSE_X), position.w = w/3, win_flags =
|
|
NK_WINDOW_TITLE|NK_WINDOW_CLOSABLE|
|
|
NK_WINDOW_SCALABLE|NK_WINDOW_MOVABLE| //< nuklear requires these two to `remember` popup rects
|
|
0;
|
|
}
|
|
|
|
if( mode == 2 || mode == 3 )
|
|
if (nk_begin(ui_ctx, title, position, win_flags))
|
|
return 1;
|
|
else
|
|
return nk_end(ui_ctx), 0;
|
|
|
|
return 0;
|
|
}
|
|
int editor_end(int mode) {
|
|
if( mode == 0 ) return ui_panel_end();
|
|
if( mode == 1 ) return ui_window_end();
|
|
if( mode == 2 ) nk_end(ui_ctx);
|
|
if( mode == 3 ) nk_end(ui_ctx);
|
|
return 0;
|
|
}
|
|
|
|
#if 0 // deprecate
|
|
bool editor_active() {
|
|
return ui_hover() || ui_active() || gizmo_active() ? editor.active : 0;
|
|
}
|
|
#endif
|
|
|
|
int editor_filter() {
|
|
if( editor.filter ) {
|
|
if (nk_begin(ui_ctx, "Filter", nk_rect(window_width()-window_width()*0.33,32, window_width()*0.33, 40),
|
|
NK_WINDOW_NO_SCROLLBAR)) {
|
|
|
|
char *bak = ui_filter; ui_filter = 0;
|
|
ui_string(ICON_MD_CLOSE " Filter " ICON_MD_SEARCH, &bak);
|
|
ui_filter = bak;
|
|
|
|
if( input(KEY_ESC) || ( ui_label_icon_clicked_L.x > 0 && ui_label_icon_clicked_L.x <= 24 )) {
|
|
if( ui_filter ) ui_filter[0] = '\0';
|
|
editor.filter = 0;
|
|
}
|
|
}
|
|
nk_end(ui_ctx);
|
|
}
|
|
|
|
return editor.filter;
|
|
}
|
|
|
|
static
|
|
int editor_select_(void *o, const char *mask) {
|
|
int matches = 0;
|
|
int off = mask[0] == '!', inv = mask[0] == '~';
|
|
int match = strmatchi(obj_type(o), mask+off+inv) || strmatchi(obj_name(o), mask+off+inv);
|
|
if( match ) {
|
|
editor_setselected(o, inv ? editor_selected(o) ^ 1 : !off);
|
|
++matches;
|
|
}
|
|
for each_objchild(o, obj*, oo) {
|
|
matches += editor_select_(oo, mask);
|
|
}
|
|
return matches;
|
|
}
|
|
void editor_select(const char *mask) {
|
|
for each_array( editor.objs, obj*, o )
|
|
editor_select_(o, mask);
|
|
}
|
|
void editor_unselect() { // same than editor_select("!**");
|
|
for each_map_ptr(*editor_selected_map(), void*,o, int, k) {
|
|
if( *k ) *k = 0;
|
|
}
|
|
}
|
|
|
|
void editor_select_aabb(aabb box) {
|
|
int is_inv = input_held(KEY_CTRL);
|
|
int is_add = input_held(KEY_SHIFT);
|
|
if( !is_inv && !is_add ) editor_unselect();
|
|
|
|
aabb item = {0};
|
|
for each_set_ptr( editor.world, obj*, o ) {
|
|
if( obj_hasmethod(*o,aabb) && obj_aabb(*o, &item) ) {
|
|
if( aabb_test_aabb(item, box) ) {
|
|
if( is_inv )
|
|
editor_altselected(*o);
|
|
else
|
|
editor_setselected(*o, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static obj* active_ = 0;
|
|
static void editor_selectgroup_(obj *o, obj *first, obj *last) {
|
|
// printf("%s (looking for %s in [%s..%s])\n", obj_name(o), active_ ? obj_name(active_) : "", obj_name(first), obj_name(last));
|
|
if( !active_ ) if( o == first || o == last ) active_ = o == first ? last : first;
|
|
if( active_ ) editor_setselected(o, 1);
|
|
if( o == active_ ) active_ = 0;
|
|
for each_objchild(o, obj*, oo) {
|
|
editor_selectgroup_(oo, first, last);
|
|
}
|
|
}
|
|
void editor_selectgroup(obj *first, obj *last) {
|
|
if( last ) {
|
|
if( !first ) first = array_count(editor.objs) ? editor.objs[0] : NULL;
|
|
if( !first ) editor_setselected(last, 1);
|
|
else {
|
|
active_ = 0;
|
|
for each_array(editor.objs,obj*,o) {
|
|
editor_selectgroup_(o, first, last);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static obj *find_any_selected_(obj *o) {
|
|
if( editor_selected(o) ) return o;
|
|
for each_objchild(o,obj*,oo) {
|
|
obj *ooo = find_any_selected_(oo);
|
|
if( ooo )
|
|
return ooo;
|
|
}
|
|
return 0;
|
|
}
|
|
void* editor_first_selected() {
|
|
for each_array(editor.objs,obj*,o) {
|
|
obj *oo = find_any_selected_(o);
|
|
// if( oo ) printf("1st found: %s\n", obj_name(oo));
|
|
if( oo ) return oo;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static obj *find_last_selected_(obj *o) {
|
|
void *last = 0;
|
|
if( editor_selected(o) ) last = o;
|
|
for each_objchild(o,obj*,oo) {
|
|
obj *ooo = find_last_selected_(oo);
|
|
if( ooo )
|
|
last = ooo;
|
|
}
|
|
return last;
|
|
}
|
|
void* editor_last_selected() {
|
|
void *last = 0;
|
|
for each_array(editor.objs,obj*,o) {
|
|
obj *oo = find_last_selected_(o);
|
|
// if( oo ) printf("last found: %s\n", obj_name(oo));
|
|
if( oo ) last = oo;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
|
|
void editor_addtoworld(obj *o) {
|
|
set_find_or_add(editor.world, o);
|
|
for each_objchild(o, obj*, oo) {
|
|
editor_addtoworld(oo);
|
|
}
|
|
}
|
|
|
|
void editor_watch(const void *o) {
|
|
array_push(editor.objs, (obj*)o);
|
|
obj_push(o); // save state
|
|
|
|
editor_addtoworld((obj*)o);
|
|
}
|
|
void* editor_spawn(const char *ini) { // deprecate?
|
|
obj *o = obj_make(ini);
|
|
editor_watch(o);
|
|
return o;
|
|
}
|
|
void editor_spawn1() {
|
|
obj *selected = editor_first_selected();
|
|
obj *o = selected ? obj_make(obj_saveini(selected)) : obj_new(obj);
|
|
if( selected ) obj_attach(selected, o), editor_setopen(selected, 1);
|
|
else
|
|
editor_watch(o);
|
|
|
|
editor_unselect();
|
|
editor_setselected(o, 1);
|
|
}
|
|
|
|
typedef set(obj*) set_objp_t;
|
|
static
|
|
void editor_glob_recurse(set_objp_t*list, obj *o) {
|
|
set_find_or_add(*list, o);
|
|
for each_objchild(o,obj*,oo) {
|
|
editor_glob_recurse(list, oo);
|
|
}
|
|
}
|
|
void editor_destroy_selected() {
|
|
set_objp_t list = 0;
|
|
set_init_ptr(list);
|
|
for each_map_ptr(*editor_selected_map(), obj*,o, int,selected) {
|
|
if( *selected ) { editor_glob_recurse(&list, *o); }
|
|
}
|
|
for each_set(list, obj*, o) {
|
|
obj_detach(o);
|
|
}
|
|
for each_set(list, obj*, o) {
|
|
// printf("deleting %p %s\n", o, obj_name(o));
|
|
// remove from watched items
|
|
for (int i = 0, end = array_count(editor.objs); i < end; ++i) {
|
|
if (editor.objs[i] == o) {
|
|
editor.objs[i] = 0;
|
|
array_erase_slow(editor.objs, i);
|
|
--end;
|
|
--i;
|
|
}
|
|
}
|
|
// delete from world
|
|
set_erase(editor.world, o);
|
|
// delete properties + obj
|
|
editor_destroy_properties(o);
|
|
obj_free(o);
|
|
}
|
|
set_free(list);
|
|
}
|
|
void editor_inspect(obj *o) {
|
|
ui_section(va("%s (%s)", obj_type(o), obj_name(o)));
|
|
|
|
if( obj_hasmethod(o, menu) ) {
|
|
obj_menu(o);
|
|
}
|
|
|
|
for each_objmember(o,TYPE,NAME,PTR) {
|
|
if( !editor_changed(PTR) ) {
|
|
obj_push(o);
|
|
}
|
|
ui_label_icon_highlight = editor_changed(PTR); // @hack: remove ui_label_icon_highlight hack
|
|
char *label = va(ICON_MD_UNDO "%s", NAME);
|
|
int changed = 0;
|
|
/**/ if( !strcmp(TYPE,"float") ) changed = ui_float(label, PTR);
|
|
else if( !strcmp(TYPE,"int") ) changed = ui_int(label, PTR);
|
|
else if( !strcmp(TYPE,"vec2") ) changed = ui_float2(label, PTR);
|
|
else if( !strcmp(TYPE,"vec3") ) changed = ui_float3(label, PTR);
|
|
else if( !strcmp(TYPE,"vec4") ) changed = ui_float4(label, PTR);
|
|
else if( !strcmp(TYPE,"rgb") ) changed = ui_color3(label, PTR);
|
|
else if( !strcmp(TYPE,"rgba") ) changed = ui_color4(label, PTR);
|
|
else if( !strcmp(TYPE,"color") ) changed = ui_color4f(label, PTR);
|
|
else if( !strcmp(TYPE,"color3f") ) changed = ui_color3f(label, PTR);
|
|
else if( !strcmp(TYPE,"color4f") ) changed = ui_color4f(label, PTR);
|
|
else if( !strcmp(TYPE,"char*") ) changed = ui_string(label, PTR);
|
|
else ui_label2(label, va("(%s)", TYPE)); // INFO instead of (TYPE)?
|
|
if( changed ) {
|
|
editor_setchanged(PTR, 1);
|
|
}
|
|
if( ui_label_icon_highlight )
|
|
if( ui_label_icon_clicked_L.x >= 6 && ui_label_icon_clicked_L.x <= 26 ) { // @hack: if clicked on UNDO icon (1st icon)
|
|
editor_setchanged(PTR, 0);
|
|
}
|
|
if( !editor_changed(PTR) ) {
|
|
obj_pop(o);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
// tty
|
|
|
|
static thread_mutex_t *console_lock;
|
|
static array(char*) editor_jobs;
|
|
int editor_send(const char *cmd) { // return job-id
|
|
int skip = strspn(cmd, " \t\r\n");
|
|
char *buf = STRDUP(cmd + skip);
|
|
strswap(buf, "\r\n", "");
|
|
int jobid;
|
|
do_threadlock(console_lock) {
|
|
array_push(editor_jobs, buf);
|
|
jobid = array_count(editor_jobs) - 1;
|
|
}
|
|
return jobid;
|
|
}
|
|
const char* editor_recv(int jobid, double timeout_ss) {
|
|
char *answer = 0;
|
|
|
|
while(!answer && timeout_ss >= 0 ) {
|
|
do_threadlock(console_lock) {
|
|
if( editor_jobs[jobid][0] == '\0' )
|
|
answer = editor_jobs[jobid];
|
|
}
|
|
timeout_ss -= 0.1;
|
|
if( timeout_ss > 0 ) sleep_ms(100); // thread_yield()
|
|
}
|
|
|
|
return answer + 1;
|
|
}
|
|
|
|
// plain and ctrl keys
|
|
EDITOR_BIND(play, "down(F5)", { window_pause(0); /* if(!editor.slomo) editor.active = 0; */ editor.slomo = 1; } );
|
|
EDITOR_BIND(stop, "down(ESC)", { if(editor.t > 0) { window_pause(1), editor.frame = 0, editor.t = 0, editor.dt = 0, editor.slomo = 0, editor.active = 1; editor_select("**"); editor_destroy_selected(); }} );
|
|
EDITOR_BIND(eject, "down(F1)", { /*window_pause(!editor.active); editor.slomo = !!editor.active;*/ editor.active ^= 1; } );
|
|
EDITOR_BIND(pause, "(held(CTRL) & down(P)) | down(PAUSE)", { window_pause( window_has_pause() ^ 1 ); } );
|
|
EDITOR_BIND(frame, "held(CTRL) & down(LEFT)", { window_pause(1); editor.frame++, editor.t += (editor.dt = 1/60.f); } );
|
|
EDITOR_BIND(slomo, "held(CTRL) & down(RIGHT)", { window_pause(0); editor.slomo = maxf(fmod(editor.slomo * 2, 16), 0.125); } );
|
|
EDITOR_BIND(reload, "held(CTRL) & down(F5)", { window_reload(); } );
|
|
EDITOR_BIND(filter, "held(CTRL) & down(F)", { editor.filter ^= 1; } );
|
|
|
|
// alt keys
|
|
EDITOR_BIND(quit, "held(ALT) & down(F4)", { record_stop(), exit(0); } );
|
|
EDITOR_BIND(mute, "held(ALT) & down(M)", { audio_volume_master( 1 ^ !!audio_volume_master(-1) ); } );
|
|
EDITOR_BIND(gamepad, "held(ALT) & down(G)", { editor.gamepad ^= 1; } );
|
|
EDITOR_BIND(transparent, "held(ALT) & down(T)", { editor.transparent ^= 1; } );
|
|
EDITOR_BIND(record, "held(ALT) & down(Z)", { if(record_active()) record_stop(), ui_notify(va("Video recorded"), date_string()); else { char *name = file_counter(va("%s.mp4",app_name())); app_beep(), window_record(name); } } );
|
|
EDITOR_BIND(screenshot, "held(ALT) & down(S)", { char *name = file_counter(va("%s.png",app_name())); window_screenshot(name), ui_notify(va("Screenshot: %s", name), date_string()); } );
|
|
EDITOR_BIND(battery, "held(ALT) & down(B)", { editor.battery ^= 1; } );
|
|
EDITOR_BIND(outliner, "held(ALT) & down(O)", { ui_show("Outliner", ui_visible("Outliner") ^ true); } );
|
|
EDITOR_BIND(profiler, "held(ALT) & down(P)", { ui_show("Profiler", profiler_enable(ui_visible("Profiler") ^ true)); } );
|
|
EDITOR_BIND(fullscreen, "(held(ALT)&down(ENTER))|down(F11)",{ record_stop(), window_fullscreen( window_has_fullscreen() ^ 1 ); } ); // close any recording before framebuffer resizing, which would corrupt video stream
|
|
EDITOR_BIND(unlit, "held(ALT) & down(U)", { editor.unlit ^= 1; } );
|
|
EDITOR_BIND(ddraw, "held(ALT) & down(D)", { editor.ddraw ^= 1; } );
|
|
|
|
void editor_pump() {
|
|
for each_array(editor_binds,editor_bind_t,b) {
|
|
if( input_eval(b.bindings) ) {
|
|
editor_send(b.command);
|
|
}
|
|
}
|
|
|
|
do_threadlock(console_lock) {
|
|
for each_array_ptr(editor_jobs,char*,cmd) {
|
|
if( (*cmd)[0] ) {
|
|
int found = 0;
|
|
for each_array(editor_binds,editor_bind_t,b) {
|
|
if( !strcmpi(b.command, *cmd)) {
|
|
b.fn();
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !found ) {
|
|
// alert(va("Editor: could not handle `%s` command.", *cmd));
|
|
(*cmd)[0] = '\0'; strcatf(&(*cmd), "\1%s\n", "Err\n"); (*cmd)[0] = '\0';
|
|
}
|
|
|
|
if( (*cmd)[0] ) {
|
|
(*cmd)[0] = '\0'; strcatf(&(*cmd), "\1%s\n", "Ok\n"); (*cmd)[0] = '\0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
|
|
void editor_symbol(int x, int y, const char *sym) {
|
|
#define FONT_SYMBOLS FONT_FACE2
|
|
#define FONT_WHITE FONT_COLOR1
|
|
#define FONT_YELLOW FONT_COLOR2
|
|
#define FONT_ORANGE FONT_COLOR3
|
|
#define FONT_CYAN FONT_COLOR4
|
|
// style: atlas size, unicode ranges and 6 font faces max
|
|
do_once font_face(FONT_SYMBOLS, "MaterialIconsSharp-Regular.otf", 24.f, FONT_EM|FONT_2048);
|
|
// style: 10 colors max
|
|
do_once font_color(FONT_WHITE, WHITE);
|
|
do_once font_color(FONT_YELLOW, YELLOW);
|
|
do_once font_color(FONT_CYAN, CYAN);
|
|
do_once font_color(FONT_ORANGE, ORANGE);
|
|
font_goto(x,y);
|
|
font_print(va(FONT_SYMBOLS FONT_WHITE FONT_H1 "%s", sym));
|
|
}
|
|
|
|
void editor_frame( void (*game)(unsigned, float, double) ) {
|
|
do_once {
|
|
set_init_ptr(editor.world);
|
|
//set_init_ptr(editor.selection);
|
|
profiler_enable( false );
|
|
|
|
window_pause( true );
|
|
window_cursor_shape(CURSOR_SW_AUTO);
|
|
editor.hz_high = window_fps_target();
|
|
|
|
fx_load("editorOutline.fs");
|
|
fx_enable(0, 1);
|
|
|
|
obj_setname(editor.root = obj_new(obj), "Signals");
|
|
obj_setname(editor.on_init = obj_new(obj), "onInit");
|
|
obj_setname(editor.on_tick = obj_new(obj), "onTick");
|
|
obj_setname(editor.on_draw = obj_new(obj), "onDraw");
|
|
obj_setname(editor.on_edit = obj_new(obj), "onEdit");
|
|
obj_setname(editor.on_quit = obj_new(obj), "onQuit");
|
|
|
|
obj_attach(editor.root, editor.on_init);
|
|
obj_attach(editor.root, editor.on_tick);
|
|
obj_attach(editor.root, editor.on_draw);
|
|
obj_attach(editor.root, editor.on_edit);
|
|
obj_attach(editor.root, editor.on_quit);
|
|
|
|
editor_seticoninstance(editor.root, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.on_init, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.on_tick, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.on_draw, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.on_edit, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.on_quit, ICON_MDI_SIGNAL_VARIANT);
|
|
|
|
editor_seticonclass(obj_type(editor.root), ICON_MDI_CUBE_OUTLINE);
|
|
}
|
|
|
|
// game tick
|
|
game(editor.frame, editor.dt, editor.t);
|
|
|
|
// timing
|
|
editor.dt = clampf(window_delta(), 0, 1/60.f) * !window_has_pause() * editor.slomo;
|
|
editor.t += editor.dt;
|
|
editor.frame += !window_has_pause();
|
|
editor.frame += !editor.frame;
|
|
|
|
// process inputs & messages
|
|
editor_pump();
|
|
|
|
// adaptive framerate
|
|
int app_on_background = !window_has_focus();
|
|
int hz = app_on_background ? editor.hz_low : editor.battery ? editor.hz_medium : editor.hz_high;
|
|
window_fps_lock( hz < 5 ? 5 : hz );
|
|
|
|
// draw menubar
|
|
static int stats_mode = 1;
|
|
static double last_fps = 0; if(!window_has_pause()) last_fps = window_fps();
|
|
const char *STATS = va("x%4.3f %03d.%03dss %02dF %s",
|
|
editor.slomo, (int)editor.t, (int)(1000 * (editor.t - (int)editor.t)),
|
|
(editor.frame-1) % ((int)window_fps_target() + !(int)window_fps_target()),
|
|
stats_mode == 1 ? va("%5.2f/%dfps", last_fps, (int)window_fps_target()) : stats_mode == 0 ? "0/0 KiB" : xstats());
|
|
const char *ICON_PL4Y = window_has_pause() ? ICON_MDI_PLAY : ICON_MDI_PAUSE;
|
|
const char *ICON_SKIP = window_has_pause() ? ICON_MDI_STEP_FORWARD/*ICON_MDI_SKIP_NEXT*/ : ICON_MDI_FAST_FORWARD;
|
|
|
|
int is_borderless = !glfwGetWindowAttrib(window, GLFW_DECORATED);
|
|
int ingame = !editor.active;
|
|
static double clicked_titlebar = 0;
|
|
UI_MENU(14+is_borderless, \
|
|
if(ingame) ui_disable(); \
|
|
UI_MENU_ITEM(ICON_MDI_FILE_TREE, editor_send("scene")) \
|
|
if(ingame) ui_enable(); \
|
|
UI_MENU_ITEM(ICON_PL4Y, if(editor.t == 0) editor_send("eject"); editor_send(window_has_pause() ? "play" : "pause")) \
|
|
UI_MENU_ITEM(ICON_SKIP, editor_send(window_has_pause() ? "frame" : "slomo")) \
|
|
UI_MENU_ITEM(ICON_MDI_STOP, editor_send("stop")) \
|
|
UI_MENU_ITEM(ICON_MDI_EJECT, editor_send("eject")) \
|
|
UI_MENU_ITEM(STATS, stats_mode = (++stats_mode) % 3) \
|
|
UI_MENU_ALIGN_RIGHT(32+32+32+32+32+32+34 + 32*is_borderless, clicked_titlebar = time_ms()) \
|
|
if(ingame) ui_disable(); \
|
|
UI_MENU_ITEM(ICON_MD_FOLDER_SPECIAL, editor_send("browser")) \
|
|
UI_MENU_ITEM(ICON_MDI_SCRIPT_TEXT, editor_send("script")) \
|
|
UI_MENU_ITEM(ICON_MDI_CHART_TIMELINE, editor_send("timeline")) \
|
|
UI_MENU_ITEM(ICON_MDI_CONSOLE, editor_send("console")) \
|
|
UI_MENU_ITEM(ICON_MDI_GRAPH, editor_send("nodes")) \
|
|
UI_MENU_ITEM(ICON_MD_SEARCH, editor_send("filter")) \
|
|
UI_MENU_POPUP(ICON_MD_SETTINGS, vec2(0.33,1.00), ui_debug()) \
|
|
if(ingame) ui_enable(); \
|
|
UI_MENU_ITEM(ICON_MD_CLOSE, editor_send("quit")) \
|
|
);
|
|
|
|
if( is_borderless ) {
|
|
static vec3 drag = {0};
|
|
if( clicked_titlebar ) {
|
|
static double clicks = 0;
|
|
if( input_up(MOUSE_L) ) ++clicks;
|
|
if( input_up(MOUSE_L) && clicks == 2 ) window_visible(false), window_maximize( window_has_maximize() ^ 1 ), window_visible(true);
|
|
if( (time_ms() - clicked_titlebar) > 400 ) clicks = 0, clicked_titlebar = 0;
|
|
|
|
if( input_down(MOUSE_L) ) drag = vec3(input(MOUSE_X), input(MOUSE_Y), 1);
|
|
}
|
|
if( drag.z *= !input_up(MOUSE_L) ) {
|
|
int wx = 0, wy = 0;
|
|
glfwGetWindowPos(window_handle(), &wx, &wy);
|
|
glfwSetWindowPos(window_handle(), wx + input(MOUSE_X) - drag.x, wy + input(MOUSE_Y) - drag.y);
|
|
}
|
|
}
|
|
|
|
if( !editor.active ) return;
|
|
|
|
// draw edit view (gizmos, position markers, etc).
|
|
for each_set_ptr(editor.world,obj*,o) {
|
|
if( obj_hasmethod(*o,edit) ) {
|
|
obj_edit(*o);
|
|
}
|
|
}
|
|
|
|
// draw silhouettes
|
|
sprite_flush();
|
|
fx_begin();
|
|
for each_map_ptr(*editor_selected_map(),void*,o,int,selected) {
|
|
if( !*selected ) continue;
|
|
if( obj_hasmethod(*o,draw) ) {
|
|
obj_draw(*o);
|
|
}
|
|
if( obj_hasmethod(*o,edit) ) {
|
|
obj_edit(*o);
|
|
}
|
|
}
|
|
sprite_flush();
|
|
fx_end();
|
|
|
|
// draw box selection
|
|
if( !ui_active() ) { //< check that we're not moving a window
|
|
static vec2 from = {0}, to = {0};
|
|
if( input_down(MOUSE_L) ) to = vec2(input(MOUSE_X), input(MOUSE_Y)), from = to;
|
|
if( input(MOUSE_L) ) to = vec2(input(MOUSE_X), input(MOUSE_Y));
|
|
if( len2sq(sub2(from,to)) > 0 ) {
|
|
vec2 a = min2(from, to), b = max2(from, to);
|
|
ddraw_push_2d();
|
|
ddraw_color_push(YELLOW);
|
|
ddraw_line( vec3(a.x,a.y,0),vec3(b.x-1,a.y,0) );
|
|
ddraw_line( vec3(b.x,a.y,0),vec3(b.x,b.y-1,0) );
|
|
ddraw_line( vec3(b.x,b.y,0),vec3(a.x-1,b.y,0) );
|
|
ddraw_line( vec3(a.x,b.y,0),vec3(a.x,a.y-1,0) );
|
|
ddraw_color_pop();
|
|
ddraw_pop_2d();
|
|
}
|
|
if( input_up(MOUSE_L) ) {
|
|
vec2 a = min2(from, to), b = max2(from, to);
|
|
from = to = vec2(0,0);
|
|
|
|
editor_select_aabb(aabb(vec3(a.x,a.y,0),vec3(b.x,b.y,0)));
|
|
}
|
|
}
|
|
|
|
// draw mouse aabb
|
|
aabb mouse = { vec3(input(MOUSE_X),input(MOUSE_Y),0), vec3(input(MOUSE_X),input(MOUSE_Y),1)};
|
|
if( 1 ) {
|
|
ddraw_color_push(YELLOW);
|
|
ddraw_push_2d();
|
|
ddraw_aabb(mouse.min, mouse.max);
|
|
ddraw_pop_2d();
|
|
ddraw_color_pop();
|
|
}
|
|
|
|
// tick mouse aabb selection and contextual tab (RMB)
|
|
aabb box = {0};
|
|
for each_set(editor.world,obj*,o) {
|
|
if( !obj_hasmethod(o, aabb) ) continue;
|
|
if( !obj_aabb(o, &box) ) continue;
|
|
|
|
// trigger contextual inspector
|
|
if( input_down(MOUSE_R) ) {
|
|
int is_selected = editor_selected(o);
|
|
editor_setpopup(o, is_selected);
|
|
}
|
|
|
|
// draw contextual inspector
|
|
if( editor_popup(o) ) {
|
|
if( editor_begin(va("%s (%s)", obj_name(o), obj_type(o)),EDITOR_WINDOW_NK_SMALL) ) {
|
|
ui_label2(obj_name(o), obj_type(o));
|
|
editor_inspect(o);
|
|
editor_end(EDITOR_WINDOW_NK_SMALL);
|
|
} else {
|
|
editor_setpopup(o, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// draw subeditors
|
|
static int preferred_window_mode = EDITOR_WINDOW;
|
|
static struct nk_color bak, *on = 0; do_once bak = ui_ctx->style.window.fixed_background.data.color; // ui_ctx->style.window.fixed_background.data.color = !!(on = (on ? NULL : &bak)) ? AS_NKCOLOR(0) : bak; };
|
|
if( editor.transparent ) ui_ctx->style.window.fixed_background.data.color = AS_NKCOLOR(0);
|
|
for each_array(editor.subeditors, subeditor, fn) {
|
|
fn(preferred_window_mode);
|
|
}
|
|
ui_ctx->style.window.fixed_background.data.color = bak;
|
|
|
|
// draw ui filter (note: render at end-of-frame, so it's hopefully on-top)
|
|
editor_filter();
|
|
}
|
|
|
|
#include "v4k_editor1_scene.h"
|
|
#include "v4k_editor2_browser.h"
|
|
#include "v4k_editor3_timeline.h"
|
|
#include "v4k_editor4_console.h"
|
|
#include "v4k_editor5_nodes.h"
|
|
#include "v4k_editor6_script.h"
|