diff options
| author | tech kaczynski <tk@techkaczynski.dev> | 2026-09-22 22:38:57 +1000 |
|---|---|---|
| committer | tech kaczynski <tk@techkaczynski.dev> | 2026-09-22 22:38:57 +1000 |
| commit | 08cb8d6a585dd552b69a488d9db1be123f01467a (patch) | |
| tree | 77d50ce30bbc102b08486929ba654b5bef70af43 /editor.c | |
initial commit.
Diffstat (limited to 'editor.c')
| -rw-r--r-- | editor.c | 1744 |
1 files changed, 1744 insertions, 0 deletions
diff --git a/editor.c b/editor.c new file mode 100644 index 0000000..6550ecf --- /dev/null +++ b/editor.c @@ -0,0 +1,1744 @@ +#include "editor.h" +#include "save.h" +#include "cam.h" +#include "physics.h" +#include "ent.h" +#include "app.h" +#include "res.h" +#include "raygui.h" + +static bool started = false; +static Vec2 start_point = {0}; +static Vec2 end_point = {0}; +static Rect sel_rec = {0}; +static f32 sel_dist = 0; +static EditorAction tr_action = {0}; + +EditorInfo ed = { + .panel = { + [ED_PNL_LEFT] = { + .type = ED_PNL_TYPE_SCENE, + // .enabled = true, + }, + [ED_PNL_RIGHT] = { + .type = ED_PNL_TYPE_PROPERTIES, + }, + [ED_PNL_CENTER] = { + .type = ED_PNL_TYPE_SPACE, + }, + }, +}; + +/* forward decl */ +static bool selection_root(Entity *ent); +static void action_apply(EditorAction*, bool); +static void action_free(EditorAction*); + +static void +history_push(EditorAction action) { + if (ed.history_save_csr > ed.history_csr) { + ed.history_save_csr = UINT32_MAX; + } + + /* discard redo branch */ + while (ed.history_cnt > ed.history_csr) { + action_free(&ed.history[--ed.history_cnt]); + } + + if (ed.history_cnt == ed.history_cap) { + u32 cap = ed.history_cap ? ed.history_cap * 2 : 64; + void *history = realloc(ed.history, cap * sizeof(action)); + if (!history) { + perror("editor history alloc failed."); + exit(1); + } + ed.history = history; + ed.history_cap = cap; + } + + ed.history[ed.history_cnt++] = action; + ed.history_csr = ed.history_cnt; +} + +static void +history_undo(void) { + if (ed.history_csr > 0) { + action_apply(&ed.history[ed.history_csr - 1], true); + ed.history_csr -= 1; + } +} + +static void +history_redo(void) { + if (ed.history_csr < ed.history_cnt) { + action_apply(&ed.history[ed.history_csr], false); + ed.history_csr += 1; + } +} + +static void +history_clear(void) { + for (u32 i = 0; i < ed.history_cnt; ++i) { + action_free(&ed.history[i]); + } + + ed.history_cnt = 0; + ed.history_csr = 0; + ed.history_save_csr = UINT32_MAX; +} + +static void +action_entity_extract(EditorAction *action) { + for (u32 i = 0; i < action->item_cnt; ++i) { + Entity *held = &action->item[i].before.stored.ent; + assert(!held->pid); + + u64 id = entity_id_from_pid(action->item[i].pid); + *held = entity_extract_now(id); + } +} + +static void +action_entity_restore(EditorAction *action) { + /* Restore all entities before resolving their relationships. */ + for (u32 i = 0; i < action->item_cnt; ++i) { + Entity *held = &action->item[i].before.stored.ent; + assert(held->pid == action->item[i].pid); + + u64 id = entity_restore(action->item[i].pid); + assert(id); + + *entity_get(id) = *held; + *held = (Entity){0}; + } + + for (u32 i = 0; i < action->item_cnt; ++i) { + const EditorActionData *data = &action->item[i].before; + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + + ent->tr.parent = entity_id_from_pid(data->stored.parent); + ent->tr.prev = entity_id_from_pid(data->stored.prev); + ent->tr.next = entity_id_from_pid(data->stored.next); + + if (ent->tr.prev) { + entity_get(ent->tr.prev)->tr.next = id; + } else if (ent->tr.parent) { + entity_get(ent->tr.parent)->tr.first_child = id; + } + + if (ent->tr.next) { + entity_get(ent->tr.next)->tr.prev = id; + } + } +} + +/* collects PIDs, recursive visits children first. */ +static void +action_collect(EditorAction *action, u64 id, bool recursive) { + Entity *ent = entity_get(id); + assert(ent && ent->pid); + + if (recursive) { + for (u64 child = ent->tr.first_child; child; + child = entity_get(child)->tr.next) { + action_collect(action, child, true); + } + } + + u32 i = action->item_cnt++; + + /* First pass counts, second pass fills the allocated array. */ + if (action->item) { + action->item[i].pid = ent->pid; + } +} + +static EditorAction +action_begin(EditorActionTag tag) { + EditorAction action = { .tag = tag }; + + bool roots_only = false; + bool recursive = false; + + switch (tag) { + case ED_ACTION_TRANSFORM: + roots_only = true; + break; + + case ED_ACTION_CREATE: + action.item_cnt = 1; + action.item = calloc(1, sizeof(*action.item)); + if (!action.item) { + perror("action_begin: create entity alloc failed."); + exit(1); + } + + u64 id = entity_create(false); + Entity *ent = entity_get(id); + action.item[0].pid = ent->pid; + entities_clear_selected(); + ent->ed.selected = true; + return action; + + case ED_ACTION_DELETE: + roots_only = true; + recursive = true; + break; + + case ED_ACTION_RENAME: + break; + + default: + TraceLog(LOG_FATAL, "action_begin: unsupported tag."); + } + + /* Count, allocate, then collect PIDs. */ + for (u32 pass = 0; pass < 2; ++pass) { + for (u32 i = 0; i < entity_count(); ++i) { + Entity *ent = &entity_data()[i]; + if (!ent || !ent->pid) { + continue; + } + + if (roots_only ? + !selection_root(ent) : + !ent->ed.selected) { + continue; + } + action_collect(&action, entity_id_at(i), recursive); + } + if (pass == 0) { + if (!action.item_cnt) { + return action; + } + action.item = calloc(action.item_cnt, + sizeof(*action.item)); + if (!action.item) { + perror("editor action alloc failed."); + exit(1); + } + action.item_cnt = 0; + } + } + /* Capture action-specific data for the collected entities. */ + for (u32 i = 0; i < action.item_cnt; ++i) { + Entity *ent = entity_get( + entity_id_from_pid(action.item[i].pid)); + assert(ent); + EditorActionData *before = &action.item[i].before; + switch (tag) { + case ED_ACTION_TRANSFORM: + before->transform.pos = ent->tr.pos; + before->transform.rot = ent->tr.rot; + before->transform.scale = ent->tr.scale; + break; + case ED_ACTION_DELETE: { + Entity *parent = entity_get(ent->tr.parent); + Entity *prev = entity_get(ent->tr.prev); + Entity *next = entity_get(ent->tr.next); + assert(!parent || parent->pid); + assert(!prev || prev->pid); + assert(!next || next->pid); + before->stored.parent = parent ? parent->pid : 0; + before->stored.prev = prev ? prev->pid : 0; + before->stored.next = next ? next->pid : 0; + break; + } + case ED_ACTION_RENAME: + // Name ownership will transfer when the rename is submitted. + break; + default: + TraceLog(LOG_FATAL, + "action_begin: unhandled action tag."); + break; + } + } + + return action; +} + +static void +action_apply(EditorAction *action, bool undo) { + switch (action->tag) { + case ED_ACTION_TRANSFORM: + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + const EditorActionData *data = undo ? + &action->item[i].before : + &action->item[i].after; + + ent->tr.pos = data->transform.pos; + ent->tr.rot = data->transform.rot; + ent->tr.scale = data->transform.scale; + } + break; + + case ED_ACTION_CREATE: + if (undo) { + action_entity_extract(action); + } else { + action_entity_restore(action); + } + break; + + case ED_ACTION_DELETE: + if (undo) { + action_entity_restore(action); + } else { + action_entity_extract(action); + } + break; + + case ED_ACTION_RENAME: + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + char *name = ent->name; + ent->name = action->item[i].before.rename.name; + action->item[i].before.rename.name = name; + } + break; + + default: + TraceLog(LOG_FATAL, "action with invalid tag."); + break; + } +} + +static void +action_free(EditorAction *action) { + switch (action->tag) { + case ED_ACTION_TRANSFORM: + break; + + case ED_ACTION_CREATE: + case ED_ACTION_DELETE: + for (u32 i = 0; i < action->item_cnt; ++i) { + entity_clean(&action->item[i].before.stored.ent); + } + break; + + case ED_ACTION_RENAME: + for (u32 i = 0; i < action->item_cnt; ++i) { + free(action->item[i].before.rename.name); + } + break; + + default: + TraceLog(LOG_FATAL, "action_free: unhandled action tag."); + break; + } + free(action->item); +} + +static void +action_commit(EditorAction *action) { + bool changed = false; + + switch (action->tag) { + case ED_ACTION_TRANSFORM: + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + const EditorActionData *before = + &action->item[i].before; + EditorActionData *after = &action->item[i].after; + after->transform.pos = ent->tr.pos; + after->transform.rot = ent->tr.rot; + after->transform.scale = ent->tr.scale; + + changed |= !Vector3Equals(before->transform.pos, + after->transform.pos) + || !QuaternionEquals(before->transform.rot, + after->transform.rot) + || !Vector3Equals(before->transform.scale, + after->transform.scale); + } + + if (!changed) { + action_apply(action, true); + } + break; + + case ED_ACTION_CREATE: + case ED_ACTION_DELETE: + changed = action->item_cnt != 0; + break; + + case ED_ACTION_RENAME: + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + cstr before = action->item[i].before.rename.name; + changed |= strcmp(before ? before : "", + ent->name ? ent->name : "") != 0; + } + + if (!changed) { + action_apply(action, true); + } + break; + + default: + TraceLog(LOG_FATAL, "action_commit: unhangled action tag."); + break; + } + + if (changed) { + history_push(*action); + } else { + action_free(action); + } + + *action = (EditorAction){0}; +} + +void +editor_init(void) { + // GuiLoadStyle("style_amber.rgs"); + + Asset *fnt_asset = + asset_get("res/fnt/Aileron-Regular.otf"); + if (fnt_asset) { + GuiSetFont(fnt_asset->fnt); + GuiSetStyle(DEFAULT, TEXT_SIZE, 16); + } + + /* probably will delete this later */ + ed.history_save_csr = + save_file_read("res/save/scene.fgs") ? + ed.history_csr : UINT32_MAX; +} + +void +editor_exit(void) { + action_free(&tr_action); + tr_action = (EditorAction){0}; + history_clear(); + free(ed.history); + ed.history = NULL; + ed.history_cap = 0; +} + +void +editor_update(void) { + bool ctrl = IsKeyDown(KEY_LEFT_CONTROL); + bool shift = IsKeyDown(KEY_LEFT_SHIFT); + bool alt = IsKeyDown(KEY_LEFT_ALT); + + /* sorry about the indentation */ + switch (app.mode) { +case APP_MODE_EDIT_NONE: + if (!ed.focus_captured && IsKeyPressed(KEY_SPACE)) { + ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_SPACE; + ed.panel[ED_PNL_CENTER].enabled = true; + } + + if (ed.panel[ED_PNL_CENTER].enabled) { + ed.focus = ED_PNL_CENTER; + ed.focus_captured = true; + break; + } + + if (!ed.focus_captured) { + ed.focus = 0; + Vec2 mpos = GetMousePosition(); + + if (ed.panel[ED_PNL_LEFT].enabled && + CheckCollisionPointRec(mpos, + ed.panel[ED_PNL_LEFT].bounds)) { + ed.focus = ED_PNL_LEFT; + } + + if (ed.panel[ED_PNL_RIGHT].enabled && + CheckCollisionPointRec(mpos, + ed.panel[ED_PNL_RIGHT].bounds)) { + ed.focus = ED_PNL_RIGHT; + } + } + + if (ed.focus != 0) { + break; + } + + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { + app.mode = APP_MODE_EDIT_CTRL; + main_cam.update = true; + HideCursor(); + DisableCursor(); + break; + } + + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + app.mode = APP_MODE_EDIT_SELECT; + editor_select(); // capture this frame + break; + } + + if (IsKeyPressed(KEY_G)) { + if (!ctrl && ed.sel_cnt) { + app.mode = APP_MODE_EDIT_MOVE; + break; + } + } + + if (IsKeyPressed(KEY_R)) { + if (!ctrl && ed.sel_cnt) { + app.mode = APP_MODE_EDIT_ROTATE; + break; + } + + if (ctrl) { + history_clear(); + entity_system_clear(); + + ed.history_save_csr = + save_file_read("res/save/scene.fgs") ? + ed.history_csr : UINT32_MAX; + } + } + + if (IsKeyPressed(KEY_S)) { + if (ctrl) { + ed.history_save_csr = + save_file_write("res/save/scene.fgs") ? + ed.history_csr : UINT32_MAX; + } + + /* scale */ + } + + if (IsKeyPressed(KEY_X)) { + if (!ctrl && ed.sel_cnt) { + EditorAction action = action_begin(ED_ACTION_DELETE); + if (action.item_cnt) { + action_apply(&action, false); + action_commit(&action); + } + } + } + + if (IsKeyPressed(KEY_A)) { + if (ctrl) { + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + if (ent && ent->pid) { + ent->ed.selected = true; + } + } + } + } + + if (ctrl && IsKeyPressed(KEY_Z)) { + if (!shift) { + history_undo(); + } else { + history_redo(); + } + } + break; + +case APP_MODE_EDIT_SELECT: + editor_select(); + break; + +case APP_MODE_EDIT_CTRL: + cam_ctrl_editor(); + if (IsMouseButtonReleased(MOUSE_RIGHT_BUTTON)) { + app.mode = APP_MODE_EDIT_NONE; + main_cam.update = false; + ShowCursor(); + } + break; + +case APP_MODE_EDIT_MOVE: + editor_move(); + break; + +case APP_MODE_EDIT_ROTATE: + editor_rotate(); + break; + +case APP_MODE_EDIT_SPACE: + break; + +// case APP_MODE_EDIT_CMD: + // break; + +default: + TraceLog(LOG_FATAL, "editor_update: unhandled app.mode value"); + break; + } + + /* reset selection data */ + ed.sel_cnt = 0; + ed.sel_point = Vector3Zero(); + ed.sel_rot = QuaternionIdentity(); + + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (!ent->ed.selected) { + continue; + } + + ed.sel_cnt++; + ed.sel_point = Vector3Add(ed.sel_point, entity_world_pos(id)); + ed.sel_rot = entity_world_rot(id); + } + + if (ed.sel_cnt > 0) { + ed.sel_point = Vector3Scale( + ed.sel_point, 1.0f / ed.sel_cnt); + } + + if (ed.sel_cnt != 1) { + ed.sel_rot = QuaternionIdentity(); + } +} + +/* tools */ +static void +sel_ray(void) { + Ray ray = GetScreenToWorldRay(end_point, main_cam.cam); + + b3QueryFilter filter = { + .categoryBits = BIT(PHYS_MASK_EDITOR_RAYCAST), + .maskBits = BIT(PHYS_MASK_EDITOR_SELECTABLE), + }; + + Vec3 dir = Vector3Scale(ray.direction, 1000.0f); + b3RayResult res = + b3World_CastRayClosest(physics.world_id, + *(b3Pos*)&ray.position, + *(b3Pos*)&dir, + filter); + + if (ed.focus == ED_PNL_NULL && + !IsKeyDown(KEY_LEFT_SHIFT)) { + entities_clear_selected(); + } + + if (res.hit) { + b3BodyId bid = b3Shape_GetBody(res.shapeId); + u64 eid = *(u64*)b3Body_GetUserData(bid); + Entity *ent = entity_get(eid); + + if (ent->pid == 0) { + return; + } + + ent->ed.selected = !ent->ed.selected; + } +} + +static void +sel_area(void) { + Vec3 fwd = Vector3Subtract(main_cam.cam.target, main_cam.cam.position); + + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (ent->pid == 0) { + continue; + } + + if (!IsKeyDown(KEY_LEFT_SHIFT)) { + ent->ed.selected = false; + } + + Vec3 world_pos = entity_world_pos(id); + Vec3 offset = + Vector3Subtract(world_pos, main_cam.cam.position); + + // Only the dot product’s sign matters, so neither vector + // needs normalization. + if (Vector3DotProduct(fwd, offset) <= 0) { + continue; + } + + Vec2 pos = GetWorldToScreen(world_pos, main_cam.cam); + if (CheckCollisionPointRec(pos, sel_rec)) { + ent->ed.selected = true; + } + } +} + +static void +sel_rec_draw(void) { + DrawRectangleRec(sel_rec, (Color) { 255, 255, 255, 40 }); + DrawRectangleLines(sel_rec.x, sel_rec.y, + sel_rec.width, sel_rec.height, + (Color) { 255, 255, 255, 80 }); +} + +static bool +selection_root(Entity *ent) { + if (!ent->ed.selected) { + return false; + } + + for (u64 parent = ent->tr.parent; parent;) { + Entity *ancestor = entity_get(parent); + assert(ancestor); + + if (ancestor->ed.selected) { + return false; + } + + parent = ancestor->tr.parent; + } + + return true; +} + +static Vec3 +selected_pivot(void) { + Vec3 pivot = {0}; + int count = 0; + + for (int i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (!ent || !ent->ed.selected) { + continue; + } + + pivot = Vector3Add(pivot, entity_world_pos(id)); + count++; + } + + if (count > 0) { + pivot = Vector3Scale(pivot, 1.0f / (f32)count); + } + + return pivot; +} + +void +editor_select(void) { + if (!started) { + start_point = GetMousePosition(); + started = true; + } + + end_point = GetMousePosition(); + sel_dist = Vector2Distance(start_point, end_point); + sel_rec = (Rect) { + fminf(start_point.x, end_point.x), + fminf(start_point.y, end_point.y), + fabs(end_point.x - start_point.x), + fabs(end_point.y - start_point.y) + }; + + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + if (sel_dist < 20) { + sel_ray(); + } else { + sel_area(); + } + started = false; + sel_dist = 0; + app.mode = APP_MODE_EDIT_NONE; + } +} + +static bool +ray_plane_hit(Vec3 plane_pos, Vec3 plane_dir, Vec3 *out) { + Ray ray = GetScreenToWorldRay(GetMousePosition(), main_cam.cam); + + f32 denom = Vector3DotProduct(ray.direction, plane_dir); + if (fabsf(denom) < 0.0001f) { + return false; + } + + Vec3 offset = Vector3Subtract(plane_pos, ray.position); + f32 dist = Vector3DotProduct(offset, plane_dir) / denom; + + if (dist < 0.0f) { + return false; + } + + *out = Vector3Add(ray.position, Vector3Scale(ray.direction, dist)); + + return true; +} + +void +editor_move(void) { + static Vec3 mv_start = {0}; + + if (!tr_action.item) { + tr_action = action_begin(ED_ACTION_TRANSFORM); + started = false; + } + + bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt; + if (cancel || IsKeyPressed(KEY_ENTER) || + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (cancel) { + action_apply(&tr_action, true); + action_free(&tr_action); + tr_action = (EditorAction){0}; + } else { + action_commit(&tr_action); + } + started = false; + app.mode = APP_MODE_EDIT_NONE; + return; + } + + Vec3 pivot = selected_pivot(); + Vec3 axis = { 0, 1, 0 }; // mv plane normal + Vec3 delta = {0}; + + if (IsKeyDown(KEY_LEFT_ALT)) { + started = false; + + Vec2 a = GetWorldToScreen(pivot, main_cam.cam); + Vec2 b = GetWorldToScreen( + Vector3Add(pivot, axis), main_cam.cam); + + Vec2 scr_axis = Vector2Subtract(b, a); + f32 lensqr = Vector2LengthSqr(scr_axis); + + if (lensqr <= 0.001f) { + return; + } + + f32 amount = + Vector2DotProduct(GetMouseDelta(), scr_axis) / lensqr; + + delta = Vector3Scale(axis, amount); + } else { + Vec3 hit; + if (!ray_plane_hit(pivot, axis, &hit)) { + return; + } + + if (!started) { + mv_start = hit; + started = true; + return; + } + + delta = Vector3Subtract(hit, mv_start); + mv_start = hit; + } + + for (u32 i = 0; i < tr_action.item_cnt; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + Vec3 pos = Vector3Add(entity_world_pos(id), delta); + entity_set_world_pos(ent, pos); + } +} + +void +editor_rotate(void) { + if (!tr_action.item) { + tr_action = action_begin(ED_ACTION_TRANSFORM); + started = false; + } + + bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt; + if (cancel || IsKeyPressed(KEY_ENTER) || + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (cancel) { + action_apply(&tr_action, true); + action_free(&tr_action); + tr_action = (EditorAction){0}; + } else { + action_commit(&tr_action); + } + + started = false; + app.mode = APP_MODE_EDIT_NONE; + return; + } + + Vec3 pivot = selected_pivot(); + Vec3 mouse_hit = {0}; + if (!ray_plane_hit(pivot, (Vec3) {0,1,0}, &mouse_hit)) { + return; + } + + Vec3 dir = Vector3Subtract(mouse_hit, pivot); + if (Vector3LengthSqr(dir) < 0.0001f) { + return; + } + + f32 yaw = atan2f(dir.x, dir.z); + Quaternion rot = QuaternionFromAxisAngle((Vec3){0,1,0}, yaw); + + for (u32 i = 0; i < tr_action.item_cnt; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + entity_set_world_rot(ent, rot); + } +} + +/* draw */ +static void +panel_vec3(Rect bounds, Vec3 *val, cstr label) { + +} + +static void +statusline(void) { + char mode_str[16] = {0}; + char ctx_str[256] = {0}; + // Color bg = { 255, 255, 255, 150 }; + Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); + + switch (app.mode) { + case APP_MODE_EDIT_NONE: + snprintf(mode_str, 16, "EDITOR"); + if (ed.sel_cnt > 0) { + snprintf(ctx_str, sizeof(ctx_str), + "%i Selected | " + "G: move, R: rotate, D: duplicate, " + "X: delete", + ed.sel_cnt); + } else { + snprintf(ctx_str, sizeof(ctx_str), "Nothing selected"); + } + break; + + case APP_MODE_EDIT_SELECT: + snprintf(mode_str, 16, "SELECT"); + break; + + case APP_MODE_EDIT_CTRL: + snprintf(mode_str, 16, "VIEW"); + snprintf(ctx_str, sizeof(ctx_str), "controls: " + "shift: boost speed, scroll: adjust speed"); + break; + + case APP_MODE_EDIT_MOVE: + snprintf(mode_str, 16, "MOVE"); + + cstr axis_str = IsKeyDown(KEY_LEFT_ALT) ? + "up axis" : + "xz plane"; + + snprintf(ctx_str, sizeof(ctx_str), + "%i Selected | " + "current axis: %s | " + "controls: shift: grid snap, " + "alt: up axis, X/Y/Z: toggle axis", + ed.sel_cnt, axis_str); + break; + + case APP_MODE_EDIT_ROTATE: + snprintf(mode_str, 16, "ROTATE"); + snprintf(ctx_str, sizeof(ctx_str), "%i Selected | " + "axis: global up, pivot: each entity | " + "controls: ctrl: change pivot, alt: local/global, " + "X/Y/Z: toggle axis", + ed.sel_cnt); + break; + + default: + return; + break; + }; + + /* statusline bg */ + Rect rec = { + // 0, 0, /* top bar */ + 0, GetScreenHeight() - 25, + GetScreenWidth(), 25 + // editor_label_w(mode_str) , 25 + }; + DrawRectangleRec(rec, bg); + + /* mode str */ + f32 mode_str_w = editor_label_w(mode_str); + rec.width = 70; + DrawRectangleRec(rec, (Color){0,0,0,25}); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiLabel(rec, mode_str); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + + /* context str */ + f32 ctx_str_w = editor_label_w(ctx_str); + rec.x = 80; + rec.width = ctx_str_w; + GuiLabel(rec, ctx_str); + rec.x += ctx_str_w; + + /* scene file status str */ + char scene_str[96]; + if (ed.history_save_csr == UINT32_MAX) { + snprintf(scene_str, sizeof(scene_str), + "| scene.fgs * - unsaved changes"); + } else { + u32 changes = ed.history_csr > ed.history_save_csr + ? ed.history_csr - ed.history_save_csr + : ed.history_save_csr - ed.history_csr; + + snprintf(scene_str, sizeof(scene_str), + "| scene.fgs%s - %u change%s", + changes ? " *" : "", + changes, changes == 1 ? "" : "s"); + } + + f32 scene_str_w = editor_label_w(scene_str); + rec.x = GetScreenWidth() - scene_str_w - 5; + rec.width = scene_str_w; + + DrawRectangleRec(rec, bg); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiLabel(rec, scene_str); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); +} + +/* Scene panel */ +static void +scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { + Entity *ent = entity_get(id); + if (!ent) { + return; + } + + /* Transient entities are hidden, but their children are visited. */ + if (ent->pid) { + Rect row = { view.x, *y, view.width, 26 }; + bool hovered = input && + CheckCollisionPointRec(GetMousePosition(), view) && + CheckCollisionPointRec(GetMousePosition(), row); + + if (hovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (IsKeyDown(KEY_LEFT_SHIFT)) { + ent->ed.selected = !ent->ed.selected; + } else { + entities_clear_selected(); + ent->ed.selected = true; + } + } + + if (row.y + row.height > view.y && + row.y < view.y + view.height) { + if (ent->ed.selected || hovered) { + int colour = ent->ed.selected + ? BASE_COLOR_PRESSED + : BASE_COLOR_FOCUSED; + + DrawRectangleRec(row, + GetColor(GuiGetStyle(DEFAULT, + colour))); + } + + Rect label = { + row.x + 8 + depth * 18, + row.y, + fmaxf(0, row.width - 8 - depth * 18), + row.height + }; + + if (label.width > 0) { + GuiLabel(label, + ent->name && ent->name[0] ? + ent->name : "Entity"); + } + } + + *y += row.height; + depth++; + } + + for (u64 child = ent->tr.first_child; child;) { + Entity *next = entity_get(child); + if (!next) { + break; + } + + u64 sibling = next->tr.next; + scene_entity(child, view, y, depth, input); + child = sibling; + } +} + +static void +panel_scene(EditorPanelContainer container, bool input) { + Rect view = {0}; + Rect bounds = ed.panel[container].bounds; + Vec2 *scroll = &ed.panel[container].scroll; + + input = input && app.mode == APP_MODE_EDIT_NONE; + + int state = GuiGetState(); + if (!input) { + GuiSetState(STATE_DISABLED); + } + + if (input && state != STATE_DISABLED && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + ed.focus_captured = true; + } + + GuiPanel(bounds, "Entities"); + + f32 btn_w = editor_label_w("Create Entity") + 16; + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + if (GuiButton( + (Rect){ bounds.x + 8, bounds.y + 30, btn_w, 26 }, + "Create Entity")) { + EditorAction action = action_begin(ED_ACTION_CREATE); + action_commit(&action); + } + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + + bounds.x += 8; + bounds.y += 64; + bounds.width -= 16; + bounds.height -= 72; + + u32 count = 0; + for (u32 i = 0; i < entity_count(); ++i) { + if (entity_get(entity_id_at(i))->pid) { + count++; + } + } + + f32 width = bounds.width + - 2 * GuiGetStyle(DEFAULT, BORDER_WIDTH) + - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); + + GuiScrollPanel(bounds, NULL, + (Rect){ 0, 0, width, count * 26.0f }, + scroll, &view); + + BeginScissorMode( + (int)view.x, (int)view.y, + (int)view.width, (int)view.height); + + f32 y = view.y + scroll->y; + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (!ent->tr.parent) { + scene_entity(id, view, &y, 0, + input && state != STATE_DISABLED); + } + } + + EndScissorMode(); + GuiSetState(state); + + if (input && container != ED_PNL_CENTER && + !IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { + ed.focus_captured = false; + } +} + +// static void +// panel_properties(EditorPanelContainer container, bool input) { +// Rect view = {0}; +// Rect bounds = ed.panel[container].bounds; +// Vec2 *scroll = &ed.panel[container].scroll; + +// int state = GuiGetState(); + +// input = input && app.mode == +// APP_MODE_EDIT_NONE && state != STATE_DISABLED; + +// if (!input) { +// GuiSetState(STATE_DISABLED); +// } + +// if (input && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { +// ed.focus_captured = true; +// } + +// GuiPanel(bounds, "Entity Properties"); + +// /* needed for input field focus */ +// enum { +// TEXT_FIELD_NONE, + +// TEXT_FIELD_NAME, + +// /* transform */ +// TEXT_FIELD_TR_POS_X, +// TEXT_FIELD_TR_POS_Y, +// TEXT_FIELD_TR_POS_Z, +// TEXT_FIELD_TR_ROT_X, +// TEXT_FIELD_TR_ROT_Y, +// TEXT_FIELD_TR_ROT_Z, +// TEXT_FIELD_TR_SCALE_X, +// TEXT_FIELD_TR_SCALE_Y, +// TEXT_FIELD_TR_SCALE_Z, +// }; + +// static int text_focus; +// static char text_input[64]; +// char display_text[sizeof(text_input)]; + +// Rect prec = bounds; +// f32 pad = 8; +// cstr substr = ""; + +// if (!ed.sel_cnt) { +// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); +// cstr str = "no entities selected"; +// GuiLabel(bounds, str); +// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); +// text_focus = 0; +// goto reset; +// } + +// /* ed.sel_cnt can't be 0 because we've already checked and returned */ +// if (ed.sel_cnt > 1) { +// substr = "<multiple entities selected>"; +// } else { +// for (u32 i = 0; i < entity_count(); ++i) { +// Entity *ent = entity_get(entity_id_at(i)); +// if (ent && ent->pid && ent->ed.selected) { +// substr = ent->name ? ent->name : ""; +// break; +// } +// } +// } + +// if (text_focus != TEXT_FIELD_NAME) { +// snprintf(display_text, sizeof(display_text), "%s", substr); +// } + +// cstr name_lbl = "Name"; +// u32 name_lbl_w = editor_label_w(name_lbl); +// prec.x += pad; +// prec.y += 30; +// prec.width = name_lbl_w; +// prec.height = 20; +// GuiLabel(prec, name_lbl); + +// prec.x += name_lbl_w + pad; +// prec.width = bounds.x + bounds.width - prec.x - pad; + +// bool name_edit = text_focus == TEXT_FIELD_NAME; +// bool name_toggle = +// GuiTextBox( +// prec, +// name_edit ? text_input : display_text, +// sizeof(text_input), +// input && name_edit +// ); + +// if (input) { +// if (IsKeyPressed(KEY_ESCAPE)) { +// if (name_edit) { +// text_focus = 0; +// } +// } else if (name_toggle) { +// if (name_edit) { +// if (IsKeyPressed(KEY_ENTER)) { +// editor_rename_selected(text_input); +// } +// text_focus = 0; +// } else { +// snprintf(text_input, +// sizeof(text_input), +// "%s", +// ed.sel_cnt > 1 ? +// "" : display_text); +// text_focus = TEXT_FIELD_NAME; +// } +// } +// } + +// /* +// NOTE (tech kaczynski - 21/9/6): +// I had a choice here to either create a tagged union of components and +// iterate over them, or to write a giant function like this. I chose the +// latter because the former would require a big refactor that I don't +// really care for at this stage. The former still requires a loop and a +// switch or branches for each type, which requires extra indentation or +// abstraction into special helper functions etc. For now, the code +// duplication and verbosity I can live with and might not be much worse +// than the alternative anyway, plus I get to avoid refactoring every +// system that touches entity component data. +// */ + +// /* transform comp */ +// { +// prec.x = bounds.x + pad; +// prec.y += 30; +// prec.width = bounds.width - pad * 2; +// prec.height = 90 + pad * 2; +// GuiGroupBox(prec, "Transform"); + +// /* pos */ +// prec.height = 20; +// prec.y += 15; +// prec.x += pad; +// prec.width -= pad; +// GuiLabel(prec, "Pos"); +// // panel_vec3(prec, ent->pos, cstr label) + +// /* rot */ +// static Vec3 rot; // euler angle representation of entity rot +// prec.y += 30; +// GuiLabel(prec, "Rot"); +// { + +// } + +// /* scale */ +// prec.y += 30; +// GuiLabel(prec, "Scale"); + +// // GuiTextBox(prec, char *text, int textSize, false); +// } + +// /* reset */ +// reset: +// GuiSetState(state); +// if (input && container != ED_PNL_CENTER) { +// ed.focus_captured = text_focus; +// } +// } + +static void +panel_properties(EditorPanelContainer container, bool input) { + Rect view = {0}; + Rect bounds = ed.panel[container].bounds; + Vec2 *scroll = &ed.panel[container].scroll; + + int state = GuiGetState(); + + input = input && app.mode == + APP_MODE_EDIT_NONE && state != STATE_DISABLED; + + if (!input) { + GuiSetState(STATE_DISABLED); + } + + if (input && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + ed.focus_captured = true; + } + + GuiPanel(bounds, "Entity Properties"); + + /* needed for input field focus */ + enum { + TEXT_FIELD_NONE, + + TEXT_FIELD_NAME, + + /* transform */ + TEXT_FIELD_TR_POS_X, + TEXT_FIELD_TR_POS_Y, + TEXT_FIELD_TR_POS_Z, + TEXT_FIELD_TR_ROT_X, + TEXT_FIELD_TR_ROT_Y, + TEXT_FIELD_TR_ROT_Z, + TEXT_FIELD_TR_SCALE_X, + TEXT_FIELD_TR_SCALE_Y, + TEXT_FIELD_TR_SCALE_Z, + }; + + static int text_focus; + static char text_input[64]; + char display_text[sizeof(text_input)]; + + Rect prec = bounds; + f32 pad = 8; + cstr substr = ""; + + if (!ed.sel_cnt) { + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + cstr str = "no entities selected"; + GuiLabel(bounds, str); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + text_focus = 0; + goto reset; + } + + /* ed.sel_cnt can't be 0 because we've already checked and returned */ + Entity *ent = NULL; // only set if sel_cnt == 1 + if (ed.sel_cnt > 1) { + substr = "<multiple entities selected>"; + } else { + for (u32 i = 0; i < entity_count(); ++i) { + ent = entity_get(entity_id_at(i)); + if (ent && ent->pid && ent->ed.selected) { + substr = ent->name ? ent->name : ""; + break; + } + } + } + + if (text_focus != TEXT_FIELD_NAME) { + snprintf(display_text, sizeof(display_text), "%s", substr); + } + + cstr name_lbl = "Name"; + u32 name_lbl_w = editor_label_w(name_lbl); + prec.x += pad; + prec.y += 30; + prec.width = name_lbl_w; + prec.height = 20; + GuiLabel(prec, name_lbl); + + prec.x += name_lbl_w + pad; + prec.width = bounds.x + bounds.width - prec.x - pad; + + bool name_edit = text_focus == TEXT_FIELD_NAME; + bool name_toggle = + GuiTextBox( + prec, + name_edit ? text_input : display_text, + sizeof(text_input), + input && name_edit + ); + + if (input) { + if (IsKeyPressed(KEY_ESCAPE)) { + if (name_edit) { + text_focus = 0; + } + } else if (name_toggle) { + if (name_edit) { + if (IsKeyPressed(KEY_ENTER)) { + editor_rename_selected(text_input); + } + text_focus = 0; + } else { + snprintf(text_input, + sizeof(text_input), + "%s", + ed.sel_cnt > 1 ? + "" : display_text); + text_focus = TEXT_FIELD_NAME; + } + } + } + + /* + NOTE (tech kaczynski - 21/9/6): + I had a choice here to either create a tagged union of components and + iterate over them, or to write a giant function like this. I chose the + latter because the former would require a big refactor that I don't + really care for at this stage. The former still requires a loop and a + switch or branches for each type, which requires extra indentation or + abstraction into special helper functions etc. For now, the code + duplication and verbosity I can live with and might not be much worse + than the alternative anyway, plus I get to avoid refactoring every + system that touches entity component data. + */ + + /* transform comp */ + { + prec.x = bounds.x + pad; + prec.y += 30; + prec.width = bounds.width - pad * 2; + prec.height = 90 + pad * 2; + GuiGroupBox(prec, "Transform"); + + static Vec3 pos, rot, scale; + // pos = panel_vec3(prec, pos, "pos"); + // rot = panel_vec3(prec, rot, "rot"); + // scale = panel_vec3(prec, scale, "scale"); + + // /* pos */ + // prec.height = 20; + // prec.y += 15; + // prec.x += pad; + // prec.width -= pad; + // GuiLabel(prec, "Pos"); + // // panel_vec3(prec, ent->pos, cstr label) + + // /* rot */ + // static Vec3 rot; // euler angle representation of entity rot + // prec.y += 30; + // GuiLabel(prec, "Rot"); + // if (ent) { + // rot = QuaternionToEuler(ent->tr.rot); + // } + // for (int i = 0; i < 3; ++i) { + + // } + + // /* scale */ + // prec.y += 30; + // GuiLabel(prec, "Scale"); + + // GuiTextBox(prec, char *text, int textSize, false); + } + + /* reset */ + reset: + GuiSetState(state); + if (input && container != ED_PNL_CENTER) { + ed.focus_captured = text_focus; + } +} + +static void +panel_space(EditorPanelContainer container) { + static cstr strs[][2] = { + { "Q", "Toggle left panel" }, + { "E", "Toggle right panel" }, + { "/", "Command Menu" }, + }; + static const int str_cnt = sizeof(strs) / sizeof(strs[0]); + + Rect bounds = { + .x = (f32)GetScreenWidth() / 2 - 125, + .y = (f32)GetScreenHeight() / 2 - 65, + .width = 250, + .height = 130, + }; + + Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); + // GuiPanel(bounds, "Space Menu"); + DrawRectangleRec(bounds, bg); + GuiGroupBox((Rect) { + .x = bounds.x + 5, + .y = bounds.y + 10, + .width = bounds.width - 10, + .height = bounds.height - 15, + }, "Space Menu"); + + Rect lrec = bounds; + lrec.height = 20; + lrec.x += 16; + + for (int i = 0; i < str_cnt; ++i) { + lrec.y += 30; + GuiLabel(lrec, strs[i][0]); + + lrec.x += 50; + GuiLabel(lrec, strs[i][1]); + lrec.x -= 50; + } +} + +static void +panel_draw(EditorPanelContainer container) { + if (!ed.panel[container].enabled) { + return; + } + + bool input = app.mode == APP_MODE_EDIT_NONE && + ed.focus == container && + (!ed.panel[ED_PNL_CENTER].enabled || + container == ED_PNL_CENTER); + + Rect bounds = ed.panel[container].bounds; + int state = GuiGetState(); + + if (!input) { + GuiSetState(STATE_DISABLED); + } + + switch (ed.panel[container].type) { + case ED_PNL_TYPE_SCENE: + panel_scene(container, input); + break; + + case ED_PNL_TYPE_PROPERTIES: + panel_properties(container, input); + break; + + case ED_PNL_TYPE_SPACE: + bool exit = false; + + if (IsKeyPressed(KEY_ESCAPE)) { + exit = true; + } + + if (IsKeyPressed(KEY_Q)) { + ed.panel[ED_PNL_LEFT].enabled = + !ed.panel[ED_PNL_LEFT].enabled; + exit = true; + } + + if (IsKeyPressed(KEY_E)) { + ed.panel[ED_PNL_RIGHT].enabled = + !ed.panel[ED_PNL_RIGHT].enabled; + exit = true; + } + + if (exit) { + ed.panel[container].enabled = false; + ed.focus = ED_PNL_NULL; + ed.focus_captured = false; + app.mode = APP_MODE_EDIT_NONE; + } + panel_space(container); + break; + + case ED_PNL_TYPE_CMD: + GuiPanel(bounds, "Command Menu"); + break; + } + + GuiSetState(state); +} + +void +editor_draw_ui(void) { + statusline(); + + /* L/R panel info */ + f32 screen_w = GetScreenWidth(); + f32 screen_h = GetScreenHeight(); + f32 left_w = Clamp(screen_w * 0.16f, 210.0f, 280.0f); + f32 right_w = Clamp(screen_w * 0.22f, 260.0f, 360.0f); + f32 status_h = 25.0f; + f32 margin = 8.0f; + + ed.panel[ED_PNL_LEFT].bounds = (Rect) { + margin, + margin, + left_w, + screen_h - status_h - margin * 2.0f + }; + + ed.panel[ED_PNL_RIGHT].bounds = (Rect) { + screen_w - right_w - margin, + margin, + right_w, + screen_h - status_h - margin * 2.0f + }; + + panel_draw(ED_PNL_LEFT); + panel_draw(ED_PNL_RIGHT); + + if (app.mode == APP_MODE_EDIT_SELECT) { + if (started && sel_dist > 20) { + sel_rec_draw(); + } + } + + if (ed.panel[ED_PNL_CENTER].enabled) { + f32 width = fminf(640, screen_w - margin * 2); + f32 height = fminf(420, screen_h - status_h - margin * 2); + + ed.panel[ED_PNL_CENTER].bounds = (Rect){ + (screen_w - width) * 0.5f, + (screen_h - status_h - height) * 0.5f, + width, height + }; + + DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), + Fade(BLACK, 0.35f)); + panel_draw(ED_PNL_CENTER); + } +} + +/* 3D gizmos */ +static void +gizmo_transform(void) { + Vec3 point = ed.sel_point; + Quaternion rot = ed.sel_rot; + + Vec3 right = Vector3RotateByQuaternion((Vec3){1, 0, 0}, rot); + Vec3 up = Vector3RotateByQuaternion((Vec3){0, 1, 0}, rot); + Vec3 fwd = Vector3RotateByQuaternion((Vec3){0, 0, 1}, rot); + + f32 len = 0.75f; + + right = Vector3Scale(right, len); + up = Vector3Scale(up, len); + fwd = Vector3Scale(fwd, len); + + DrawLine3D(point, Vector3Add(point, fwd), BLUE); + DrawLine3D(point, Vector3Add(point, up), GREEN); + DrawLine3D(point, Vector3Add(point, right), RED); +} + +static void +gizmo_move(void) { + Color col = { 255, 255, 255, 150 }; + Vec3 point = ed.sel_point; + + if (IsKeyDown(KEY_LEFT_ALT)) { + DrawLine3D(point, Vector3Add(point, (Vec3){0,100,0}), col); // GREEN + DrawLine3D(point, Vector3Add(point, (Vec3){0,-100,0}), col); + } else { + DrawLine3D(point, Vector3Add(point, (Vec3){100,0,0}), col); //RED + DrawLine3D(point, Vector3Add(point, (Vec3){-100,0,0}), col); + + DrawLine3D(point, Vector3Add(point, (Vec3){0,0,100}), col); //BLUE + DrawLine3D(point, Vector3Add(point, (Vec3){0,0,-100}), col); + } +} + +static void +gizmo_rotate(void) { + Vec3 point = ed.sel_point; + Color line_col = { 255, 255, 255, 150 }; + Color circle_col = { 255, 255, 255, 100 }; + if (ray_plane_hit(point, (Vec3){0,1,0}, &point)) { + DrawLine3D(ed.sel_point, point, line_col); + DrawCircle3D(ed.sel_point, 1.5, (Vec3){1,0,0}, 90, circle_col); + } +} + +void +editor_draw_3d(void) { + if (app.mode == APP_MODE_EDIT_MOVE) { + gizmo_move(); + } + + if (app.mode == APP_MODE_EDIT_ROTATE) { + gizmo_rotate(); + } + + if (ed.sel_cnt > 0) { + gizmo_transform(); + } +} + +/* utils */ +f32 +editor_label_w(cstr text) { + return (f32)GuiGetTextWidth(text) + + 2.0f * GuiGetStyle(LABEL, BORDER_WIDTH) + + 2.0f * GuiGetStyle(LABEL, TEXT_PADDING) + + 2.0f; +} + +void +editor_rename_selected(cstr name) { + EditorAction action = action_begin(ED_ACTION_RENAME); + + for (u32 i = 0; i < action.item_cnt; ++i) { + u64 id = entity_id_from_pid(action.item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + char *new_name = strdup(name); + if (!new_name) { + perror("rename alloc failed."); + exit(1); + } + + action.item[i].before.rename.name = ent->name; + ent->name = new_name; + } + + action_commit(&action); +} |
