From 23783892f0affd3ba65355f749b06b24bc17948a Mon Sep 17 00:00:00 2001 From: Daniu Date: Mon, 23 Feb 2026 21:31:31 +0100 Subject: [PATCH] Undo/Redo system, hierarchy for models in scene, uuid for basic_nodes, menu bar 'windows' options --- editorkeyboardinput.cpp | 12 +- editormode.cpp | 1198 +++++++++++++++++++++------------ editormode.h | 65 +- editoruipanels.cpp | 4 +- scene.cpp | 6 +- scene.h | 3 + scenenode.cpp | 2 + scenenode.h | 5 +- simulationstateserializer.cpp | 4 + uilayer.cpp | 58 +- uilayer.h | 3 + utils/uuid.hpp | 69 ++ 12 files changed, 975 insertions(+), 454 deletions(-) create mode 100644 utils/uuid.hpp diff --git a/editorkeyboardinput.cpp b/editorkeyboardinput.cpp index 154af92b..2b306f5c 100644 --- a/editorkeyboardinput.cpp +++ b/editorkeyboardinput.cpp @@ -25,12 +25,12 @@ void editorkeyboard_input::default_bindings() { m_bindingsetups = { - { user_command::moveleft, {GLFW_KEY_LEFT, "Move left"} }, - { user_command::moveright, {GLFW_KEY_RIGHT, "Move right"} }, - { user_command::moveforward, {GLFW_KEY_UP, "Move forwards"} }, - { user_command::moveback, {GLFW_KEY_DOWN, "Move backwards"} }, - { user_command::moveup, {GLFW_KEY_PAGE_UP, "Move up"} }, - { user_command::movedown, {GLFW_KEY_PAGE_DOWN, "Move down"} }, + { user_command::moveleft, {GLFW_KEY_A, "Move left"} }, + { user_command::moveright, {GLFW_KEY_D, "Move right"} }, + { user_command::moveforward, {GLFW_KEY_W, "Move forwards"} }, + { user_command::moveback, {GLFW_KEY_S, "Move backwards"} }, + { user_command::moveup, {GLFW_KEY_E, "Move up"} }, + { user_command::movedown, {GLFW_KEY_Q, "Move down"} }, }; } diff --git a/editormode.cpp b/editormode.cpp index cce7c42c..1c42242e 100644 --- a/editormode.cpp +++ b/editormode.cpp @@ -20,570 +20,894 @@ http://mozilla.org/MPL/2.0/. #include "Console.h" #include "renderer.h" #include "AnimModel.h" +#include "scene.h" + + +#include "imgui/imgui.h" +#include "Logs.h" +#include +#include +#include + +// Static member initialization +TCamera editor_mode::Camera; +bool editor_mode::m_focus_active = false; +bool editor_mode::m_change_history = true; + +namespace +{ + using vec3 = glm::vec3; + using dvec2 = glm::dvec2; + + inline bool is_release(int state) + { + return state == GLFW_RELEASE; + } + + inline bool is_press(int state) + { + return state == GLFW_PRESS; + } + +} bool editor_mode::editormode_input::init() { - - return (mouse.init() && keyboard.init()); + return (mouse.init() && keyboard.init()); } void editor_mode::editormode_input::poll() { - - keyboard.poll(); + keyboard.poll(); } -editor_mode::editor_mode() -{ - +editor_mode::editor_mode() { m_userinterface = std::make_shared(); + } + +editor_ui *editor_mode::ui() const +{ + return static_cast(m_userinterface.get()); } -// initializes internal data structures of the mode. returns: true on success, false otherwise bool editor_mode::init() { - - Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); - - return m_input.init(); + Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); + return m_input.init(); +} + +void editor_mode::apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value) +{ + if (!node) + return; + + if (rotation_mode == functions_panel::RANDOM) + { + const vec3 rotation{0.0f, LocalRandom(0.0, 360.0), 0.0f}; + m_editor.rotate(node, rotation, 1); + } + else if (rotation_mode == functions_panel::FIXED) + { + const vec3 rotation{0.0f, fixed_rotation_value, 0.0f}; + m_editor.rotate(node, rotation, 0); + } +} + +void editor_mode::start_focus(scene::basic_node *node, double duration) +{ + if (!node) + return; + + m_focus_active = true; + m_focus_time = 0.0; + m_focus_duration = duration; + + m_focus_start_pos = Camera.Pos; + m_focus_start_lookat = Camera.LookAt; + + m_focus_target_lookat = node->location(); + + glm::dvec3 dir = m_focus_start_pos - m_focus_start_lookat; + double dist = glm::length(dir); + m_focus_target_pos = m_focus_target_lookat + glm::dvec3(10.0, 3.0, 10.0); +} + +void editor_mode::handle_brush_mouse_hold(int Action, int Button) +{ + auto const mode = ui()->mode(); + auto const rotation_mode = ui()->rot_mode(); + auto const fixed_rotation_value = ui()->rot_val(); + + if(mode != nodebank_panel::BRUSH) + return; + + GfxRenderer->Pick_Node_Callback( + [this, mode, rotation_mode, fixed_rotation_value, Action, Button](scene::basic_node * /*node*/) { + const std::string *src = ui()->get_active_node_template(); + if (!src) + return; + + std::string name = "editor_"; + + glm::dvec3 newPos = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + double distance = glm::distance(newPos, oldPos); + if (distance < ui()->getSpacing()) + return; + + TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + newPos); + oldPos = newPos; + if (!cloned) + return; + + std::string new_name = "editor_" + cloned->uuid.to_string(); + + cloned->m_name = new_name; + + std::string as_text; + cloned->export_as_text(as_text); + push_snapshot(cloned, EditorSnapshot::Action::Add, as_text); + + m_node = cloned; + apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value); + ui()->set_node(m_node); + }); +} + +void editor_mode::add_to_hierarchy(scene::basic_node *node) +{ + if (!node) return; + scene::Hierarchy[node->uuid.to_string()] = node; +} + +void editor_mode::remove_from_hierarchy(scene::basic_node *node) +{ + if (!node) return; + auto it = scene::Hierarchy.find(node->uuid.to_string()); + if (it != scene::Hierarchy.end()) + scene::Hierarchy.erase(it); +} + +scene::basic_node* editor_mode::find_in_hierarchy(const std::string &uuid_str) +{ + if (uuid_str.empty()) return nullptr; + auto it = scene::Hierarchy.find(uuid_str); + return (it != scene::Hierarchy.end()) ? it->second : nullptr; +} + +scene::basic_node* editor_mode::find_node_by_any(scene::basic_node *node_ptr, const std::string &uuid_str, const std::string &name) +{ + if (node_ptr) return node_ptr; + + if (!uuid_str.empty()) { + auto *node = find_in_hierarchy(uuid_str); + if (node) return node; + } + + if (!name.empty()) { + return simulation::Instances.find(name); + } + + return nullptr; +} + +void editor_mode::push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action, std::string const &Serialized) +{ + if (!node) + return; + + if(m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) + { + m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); + } + + EditorSnapshot snap; + snap.action = Action; + snap.node_name = node->name(); + snap.position = node->location(); + snap.uuid = node->uuid; + + if (auto *model = dynamic_cast(node)) + { + snap.rotation = model->Angles(); + } + else + { + snap.rotation = glm::vec3(0.0f); + } + + if (Action == EditorSnapshot::Action::Delete || Action == EditorSnapshot::Action::Add) + { + if (!Serialized.empty()) + snap.serialized = Serialized; + else + node->export_as_text(snap.serialized); + } + + + snap.node_ptr = node; + + m_history.push_back(std::move(snap)); + g_redo.clear(); +} + +glm::dvec3 editor_mode::clamp_mouse_offset_to_max(const glm::dvec3 &offset) +{ + double len = glm::length(offset); + if (len <= static_cast(kMaxPlacementDistance) || len <= 1e-6) + return offset; + return glm::normalize(offset) * static_cast(kMaxPlacementDistance); +} + +void editor_mode::nullify_history_pointers(scene::basic_node *node) +{ + if (!node) + return; + + for (auto &s : m_history) + { + if (s.node_ptr == node) + s.node_ptr = nullptr; + } + + for (auto &s : g_redo) + { + if (s.node_ptr == node) + s.node_ptr = nullptr; + } +} + +void editor_mode::undo_last() +{ + if (m_history.empty()) + return; + + EditorSnapshot snap = m_history.back(); + m_history.pop_back(); + + if (snap.action == EditorSnapshot::Action::Delete) + { + // undo delete -> recreate model + EditorSnapshot redoSnap; + redoSnap.action = EditorSnapshot::Action::Delete; + redoSnap.node_name = snap.node_name; + redoSnap.serialized = snap.serialized; + redoSnap.position = snap.position; + redoSnap.node_ptr = nullptr; + g_redo.push_back(std::move(redoSnap)); + + TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position); + if (created) + { + created->location(snap.position); + created->Angles(snap.rotation); + m_node = created; + m_node->uuid = snap.uuid; // restore original UUID for better tracking (not strictly necessary) + add_to_hierarchy(created); + ui()->set_node(m_node); + } + return; + } + + scene::basic_node *target = find_node_by_any(snap.node_ptr, snap.uuid.to_string(), snap.node_name); + if (!target) + return; + + EditorSnapshot current; + current.action = snap.action; + current.node_name = snap.node_name; + current.node_ptr = target; + current.position = target->location(); + if (auto *model = dynamic_cast(target)) + current.rotation = model->Angles(); + else + current.rotation = glm::vec3(0.0f); + g_redo.push_back(std::move(current)); + + if (snap.action == EditorSnapshot::Action::Add) + { + // undo add -> delete the instance + if (auto *model = dynamic_cast(target)) + { + + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + m_node = nullptr; + ui()->set_node(nullptr); + } + return; + } + + target->location(snap.position); + + if (auto *model = dynamic_cast(target)) + { + glm::vec3 cur = model->Angles(); + glm::vec3 delta = snap.rotation - cur; + m_editor.rotate(target, delta, 0); + } + + m_node = target; + ui()->set_node(m_node); +} + +void editor_mode::redo_last() +{ + if (g_redo.empty()) + return; + + EditorSnapshot snap = g_redo.back(); + g_redo.pop_back(); + + // handle delete redo (re-delete) separately + if (snap.action == EditorSnapshot::Action::Delete) + { + EditorSnapshot hist; + hist.action = snap.action; + hist.node_name = snap.node_name; + hist.serialized = snap.serialized; + hist.position = snap.position; + hist.uuid = snap.uuid; + m_history.push_back(std::move(hist)); + + scene::basic_node *target = simulation::Instances.find(snap.node_name); + if (target) + { + if (auto *model = dynamic_cast(target)) + { + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + m_node = nullptr; + ui()->set_node(nullptr); + } + } + return; + } + + scene::basic_node *target = find_node_by_any(snap.node_ptr, snap.uuid.to_string(), snap.node_name); + + EditorSnapshot hist; + hist.action = snap.action; + hist.node_name = snap.node_name; + hist.node_ptr = target; + + if (target) + { + hist.position = target->location(); + if (auto *model = dynamic_cast(target)) + hist.rotation = model->Angles(); + hist.uuid = snap.uuid; + } + m_history.push_back(std::move(hist)); + + if (snap.action == EditorSnapshot::Action::Add) + { + TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position); + if (created) + { + created->location(snap.position); + created->Angles(snap.rotation); + m_node = created; + m_node->uuid = snap.uuid; + ui()->set_node(m_node); + if (!m_history.empty()) + m_history.back().node_ptr = created; + } + return; + } + + if (!target) + return; + + // apply redo position + target->location(snap.position); + if (auto *model = dynamic_cast(target)) + { + glm::vec3 cur = model->Angles(); + glm::vec3 delta = snap.rotation - cur; + m_editor.rotate(target, delta, 0); + } + + m_node = target; + ui()->set_node(m_node); } -// mode-specific update of simulation data. returns: false on error, true otherwise bool editor_mode::update() { + Timer::UpdateTimers(true); - Timer::UpdateTimers(true); + simulation::State.update_clocks(); + simulation::Environment.update(); - simulation::State.update_clocks(); - simulation::Environment.update(); + auto const deltarealtime = Timer::GetDeltaRenderTime(); - // render time routines follow: - auto const deltarealtime = Timer::GetDeltaRenderTime(); // nie uwzględnia pauzowania ani mnożenia czasu - - // fixed step render time routines: - fTime50Hz += deltarealtime; // w pauzie też trzeba zliczać czas, bo przy dużym FPS będzie problem z odczytem ramek - while (fTime50Hz >= 1.0 / 50.0) - { + // fixed step render time routines (50 Hz) + fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable + while (fTime50Hz >= 1.0 / 50.0) + { #ifdef _WIN32 - Console::Update(); // to i tak trzeba wywoływać + Console::Update(); #endif - m_userinterface->update(); + m_userinterface->update(); - // brush settings window - editor_ui *ui = static_cast(m_userinterface.get()); - auto const mode = static_cast(m_userinterface.get())->mode(); - ui->toggleBrushSettings(mode == nodebank_panel::BRUSH); + // update brush settings visibility depending on panel mode + ui()->toggleBrushSettings(ui()->mode() == nodebank_panel::BRUSH); - if (mouseHold) - { - auto const mode = static_cast(m_userinterface.get())->mode(); - auto const rotation_mode = static_cast(m_userinterface.get())->rot_mode(); - auto const fixed_rotation_value = static_cast(m_userinterface.get())->rot_val(); - int Action = GLFW_REPEAT; - int Button = GLFW_MOUSE_BUTTON_LEFT; - { - GfxRenderer->Pick_Node_Callback( - [this, mode, rotation_mode, fixed_rotation_value, Action, Button](scene::basic_node *node) - { - editor_ui *ui = static_cast(m_userinterface.get()); + if (mouseHold) + { + // process continuous brush placement + if(ui()->mode() == nodebank_panel::BRUSH) + handle_brush_mouse_hold(GLFW_REPEAT, GLFW_MOUSE_BUTTON_LEFT); + } - if (mode == nodebank_panel::BRUSH) - { + // decelerate camera velocity with thresholding + Camera.Velocity *= 0.65f; + if (std::abs(Camera.Velocity.x) < 0.01) + Camera.Velocity.x = 0.0; + if (std::abs(Camera.Velocity.y) < 0.01) + Camera.Velocity.y = 0.0; + if (std::abs(Camera.Velocity.z) < 0.01) + Camera.Velocity.z = 0.0; - const std::string *src = ui->get_active_node_template(); + fTime50Hz -= 1.0 / 50.0; + + } - // std::string name = "editor_" + std::to_string(LocalRandom(0.0, 100000.0)); - std::string name = "editor_" + generate_uuid_v4(); + // variable step routines + update_camera(deltarealtime); - if (!src) - return; + simulation::Region->update_sounds(); + audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); - glm::vec3 newPos = GfxRenderer->Mouse_Position(); - float distance = glm::distance(newPos, oldPos); - if (distance < ui->getSpacing()) - return; - TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + newPos); - oldPos = newPos; - if (!cloned) - return; + GfxRenderer->Update(deltarealtime); - // if (!m_dragging) - // return; + simulation::is_ready = true; - m_node = cloned; + // --- ImGui: Editor History Window & Settings --- + if(!m_change_history) return true; + + render_change_history(); - if (rotation_mode == functions_panel::RANDOM) - { - auto const rotation{glm::vec3{0, LocalRandom(0.0, 360.0), 0}}; - - m_editor.rotate(m_node, rotation, 1); - } - else if (rotation_mode == functions_panel::FIXED) - { - - auto const rotation{glm::vec3{0, fixed_rotation_value, 0}}; - - m_editor.rotate(m_node, rotation, 0); - } - // Application.set_cursor( GLFW_CURSOR_DISABLED ); - ui->set_node(m_node); - } - }); - } - } - // decelerate camera - Camera.Velocity *= 0.65; - if (std::abs(Camera.Velocity.x) < 0.01) - { - Camera.Velocity.x = 0.0; - } - if (std::abs(Camera.Velocity.y) < 0.01) - { - Camera.Velocity.y = 0.0; - } - if (std::abs(Camera.Velocity.z) < 0.01) - { - Camera.Velocity.z = 0.0; - } - - fTime50Hz -= 1.0 / 50.0; - } - - // variable step render time routines: - update_camera(deltarealtime); - - simulation::Region->update_sounds(); - audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); - - GfxRenderer->Update(deltarealtime); - - simulation::is_ready = true; - - return true; + return true; } void editor_mode::update_camera(double const Deltatime) { + // account for keyboard-driven motion + // if focus animation active, interpolate camera toward target + if (m_focus_active) + { + m_focus_time += Deltatime; + double t = m_focus_duration > 0.0 ? (m_focus_time / m_focus_duration) : 1.0; + if (t >= 1.0) + t = 1.0; + // smoothstep easing + double s = t * t * (3.0 - 2.0 * t); + Camera.Pos = glm::mix(m_focus_start_pos, m_focus_target_pos, s); + Camera.LookAt = glm::mix(m_focus_start_lookat, m_focus_target_lookat, s); + if (t >= 1.0) + m_focus_active = false; + } - // uwzględnienie ruchu wywołanego klawiszami - Camera.Update(); - // reset window state, it'll be set again if applicable in a check below - Global.CabWindowOpen = false; - // all done, update camera position to the new value - Global.pCamera = Camera; + Camera.Update(); + + // reset window state (will be set again if UI requires it) + Global.CabWindowOpen = false; + + // publish camera back to global copy + Global.pCamera = Camera; } -// maintenance method, called when the mode is activated void editor_mode::enter() { + m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; - m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; + Camera = Global.pCamera; - Camera = Global.pCamera; - // NOTE: camera placement is effectively a copy of drivermode DistantView( true ) - // TBD, TODO: refactor into a common vehicle method? - if (false == FreeFlyModeFlag) - { + if (!FreeFlyModeFlag) + { + auto const *vehicle = Camera.m_owner; + if (vehicle) + { + auto const cab = (vehicle->MoverParameters->CabOccupied == 0 ? 1 : vehicle->MoverParameters->CabOccupied); + auto const left = vehicle->VectorLeft() * cab; + Camera.Pos = Math3D::vector3(Camera.Pos.x, vehicle->GetPosition().y, Camera.Pos.z) + left * vehicle->GetWidth() + Math3D::vector3(1.25f * left.x, 1.6f, 1.25f * left.z); + Camera.m_owner = nullptr; + Camera.LookAt = vehicle->GetPosition(); + Camera.RaLook(); // single camera reposition + FreeFlyModeFlag = true; + } + } - auto const *vehicle{Camera.m_owner}; - auto const cab{(vehicle->MoverParameters->CabOccupied == 0 ? 1 : vehicle->MoverParameters->CabOccupied)}; - auto const left{vehicle->VectorLeft() * cab}; - Camera.Pos = Math3D::vector3(Camera.Pos.x, vehicle->GetPosition().y, Camera.Pos.z) + left * vehicle->GetWidth() + Math3D::vector3(1.25 * left.x, 1.6, 1.25 * left.z); - Camera.m_owner = nullptr; - Camera.LookAt = vehicle->GetPosition(); - Camera.RaLook(); // jednorazowe przestawienie kamery - FreeFlyModeFlag = true; - } - Global.ControlPicking = true; - EditorModeFlag = true; + Global.ControlPicking = true; + EditorModeFlag = true; - Application.set_cursor(GLFW_CURSOR_NORMAL); + Application.set_cursor(GLFW_CURSOR_NORMAL); } -// maintenance method, called when the mode is deactivated void editor_mode::exit() { + EditorModeFlag = false; + Global.ControlPicking = m_statebackup.picking; + FreeFlyModeFlag = m_statebackup.freefly; + Global.pCamera = m_statebackup.camera; - EditorModeFlag = false; - Global.ControlPicking = m_statebackup.picking; - FreeFlyModeFlag = m_statebackup.freefly; - Global.pCamera = m_statebackup.camera; + g_redo.clear(); + m_history.clear(); - Application.set_cursor((Global.ControlPicking ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED)); + Application.set_cursor((Global.ControlPicking ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED)); - if (false == Global.ControlPicking) - { - Application.set_cursor_pos(0, 0); - } + if (!Global.ControlPicking) + { + Application.set_cursor_pos(0, 0); + } } void editor_mode::on_key(int const Key, int const Scancode, int const Action, int const Mods) { #ifndef __unix__ - Global.shiftState = (Mods & GLFW_MOD_SHIFT) ? true : false; - Global.ctrlState = (Mods & GLFW_MOD_CONTROL) ? true : false; - Global.altState = (Mods & GLFW_MOD_ALT) ? true : false; + Global.shiftState = (Mods & GLFW_MOD_SHIFT) ? true : false; + Global.ctrlState = (Mods & GLFW_MOD_CONTROL) ? true : false; + Global.altState = (Mods & GLFW_MOD_ALT) ? true : false; #endif - bool anyModifier = Mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT); + bool anyModifier = Mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT); - // give the ui first shot at the input processing... - if (!anyModifier && true == m_userinterface->on_key(Key, Action)) - { - return; - } - // ...if the input is left untouched, pass it on - if (true == m_input.keyboard.key(Key, Action)) - { - return; - } + // first give UI a chance to handle the key + if (!anyModifier && m_userinterface->on_key(Key, Action)) + return; - if (Action == GLFW_RELEASE) - { - return; - } + // then internal input handling + if (m_input.keyboard.key(Key, Action)) + return; - // legacy hardcoded keyboard commands - // TODO: replace with current command system, move to input object(s) - switch (Key) - { + if (Action == GLFW_RELEASE) + return; - case GLFW_KEY_F11: - { + // shortcuts: undo/redo + if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action)) + { + undo_last(); + return; + } + if (Global.ctrlState && Key == GLFW_KEY_Y && is_press(Action)) + { + redo_last(); + return; + } - if (Action != GLFW_PRESS) - { - break; - } - // mode switch - // TODO: unsaved changes warning - if ((false == Global.ctrlState) && (false == Global.shiftState)) - { - Application.pop_mode(); - } - // scenery export - if (Global.ctrlState && Global.shiftState) - { - simulation::State.export_as_text(Global.SceneryFile); - } - break; - } + // legacy hardcoded keyboard commands + switch (Key) + { + case GLFW_KEY_F11: + if (Action != GLFW_PRESS) + break; - case GLFW_KEY_F12: - { - // quick debug mode toggle - if (Global.ctrlState && Global.shiftState) - { - DebugModeFlag = !DebugModeFlag; - } - break; - } + if (!Global.ctrlState && !Global.shiftState) + { + Application.pop_mode(); + } + else if (Global.ctrlState && Global.shiftState) + { + simulation::State.export_as_text(Global.SceneryFile); + } + break; - // TODO: ensure delete method can play nice with history stack - case GLFW_KEY_DELETE: - { - TAnimModel *model = dynamic_cast(m_node); - if (!model) - break; + case GLFW_KEY_F12: + if (Global.ctrlState && Global.shiftState && is_press(Action)) + { + DebugModeFlag = !DebugModeFlag; + } + break; - m_node = nullptr; - m_dragging = false; - // Application.set_cursor( GLFW_CURSOR_NORMAL ); - static_cast(m_userinterface.get())->set_node(nullptr); + case GLFW_KEY_DELETE: + if (is_press(Action)) + { + TAnimModel *model = dynamic_cast(m_node); + if (model) + { + // record deletion for undo (serialize full node) + std::string as_text; + + model->export_as_text(as_text); + std::string debug = "Deleting node: " + as_text + "\nSerialized data:\n"; + push_snapshot(model, EditorSnapshot::Action::Delete, as_text); + WriteLog(debug, logtype::generic); - simulation::State.delete_model(model); + // clear history pointers referencing this model before actually deleting it + nullify_history_pointers(model); + remove_from_hierarchy(model); - break; - } + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + simulation::State.delete_model(model); + } + } + break; - default: - { - break; - } - } + case GLFW_KEY_F: + if (is_press(Action)) + { + if(!m_node) + break; + + // start smooth focus camera on selected node + start_focus(m_node, 0.6); + } + break; + + default: + break; + } } void editor_mode::on_cursor_pos(double const Horizontal, double const Vertical) { + dvec2 const mousemove = dvec2{Horizontal, Vertical} - m_input.mouse.position(); + m_input.mouse.position(Horizontal, Vertical); - auto const mousemove{glm::dvec2{Horizontal, Vertical} - m_input.mouse.position()}; - m_input.mouse.position(Horizontal, Vertical); + if (m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE) + return; + if (!m_node) + return; - if (m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE) - { - return; - } - if (m_node == nullptr) - { - return; - } + if (m_takesnapshot) + { + // record appropriate action type depending on current input mode + if (mode_rotationX() || mode_rotationY() || mode_rotationZ()) + push_snapshot(m_node, EditorSnapshot::Action::Rotate); + else + push_snapshot(m_node, EditorSnapshot::Action::Move); - if (m_takesnapshot) - { - // take a snapshot of selected node(s) - // TODO: implement this - m_takesnapshot = false; - } + m_takesnapshot = false; + } - if (mode_translation()) - { - // move selected node - if (mode_translation_vertical()) - { - auto const translation{mousemove.y * -0.01f}; - m_editor.translate(m_node, translation); - } - else - { - auto const mouseworldposition{Camera.Pos + GfxRenderer->Mouse_Position()}; - m_editor.translate(m_node, mouseworldposition, mode_snap()); - } - } - else if (mode_rotationY()) - { - // rotate selected node - // auto const rotation{glm::vec3{mousemove.y, mousemove.x, 0} * 0.25f}; - auto const rotation{glm::vec3{0, mousemove.x, 0} * 0.25f}; - auto const quantization{(mode_snap() ? 5.f : // TODO: put quantization value in a variable - 0.f)}; - m_editor.rotate(m_node, rotation, quantization); - } - else if (mode_rotationZ()) - { - // rotate selected node - - // auto const rotation{glm::vec3{mousemove.y, mousemove.x, 0} * 0.25f}; - auto const rotation{glm::vec3{0, 0, mousemove.x} * 0.25f}; - auto const quantization{(mode_snap() ? 5.f : // TODO: put quantization value in a variable - 0.f)}; - m_editor.rotate(m_node, rotation, quantization); - } - else if (mode_rotationX()) - { - // rotate selected node - // auto const rotation{glm::vec3{mousemove.y, mousemove.x, 0} * 0.25f}; - auto const rotation{glm::vec3{mousemove.y, 0, 0} * 0.25f}; - auto const quantization{(mode_snap() ? 5.f : // TODO: put quantization value in a variable - 0.f)}; - m_editor.rotate(m_node, rotation, quantization); - } + if (mode_translation()) + { + if (mode_translation_vertical()) + { + float const translation = static_cast(mousemove.y * -0.01); + m_editor.translate(m_node, translation); + } + else + { + auto mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + auto const mouseworldposition = Camera.Pos + mouseOffset; + m_editor.translate(m_node, mouseworldposition, mode_snap()); + } + } + else if (mode_rotationY()) + { + vec3 const rotation{0.0f, static_cast(mousemove.x) * 0.25f, 0.0f}; + float const quantization = (mode_snap() ? 5.0f : 0.0f); + m_editor.rotate(m_node, rotation, quantization); + } + else if (mode_rotationZ()) + { + vec3 const rotation{0.0f, 0.0f, static_cast(mousemove.x) * 0.25f}; + float const quantization = (mode_snap() ? 5.0f : 0.0f); + m_editor.rotate(m_node, rotation, quantization); + } + else if (mode_rotationX()) + { + vec3 const rotation{static_cast(mousemove.y) * 0.25f, 0.0f, 0.0f}; + float const quantization = (mode_snap() ? 5.0f : 0.0f); + m_editor.rotate(m_node, rotation, quantization); + } } -/* -TODO: re-enable in post-merge cleanup -void -editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) { +void editor_mode::on_mouse_button(int const Button, int const Action, int const Mods) +{ + // UI first + if (m_userinterface->on_mouse_button(Button, Action)) + { + m_input.mouse.button(Button, Action); + return; + } - if( Button == GLFW_MOUSE_BUTTON_LEFT ) { + if (Button == GLFW_MOUSE_BUTTON_LEFT) + { + + auto const mode = ui()->mode(); + auto const rotation_mode = ui()->rot_mode(); + auto const fixed_rotation_value = ui()->rot_val(); - if( Action == GLFW_PRESS ) { - // left button press + if (is_press(Action)) + { + mouseHold = true; m_node = nullptr; + + // delegate node picking behaviour depending on current panel mode GfxRenderer->Pick_Node_Callback( - [ this ]( scene::basic_node *node ) { - m_node = node; - if( m_node ) { - Application.set_cursor( GLFW_CURSOR_DISABLED ); + [this, mode, rotation_mode, fixed_rotation_value](scene::basic_node *node) { + // ignore picks that are beyond allowed placement distance + if (node) { + double const dist = glm::distance(node->location(), glm::dvec3{Global.pCamera.Pos}); + if (dist > static_cast(kMaxPlacementDistance)) + return; } - dynamic_cast( m_userinterface.get() )->set_node( m_node ); } ); - } - else { - // left button release - if( m_node ) - Application.set_cursor( GLFW_CURSOR_NORMAL ); - m_dragging = false; - // prime history stack for another snapshot + if (mode == nodebank_panel::MODIFY) + { + if (!m_dragging) + return; + + m_node = node; + ui()->set_node(m_node); + } + else if (mode == nodebank_panel::COPY) + { + if (node && typeid(*node) == typeid(TAnimModel)) + { + std::string as_text; + node->export_as_text(as_text); + ui()->add_node_template(as_text); + } + + m_dragging = false; + } + else if (mode == nodebank_panel::ADD) + { + const std::string *src = ui()->get_active_node_template(); + if (!src) + return; + + std::string name = "editor_"; + glm::dvec3 mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + mouseOffset); + if (!cloned) + return; + + // record addition for undo + std::string as_text; + std::string new_name = "editor_" + cloned->uuid.to_string(); + + cloned->m_name = new_name; + cloned->export_as_text(as_text); + push_snapshot(cloned, EditorSnapshot::Action::Add, as_text); + + if (!m_dragging) + return; + + m_node = cloned; + apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value); + ui()->set_node(m_node); + } + }); + + m_dragging = true; m_takesnapshot = true; } + else + { + if (is_release(Action)) + mouseHold = false; + + m_dragging = false; + } } - m_input.mouse.button( Button, Action ); + m_input.mouse.button(Button, Action); } -*/ -void editor_mode::on_mouse_button(int const Button, int const Action, int const Mods) -{ - // give the ui first shot at the input processing... +void editor_mode::render_change_history(){ - if (Button == GLFW_MOUSE_BUTTON_LEFT) - { - auto const mode = static_cast(m_userinterface.get())->mode(); - auto const rotation_mode = static_cast(m_userinterface.get())->rot_mode(); - auto const fixed_rotation_value = static_cast(m_userinterface.get())->rot_val(); - if (true == m_userinterface->on_mouse_button(Button, Action)) - { - return; - } - if (Action == GLFW_PRESS) - { - // left button press - mouseHold = true; - m_node = nullptr; + ImGui::Begin("Editor History", &m_change_history, ImGuiWindowFlags_AlwaysAutoResize); + int maxsize = m_max_history_size; + if (ImGui::InputInt("Max history size", &maxsize)) + { + m_max_history_size = std::max(0, maxsize); + if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0) + { + auto remove_count = (int)m_history.size() - m_max_history_size; + m_history.erase(m_history.begin(), m_history.begin() + remove_count); + // adjust selected index + if (m_selected_history_idx >= (int)m_history.size()) + m_selected_history_idx = (int)m_history.size() - 1; + } + + } - GfxRenderer->Pick_Node_Callback( - [this, mode, rotation_mode, fixed_rotation_value, Action, Button](scene::basic_node *node) - { - editor_ui *ui = static_cast(m_userinterface.get()); - if (mode == nodebank_panel::MODIFY) - { - if (!m_dragging) - return; + float dist = kMaxPlacementDistance; + if (ImGui::InputFloat("Max placement distance", &dist)) + { + kMaxPlacementDistance = std::max(0.0f, dist); + } - m_node = node; - // if( m_node ) - // Application.set_cursor( GLFW_CURSOR_DISABLED ); - // else - // m_dragging = false; - ui->set_node(m_node); - } - else if (mode == nodebank_panel::COPY) - { - if (node && typeid(*node) == typeid(TAnimModel)) - { - std::string as_text; - node->export_as_text(as_text); + ImGui::Separator(); - ui->add_node_template(as_text); - } + ImGui::Text("History (newest at end): %zu entries", m_history.size()); + ImGui::BeginChild("history_list", ImVec2(400, 200), true); + for (int i = 0; i < (int)m_history.size(); ++i) + { + auto &s = m_history[i]; + char buf[256]; + std::snprintf(buf, sizeof(buf), "%3d: %s %s pos=(%.1f,%.1f,%.1f)", i, + (s.action == EditorSnapshot::Action::Add) ? "ADD" : + (s.action == EditorSnapshot::Action::Delete) ? "DEL" : + (s.action == EditorSnapshot::Action::Move) ? "MOV" : + (s.action == EditorSnapshot::Action::Rotate) ? "ROT" : "OTH", + s.node_name.empty() ? "(noname)" : s.node_name.c_str(), + s.position.x, s.position.y, s.position.z); - m_dragging = false; - } - else if (mode == nodebank_panel::ADD) - { - const std::string *src = ui->get_active_node_template(); - std::string name = "editor_" + generate_uuid_v4(); + if (ImGui::Selectable(buf, m_selected_history_idx == i)) + m_selected_history_idx = i; + } + ImGui::EndChild(); - if (!src) - return; + ImGui::Separator(); + if (ImGui::Button("Clear History")) + { + m_history.clear(); + g_redo.clear(); + m_selected_history_idx = -1; + } + ImGui::SameLine(); + + ImGui::SameLine(); + if (ImGui::Button("Undo Selected")) + { + if (m_selected_history_idx >= 0 && m_selected_history_idx < (int)m_history.size()) + { + int target = m_selected_history_idx; + int undoCount = (int)m_history.size() - 1 - target; + for (int k = 0; k < undoCount; ++k) + undo_last(); + m_selected_history_idx = -1; + } + } - TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + GfxRenderer->Mouse_Position()); - - if (!cloned) - return; - - if (!m_dragging) - return; - - m_node = cloned; - - if (rotation_mode == functions_panel::RANDOM) - { - auto const rotation{glm::vec3{0, LocalRandom(0.0, 360.0), 0}}; - - m_editor.rotate(m_node, rotation, 1); - } - else if (rotation_mode == functions_panel::FIXED) - { - - auto const rotation{glm::vec3{0, fixed_rotation_value, 0}}; - - m_editor.rotate(m_node, rotation, 0); - } - // Application.set_cursor( GLFW_CURSOR_DISABLED ); - ui->set_node(m_node); - } - }); - - m_dragging = true; - } - else - { - if (Action == GLFW_RELEASE) - mouseHold = false; - - // left button release - // if( m_node ) - // Application.set_cursor( GLFW_CURSOR_NORMAL ); - m_dragging = false; - // prime history stack for another snapshot - m_takesnapshot = true; - } - } - if (Button == GLFW_MOUSE_BUTTON_RIGHT) - { - - /*if (Action == GLFW_PRESS) - { - // left button press - auto const mode = static_cast(m_userinterface.get())->mode(); - - m_node = nullptr; - - GfxRenderer->Pick_Node_Callback([this, mode](scene::basic_node *node) { - editor_ui *ui = static_cast(m_userinterface.get()); - - if (mode == nodebank_panel::MODIFY) - { - if (!m_dragging) - return; - - m_node = node; - if (m_node) - Application.set_cursor(GLFW_CURSOR_DISABLED); - else - m_dragging = false; - ui->set_node(m_node); - } - }); - - m_dragging = true; - } - else - { - // left button release - if (m_node) - Application.set_cursor(GLFW_CURSOR_NORMAL); - m_dragging = false; - // prime history stack for another snapshot - m_takesnapshot = true; - }*/ - } - - m_input.mouse.button(Button, Action); + ImGui::End(); } + void editor_mode::on_event_poll() { - - m_input.poll(); + m_input.poll(); } bool editor_mode::is_command_processor() const { - - return false; + return false; } bool editor_mode::mode_translation() const { - - return (false == Global.altState); + return (false == Global.altState); } bool editor_mode::mode_translation_vertical() const { - - return (true == Global.shiftState); + return (true == Global.shiftState); } bool editor_mode::mode_rotationY() const { - - return ((true == Global.altState) && (false == Global.ctrlState) && (false == Global.shiftState)); + return ((true == Global.altState) && (false == Global.ctrlState) && (false == Global.shiftState)); } + bool editor_mode::mode_rotationX() const { - - return ((true == Global.altState) && (true == Global.ctrlState) && (false == Global.shiftState)); + return ((true == Global.altState) && (true == Global.ctrlState) && (false == Global.shiftState)); } + bool editor_mode::mode_rotationZ() const { - - return ((true == Global.altState) && (true == Global.ctrlState) && (true == Global.shiftState)); + return ((true == Global.altState) && (true == Global.ctrlState) && (true == Global.shiftState)); } bool editor_mode::mode_snap() const { - - return ((false == Global.altState) && (true == Global.ctrlState) && (false == Global.shiftState)); + return ((false == Global.altState) && (true == Global.ctrlState) && (false == Global.shiftState)); +} + +bool editor_mode::focus_active() +{ + return m_focus_active; +} + +void editor_mode::set_focus_active(bool isActive) +{ + m_focus_active = isActive; } diff --git a/editormode.h b/editormode.h index 4b412e57..9c5488b9 100644 --- a/editormode.h +++ b/editormode.h @@ -45,7 +45,12 @@ class editor_mode : public application_mode } void on_event_poll() override; bool is_command_processor() const override; - + void undo_last(); + static bool focus_active(); + static void set_focus_active(bool isActive); + static TCamera& get_camera() { return Camera; } + static bool change_history() { return m_change_history; } + static void set_change_history(bool enabled) { m_change_history = enabled; } private: // types struct editormode_input @@ -65,6 +70,25 @@ class editor_mode : public application_mode bool freefly; bool picking; }; + + struct EditorSnapshot + { + enum class Action { Move, Rotate, Add, Delete, Other }; + + Action action{Action::Other}; + std::string node_name; // node identifier (basic_node::name()) + // direct pointer to node when available; used for in-memory undo/redo lookup + scene::basic_node *node_ptr{nullptr}; + std::string serialized; // full text for recreate (used for Add/Delete) + glm::dvec3 position{0.0, 0.0, 0.0}; + glm::vec3 rotation{0.0f, 0.0f, 0.0f}; + UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes + + }; + void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); + + std::vector m_history; // history of changes to nodes, used for undo functionality + std::vector g_redo; // methods void update_camera(double const Deltatime); bool mode_translation() const; @@ -73,15 +97,50 @@ class editor_mode : public application_mode bool mode_rotationX() const; bool mode_rotationZ() const; bool mode_snap() const; + + editor_ui *ui() const; + void redo_last(); + void handle_brush_mouse_hold(int Action, int Button); + void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); // members state_backup m_statebackup; // helper, cached variables to be restored on mode exit editormode_input m_input; - TCamera Camera; + static TCamera Camera; + + // focus (smooth camera fly-to) state + static bool m_focus_active; + glm::dvec3 m_focus_start_pos{0.0,0.0,0.0}; + glm::dvec3 m_focus_target_pos{0.0,0.0,0.0}; + glm::dvec3 m_focus_start_lookat{0.0,0.0,0.0}; + glm::dvec3 m_focus_target_lookat{0.0,0.0,0.0}; + double m_focus_time{0.0}; + double m_focus_duration{0.6}; + double fTime50Hz{0.0}; // bufor czasu dla komunikacji z PoKeys scene::basic_editor m_editor; scene::basic_node *m_node; // currently selected scene node bool m_takesnapshot{true}; // helper, hints whether snapshot of selected node(s) should be taken before modification bool m_dragging = false; - glm::vec3 oldPos; + glm::dvec3 oldPos; bool mouseHold{false}; + float kMaxPlacementDistance = 200.0f; + static bool m_change_history; + + // UI/history settings + int m_max_history_size{200}; + int m_selected_history_idx{-1}; + glm::dvec3 clamp_mouse_offset_to_max(const glm::dvec3 &offset); + + // focus camera smoothly on specified node + void start_focus(scene::basic_node *node, double duration = 0.6); + + // hierarchy management + void add_to_hierarchy(scene::basic_node *node); + void remove_from_hierarchy(scene::basic_node *node); + scene::basic_node* find_in_hierarchy(const std::string &uuid_str); + scene::basic_node* find_node_by_any(scene::basic_node *node_ptr, const std::string &uuid_str, const std::string &name); + + // clear history/redo pointers that reference the given node (prevent dangling pointers) + void nullify_history_pointers(scene::basic_node *node); + void render_change_history(); }; diff --git a/editoruipanels.cpp b/editoruipanels.cpp index 5d216a64..0819e920 100644 --- a/editoruipanels.cpp +++ b/editoruipanels.cpp @@ -61,9 +61,9 @@ void itemproperties_panel::update(scene::basic_node const *Node) } } */ - textline = "name: " + (node->name().empty() ? "(none)" : Bezogonkow(node->name())) + "\nlocation: [" + to_string(node->location().x, 2) + ", " + to_string(node->location().y, 2) + ", " + + textline = "name: " + (node->name().empty() ? "(none)" : Bezogonkow(node->name())) + "\ntype: " + node->node_type + "\nlocation: [" + to_string(node->location().x, 2) + ", " + to_string(node->location().y, 2) + ", " + to_string(node->location().z, 2) + "]" + - " (distance: " + to_string(glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z}), 1) + " m)"; + " (distance: " + to_string(glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z}), 1) + " m)" + "\nUUID: " + node->uuid.to_string(); text_lines.emplace_back(textline, Global.UITextColor); // subclass-specific data diff --git a/scene.cpp b/scene.cpp index fc3efc4e..896e1987 100644 --- a/scene.cpp +++ b/scene.cpp @@ -27,7 +27,8 @@ namespace scene { std::string const EU07_FILEEXTENSION_REGION { ".sbt" }; std::uint32_t const EU07_FILEHEADER { MAKE_ID4( 'E','U','0','7' ) }; std::uint32_t const EU07_FILEVERSION_REGION { MAKE_ID4( 'S', 'B', 'T', '2' ) }; - +std::map Hierarchy; + // potentially activates event handler with the same name as provided node, and within handler activation range void basic_cell::on_click( TAnimModel const *Instance ) { @@ -678,9 +679,6 @@ basic_cell::enclose_area( scene::basic_node *Node ) { m_area.radius, static_cast( glm::length( m_area.center - Node->location() ) + Node->radius() ) ); } - - - // potentially activates event handler with the same name as provided node, and within handler activation range void basic_section::on_click( TAnimModel const *Instance ) { diff --git a/scene.h b/scene.h index 82d7d1c6..dd430eaf 100644 --- a/scene.h +++ b/scene.h @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include "parser.h" #include "geometrybank.h" @@ -455,6 +456,8 @@ public: }; +// global hierarchy map for scene nodes +extern std::map Hierarchy; } // scene //--------------------------------------------------------------------------- diff --git a/scenenode.cpp b/scenenode.cpp index df9e2b21..1ad0d021 100644 --- a/scenenode.cpp +++ b/scenenode.cpp @@ -725,6 +725,8 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) { basic_node::basic_node( scene::node_data const &Nodedata ) : m_name( Nodedata.name ) { + uuid = UID::random(); + node_type = Nodedata.type; m_rangesquaredmin = Nodedata.range_min * Nodedata.range_min; m_rangesquaredmax = ( Nodedata.range_max >= 0.0 ? diff --git a/scenenode.h b/scenenode.h index 155c1d5d..89598280 100644 --- a/scenenode.h +++ b/scenenode.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "material.h" #include "vertex.h" #include "geometrybank.h" +#include "utils/uuid.hpp" struct lighting_data { @@ -352,6 +353,8 @@ public: bool dirty() const { return m_dirty; } + std::string node_type; + public: // members scene::group_handle m_group { null_handle }; // group this node belongs to, if any @@ -361,6 +364,7 @@ public: bool m_visible { true }; // visibility flag std::string m_name; bool m_dirty { false }; + UID uuid; private: // methods @@ -386,7 +390,6 @@ std::string basic_node::tooltip() const { return m_name; } - inline void basic_node::location( glm::dvec3 const Location ) { diff --git a/simulationstateserializer.cpp b/simulationstateserializer.cpp index 7e651c69..b7274691 100644 --- a/simulationstateserializer.cpp +++ b/simulationstateserializer.cpp @@ -550,6 +550,10 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), instance ); simulation::Region->insert( instance ); + scene::basic_node *hierarchy_node = instance; + if (hierarchy_node) + { scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node; + } } } else if( ( nodedata.type == "triangles" ) diff --git a/uilayer.cpp b/uilayer.cpp index bbdf4f65..806c72f3 100644 --- a/uilayer.cpp +++ b/uilayer.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation.h" #include "translation.h" #include "application.h" +#include "editormode.h" #include "imgui/imgui_impl_glfw.h" @@ -381,7 +382,7 @@ void ui_layer::render() render_tooltip(); render_menu(); render_quit_widget(); - + render_hierarchy(); // template method implementation render_(); @@ -423,6 +424,53 @@ void ui_layer::render_quit_widget() ImGui::End(); } +void ui_layer::render_hierarchy(){ + if(!m_editor_hierarchy) + return; + + ImGui::SetNextWindowSize(ImVec2(0, 0)); + ImGui::Begin(STR_C("Scene Hierarchy"), &m_editor_hierarchy, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); + ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); + + for (auto &entry : scene::Hierarchy) + { + const std::string &uuid = entry.first; + scene::basic_node *node = entry.second; + + if (node) + { + char buf[512]; + std::snprintf(buf, sizeof(buf), "%s | %s (%.1f, %.1f, %.1f)", + node->name().c_str(), + uuid.c_str(), + node->location().x, + node->location().y, + node->location().z); + + if (ImGui::Selectable(buf, false)) + { + // Focus camera on selected node + auto const node_pos = node->location(); + auto const camera_offset = glm::dvec3(0.0, 10.0, -20.0); + editor_mode::set_focus_active(false); + TCamera &camera = editor_mode::get_camera(); + camera.Pos = node_pos + camera_offset; + + } + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("UUID: %s\nNode type: %s", uuid.c_str(), typeid(*node).name()); + } + } + } + + ImGui::EndChild(); + ImGui::End(); + +} + void ui_layer::set_cursor(int const Mode) { glfwSetInputMode(m_window, GLFW_CURSOR, Mode); @@ -522,6 +570,14 @@ void ui_layer::render_menu_contents() GfxRenderer->Debug_Ui_State(ret); } + if(EditorModeFlag){ + ImGui::MenuItem("Hierarchy", nullptr, &m_editor_hierarchy); + bool change_history_enabled = editor_mode::change_history(); + if (ImGui::MenuItem("Change History", nullptr, &change_history_enabled)) + { + editor_mode::set_change_history(change_history_enabled); + } + } ImGui::EndMenu(); } } diff --git a/uilayer.h b/uilayer.h index c78a6d49..25399612 100644 --- a/uilayer.h +++ b/uilayer.h @@ -147,6 +147,7 @@ protected: void render_panels(); void render_menu(); void render_quit_widget(); + void render_hierarchy(); // draws a quad between coordinates x,y and z,w with uv-coordinates spanning 0-1 void quad( glm::vec4 const &Coordinates, glm::vec4 const &Color ); // members @@ -158,4 +159,6 @@ protected: std::string m_tooltip; bool m_quit_active = false; bool m_imgui_demo = false; + bool m_editor_hierarchy = false; + bool m_editor_change_history = false; }; diff --git a/utils/uuid.hpp b/utils/uuid.hpp new file mode 100644 index 00000000..a69b3191 --- /dev/null +++ b/utils/uuid.hpp @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +class UID { +public: + std::array bytes; + + + static UID random() { + static thread_local std::mt19937_64 gen(std::random_device{}()); + UID u; + uint64_t a = gen(); + uint64_t b = gen(); + for (int i = 0; i < 8; ++i) u.bytes[i] = uint8_t((a >> (i*8)) & 0xFF); + for (int i = 0; i < 8; ++i) u.bytes[8 + i] = uint8_t((b >> (i*8)) & 0xFF); + + u.bytes[6] = (u.bytes[6] & 0x0F) | 0x40; + u.bytes[8] = (u.bytes[8] & 0x3F) | 0x80; + + return u; + } + + std::string to_string() const { + std::ostringstream os; + os << std::hex << std::setfill('0'); + auto put = [&](int i){ + os << std::setw(2) << static_cast(bytes[i]); + }; + // format 8-4-4-4-12 + for (int i = 0; i < 4; ++i) put(i); + for (int i = 4; i < 6; ++i) put(i); + os << '-'; + for (int i = 6; i < 8; ++i) put(i); + os << '-'; + for (int i = 8; i < 10; ++i) put(i); + os << '-'; + for (int i = 10; i < 12; ++i) put(i); + os << '-'; + for (int i = 12; i < 16; ++i) put(i); + return os.str(); + } + + static UID from_string(const std::string& str) { + std::istringstream is(str); + is >> std::hex; + UID u; + for (int i = 0; i < 16; ++i) { + int byte; + is >> byte; + u.bytes[i] = static_cast(byte); + if (is.peek() == '-') is.get(); + } + return u; + } + + bool operator==(const UID &other) const noexcept { + return bytes == other.bytes; + } + + bool operator!=(const UID &other) const noexcept { + return !(*this == other); + } +}; \ No newline at end of file