#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, }, }, }; typedef enum { MDL_PICK_COMP_ADD, MDL_PICK_COMP_UPDATE, MDL_PICK_ENT_CREATE, } ModelPickerMode; /* forward decl */ static bool selection_root(Entity *ent); static void action_apply(EditorAction*, bool); static void action_free(EditorAction*); static void model_picker_open(ModelPickerMode mode); static void model_picker_close(void); 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, bool roots_only) { 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: recursive = true; break; case ED_ACTION_RENAME: case ED_ACTION_MODEL: 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; case ED_ACTION_MODEL: before->model = ent->mdl; before->model.key = ent->mdl.key ? strdup(ent->mdl.key) : NULL; 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; case ED_ACTION_MODEL: for (u32 i = 0; i < action->item_cnt; ++i) { u64 id = entity_id_from_pid(action->item[i].pid); Entity *ent = entity_get(id); ModelCmp model = ent->mdl; ent->mdl = action->item[i].before.model; action->item[i].before.model = model; } 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; case ED_ACTION_MODEL: for (u32 i = 0; i < action->item_cnt; ++i) { free(action->item[i].before.model.key); } 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; case ED_ACTION_MODEL: 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); ModelCmp *before = &action->item[i].before.model; ModelCmp *after = &ent->mdl; changed |= before->has != after->has || before->draw != after->draw || strcmp(before->key ? before->key : "", after->key ? after->key : "") != 0; } 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) { model_picker_close(); 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, true); 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, true); 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, true); 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 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, false); 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; } } typedef struct { int focus; int next_id; bool input; char text[64]; } PanelEdit; // mixed means “show a blank field and start editing with an empty buffer”, // currently used for when there's multiple entities selected. static bool panel_field(PanelEdit *edit, Rect box, cstr text, bool mixed, bool numeric) { int id = edit->next_id++; bool editing = edit->focus == id; char display[sizeof(edit->text)]; snprintf(display, sizeof(display), "%s", text); bool toggle; if (numeric) { f32 draft = 0; toggle = GuiValueBoxFloat(box, NULL, editing ? edit->text : display, &draft, edit->input && editing); } else { toggle = GuiTextBox(box, editing ? edit->text : display, sizeof(edit->text), edit->input && editing); } if (!edit->input) { return false; } if (editing && IsKeyPressed(KEY_ESCAPE)) { edit->focus = 0; return false; } if (!toggle) { return false; } if (!editing) { memset(edit->text, 0, sizeof(edit->text)); // snprintf(edit->text, sizeof(edit->text), "%s", // mixed ? "" : display); edit->focus = id; return false; } edit->focus = 0; return IsKeyPressed(KEY_ENTER); } static bool panel_bool(Rect row, cstr label, bool *value, bool mixed) { GuiLabel((Rect){row.x, row.y, 40, row.height}, label); if (mixed) { *value = false; GuiLabel((Rect){ row.x + 68, row.y, row.width - 68, row.height, }, "(mixed)"); } return GuiCheckBox((Rect){ row.x + 40, row.y + (row.height - 20) * 0.5f, 20, 20, }, "", value); } static bool panel_float(PanelEdit *edit, Rect box, f32 *value, bool mixed) { char display[64] = ""; if (!mixed) { snprintf(display, sizeof(display), "%.3f", *value); } if (!panel_field(edit, box, display, mixed, true)) { return false; } char *end; f32 parsed = strtof(edit->text, &end); if (end == edit->text || *end != '\0') { return false; } *value = parsed; return true; } /* Returns the submitted axis, or -1 when nothing was submitted. */ static int panel_vec3(PanelEdit *edit, Rect row, cstr label, Vec3 *value, bool mixed) { GuiLabel((Rect){ row.x, row.y, 45, row.height }, label); f32 *axis[3] = { &value->x, &value->y, &value->z }; f32 width = (row.width - 45) / 3; int submitted = -1; for (int i = 0; i < 3; ++i) { Rect box = { row.x + 45 + i * width, row.y, width - 4, row.height }; if (panel_float(edit, box, axis[i], mixed)) { submitted = i; } } return submitted; } static void panel_properties(EditorPanelContainer container, bool input) { static PanelEdit edit; static bool add_open; static int add_choice; Rect bounds = ed.panel[container].bounds; int state = GuiGetState(); f32 pad = 8; input = input && app.mode == APP_MODE_EDIT_NONE && state != STATE_DISABLED; edit.next_id = 1; edit.input = input; if (!input) { add_open = false; edit.focus = 0; GuiSetState(STATE_DISABLED); } if (add_open) { GuiLock(); } GuiPanel(bounds, "Entity Properties"); if (!ed.sel_cnt) { edit.focus = 0; GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); GuiLabel(bounds, "no entities selected"); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); goto reset; } Entity *ent = NULL; bool mixed = ed.sel_cnt > 1; if (!mixed) { for (u32 i = 0; i < entity_count(); ++i) { Entity *candidate = entity_get(entity_id_at(i)); if (candidate && candidate->pid && candidate->ed.selected) { ent = candidate; break; } } } ModelCmp *model = NULL; int model_cnt = 0; bool key_mixed = false; bool draw_mixed = false; for (u32 i = 0; i < entity_count(); ++i) { Entity *candidate = entity_get(entity_id_at(i)); if (!candidate->pid || !candidate->ed.selected || !candidate->mdl.has) { continue; } model_cnt++; if (!model) { model = &candidate->mdl; } else { key_mixed |= strcmp(model->key, candidate->mdl.key) != 0; draw_mixed |= model->draw != candidate->mdl.draw; } } bool all_have_model = model_cnt == ed.sel_cnt; /* Name */ cstr name = mixed ? "" : (ent && ent->name ? ent->name : ""); f32 label_w = editor_label_w("Name"); Rect name_box = { bounds.x + pad, bounds.y + 30, label_w, 20 }; GuiLabel(name_box, "Name"); name_box.x += label_w + pad; name_box.width = bounds.x + bounds.width - name_box.x - pad; if (panel_field(&edit, name_box, name, mixed, false)) { editor_rename_selected(edit.text); } /* Transform */ Rect group = { bounds.x + pad, bounds.y + 60, bounds.width - 2 * pad, 90 + 2 * pad }; GuiGroupBox(group, "Transform"); Rect row = { group.x + pad, group.y + 15, group.width - 2 * pad, 20 }; Vec3 pos = ent ? ent->tr.pos : (Vec3){0}; int axis = panel_vec3(&edit, row, "pos", &pos, mixed); if (axis >= 0) { EditorAction action = action_begin(ED_ACTION_TRANSFORM, false); f32 value[3] = { pos.x, pos.y, pos.z }; for (u32 j = 0; j < action.item_cnt; ++j) { Entity *target = entity_get( entity_id_from_pid(action.item[j].pid) ); f32 *component[3] = { &target->tr.pos.x, &target->tr.pos.y, &target->tr.pos.z, }; *component[axis] = value[axis]; } action_commit(&action); } row.y += 30; Vec3 rot = ent ? QuaternionToEuler(ent->tr.rot) : (Vec3) {0}; rot.x *= RAD2DEG; rot.y *= RAD2DEG; rot.z *= RAD2DEG; axis = panel_vec3(&edit, row, "rot", &rot, mixed); if (axis >= 0) { EditorAction action = action_begin(ED_ACTION_TRANSFORM, false); f32 value[3] = { rot.x * DEG2RAD, rot.y * DEG2RAD, rot.z * DEG2RAD }; for (u32 j = 0; j < action.item_cnt; ++j) { Entity *target = entity_get( entity_id_from_pid(action.item[j].pid) ); Vec3 rt = QuaternionToEuler(target->tr.rot); f32 *component[3] = { &rt.x, &rt.y, &rt.z }; *component[axis] = value[axis]; target->tr.rot = QuaternionFromEuler(rt.x, rt.y, rt.z); } action_commit(&action); } row.y += 30; Vec3 scale = ent ? ent->tr.scale : (Vec3){0}; axis = panel_vec3(&edit, row, "scale", &scale, mixed); if (axis >= 0) { EditorAction action = action_begin(ED_ACTION_TRANSFORM, false); f32 value[3] = { scale.x, scale.y, scale.z }; for (u32 j = 0; j < action.item_cnt; ++j) { Entity *target = entity_get( entity_id_from_pid(action.item[j].pid) ); f32 *component[3] = { &target->tr.scale.x, &target->tr.scale.y, &target->tr.scale.z, }; *component[axis] = value[axis]; } action_commit(&action); } model_comp: if (!all_have_model) { goto comp_add; } group.y += group.height + 16; group.height = 80; GuiGroupBox(group, "Model"); row = (Rect){ group.x + pad, group.y + 15, group.width - 2 * pad, 20, }; GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); if (GuiButton((Rect){ group.x, group.y + group.height + pad, 120, 20 }, "Remove Model")) { edit.focus = 0; EditorAction action = action_begin(ED_ACTION_MODEL, false); for (u32 i = 0; i < action.item_cnt; ++i) { Entity *target = entity_get( entity_id_from_pid(action.item[i].pid) ); free(target->mdl.key); target->mdl = (ModelCmp){0}; } action_commit(&action); goto reset; } GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); GuiLabel((Rect){row.x, row.y, 32, row.height}, "Key"); if (GuiButton( (Rect){row.x + 40, row.y, row.width - 40, row.height}, key_mixed ? "" : GetFileName(model->key))) { edit.focus = 0; model_picker_open(MDL_PICK_COMP_UPDATE); } row.y += 30; bool draw = draw_mixed ? false : model->draw; if (panel_bool(row, "Draw", &draw, draw_mixed)) { edit.focus = 0; EditorAction action = action_begin(ED_ACTION_MODEL, false); for (u32 i = 0; i < action.item_cnt; ++i) { Entity *target = entity_get( entity_id_from_pid(action.item[i].pid) ); target->mdl.draw = draw; } action_commit(&action); } row.y = group.y + group.height + 20; comp_add: GuiUnlock(); row.y += 40; int add_state = GuiGetState(); if (all_have_model) { add_open = false; GuiSetState(STATE_DISABLED); } cstr choices = model_cnt ? "Add Component;Add Model to Missing" : "Add Component;Model"; if (GuiDropdownBox(row, choices, &add_choice, add_open)) { add_open = !add_open; edit.focus = 0; if (!add_open && add_choice == 1) { model_picker_open(MDL_PICK_COMP_ADD); add_choice = 0; } } GuiSetState(add_state); reset: GuiUnlock(); GuiSetState(state); if (input && container != ED_PNL_CENTER) { ed.focus_captured = edit.focus != 0 || add_open || ed.panel[ED_PNL_CENTER].enabled; } } 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; } } /* model picker */ typedef struct { cstr key; int score; Asset *asset; bool tried_loading; } ModelChoice; static struct { RenderTexture preview; ModelChoice *items; int item_cnt, match_cnt, sel_idx, scroll; ModelPickerMode mode; char query[128]; } model_picker; static bool editor_set_model(cstr key, ModelPickerMode mode) { Asset *asset = asset_get(key); if (!asset || asset->tag != ASSET_TAG_MODEL) { return false; } EditorAction action = action_begin(ED_ACTION_MODEL, false); for (u32 i = 0; i < action.item_cnt; ++i) { u64 id = entity_id_from_pid(action.item[i].pid); Entity *ent = entity_get(id); if ((mode == MDL_PICK_COMP_ADD && ent->mdl.has) || (mode == MDL_PICK_COMP_UPDATE && !ent->mdl.has)) { continue; } free(ent->mdl.key); ent->mdl = (ModelCmp){ .key = strdup(key), .mdl = &asset->mdl, .has = true, .draw = ent->mdl.has ? ent->mdl.draw : true, }; } action_commit(&action); return true; } /* qsort compare func */ static int model_choice_compare(const void *a, const void *b) { const ModelChoice *left = a; const ModelChoice *right = b; if (left->score > right->score) { return -1; } if (left->score < right->score) { return 1; } int order = strcmp(GetFileName(left->key), GetFileName(right->key)); if (order != 0) { return order; } return strcmp(left->key, right->key); } static void model_picker_close(void) { if (model_picker.preview.id) { UnloadRenderTexture(model_picker.preview); } free(model_picker.items); memset(&model_picker, 0, sizeof(model_picker)); ed.panel[ED_PNL_CENTER].enabled = false; ed.focus = ED_PNL_NULL; ed.focus_captured = false; } static void model_picker_open(ModelPickerMode mode) { model_picker.mode = mode; char **keys = asset_mdl_keys(&model_picker.item_cnt); if (model_picker.item_cnt) { model_picker.items = calloc(model_picker.item_cnt, sizeof(*model_picker.items)); if (!model_picker.items) { perror("model_picker_open: alloc failed."); exit(1); } } for (int i = 0; i < model_picker.item_cnt; ++i) { model_picker.items[i].key = keys[i]; } free(keys); model_picker.match_cnt = model_picker.item_cnt; if (model_picker.item_cnt > 1) { qsort(model_picker.items, model_picker.item_cnt, sizeof(*model_picker.items), model_choice_compare); } ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_MODEL_PICKER; ed.panel[ED_PNL_CENTER].enabled = true; ed.focus = ED_PNL_CENTER; ed.focus_captured = true; } static void model_picker_preview(Rect bounds, Model mdl) { if (model_picker.preview.texture.width != bounds.width || model_picker.preview.texture.height != bounds.height) { if (model_picker.preview.texture.id) { UnloadRenderTexture(model_picker.preview); } model_picker.preview = LoadRenderTexture(bounds.width, bounds.height); } Matrix transform = mdl.transform; mdl.transform = MatrixIdentity(); BoundingBox bb = GetModelBoundingBox(mdl); Vec3 center = Vector3Scale(Vector3Add(bb.min, bb.max), 0.5); center = Vector3Transform(center, transform); f32 radius = 0.001f; for (int i = 0; i < 8; ++i) { Vec3 corner = { (i & 1) ? bb.max.x : bb.min.x, (i & 2) ? bb.max.y : bb.min.y, (i & 4) ? bb.max.z : bb.min.z }; corner = Vector3Transform(corner, transform); radius = fmaxf(radius, Vector3Distance(corner, center)); } transform = MatrixMultiply(transform, MatrixTranslate(-center.x, -center.y, -center.z)); transform = MatrixMultiply(transform, MatrixScale(1.0/radius, 1.0/radius, 1.0/radius)); f32 dist = 3.0f * fmaxf(1.0f, (f32)bounds.height / (f32)bounds.width); Camera3D cam = { .position = Vector3Scale(Vector3Normalize((Vec3){1,0.7,1}), dist), .target = {0}, .up = {0,1,0}, .fovy = 45, .projection = CAMERA_PERSPECTIVE, }; BeginTextureMode(model_picker.preview); ClearBackground((Color){ 35, 35, 40, 255 }); BeginMode3D(cam); /* we do this bc our normal draw calls override default shader */ for (int i = 0; i < mdl.meshCount; ++i) { Material mat = mdl.materials[mdl.meshMaterial[i]]; mat.shader = (Shader) { .id = rlGetShaderIdDefault(), .locs = rlGetShaderLocsDefault(), }; DrawMesh(mdl.meshes[i], mat, transform); } EndMode3D(); EndTextureMode(); DrawTexturePro(model_picker.preview.texture, (Rect) { 0, 0, bounds.width, -bounds.height, }, bounds, (Vec2){0}, 0, WHITE); } static cstr panel_model_picker(Rect bounds, bool input) { if (input && IsKeyPressed(KEY_ESCAPE)) { model_picker_close(); return NULL; } GuiPanel(bounds, "Choose Model"); f32 left_width = floorf((bounds.width - 24) * 0.45f); Rect search = { bounds.x + 8, bounds.y + 30, left_width, 24, }; Rect list = { search.x, search.y + 32, left_width, fmaxf(24, bounds.height - 100), }; Rect preview = { search.x + left_width + 8, search.y, bounds.width - left_width - 24, bounds.height - 38, }; char previous[sizeof(model_picker.query)]; memcpy(previous, model_picker.query, sizeof(previous)); /* Ignore the toggle result: the search always keeps text focus. */ GuiTextBox(search, model_picker.query, sizeof(model_picker.query), input); if (strcmp(previous, model_picker.query) != 0) { model_picker.match_cnt = 0; for (int i = 0; i < model_picker.item_cnt; ++i) { ModelChoice *item = &model_picker.items[i]; item->score = fuzzy_match(model_picker.query, GetFileName(item->key)); if (item->score != INT32_MIN) { model_picker.match_cnt++; } } if (model_picker.item_cnt > 1) { qsort(model_picker.items, model_picker.item_cnt, sizeof(*model_picker.items), model_choice_compare); } model_picker.sel_idx = 0; model_picker.scroll = 0; } if (input && model_picker.match_cnt) { int step = 0; if (IsKeyPressed(KEY_UP)) { step = -1; } else if (IsKeyPressed(KEY_DOWN)) { step = 1; } else if (IsKeyPressed(KEY_TAB)) { bool backwards = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); step = backwards ? -1 : 1; } model_picker.sel_idx = (model_picker.sel_idx + step + model_picker.match_cnt) % model_picker.match_cnt; } int visible = MAX(1, (int)(list.height / 24)); if (model_picker.sel_idx < model_picker.scroll) { model_picker.scroll = model_picker.sel_idx; } if (model_picker.sel_idx >= model_picker.scroll + visible) { model_picker.scroll = model_picker.sel_idx - visible + 1; } BeginScissorMode((int)list.x, (int)list.y, (int)list.width, (int)list.height); for (int row = 0; row < visible; ++row) { int index = model_picker.scroll + row; if (index >= model_picker.match_cnt) { break; } Rect item_box = { list.x, list.y + row * 24, list.width, 24, }; if (index == model_picker.sel_idx) { DrawRectangleRec(item_box, GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED))); } GuiLabel(item_box, GetFileName(model_picker.items[index].key)); } if (!model_picker.match_cnt) { GuiLabel((Rect) { list.x, list.y, list.width, 24 }, "No matching models"); } EndScissorMode(); GuiPanel(preview, NULL); if (model_picker.match_cnt) { ModelChoice *item = &model_picker.items[model_picker.sel_idx]; if (!item->tried_loading) { item->asset = asset_get(item->key); item->tried_loading = true; } /* model preview */ if (item->asset) { Rect image = { preview.x + 1, preview.y + 34, preview.width - 2, preview.height - 35, }; model_picker_preview(image, item->asset->mdl); } GuiLabel((Rect){ preview.x + 8, preview.y + 8, preview.width - 16, 24, }, item->asset ? GetFileName(item->key) : "Could not load model"); if (input && item->asset && IsKeyPressed(KEY_ENTER)) { return item->key; } } GuiLabel((Rect){ list.x, bounds.y + bounds.height - 30, list.width, 22, }, "Tab: cycle Enter: add Esc: cancel"); return NULL; } 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; case ED_PNL_TYPE_MODEL_PICKER: {} cstr key = panel_model_picker(bounds, input); if (!key) { break; } switch (model_picker.mode) { case MDL_PICK_COMP_ADD: case MDL_PICK_COMP_UPDATE: if (editor_set_model(key, model_picker.mode)) { model_picker_close(); } break; case MDL_PICK_ENT_CREATE: /* TODO: */ break; } 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, false); 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); }