927 lines
34 KiB
C
927 lines
34 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 COOK_ON_DEMAND 1 // @fixme: these directives should be on client, not within v4k.dll
|
|
#define ENABLE_AUTOTESTS 1
|
|
#define V4K_IMPLEMENTATION
|
|
#include "v4k.c"
|
|
#include "3rd_icon_mdi.h"
|
|
//#include "objtests.h"
|
|
#include "editor3.h"
|
|
#define EXTEND obj_extend
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
TODO("file_id: glow.hdr vs glow.png");
|
|
TODO("reflect: iterate components+metas on REFLECT too, so they're properly saved/loaded");
|
|
|
|
TODO("edit: tree nav");
|
|
TODO("edit: keys up,down,left,right -> move selection");
|
|
TODO("edit: reordering/dragging items on a list. ctrl+cursors");
|
|
TODO("edit: tab -> cycle next node of matching highlighted type");
|
|
TODO("edit: ^C^V^X thru clipboard. ^C to serialize to clipboard.");
|
|
TODO("edit: ^Z^Y cursor too. also fix undo ordering");
|
|
TODO("edit: ^S^L^N. load/save as filesystems");
|
|
TODO("edit: ^B(toolbar)");
|
|
TODO("edit: ^E prop single-view for multiple selections: should inspect common fields only");
|
|
TODO("edit: two-column way (or Nth) for multiple selections");
|
|
TODO("edit: tab/caps view, world + entity only, obj printf");
|
|
TODO("edit: obj bounds, obj picking, obj collisions");
|
|
TODO("edit: LMB object picking, RMB object serialization + log, floating ICONS bulb light");
|
|
TODO("edit: worldtraveller component");
|
|
TODO("edit: levelstreamer component");
|
|
TODO("edit: OSC server/client port 2023");
|
|
TODO("edit: add/rem entities, add/rem components, add/rem/pause/resume systems");
|
|
TODO("edit: event loop: tick,draw*,spawn,delete,un/load from bvh stream,");
|
|
|
|
TODO("edit: overlay scene editor");
|
|
TODO("edit: overlay0 artwork");
|
|
TODO("edit: overlay1 gizmo, grid, snap, splats (@todo: fixed screen size gizmos)");
|
|
TODO("edit: overlay2 script editor");
|
|
TODO("edit: overlay3 track, spline, keys editor");
|
|
TODO("edit: overlay4 node editor (shader/anim/bt/hfsm/material/audio/blueprints)");
|
|
TODO("edit: overlay5 csv editor");
|
|
TODO("edit: overlay6 bug/task editor");
|
|
|
|
TODO("gfx: tree traversal from game");
|
|
TODO("gfx: bvh and collision queries");
|
|
TODO("gfx: visibility and pvs queries");
|
|
|
|
TODO("obj: finish SYSTEMS and join queries");
|
|
TODO("obj: make it work with /GL flag (VC)");
|
|
TODO("obj: impl obj_mutate() ... deprecate?");
|
|
TODO("obj: make() from mpack(m) + native(n)");
|
|
TODO("obj: make() should save schema `[{mn`+version. and (m)pack and (n)ative should start with objtype");
|
|
TODO("obj: super()");
|
|
TODO("obj: lock()/trylock()/unlock()/barrier(N)");
|
|
TODO("obj: diff()/patch()");
|
|
TODO("obj: free obj_children()/payload");
|
|
TODO("obj: free obj_components()/payload2");
|
|
|
|
TODO("pack: mp2json, json2mp");
|
|
TODO("pack: simplify msgpack API, make it growth similar to va()")
|
|
#if 0 // v4k_pack proposal
|
|
static __thread char* mpin;
|
|
static __thread unsigned mpinlen;
|
|
static __thread char mpinbuf[256];
|
|
static __thread char* mpout;
|
|
static __thread unsigned mpoutlen;
|
|
static __thread char mpoutbuf[256];
|
|
#define PACKMSG(...) (msgpack_new(mpin = mpinbuf, mpinlen = sizeof(mpinbuf)), mpinlen = msgpack(__VA_ARGS__), cobs_encode(mpin, mpinlen, mpout = mpoutbuf, mpoutlen = cobs_bounds(mpinlen)), mpout)
|
|
#define UNPACKMSG(ptr,fmt,...) (mpin = (char*)ptr, mpinlen = strlen(ptr), mpout = mpoutbuf, mpoutlen = sizeof(mpoutbuf), mpoutlen = cobs_decode(mpin, mpinlen, mpout, mpoutlen), msgunpack_new(mpout, mpoutlen) && msgunpack(fmt, __VA_ARGS__))
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
array(void*) editor_persist_kv;
|
|
|
|
#define editor_new_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()); }
|
|
|
|
editor_new_property(open, int, 0);
|
|
editor_new_property(selected, int, 0);
|
|
editor_new_property(changed, int, 0);
|
|
editor_new_property(bookmarked, int, 0);
|
|
editor_new_property(visible, int, 0);
|
|
editor_new_property(script, int, 0);
|
|
editor_new_property(event, int, 0);
|
|
editor_new_property(iconinstance, char*, 0);
|
|
editor_new_property(iconclass, char*, 0);
|
|
editor_new_property(treeoffsety, int, 0);
|
|
// new prop: breakpoint: request to break on any given node
|
|
|
|
void editor_no_properties(void *o) {
|
|
editor_noopen(o);
|
|
editor_noselected(o);
|
|
editor_nochanged(o);
|
|
editor_nobookmarked(o);
|
|
editor_novisible(o);
|
|
editor_noscript(o);
|
|
editor_noevent(o);
|
|
editor_noiconinstance(o);
|
|
editor_noiconclass(o);
|
|
editor_notreeoffsety(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);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
struct editor_t {
|
|
// time
|
|
unsigned frame;
|
|
double t, dt, slomo;
|
|
// controls
|
|
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;
|
|
int hz_mid;
|
|
int hz_low;
|
|
int filter;
|
|
bool powersave;
|
|
bool lit;
|
|
bool ddraw;
|
|
// event root nodes
|
|
obj* root;
|
|
obj* init;
|
|
obj* tick;
|
|
obj* draw;
|
|
obj* edit;
|
|
obj* quit;
|
|
// all of them
|
|
array(obj*) objs; // @todo:set() world?
|
|
array(char*) cmds;
|
|
} editor = {
|
|
.active = 1,
|
|
.gamepad = 1,
|
|
.hz = 60,
|
|
.hz_mid = 18,
|
|
.hz_low = 5,
|
|
.lit = 1,
|
|
.ddraw = 1,
|
|
};
|
|
|
|
bool editor_active() {
|
|
return ui_hover() || ui_active() || gizmo_active() ? editor.active : 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
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_watch(const void *o) {
|
|
array_push(editor.objs, (obj*)o);
|
|
obj_push(o); // save state
|
|
}
|
|
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 properties + obj
|
|
editor_no_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_edit[obj_typeid(o)] ) {
|
|
obj_edit(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,"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,"color") ) changed = ui_color4(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
enum {
|
|
TREE_RECURSE = 1,
|
|
TREE_SELECTION = 2,
|
|
TREE_CHECKBOX = 4,
|
|
TREE_INDENT = 8,
|
|
TREE_ALL = ~0u
|
|
};
|
|
|
|
static
|
|
void editor_tree_(obj *o, unsigned flags) {
|
|
static unsigned tabs = ~0u;
|
|
++tabs;
|
|
|
|
if( o ) {
|
|
unsigned do_tags = 1;
|
|
unsigned do_indent = !!(flags & TREE_INDENT);
|
|
unsigned do_checkbox = !!(flags & TREE_CHECKBOX);
|
|
unsigned do_recurse = !!(flags & TREE_RECURSE);
|
|
unsigned do_selection = !!(flags & TREE_SELECTION);
|
|
|
|
nk_layout_row_dynamic(ui_ctx, 25, 1);
|
|
|
|
const char *objicon = editor_iconinstance(o);
|
|
if(!objicon) objicon = editor_iconclass(obj_type(o));
|
|
if(!objicon) objicon = ICON_MDI_CUBE_OUTLINE;
|
|
|
|
const char *objname = va("%s (%s)", obj_type(o), obj_name(o));
|
|
|
|
const char *objchevron =
|
|
!do_recurse || array_count(*obj_children(o)) <= 1 ? ICON_MDI_CIRCLE_SMALL :
|
|
editor_open(o) ? ICON_MDI_CHEVRON_DOWN : ICON_MDI_CHEVRON_RIGHT;
|
|
|
|
char *label = va("%*s%s%s %s", do_indent*(4+2*tabs), "", objchevron, objicon, objname);
|
|
|
|
const char *iconsL =
|
|
//editor_selected(o) ? ICON_MD_CHECK_BOX : ICON_MD_CHECK_BOX_OUTLINE_BLANK;
|
|
editor_selected(o) ? ICON_MDI_CHECKBOX_MARKED : ICON_MDI_CHECKBOX_BLANK_OUTLINE;
|
|
|
|
const char *iconsR = va("%s%s%s",
|
|
editor_script(o) ? ICON_MDI_SCRIPT : ICON_MDI_CIRCLE_SMALL,
|
|
editor_event(o) ? ICON_MDI_CALENDAR : ICON_MDI_CIRCLE_SMALL,
|
|
editor_visible(o) ? ICON_MDI_EYE_OUTLINE : ICON_MDI_EYE_CLOSED );
|
|
|
|
UI_TOOLBAR_OVERLAY_DECLARE(int choiceL, choiceR);
|
|
|
|
struct nk_command_buffer *canvas = nk_window_get_canvas(ui_ctx);
|
|
struct nk_rect bounds; nk_layout_peek(&bounds, ui_ctx);
|
|
|
|
int clicked = nk_hovered_text(ui_ctx, label, strlen(label), NK_TEXT_LEFT, editor_selected(o));
|
|
if( clicked && nk_input_is_mouse_hovering_rect(&ui_ctx->input, ((struct nk_rect) { bounds.x,bounds.y,bounds.w*0.66,bounds.h })) )
|
|
editor_altselected( o );
|
|
|
|
vec2i treeoffset = {0};
|
|
|
|
if( do_indent ) {
|
|
float thickness = 2.f;
|
|
struct nk_color color = {255,255,255,64};
|
|
|
|
int offsx = 30;
|
|
int spacx = 10;
|
|
int lenx = (tabs+1)*spacx;
|
|
int halfy = bounds.h / 2;
|
|
int offsy = halfy + 2;
|
|
|
|
treeoffset = vec2i(bounds.x+offsx+lenx-spacx,bounds.y+offsy);
|
|
|
|
editor_settreeoffsety(o, treeoffset.y);
|
|
|
|
for( obj *p = obj_parent(o); p ; p = 0 )
|
|
nk_stroke_line(canvas, treeoffset.x-6,treeoffset.y, treeoffset.x-spacx,treeoffset.y, thickness, color),
|
|
nk_stroke_line(canvas, treeoffset.x-spacx,treeoffset.y,treeoffset.x-spacx,editor_treeoffsety(p)+4, thickness, color);
|
|
}
|
|
|
|
if( ui_contextual() ) {
|
|
API int editor_send(const char *);
|
|
|
|
int choice = ui_label(ICON_MD_BOOKMARK_ADDED "Toggle bookmarks (CTRL+B)");
|
|
if( choice & 1 ) editor_send("bookmark");
|
|
|
|
ui_contextual_end(!!choice);
|
|
}
|
|
|
|
UI_TOOLBAR_OVERLAY(choiceL,iconsL,nk_rgba_f(1,1,1,do_checkbox*ui_alpha*0.65),NK_TEXT_LEFT);
|
|
|
|
if( do_tags )
|
|
UI_TOOLBAR_OVERLAY(choiceR,iconsR,nk_rgba_f(1,1,1,ui_alpha*0.65),NK_TEXT_RIGHT);
|
|
|
|
if( choiceR == 3 ) editor_altscript( o );
|
|
if( choiceR == 2 ) editor_altevent( o);
|
|
if( choiceR == 1 ) editor_altvisible( o );
|
|
|
|
if( do_recurse && editor_open(o) ) {
|
|
for each_objchild(o,obj*,oo) {
|
|
editor_tree_(oo,flags);
|
|
}
|
|
}
|
|
|
|
if( clicked && !choiceL && !choiceR ) {
|
|
int is_picking = input(KEY_CTRL);
|
|
if( !is_picking ) {
|
|
if( input(KEY_SHIFT) ) {
|
|
editor_selectgroup( editor_first_selected(), editor_last_selected() );
|
|
} else {
|
|
editor_unselect();
|
|
editor_setselected(o, 1);
|
|
}
|
|
}
|
|
for( obj *p = obj_parent(o); p; p = obj_parent(p) ) {
|
|
editor_setopen(p, 1);
|
|
}
|
|
if( nk_input_is_mouse_hovering_rect(&ui_ctx->input, ((struct nk_rect) { bounds.x,bounds.y,treeoffset.x-bounds.x+UI_ICON_FONTSIZE/2,bounds.h })) ) {
|
|
editor_altopen( o );
|
|
}
|
|
}
|
|
}
|
|
|
|
--tabs;
|
|
}
|
|
|
|
void editor_tree() {
|
|
// #define HELP ICON_MDI_INFORMATION_OUTLINE "@-A\n-B\n-C\n" ";"
|
|
int choice = ui_toolbar(ICON_MDI_PLUS "@New node (CTRL+N);" ICON_MDI_DOWNLOAD "@Save node (CTRL+S);" ICON_MDI_DOWNLOAD "@Save scene (SHIFT+CTRL+S);" ICON_MD_BOOKMARK_ADDED "@Toggle Bookmark (CTRL+B);");
|
|
if( choice == 1 ) editor_send("node.new");
|
|
if( choice == 2 ) editor_send("node.save");
|
|
if( choice == 3 ) editor_send("tree.save");
|
|
if( choice == 4 ) editor_send("bookmark");
|
|
|
|
array(obj*) bookmarks = 0;
|
|
for each_map_ptr(*editor_bookmarked_map(), void*,o,int,bookmarked) {
|
|
if( *bookmarked ) {
|
|
array_push(bookmarks, *o);
|
|
}
|
|
}
|
|
if( ui_collapse("!" ICON_MD_BOOKMARK "Bookmark.toggles", "DEBUG:BOOKMARK")) {
|
|
for each_array( bookmarks, obj*, o )
|
|
editor_tree_( o, TREE_ALL & ~(TREE_RECURSE|TREE_INDENT|TREE_CHECKBOX) );
|
|
ui_collapse_end();
|
|
}
|
|
array_free(bookmarks);
|
|
|
|
editor_tree_( editor.root, TREE_ALL );
|
|
|
|
for each_array( editor.objs, obj*, o )
|
|
editor_tree_( o, TREE_ALL );
|
|
|
|
ui_separator();
|
|
|
|
// edit selection
|
|
for each_map(*editor_selected_map(), void*,o, int, k) {
|
|
if( k ) editor_inspect(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;
|
|
}
|
|
|
|
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_fn_)() { __VA_ARGS__ }; AUTORUN { array_push(editor_binds, ((editor_bind_t){CMD,KEYS,macro(editor_bind_fn_)}) ); }
|
|
|
|
EDITOR_BIND("play", "held(CTRL) & down(SPC)", { window_pause(0); if(!editor.slomo) editor.active = 0; editor.slomo = 1; } );
|
|
EDITOR_BIND("slomo", "", { window_pause(0); editor.slomo = maxf(fmod(editor.slomo * 2, 16), 0.125); } );
|
|
EDITOR_BIND("stop", "(held(ALT)|held(SHIFT))&down(ESC)", { 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("pause", "down(ESC)", { window_pause( window_has_pause() ^ 1 ); } );
|
|
EDITOR_BIND("frame", "held(CTRL) & held(RIGHT)", { window_pause(1); editor.frame++, editor.t += (editor.dt = 1/60.f); } );
|
|
EDITOR_BIND("eject", "held(SHIFT) & down(F1)", { editor.active ^= 1; } );
|
|
EDITOR_BIND("reload", "down(F5)", { window_reload(); } );
|
|
EDITOR_BIND("quit", "held(ALT) & down(F4)", { record_stop(), exit(0); } );
|
|
EDITOR_BIND("battery", "held(ALT) & down(B)", { editor.powersave ^= 1; } );
|
|
EDITOR_BIND("browser", "down(F1)", { ui_show("Browser", ui_visible("Browser") ^ true); } );
|
|
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", "down(F11)", { record_stop(), window_fullscreen( window_has_fullscreen() ^ 1 ); } ); // close any recording before framebuffer resizing, which would corrupt video stream
|
|
EDITOR_BIND("mute", "held(ALT) & down(M)", { audio_volume_master( 1 ^ !!audio_volume_master(-1) ); } );
|
|
EDITOR_BIND("filter", "held(CTRL) & down(F)", { editor.filter ^= 1; } );
|
|
EDITOR_BIND("gamepad", "held(ALT) & down(G)", { editor.gamepad ^= 1; } );
|
|
EDITOR_BIND("lit", "held(ALT) & down(L)", { editor.lit ^= 1; } );
|
|
EDITOR_BIND("ddraw", "held(ALT) & down(D)", { editor.ddraw ^= 1; } );
|
|
EDITOR_BIND("node.new", "down(INS)", { editor_spawn1(); } );
|
|
EDITOR_BIND("node.del", "down(DEL)", { editor_destroy_selected(); } );
|
|
EDITOR_BIND("node.save", "held(CTRL)&down(S)", { puts("@todo"); } );
|
|
EDITOR_BIND("tree.save", "held(CTRL)&down(S)&held(SHIFT)",{ puts("@todo"); } );
|
|
EDITOR_BIND("select.all", "held(CTRL) & down(A)", { editor_select("**"); } );
|
|
EDITOR_BIND("select.none", "held(CTRL) & down(D)", { editor_select("!**"); } );
|
|
EDITOR_BIND("select.invert", "held(CTRL) & down(I)", { editor_select("~**"); } );
|
|
EDITOR_BIND("screenshot", "held(ALT) & down(X)", { char *name = file_counter(va("%s.png",app_name())); window_screenshot(name), ui_notify(va("Screenshot: %s", name), date_string()); } );
|
|
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("bookmark", "held(CTRL) & down(B)", { editor_selected_map_t *map = editor_selected_map(); \
|
|
int on = 0; \
|
|
for each_map_ptr(*map,void*,o,int,selected) if(*selected) on |= !editor_bookmarked(*o); \
|
|
for each_map_ptr(*map,void*,o,int,selected) if(*selected) editor_setbookmarked(*o, on); \
|
|
} );
|
|
|
|
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_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);
|
|
|
|
fx_load("editorOutline.fs");
|
|
fx_enable(0, 1);
|
|
|
|
obj_setname(editor.root = obj_new(obj), "Signals");
|
|
obj_setname(editor.init = obj_new(obj), "Init");
|
|
obj_setname(editor.tick = obj_new(obj), "Tick");
|
|
obj_setname(editor.draw = obj_new(obj), "Draw");
|
|
obj_setname(editor.quit = obj_new(obj), "Quit");
|
|
// obj_setname(editor.book = obj_new(obj), "Bookmark.toggles");
|
|
|
|
obj_attach(editor.root, editor.init);
|
|
obj_attach(editor.root, editor.tick);
|
|
obj_attach(editor.root, editor.draw);
|
|
obj_attach(editor.root, editor.quit);
|
|
|
|
editor_seticoninstance(editor.root, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.init, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.tick, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.draw, ICON_MDI_SIGNAL_VARIANT);
|
|
editor_seticoninstance(editor.quit, ICON_MDI_SIGNAL_VARIANT);
|
|
// editor_seticoninstance(editor.book, ICON_MD_BOOKMARK_ADDED);
|
|
|
|
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();
|
|
|
|
// draw menubar
|
|
static double last_fps = 0; if(!window_has_pause()) last_fps = window_fps();
|
|
int fps_target = window_fps_target() > 0.0f ? (int)window_fps_target() : 60;
|
|
const char *TITLE = va("%02dF %5.2f/%dfps x%4.3f", editor.frame % (int)fps_target, last_fps, fps_target, editor.slomo);
|
|
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;
|
|
|
|
#define EDITOR_TOOLBAR \
|
|
UI_MENU(10, \
|
|
UI_MENU_POPUP(ICON_MD_SETTINGS, vec2(0.33,1.00), ui_debug()) \
|
|
UI_MENU_ITEM(ICON_PL4Y, 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(TITLE,) \
|
|
UI_MENU_ALIGN_RIGHT(30+30+30) \
|
|
UI_MENU_ITEM(ICON_MD_FOLDER_SPECIAL, editor_send("browser")) \
|
|
UI_MENU_ITEM(ICON_MD_SEARCH, editor_send("filter")) \
|
|
UI_MENU_ITEM(ICON_MD_CLOSE, editor_send("quit")) \
|
|
);
|
|
|
|
EDITOR_TOOLBAR
|
|
|
|
if(! editor.active ) return;
|
|
|
|
// draw silhouettes
|
|
sprite_flush();
|
|
for each_map_ptr(*editor_selected_map(),void*,o,int,selected) {
|
|
if(*selected && obj_draw[obj_typeid(*o)]) {
|
|
fx_begin();
|
|
obj_draw(*o);
|
|
sprite_flush();
|
|
fx_end();
|
|
}
|
|
}
|
|
|
|
// draw ui
|
|
if( ui_panel("Console " ICON_MDI_CONSOLE, 0)) {
|
|
// allocate complete window space
|
|
struct nk_rect bounds = nk_window_get_content_region(ui_ctx);
|
|
|
|
enum { CONSOLE_LINE_HEIGHT = 20 };
|
|
static array(char*) lines = 0;
|
|
do_once {
|
|
array_push(lines, stringf("> Editor v%s. Type `%s` for help.", EDITOR_VERSION, ""));
|
|
}
|
|
int max_lines = (bounds.h - UI_ROW_HEIGHT) / (CONSOLE_LINE_HEIGHT * 2);
|
|
if( max_lines >= 1 ) {
|
|
nk_layout_row_static(ui_ctx, bounds.h - UI_ROW_HEIGHT, bounds.w, 1);
|
|
if(nk_group_begin(ui_ctx, "console.group", NK_WINDOW_NO_SCROLLBAR|NK_WINDOW_BORDER)) {
|
|
nk_layout_row_static(ui_ctx, CONSOLE_LINE_HEIGHT, bounds.w, 1);
|
|
for( int i = array_count(lines); i < max_lines; ++i )
|
|
array_push_front(lines, 0);
|
|
for( int i = array_count(lines) - max_lines; i < array_count(lines); ++i ) {
|
|
if( !lines[i] ) {
|
|
#if 0 // debug
|
|
nk_label_wrap(ui_ctx, va("%d.A/%d",i+1,max_lines));
|
|
nk_label_wrap(ui_ctx, va("%d.B/%d",i+1,max_lines));
|
|
#else
|
|
nk_label_wrap(ui_ctx, "");
|
|
nk_label_wrap(ui_ctx, "");
|
|
#endif
|
|
} else {
|
|
nk_label_wrap(ui_ctx, lines[i]);
|
|
const char *answer = isdigit(*lines[i]) ? editor_recv( atoi(lines[i]), 0 ) : NULL;
|
|
nk_label_wrap(ui_ctx, answer ? answer : "");
|
|
}
|
|
}
|
|
nk_group_end(ui_ctx);
|
|
}
|
|
}
|
|
static char *cmd = 0;
|
|
if( ui_string(NULL, &cmd) ) {
|
|
int jobid = editor_send(cmd);
|
|
array_push(lines, stringf("%d> %s", jobid, cmd));
|
|
cmd[0] = 0;
|
|
}
|
|
|
|
ui_panel_end();
|
|
}
|
|
if( ui_panel("Scene " ICON_MDI_FILE_TREE, PANEL_OPEN)) {
|
|
editor_tree();
|
|
ui_panel_end();
|
|
}
|
|
|
|
// draw ui browser
|
|
if( ui_window("Browser", 0) ) {
|
|
const char *file = 0;
|
|
if( ui_browse(&file, NULL) ) {
|
|
const char *sep = ifdef(win32, "\"", "'");
|
|
app_exec(va("%s %s%s%s", ifdef(win32, "start \"\"", ifdef(osx, "open", "xdg-open")), sep, file, sep));
|
|
}
|
|
ui_window_end();
|
|
}
|
|
|
|
// draw ui filter (note: render at end-of-frame, so it's hopefully on-top)
|
|
editor_filter();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// demo
|
|
|
|
typedef struct my_sprite { OBJ
|
|
char *filename;
|
|
vec3 position;
|
|
float tilt;
|
|
vec4 tint;
|
|
// --- private
|
|
unsigned rgba_;
|
|
texture_t texture_;
|
|
} my_sprite;
|
|
|
|
OBJTYPEDEF(my_sprite,201);
|
|
|
|
void my_sprite_ctor(my_sprite *obj) {
|
|
obj->texture_ = texture(obj->filename, TEXTURE_RGBA);
|
|
obj->rgba_ = rgbaf( obj->tint.x/255.0, obj->tint.y/255.0, obj->tint.z/255.0, obj->tint.w/255.0 );
|
|
}
|
|
void my_sprite_draw(my_sprite *obj) {
|
|
obj->rgba_ = rgbaf( obj->tint.x/255.0, obj->tint.y/255.0, obj->tint.z/255.0, obj->tint.w/255.0 ); // @fixme: del me
|
|
sprite( obj->texture_, &(obj->position.x), obj->tilt, obj->rgba_ );
|
|
}
|
|
void my_sprite_edit(my_sprite *obj) {
|
|
ui_label("Private section");
|
|
ui_color4("Tint_", &obj->tint.x);
|
|
ui_texture("Texture_", obj->texture_);
|
|
ui_separator();
|
|
}
|
|
|
|
AUTORUN {
|
|
// reflect
|
|
STRUCT( my_sprite, char*, filename, "Filename" );
|
|
STRUCT( my_sprite, vec3, position, "Position" );
|
|
STRUCT( my_sprite, float, tilt, "Tilt degrees" );
|
|
STRUCT( my_sprite, vec4, tint, "Tint color" );
|
|
|
|
// extend
|
|
EXTEND(my_sprite,ctor);
|
|
EXTEND(my_sprite,draw);
|
|
EXTEND(my_sprite,edit);
|
|
}
|
|
|
|
void game(unsigned frame, float dt, double t) {
|
|
static my_sprite *root;
|
|
static my_sprite *o1;
|
|
static my_sprite *o2;
|
|
static camera_t cam;
|
|
if( !frame ) {
|
|
cam = camera();
|
|
camera_enable(&cam);
|
|
|
|
root = obj_make(
|
|
"[my_sprite]\n"
|
|
"filename=cat.png\n"
|
|
"position=5,2,100\n"
|
|
"tilt=46 + 45 -90\n"
|
|
"tint=255, 255, 0, 255\n"
|
|
);
|
|
o1 = obj_make(
|
|
"[my_sprite]\n"
|
|
"filename=cat.png\n"
|
|
"position=1,2,100\n"
|
|
"tilt=45 + 45 -90\n"
|
|
"tint=255, 0, 0, 255\n"
|
|
);
|
|
o2 = obj_make(
|
|
"[my_sprite]\n"
|
|
"filename=cat.png\n"
|
|
"position=1,2,100\n"
|
|
"tilt=45\n"
|
|
"tint=0, 0, 255, 255\n"
|
|
);
|
|
|
|
//obj_setname(root, "root");
|
|
obj_setname(o1, "o1");
|
|
obj_setname(o2, "o2");
|
|
|
|
obj*o3 = obj_new_ext(obj, "o3");
|
|
obj*o4 = obj_new_ext(obj, "o4");
|
|
obj*o5 = obj_new_ext(obj, "o5");
|
|
|
|
obj_attach(root, o1);
|
|
obj_attach(o1, o2);
|
|
obj_attach(o2, o3);
|
|
obj_attach(o1, o4);
|
|
obj_attach(root, o5);
|
|
|
|
editor_watch(root);
|
|
}
|
|
|
|
// draw game
|
|
my_sprite_draw(root);
|
|
my_sprite_draw(o1);
|
|
my_sprite_draw(o2);
|
|
|
|
// tick game
|
|
root->tilt = 5 * sin(t+dt);
|
|
}
|
|
|
|
int main(){
|
|
for( window_create(flag("--transparent") ? 101 : 80,0); window_swap(); ) {
|
|
editor_frame(game);
|
|
}
|
|
}
|