375 lines
13 KiB
C++
375 lines
13 KiB
C++
#include <editor/Editor.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
|
|
#include <destrum/App.h>
|
|
#include <destrum/Assets/AssetTypes.h>
|
|
#include <destrum/Components/CameraComponent.h>
|
|
#include <destrum/FS/AssetFS.h>
|
|
#include <destrum/ObjectModel/GameObject.h>
|
|
#include <destrum/Scene/Scene.h>
|
|
#include <destrum/Serialization/PrefabSerializer.h>
|
|
#include <destrum/Util/ImGuiUtils.h>
|
|
|
|
namespace {
|
|
[[nodiscard]] std::vector<std::string> FindScenes()
|
|
{
|
|
std::vector<std::string> scenes;
|
|
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
|
|
std::error_code error;
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(mount.root, error)) {
|
|
if (entry.is_regular_file(error) && AssetTypes::IsScene(entry.path().string())) {
|
|
scenes.push_back(mount.scheme + "://" + entry.path().lexically_relative(mount.root).generic_string());
|
|
}
|
|
}
|
|
}
|
|
std::ranges::sort(scenes);
|
|
return scenes;
|
|
}
|
|
}
|
|
|
|
void Editor::DrawDockSpace()
|
|
{
|
|
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
|
|
const bool hasSavedLayout = ImGui::DockBuilderGetNode(dockspaceId) != nullptr;
|
|
const ImGuiViewport* mainViewport = ImGui::GetMainViewport();
|
|
ImGui::DockSpaceOverViewport(dockspaceId, mainViewport);
|
|
if (hasSavedLayout) {
|
|
return;
|
|
}
|
|
|
|
// First run without an imgui.ini: scene in the middle, inspector right, tools left, assets below
|
|
ImGui::DockBuilderRemoveNode(dockspaceId);
|
|
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
|
|
ImGui::DockBuilderSetNodeSize(dockspaceId, mainViewport->WorkSize);
|
|
|
|
ImGuiID center = dockspaceId;
|
|
const ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.25f, nullptr, ¢er);
|
|
const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, ¢er);
|
|
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, ¢er);
|
|
|
|
ImGui::DockBuilderDockWindow("Viewport", center);
|
|
ImGui::DockBuilderDockWindow("Inspector", right);
|
|
ImGui::DockBuilderDockWindow("Stats", left);
|
|
ImGui::DockBuilderDockWindow("Physics", left);
|
|
ImGui::DockBuilderDockWindow("Assets", bottom);
|
|
ImGui::DockBuilderFinish(dockspaceId);
|
|
}
|
|
|
|
void Editor::DrawMainMenuBar()
|
|
{
|
|
if (!ImGui::BeginMainMenuBar()) {
|
|
return;
|
|
}
|
|
DrawFileMenu();
|
|
DrawEditMenu();
|
|
DrawViewMenu();
|
|
DrawDisplayMenu();
|
|
DrawPlayControls();
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
|
|
void Editor::DrawFileMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("File")) {
|
|
return;
|
|
}
|
|
|
|
const bool editing = m_PlayMode.IsEditing();
|
|
if (ImGui::MenuItem("New Scene", nullptr, false, editing)) {
|
|
NewScene();
|
|
}
|
|
if (ImGui::MenuItem("Open Scene...", nullptr, false, editing)) {
|
|
m_OpenSceneDialog = true;
|
|
}
|
|
if (ImGui::MenuItem("Save Scene", "Ctrl+S", false, editing)) {
|
|
SaveScene();
|
|
}
|
|
if (ImGui::MenuItem("Save Scene As...", nullptr, false, editing)) {
|
|
m_SaveAsDialog = true;
|
|
}
|
|
|
|
ImGui::Separator();
|
|
Scene* scene = CurrentScene();
|
|
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
|
|
if (ImGui::MenuItem("Save Selection as Prefab", nullptr, false, selected != nullptr)) {
|
|
const std::string path = "game://prefabs/" + selected->GetName() + std::string{AssetTypes::PrefabExtension};
|
|
const auto prefab = PrefabSerializer::Capture(*selected);
|
|
SetStatus(prefab && AssetFS::GetInstance().WriteFile(path, prefab->dump(4))
|
|
? "Saved " + path + ", drag it from Assets into the viewport to place it"
|
|
: "Could not save " + path);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Quit")) {
|
|
m_App.requestQuit();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
ImGui::TextDisabled("%s", m_ScenePath.empty() ? "Unsaved scene" : m_ScenePath.c_str());
|
|
if (!m_Status.empty()) {
|
|
ImGui::TextWrapped("%s", m_Status.c_str());
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void Editor::DrawEditMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("Edit")) {
|
|
return;
|
|
}
|
|
|
|
const bool editing = m_PlayMode.IsEditing();
|
|
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, editing && m_UndoStack.CanUndo())) {
|
|
Undo();
|
|
}
|
|
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, editing && m_UndoStack.CanRedo())) {
|
|
Redo();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
Scene* scene = CurrentScene();
|
|
const bool hasSelection = scene != nullptr && scene->GetSelectedObject() != nullptr;
|
|
if (ImGui::MenuItem("Duplicate", "Ctrl+D", false, hasSelection)) {
|
|
DuplicateSelection();
|
|
}
|
|
if (ImGui::MenuItem("Delete", "Del", false, hasSelection)) {
|
|
DeleteSelection();
|
|
}
|
|
if (ImGui::MenuItem("Focus Selection", "F", false, hasSelection)) {
|
|
FocusSelection();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void Editor::DrawViewMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("View")) {
|
|
return;
|
|
}
|
|
|
|
ImGui::MenuItem("Inspector", nullptr, &m_Panels.inspector);
|
|
ImGui::MenuItem("Assets", nullptr, &m_Panels.assets);
|
|
ImGui::MenuItem("Stats", nullptr, &m_Panels.stats);
|
|
ImGui::MenuItem("Physics", nullptr, &m_Panels.physics);
|
|
ImGui::MenuItem("Image Cache", nullptr, &m_Panels.imageCache);
|
|
m_App.customWindowsMenu();
|
|
|
|
ImGui::SeparatorText("Scene View");
|
|
GameRenderer& renderer = m_App.getRenderer();
|
|
ImGui::MenuItem("Gizmos", nullptr, &m_View.gizmos);
|
|
ImGui::MenuItem("Colliders", nullptr, &m_View.colliders);
|
|
if (bool wireframe = renderer.getRenderWireframe(); ImGui::MenuItem("Wireframe", nullptr, &wireframe)) {
|
|
renderer.setRenderWireframe(wireframe);
|
|
}
|
|
if (bool heatmap = renderer.getRenderLightHeatmap(); ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &heatmap)) {
|
|
renderer.setRenderLightHeatmap(heatmap);
|
|
}
|
|
if (bool selectionView = renderer.getRenderSelectionView(); ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
|
|
renderer.setRenderSelectionView(selectionView);
|
|
}
|
|
|
|
if (Scene* scene = CurrentScene(); scene != nullptr && ImGui::BeginMenu("Play Camera")) {
|
|
const auto cameras = scene->GetCameras();
|
|
if (cameras.empty()) {
|
|
ImGui::TextDisabled("No cameras in the scene");
|
|
}
|
|
for (CameraComponent* camera : cameras) {
|
|
const GameObject* owner = camera->GetGameObject();
|
|
ImGui::PushID(owner);
|
|
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, camera->IsCurrent())) {
|
|
scene->SetCurrentCamera(camera);
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
const bool cooking = m_ShaderHotReload.IsCooking();
|
|
if (ImGui::MenuItem(cooking ? "Cooking Shaders..." : "Reload Shaders", nullptr, false, m_ShaderHotReload.IsConfigured() && !cooking)) {
|
|
m_ShaderHotReload.RequestCook();
|
|
}
|
|
if (ImGui::MenuItem("Free Unused Resources")) {
|
|
m_App.collectUnusedResources();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void Editor::DrawDisplayMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("Display")) {
|
|
return;
|
|
}
|
|
|
|
Window& window = m_App.getWindow();
|
|
const WindowMode mode = window.GetMode();
|
|
const bool fullscreen = window.IsFullscreen();
|
|
const bool maximized = window.IsMaximized();
|
|
|
|
if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) {
|
|
window.ToggleFullscreen();
|
|
}
|
|
if (ImGui::BeginMenu("Mode")) {
|
|
if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) {
|
|
window.SetMode(WindowMode::Windowed);
|
|
}
|
|
if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) {
|
|
window.SetMode(WindowMode::BorderlessFullscreen);
|
|
}
|
|
if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) {
|
|
window.SetMode(WindowMode::Fullscreen);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) {
|
|
window.ToggleMaximized();
|
|
}
|
|
if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) {
|
|
window.Center();
|
|
}
|
|
if (ImGui::BeginMenu("Size", !fullscreen)) {
|
|
constexpr std::array sizes{glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}};
|
|
for (const glm::ivec2& size : sizes) {
|
|
const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y);
|
|
if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) {
|
|
if (maximized) {
|
|
window.Restore();
|
|
}
|
|
window.SetSize(size);
|
|
window.Center();
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void Editor::DrawPlayControls()
|
|
{
|
|
const bool playing = m_PlayMode.IsPlaying();
|
|
const bool paused = m_PlayMode.IsPaused();
|
|
const char* state = !playing ? "EDIT" : paused ? "PAUSED" : "PLAYING";
|
|
const char* action = playing ? "Stop (F5)" : "Play (F5)";
|
|
const char* pauseAction = paused ? "Resume (F7)" : "Pause (F7)";
|
|
|
|
const ImGuiStyle& style = ImGui::GetStyle();
|
|
float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x + style.ItemSpacing.x * 2.0f +
|
|
style.FramePadding.x * 2.0f;
|
|
if (playing) {
|
|
width += ImGui::CalcTextSize(pauseAction).x + style.ItemSpacing.x + style.FramePadding.x * 2.0f;
|
|
}
|
|
|
|
// Centered like Godot's run buttons, unless the menus already reach past the middle
|
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), (ImGui::GetWindowWidth() - width) * 0.5f));
|
|
const ImVec4 stateColor = !playing ? ImVec4{0.7f, 0.7f, 0.7f, 1.0f}
|
|
: paused ? ImVec4{0.95f, 0.75f, 0.3f, 1.0f}
|
|
: ImVec4{0.4f, 0.9f, 0.4f, 1.0f};
|
|
ImGui::TextColored(stateColor, "%s", state);
|
|
if (playing && ImGui::MenuItem(pauseAction)) {
|
|
m_PlayMode.SetPaused(!paused);
|
|
}
|
|
if (ImGui::MenuItem(action)) {
|
|
m_PlayMode.RequestToggle();
|
|
}
|
|
}
|
|
|
|
void Editor::DrawSceneDialogs()
|
|
{
|
|
if (std::exchange(m_OpenSceneDialog, false)) {
|
|
ImGui::OpenPopup("Open Scene");
|
|
}
|
|
if (std::exchange(m_SaveAsDialog, false)) {
|
|
m_SaveAsPath = m_ScenePath.empty() ? "game://scenes/untitled.scene" : m_ScenePath;
|
|
ImGui::OpenPopup("Save Scene As");
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("Open Scene", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
const std::vector<std::string> scenes = FindScenes();
|
|
if (scenes.empty()) {
|
|
ImGui::TextDisabled("No .scene files in the mounted asset folders");
|
|
}
|
|
for (const std::string& path : scenes) {
|
|
if (ImGui::Selectable(path.c_str(), path == m_ScenePath)) {
|
|
OpenScene(path);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Button("Cancel")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("Save Scene As", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::SetNextItemWidth(420.0f);
|
|
const bool submitted = ImGuiUtils::InputString("##Path", m_SaveAsPath, ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::TextDisabled("A virtual path like game://scenes/level.scene");
|
|
if (ImGui::Button("Save") || submitted) {
|
|
SaveSceneAs(m_SaveAsPath);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
void Editor::DrawStatsPanel()
|
|
{
|
|
if (ImGui::Begin("Stats", &m_Panels.stats)) {
|
|
ImGui::Text("FPS: %.1f", m_App.getAverageFps());
|
|
|
|
const auto& stats = m_App.getRenderer().getDrawStats();
|
|
ImGui::SeparatorText("Meshes");
|
|
ImGui::Text("Submitted meshes: %u", stats.meshDraws);
|
|
ImGui::Text("Camera: %u indirect draws, %u instances", stats.mainViewCommands, stats.mainViewInstances);
|
|
ImGui::Text("All views: %u indirect draws, %u instances", stats.indirectCommands, stats.instances);
|
|
|
|
ImGui::SeparatorText("Memory");
|
|
ImGui::Text("Pending GPU deletions: %zu", m_App.getGfxDevice().getPendingDeletionCount());
|
|
ImGui::Text("Undo steps: %zu", m_UndoStack.UndoCount());
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void Editor::DrawPhysicsPanel()
|
|
{
|
|
if (!ImGui::Begin("Physics", &m_Panels.physics)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
App::SimulationControls& simulation = m_App.getSimulation();
|
|
ImGui::Text("Fixed dt: %.3f ms", simulation.fixedDeltaTime * 1000.0f);
|
|
ImGui::Text("Accumulator: %.3f ms", simulation.accumulator * 1000.0f);
|
|
ImGui::Text("Steps last frame: %d", simulation.stepsLastFrame);
|
|
|
|
ImGui::Separator();
|
|
ImGui::Checkbox("Paused", &simulation.paused);
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(!simulation.paused);
|
|
if (ImGui::Button("Step")) {
|
|
simulation.stepOnce = true;
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SliderFloat("Time Scale", &simulation.timeScale, 0.0f, 2.0f, "%.2fx");
|
|
if (ImGui::Button("Reset Time Scale")) {
|
|
simulation.timeScale = 1.0f;
|
|
}
|
|
ImGui::End();
|
|
}
|