techkaczynski.dev / code

summaryrefslogtreecommitdiff
path: root/editor.c
diff options
context:
space:
mode:
Diffstat (limited to 'editor.c')
-rw-r--r--editor.c1744
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);
+}