feat: move editor to project / add post processing etc

This commit is contained in:
2026-10-23 20:54:12 +02:00
parent f7e7c56392
commit a3d3da4a67
104 changed files with 11750 additions and 2675 deletions
+31
View File
@@ -0,0 +1,31 @@
set(EDITOR_SRC
src/Editor.cpp
src/EditorMenus.cpp
src/EditorViewport.cpp
src/EditorCamera.cpp
src/PlayMode.cpp
src/UndoStack.cpp
src/ShaderHotReload.cpp
src/SceneInspector.cpp
src/AssetBrowser.cpp
src/TransformGizmo.cpp
src/RectGizmo.cpp
src/ResourcesInspector.cpp
)
add_library(destrum_editor ${EDITOR_SRC})
add_library(destrum::editor ALIAS destrum_editor)
set_target_properties(destrum_editor PROPERTIES
CXX_STANDARD 20
CXX_EXTENSIONS OFF
)
target_include_directories(destrum_editor PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_link_libraries(destrum_editor PUBLIC destrum::destrum)
target_compile_definitions(destrum_editor PRIVATE
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
DESTRUM_THECHEF_PATH="$<TARGET_FILE:TheChef>"
)
add_dependencies(destrum_editor TheChef)
+32
View File
@@ -0,0 +1,32 @@
#ifndef EDITOR_ASSETBROWSER_H
#define EDITOR_ASSETBROWSER_H
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
class AssetBrowser final {
public:
std::optional<std::string> Draw(bool* open = nullptr);
private:
struct Entry {
std::string name;
bool directory{false};
};
void Refresh();
void Navigate(const std::string& scheme, const std::filesystem::path& folder);
[[nodiscard]] std::string VirtualPath(const std::string& name) const;
void DrawEntry(const Entry& entry, std::optional<std::string>& opened, bool& navigated);
void DrawFolderMenu();
std::string m_Scheme;
std::filesystem::path m_Folder;
std::vector<Entry> m_Entries;
std::string m_Filter;
bool m_Dirty{true};
};
#endif // EDITOR_ASSETBROWSER_H
+141
View File
@@ -0,0 +1,141 @@
#ifndef EDITOR_EDITOR_H
#define EDITOR_EDITOR_H
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <glm/vec2.hpp>
#include <destrum/AppLayer.h>
#include <editor/AssetBrowser.h>
#include <editor/EditorCamera.h>
#include <editor/PlayMode.h>
#include <editor/RectGizmo.h>
#include <editor/ResourcesInspector.h>
#include <editor/SceneInspector.h>
#include <editor/ShaderHotReload.h>
#include <editor/TransformGizmo.h>
#include <editor/UndoStack.h>
class App;
class Scene;
struct EditorSettings {
std::filesystem::path gameAssetsSource;
std::string scenePath;
bool startInEditMode{true};
};
class Editor final : public AppLayer {
public:
Editor(App& app, EditorSettings settings);
void onAttach() override;
void onBeginFrame() override;
void onImGuiBegin(float dt) override;
void onImGui(float dt) override;
void onEndFrame() override;
void onRender(GameRenderer& renderer) override;
[[nodiscard]] Camera* viewCamera() override;
[[nodiscard]] bool hostsViewport() const override { return true; }
[[nodiscard]] InputFocus gameInputFocus() const override;
bool OpenScene(std::string_view path);
bool SaveScene();
bool SaveSceneAs(std::string_view path);
void NewScene();
[[nodiscard]] UndoStack& GetUndoStack() { return m_UndoStack; }
[[nodiscard]] PlayMode& GetPlayMode() { return m_PlayMode; }
private:
struct Panels {
bool inspector{true};
bool assets{true};
bool stats{true};
bool physics{false};
bool imageCache{false};
};
struct ViewOptions {
bool colliders{false};
bool gizmos{true};
};
struct Viewport {
glm::vec2 min{0.0f};
glm::vec2 size{1.0f};
glm::ivec2 desiredSize{0};
bool hovered{false};
bool focused{false};
bool mouseClaimed{false};
bool pickSelectsParent{false};
std::optional<glm::ivec2> hoverPixel;
};
// Editor.cpp
[[nodiscard]] Scene* CurrentScene() const;
[[nodiscard]] bool UsesEditorCamera();
void HandleShortcuts();
void Undo();
void Redo();
void RecordEdits();
void AfterSceneReplaced(bool resetHistory);
void DeleteSelection();
void DuplicateSelection();
void FocusSelection();
void SetStatus(std::string status);
bool WriteScene(Scene& scene, std::string_view path);
// EditorViewport.cpp
void ApplyViewportResize();
void DrawViewport();
void DrawGizmos(Scene& scene);
void RequestPick();
void ApplyPick();
void OnAssetDropped(std::string_view path);
void SubmitColliderLines(Scene& scene) const;
// EditorMenus.cpp
void DrawDockSpace();
void DrawMainMenuBar();
void DrawFileMenu();
void DrawEditMenu();
void DrawViewMenu();
void DrawDisplayMenu();
void DrawPlayControls();
void DrawSceneDialogs();
void DrawStatsPanel();
void DrawPhysicsPanel();
App& m_App;
EditorSettings m_Settings;
EditorCamera m_Camera;
PlayMode m_PlayMode;
UndoStack m_UndoStack;
SceneInspector m_Inspector;
AssetBrowser m_AssetBrowser;
ResourcesInspector m_ResourcesInspector;
TransformGizmo m_TransformGizmo;
RectGizmo m_RectGizmo;
ShaderHotReload m_ShaderHotReload;
Panels m_Panels;
ViewOptions m_View;
Viewport m_Viewport;
std::string m_ScenePath;
std::string m_Status;
std::string m_SaveAsPath;
bool m_OpenSceneDialog{false};
bool m_SaveAsDialog{false};
bool m_ItemWasActive{false};
bool m_EditPending{false};
};
#endif // EDITOR_EDITOR_H
+36
View File
@@ -0,0 +1,36 @@
#ifndef EDITOR_EDITORCAMERA_H
#define EDITOR_EDITORCAMERA_H
#include <optional>
#include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h>
class GameObject;
class RenderResources;
struct WorldBounds {
glm::vec3 center{0.0f};
float radius{1.0f};
};
[[nodiscard]] WorldBounds ComputeWorldBounds(GameObject& object, const RenderResources& resources);
class EditorCamera final {
public:
EditorCamera();
void Update(float dt, bool hasInput);
void Focus(const WorldBounds& bounds);
[[nodiscard]] Camera& GetCamera() { return m_Camera; }
[[nodiscard]] bool IsLooking() const { return m_Looking; }
private:
Camera m_Camera;
std::optional<glm::vec3> m_FocusTarget;
bool m_Looking{false};
};
#endif // EDITOR_EDITORCAMERA_H
+39
View File
@@ -0,0 +1,39 @@
#ifndef EDITOR_PLAYMODE_H
#define EDITOR_PLAYMODE_H
#include <memory>
#include <optional>
#include <nlohmann/json.hpp>
class App;
class Scene;
class PlayMode final {
public:
explicit PlayMode(App& app);
void EnterEditMode();
void RequestToggle() { m_ToggleRequested = true; }
void SetPaused(bool paused);
[[nodiscard]] bool ApplyPendingToggle();
[[nodiscard]] bool IsPlaying() const { return m_Playing; }
[[nodiscard]] bool IsPaused() const { return m_Paused; }
[[nodiscard]] bool IsEditing() const { return !m_Playing; }
private:
bool Enter();
void Exit();
App& m_App;
bool m_Playing{true};
bool m_Paused{false};
bool m_ToggleRequested{false};
bool m_MouseCapturedBeforePause{false};
std::optional<nlohmann::json> m_EditSnapshot;
std::weak_ptr<Scene> m_EditScene;
};
#endif // EDITOR_PLAYMODE_H
+28
View File
@@ -0,0 +1,28 @@
#ifndef EDITOR_RECTGIZMO_H
#define EDITOR_RECTGIZMO_H
#include <glm/glm.hpp>
class GameObject;
class RectGizmo final {
public:
[[nodiscard]] static bool CanManipulate(GameObject& object);
bool Manipulate(GameObject& object, const glm::vec2& drawImageSize, const glm::vec2& viewMin, const glm::vec2& viewSize);
[[nodiscard]] bool IsUsing() const { return m_ActiveHandle >= 0; }
[[nodiscard]] bool IsInteracting() const { return m_ActiveHandle >= 0 || m_Hovered; }
bool snap{false};
float snapStep{5.0f};
private:
int m_ActiveHandle{-1};
bool m_Hovered{false};
glm::vec2 m_DragStartMouse{0.0f};
glm::vec2 m_DragStartPosition{0.0f};
glm::vec2 m_DragStartSize{0.0f};
};
#endif // EDITOR_RECTGIZMO_H
@@ -0,0 +1,35 @@
#ifndef EDITOR_RESOURCESINSPECTOR_H
#define EDITOR_RESOURCESINSPECTOR_H
#include <destrum/Graphics/ids.h>
struct GPUImage;
class ImguiPass;
class RenderResources;
// ImGui windows for looking into the render resource caches
class ResourcesInspector final
{
public:
void drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass);
private:
struct PreviewInfo
{
bool previewable{false};
const char* reason{""};
};
[[nodiscard]] static PreviewInfo getPreviewInfo(const GPUImage& image);
void drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const;
void drawImageDetails(ImageID id, const RenderResources& resources) const;
[[nodiscard]] bool matchesFilter(ImageID id, const RenderResources& resources) const;
char filter[128]{};
float thumbnailSize{48.0f};
bool onlyPreviewable{false};
ImageID selectedImageId{NULL_IMAGE_ID};
};
#endif //EDITOR_RESOURCESINSPECTOR_H
+60
View File
@@ -0,0 +1,60 @@
#ifndef EDITOR_SCENEINSPECTOR_H
#define EDITOR_SCENEINSPECTOR_H
#include <functional>
#include <string>
#include <unordered_set>
#include <nlohmann/json.hpp>
#include <destrum/ObjectModel/ObjectId.h>
class Component;
class GameObject;
class Scene;
class SceneInspector final {
public:
void Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open);
private:
struct PendingMove {
ObjectId object{InvalidObjectId};
ObjectId parent{InvalidObjectId};
ObjectId anchor{InvalidObjectId};
bool after{false};
};
void DrawHierarchy(Scene& scene, ObjectId& selectedObjectId);
void DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId, std::unordered_set<const GameObject*>& visited);
void DrawRenameField(GameObject& object);
void DrawNodeDragDrop(GameObject& object);
void DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId);
void HandleShortcuts(Scene& scene, ObjectId& selectedObjectId);
void ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId);
void DrawObject(GameObject& object);
void DrawTransform(GameObject& object);
void DrawComponents(GameObject& object);
void DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable);
void DrawAddComponent(GameObject& object);
void BeginRename(const GameObject& object);
std::string m_Filter;
std::string m_ComponentFilter;
ObjectId m_RenamingId{InvalidObjectId};
std::string m_RenameBuffer;
bool m_FocusRename{false};
ObjectId m_OpenNextFrame{InvalidObjectId};
PendingMove m_PendingMove;
ObjectId m_PendingDelete{InvalidObjectId};
ObjectId m_PendingDuplicate{InvalidObjectId};
std::string m_ClipboardType;
nlohmann::json m_Clipboard;
};
#endif // EDITOR_SCENEINSPECTOR_H
+33
View File
@@ -0,0 +1,33 @@
#ifndef EDITOR_SHADERHOTRELOAD_H
#define EDITOR_SHADERHOTRELOAD_H
#include <filesystem>
#include <future>
#include <string>
#include <unordered_map>
class ShaderHotReload final {
public:
void Configure(std::filesystem::path cooker, std::filesystem::path sourceRoot, std::filesystem::path runtimeRoot);
[[nodiscard]] bool IsConfigured() const { return !m_Cooker.empty(); }
void RequestCook() { m_CookRequested = true; }
[[nodiscard]] bool Poll(float dt);
[[nodiscard]] bool IsCooking() const { return m_Cook.valid(); }
[[nodiscard]] const std::string& GetLastResult() const { return m_LastResult; }
private:
[[nodiscard]] bool SourcesChanged();
void StartCook();
std::filesystem::path m_Cooker;
std::filesystem::path m_SourceRoot;
std::filesystem::path m_RuntimeRoot;
std::unordered_map<std::string, std::filesystem::file_time_type> m_Stamps;
std::future<int> m_Cook;
std::string m_LastResult;
float m_PollTimer{0.0f};
bool m_CookRequested{false};
};
#endif // EDITOR_SHADERHOTRELOAD_H
+37
View File
@@ -0,0 +1,37 @@
#ifndef EDITOR_TRANSFORMGIZMO_H
#define EDITOR_TRANSFORMGIZMO_H
#include <glm/glm.hpp>
class Camera;
class GameObject;
enum class GizmoOperation {
Translate,
Rotate,
Scale,
};
enum class GizmoSpace {
World,
Local,
};
class TransformGizmo final {
public:
void HandleShortcuts();
void DrawToolbar();
bool Manipulate(GameObject& object, const Camera& camera, const glm::vec2& viewMin, const glm::vec2& viewSize);
[[nodiscard]] bool IsUsing() const;
[[nodiscard]] bool IsOver() const;
GizmoOperation operation{GizmoOperation::Translate};
GizmoSpace space{GizmoSpace::World};
bool snap{false};
float translationSnap{0.5f};
float rotationSnapDegrees{15.0f};
float scaleSnap{0.1f};
};
#endif // EDITOR_TRANSFORMGIZMO_H
+42
View File
@@ -0,0 +1,42 @@
#ifndef EDITOR_UNDOSTACK_H
#define EDITOR_UNDOSTACK_H
#include <cstddef>
#include <deque>
#include <optional>
#include <string>
#include <vector>
#include <destrum/ObjectModel/ObjectId.h>
class Scene;
class UndoStack final {
public:
static constexpr std::size_t MaxSteps = 64;
void Reset(Scene& scene);
void Rebase(Scene& scene);
bool Record(Scene& scene);
bool Undo(Scene& scene);
bool Redo(Scene& scene);
[[nodiscard]] bool CanUndo() const { return !m_Undo.empty(); }
[[nodiscard]] bool CanRedo() const { return !m_Redo.empty(); }
[[nodiscard]] std::size_t UndoCount() const { return m_Undo.size(); }
private:
struct Snapshot {
std::string scene;
ObjectId selected{InvalidObjectId};
};
[[nodiscard]] static std::optional<Snapshot> Capture(Scene& scene);
bool Restore(Scene& scene, const Snapshot& snapshot);
std::deque<Snapshot> m_Undo;
std::vector<Snapshot> m_Redo;
std::optional<Snapshot> m_Current;
};
#endif // EDITOR_UNDOSTACK_H
+198
View File
@@ -0,0 +1,198 @@
#include <editor/AssetBrowser.h>
#include <algorithm>
#include <cctype>
#include <imgui.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Material.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] std::string Lowercase(std::string_view value)
{
std::string result{value};
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
// Cooker outputs and bookkeeping sit next to the sources in the runtime folders, nobody picks those
[[nodiscard]] bool IsHidden(const std::filesystem::path& path)
{
const std::string name = path.filename().string();
return name == "manifest.json" || name.ends_with(".tmp") ||
AssetTypes::HasExtension(name, {".dmodel", ".dtex", ".spv", AssetTypes::MetaExtension});
}
[[nodiscard]] const FSMount* FindMount(const std::string& scheme)
{
const auto& mounts = AssetFS::GetInstance().GetMounts();
const auto it = std::ranges::find(mounts, scheme, &FSMount::scheme);
return it != mounts.end() ? &*it : nullptr;
}
[[nodiscard]] const char* Tag(const std::string& name, bool directory)
{
if (directory) return "[dir] ";
if (AssetTypes::IsModel(name)) return "[model] ";
if (AssetTypes::IsScene(name)) return "[scene] ";
if (AssetTypes::IsPrefab(name)) return "[prefab] ";
if (AssetTypes::IsMaterial(name)) return "[material] ";
if (AssetTypes::IsImage(name)) return "[image] ";
return "";
}
}
std::optional<std::string> AssetBrowser::Draw(bool* open)
{
std::optional<std::string> opened;
if (!ImGui::Begin("Assets", open)) {
ImGui::End();
return opened;
}
const auto& mounts = AssetFS::GetInstance().GetMounts();
if (mounts.empty()) {
ImGui::TextDisabled("No asset folders are mounted");
ImGui::End();
return opened;
}
if (FindMount(m_Scheme) == nullptr) {
Navigate(mounts.front().scheme, {});
}
for (const FSMount& mount : mounts) {
if (ImGui::RadioButton(mount.scheme.c_str(), m_Scheme == mount.scheme)) {
Navigate(mount.scheme, {});
}
ImGui::SameLine();
}
if (ImGui::Button("Refresh")) {
m_Dirty = true;
}
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
ImGuiUtils::InputString("##Filter", m_Filter, 0, "Search this folder");
ImGui::BeginDisabled(m_Folder.empty());
if (ImGui::ArrowButton("##Up", ImGuiDir_Up)) {
Navigate(m_Scheme, m_Folder.parent_path());
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextUnformatted((m_Scheme + "://" + m_Folder.generic_string()).c_str());
ImGui::Separator();
if (m_Dirty) {
Refresh();
}
if (ImGui::BeginChild("Entries")) {
const std::string filter = Lowercase(m_Filter);
bool navigated = false;
for (const Entry& entry : m_Entries) {
if (filter.empty() || Lowercase(entry.name).find(filter) != std::string::npos) {
DrawEntry(entry, opened, navigated);
}
if (navigated) {
break;
}
}
DrawFolderMenu();
}
ImGui::EndChild();
ImGui::End();
return opened;
}
std::string AssetBrowser::VirtualPath(const std::string& name) const
{
return m_Scheme + "://" + (m_Folder / name).generic_string();
}
void AssetBrowser::DrawEntry(const Entry& entry, std::optional<std::string>& opened, bool& navigated)
{
const std::string virtualPath = VirtualPath(entry.name);
const std::string label = Tag(entry.name, entry.directory) + entry.name;
ImGui::PushID(entry.name.c_str());
if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick) &&
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
if (entry.directory) {
Navigate(m_Scheme, m_Folder / entry.name);
navigated = true;
} else {
opened = virtualPath;
}
}
if (!entry.directory && ImGui::BeginDragDropSource()) {
ImGui::SetDragDropPayload(ImGuiUtils::AssetPayload, virtualPath.c_str(), virtualPath.size() + 1);
ImGui::TextUnformatted(virtualPath.c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginPopupContextItem("EntryMenu")) {
if (ImGui::MenuItem("Copy Path")) {
ImGui::SetClipboardText(virtualPath.c_str());
}
ImGui::EndPopup();
}
ImGui::SetItemTooltip("%s", virtualPath.c_str());
ImGui::PopID();
}
void AssetBrowser::DrawFolderMenu()
{
if (!ImGui::BeginPopupContextWindow("FolderMenu", ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) {
return;
}
if (ImGui::MenuItem("New Material")) {
std::string path = VirtualPath("new_material" + std::string{AssetTypes::MaterialExtension});
for (int suffix = 1; std::filesystem::exists(AssetFS::GetInstance().GetFullPath(path)); ++suffix) {
path = VirtualPath("new_material_" + std::to_string(suffix) + std::string{AssetTypes::MaterialExtension});
}
const Material material = Material::SimpleColor(glm::vec3{1.0f}, std::filesystem::path{path}.stem().string());
AssetFS::GetInstance().WriteFile(path, MaterialSerialization::ToFile(material).dump(4));
m_Dirty = true;
}
ImGui::EndPopup();
}
void AssetBrowser::Navigate(const std::string& scheme, const std::filesystem::path& folder)
{
m_Scheme = scheme;
m_Folder = folder;
m_Filter.clear();
m_Dirty = true;
}
void AssetBrowser::Refresh()
{
m_Dirty = false;
m_Entries.clear();
const FSMount* mount = FindMount(m_Scheme);
if (mount == nullptr) {
return;
}
std::error_code error;
for (const auto& item : std::filesystem::directory_iterator(mount->root / m_Folder, error)) {
if (!IsHidden(item.path())) {
m_Entries.push_back({.name = item.path().filename().string(), .directory = item.is_directory(error)});
}
}
std::ranges::sort(m_Entries, [](const Entry& a, const Entry& b) {
if (a.directory != b.directory) {
return a.directory;
}
return Lowercase(a.name) < Lowercase(b.name);
});
}
+387
View File
@@ -0,0 +1,387 @@
#include <editor/Editor.h>
#include <utility>
#include <imgui.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <ImGuizmo.h>
namespace {
[[nodiscard]] bool AnyKeyReleased()
{
for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
if (ImGui::IsKeyReleased(static_cast<ImGuiKey>(key))) {
return true;
}
}
return false;
}
}
Editor::Editor(App& app, EditorSettings settings)
: m_App(app),
m_Settings(std::move(settings)),
m_PlayMode(app),
m_ScenePath(m_Settings.scenePath)
{
}
void Editor::onAttach()
{
auto& assetFs = AssetFS::GetInstance();
#ifdef DESTRUM_ENGINE_ASSETS_SRC_DIR
assetFs.SetSourceRoot("engine", DESTRUM_ENGINE_ASSETS_SRC_DIR);
#endif
if (!m_Settings.gameAssetsSource.empty()) {
assetFs.SetSourceRoot("game", m_Settings.gameAssetsSource);
}
AssetDatabase::GetInstance().Refresh();
#if defined(DESTRUM_ENGINE_ASSETS_SRC_DIR) && defined(DESTRUM_THECHEF_PATH)
m_ShaderHotReload.Configure(DESTRUM_THECHEF_PATH, DESTRUM_ENGINE_ASSETS_SRC_DIR, assetFs.GetFullPath("engine://shaders").parent_path());
#endif
if (m_Settings.startInEditMode) {
m_PlayMode.EnterEditMode();
}
if (Scene* scene = CurrentScene()) {
m_UndoStack.Reset(*scene);
}
}
Scene* Editor::CurrentScene() const
{
auto& sceneManager = SceneManager::GetInstance();
return sceneManager.GetActiveScenes().empty() ? nullptr : &sceneManager.GetCurrentScene();
}
bool Editor::UsesEditorCamera()
{
return m_PlayMode.IsEditing() || m_PlayMode.IsPaused() || m_App.gameCamera() == nullptr;
}
Camera* Editor::viewCamera()
{
return UsesEditorCamera() ? &m_Camera.GetCamera() : nullptr;
}
AppLayer::InputFocus Editor::gameInputFocus() const
{
return {
.mouse = m_Viewport.hovered && !m_Viewport.mouseClaimed,
.keyboard = m_Viewport.focused || m_Camera.IsLooking(),
};
}
void Editor::onBeginFrame()
{
ApplyViewportResize();
}
void Editor::onImGuiBegin(float dt)
{
DrawDockSpace();
ApplyPick();
HandleShortcuts();
if (UsesEditorCamera()) {
m_Camera.Update(dt, m_Viewport.hovered || m_Viewport.focused || m_Camera.IsLooking());
}
if (m_ShaderHotReload.Poll(dt)) {
const bool reloaded = m_App.getRenderer().reloadShaders(m_App.getGfxDevice());
SetStatus(reloaded ? "Reloaded shaders" : "Shader reload failed, the old shaders stay active");
}
}
void Editor::onImGui(float)
{
DrawMainMenuBar();
DrawSceneDialogs();
DrawViewport();
Scene* scene = CurrentScene();
if (scene != nullptr && m_Panels.inspector) {
ObjectId selected = scene->GetSelectedObjectId();
m_Inspector.Draw(*scene, selected, [scene] { scene->DrawSettingsImGui(); }, &m_Panels.inspector);
scene->SetSelectedObject(selected);
}
if (m_Panels.assets) {
if (const auto opened = m_AssetBrowser.Draw(&m_Panels.assets)) {
OnAssetDropped(*opened);
}
}
if (m_Panels.stats) {
DrawStatsPanel();
}
if (m_Panels.physics) {
DrawPhysicsPanel();
}
if (m_Panels.imageCache) {
m_ResourcesInspector.drawImageCacheWindow(&m_Panels.imageCache, m_App.getResources(), m_App.getImguiPass());
}
if (scene != nullptr && m_View.gizmos && UsesEditorCamera()) {
GameObject* selected = scene->GetSelectedObject();
if (selected != nullptr && !RectGizmo::CanManipulate(*selected)) {
m_TransformGizmo.DrawToolbar();
}
}
}
void Editor::onEndFrame()
{
if (m_PlayMode.ApplyPendingToggle()) {
AfterSceneReplaced(false);
}
const bool itemActive = ImGui::IsAnyItemActive() || ImGuizmo::IsUsing();
const bool gestureEnded = (m_ItemWasActive && !itemActive) || ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
AnyKeyReleased() || std::exchange(m_EditPending, false);
m_ItemWasActive = itemActive;
if (gestureEnded && !itemActive && m_PlayMode.IsEditing()) {
RecordEdits();
}
}
void Editor::onRender(GameRenderer& renderer)
{
const bool picking = m_PlayMode.IsEditing() || m_PlayMode.IsPaused();
renderer.setSelectionPassEnabled(picking);
renderer.setHoverPixel(picking ? m_Viewport.hoverPixel : std::nullopt);
std::vector<std::uint32_t> highlighted;
Scene* scene = CurrentScene();
if (scene == nullptr) {
renderer.setHighlightedObjects(highlighted);
return;
}
if (GameObject* selected = scene->GetSelectedObject(); selected != nullptr && picking) {
std::vector<GameObject*> pending{selected};
while (!pending.empty()) {
GameObject* object = pending.back();
pending.pop_back();
highlighted.push_back(static_cast<std::uint32_t>(object->GetId()));
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
}
}
renderer.setHighlightedObjects(highlighted);
if (m_View.colliders) {
SubmitColliderLines(*scene);
}
}
void Editor::HandleShortcuts()
{
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) {
m_PlayMode.RequestToggle();
}
if (ImGui::IsKeyPressed(ImGuiKey_F7, false) && m_PlayMode.IsPlaying()) {
m_PlayMode.SetPaused(!m_PlayMode.IsPaused());
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S, false)) {
SaveScene();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z)) {
io.KeyShift ? Redo() : Undo();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y)) {
Redo();
}
if (!m_Viewport.focused && !m_Viewport.hovered) {
return;
}
if (UsesEditorCamera() && !m_Camera.IsLooking()) {
m_TransformGizmo.HandleShortcuts();
}
if (ImGui::IsKeyPressed(ImGuiKey_F, false) && !io.KeyCtrl) {
FocusSelection();
}
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
DeleteSelection();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D, false)) {
DuplicateSelection();
}
}
void Editor::Undo()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
if (m_UndoStack.Undo(*scene)) {
m_App.collectUnusedResources();
m_App.onPlayModeChanged(false);
}
}
void Editor::Redo()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
if (m_UndoStack.Redo(*scene)) {
m_App.collectUnusedResources();
m_App.onPlayModeChanged(false);
}
}
void Editor::RecordEdits()
{
if (Scene* scene = CurrentScene()) {
m_UndoStack.Record(*scene);
}
}
void Editor::AfterSceneReplaced(bool resetHistory)
{
m_App.collectUnusedResources();
Scene* scene = CurrentScene();
if (scene == nullptr) {
return;
}
if (resetHistory) {
m_UndoStack.Reset(*scene);
} else {
m_UndoStack.Rebase(*scene);
}
}
void Editor::DeleteSelection()
{
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (selected == nullptr) {
return;
}
selected->Destroy();
scene->SetSelectedObject(InvalidObjectId);
m_EditPending = true;
}
void Editor::DuplicateSelection()
{
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (selected == nullptr) {
return;
}
if (const GameObject* copy = PrefabSerializer::Duplicate(*selected)) {
scene->SetSelectedObject(copy->GetId());
m_EditPending = true;
}
}
void Editor::FocusSelection()
{
Scene* scene = CurrentScene();
if (GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr) {
m_Camera.Focus(ComputeWorldBounds(*selected, m_App.getResources()));
}
}
void Editor::SetStatus(std::string status)
{
spdlog::info("{}", status);
m_Status = std::move(status);
}
bool Editor::WriteScene(Scene& scene, std::string_view path)
{
const AssetDatabase::MetaFileScope createMetaFiles;
const auto root = SceneSerializer::Capture(scene);
if (!root || !AssetFS::GetInstance().WriteFile(path, root->dump(4))) {
SetStatus("Could not save the scene to " + std::string{path});
return false;
}
SetStatus("Saved " + std::string{path});
return true;
}
bool Editor::SaveScene()
{
if (!m_PlayMode.IsEditing()) {
SetStatus("Stop play mode before saving, the scene is the play copy right now");
return false;
}
if (m_ScenePath.empty()) {
m_SaveAsDialog = true;
return false;
}
Scene* scene = CurrentScene();
return scene != nullptr && WriteScene(*scene, m_ScenePath);
}
bool Editor::SaveSceneAs(std::string_view path)
{
std::string target{path};
if (!AssetTypes::IsScene(target)) {
target += AssetTypes::SceneExtension;
}
m_ScenePath = target;
return SaveScene();
}
bool Editor::OpenScene(std::string_view path)
{
Scene* scene = CurrentScene();
if (scene == nullptr) {
return false;
}
if (!m_PlayMode.IsEditing()) {
SetStatus("Stop play mode before opening another scene");
return false;
}
if (!SceneSerializer::Load(*scene, AssetFS::GetInstance().GetFullPath(path))) {
SetStatus("Could not open " + std::string{path});
return false;
}
m_ScenePath = path;
m_PlayMode.EnterEditMode();
AfterSceneReplaced(true);
SetStatus("Opened " + std::string{path});
return true;
}
void Editor::NewScene()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
const nlohmann::json empty{{"name", "Untitled"}, {"objects", nlohmann::json::array()}};
if (SceneSerializer::LoadFromJson(*scene, empty)) {
m_ScenePath.clear();
m_PlayMode.EnterEditMode();
AfterSceneReplaced(true);
}
}
+99
View File
@@ -0,0 +1,99 @@
#include <editor/EditorCamera.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
WorldBounds ComputeWorldBounds(GameObject& object, const RenderResources& resources)
{
glm::vec3 boundsMin{std::numeric_limits<float>::max()};
glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
std::vector<GameObject*> pending{&object};
while (!pending.empty()) {
GameObject* current = pending.back();
pending.pop_back();
for (const Transform* child : current->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
const auto* meshRenderer = current->GetComponent<MeshRendererComponent>();
if (meshRenderer == nullptr || !resources.meshes().isValid(meshRenderer->GetMeshID())) {
continue;
}
const GPUMesh& mesh = resources.meshes().getMesh(meshRenderer->GetMeshID());
const glm::mat4 world = current->GetTransform().GetWorldMatrix();
for (int corner = 0; corner < 8; ++corner) {
const glm::vec3 local{
corner & 1 ? mesh.maxPos.x : mesh.minPos.x,
corner & 2 ? mesh.maxPos.y : mesh.minPos.y,
corner & 4 ? mesh.maxPos.z : mesh.minPos.z,
};
const glm::vec3 point{world * glm::vec4{local, 1.0f}};
boundsMin = glm::min(boundsMin, point);
boundsMax = glm::max(boundsMax, point);
}
}
if (boundsMin.x > boundsMax.x) {
return {.center = object.GetTransform().GetWorldPosition(), .radius = 1.0f};
}
return {
.center = (boundsMin + boundsMax) * 0.5f,
.radius = std::max(glm::length(boundsMax - boundsMin) * 0.5f, 0.1f),
};
}
EditorCamera::EditorCamera()
: m_Camera{glm::vec3{0.0f, 2.0f, -6.0f}, glm::vec3{0.0f, 1.0f, 0.0f}}
{
m_Camera.SetRotation(glm::radians(glm::vec2{90.0f, -10.0f}));
}
void EditorCamera::Update(float dt, bool hasInput)
{
auto& input = InputManager::GetInstance();
const bool looking = hasInput && input.IsMouseDown(SDL_BUTTON_RIGHT);
if (looking != m_Looking) {
m_Looking = looking;
input.SetMouseCaptured(looking);
}
if (m_FocusTarget) {
if (looking) {
m_FocusTarget.reset();
} else {
m_Camera.m_position = glm::mix(m_Camera.m_position, *m_FocusTarget, 1.0f - std::exp(-12.0f * dt));
if (glm::distance(m_Camera.m_position, *m_FocusTarget) < 0.01f) {
m_Camera.m_position = *m_FocusTarget;
m_FocusTarget.reset();
}
}
}
if (hasInput) {
m_Camera.Update(dt);
} else {
m_Camera.CalculateViewMatrix();
}
}
void EditorCamera::Focus(const WorldBounds& bounds)
{
// Fit the bounding sphere in the narrower of the two field of view angles
const float halfFovY = glm::radians(m_Camera.GetFOVY()) * 0.5f;
const float halfFovX = std::atan(std::tan(halfFovY) * m_Camera.GetAspectRatio());
const float distance = bounds.radius / std::sin(std::min(halfFovX, halfFovY)) * 1.1f;
m_FocusTarget = bounds.center - m_Camera.GetForward() * distance;
}
+374
View File
@@ -0,0 +1,374 @@
#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, &center);
const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, &center);
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, &center);
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();
}
+239
View File
@@ -0,0 +1,239 @@
#include <editor/Editor.h>
#include <algorithm>
#include <filesystem>
#include <vector>
#include <imgui.h>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/ModelSpawner.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
#include <destrum/Util/ModelDoc.h>
void Editor::ApplyViewportResize()
{
const glm::ivec2 desired = m_Viewport.desiredSize;
const VkExtent3D current = m_App.getRenderer().getDrawImage().extent;
if (desired.x <= 0 || desired.y <= 0 ||
(static_cast<int>(current.width) == desired.x && static_cast<int>(current.height) == desired.y)) {
return;
}
// While a dock splitter is dragged the old image is stretched, recreating it every frame is wasteful
if (ImGui::GetIO().MouseDown[ImGuiMouseButton_Left]) {
return;
}
m_App.resizeRenderTarget(desired);
}
void Editor::DrawViewport()
{
m_Viewport.mouseClaimed = false;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0.0f, 0.0f});
const bool visible = ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
if (!visible) {
m_Viewport.hovered = false;
m_Viewport.focused = false;
m_Viewport.hoverPixel.reset();
ImGui::End();
return;
}
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 available = ImGui::GetContentRegionAvail();
const ImVec2 framebufferScale = ImGui::GetIO().DisplayFramebufferScale;
m_Viewport.min = {origin.x, origin.y};
m_Viewport.size = {std::max(available.x, 1.0f), std::max(available.y, 1.0f)};
m_Viewport.desiredSize = {
std::max(static_cast<int>(available.x * framebufferScale.x), 1),
std::max(static_cast<int>(available.y * framebufferScale.y), 1),
};
const GPUImage& drawImage = m_App.getRenderer().getDrawImage();
if (const ImTextureID texture = m_App.getImguiPass().getFrameTexture(drawImage.imageView); texture != ImTextureID{}) {
ImGui::Image(texture, ImVec2{m_Viewport.size.x, m_Viewport.size.y});
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
OnAssetDropped(static_cast<const char*>(payload->Data));
}
ImGui::EndDragDropTarget();
}
}
m_Viewport.hovered = ImGui::IsWindowHovered();
// ImGui only focuses windows on a left click, the camera looks around with the right button
if (m_Viewport.hovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Right) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle))) {
ImGui::SetWindowFocus();
}
m_Viewport.focused = ImGui::IsWindowFocused();
const ImVec2 mouse = ImGui::GetMousePos();
const glm::vec2 uv = (glm::vec2{mouse.x, mouse.y} - m_Viewport.min) / m_Viewport.size;
const bool insideImage = uv.x >= 0.0f && uv.y >= 0.0f && uv.x < 1.0f && uv.y < 1.0f;
m_Viewport.hoverPixel.reset();
if (m_Viewport.hovered && insideImage && !m_Camera.IsLooking()) {
const VkExtent3D extent = drawImage.extent;
m_Viewport.hoverPixel = glm::ivec2{uv * glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)}};
}
ImGui::SetCursorScreenPos(origin);
if (Scene* scene = CurrentScene(); scene != nullptr && m_View.gizmos && UsesEditorCamera()) {
DrawGizmos(*scene);
}
RequestPick();
ImGui::End();
}
void Editor::DrawGizmos(Scene& scene)
{
GameObject* selected = scene.GetSelectedObject();
if (selected == nullptr) {
return;
}
if (RectGizmo::CanManipulate(*selected)) {
const VkExtent3D extent = m_App.getRenderer().getDrawImage().extent;
m_RectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)},
m_Viewport.min, m_Viewport.size);
m_Viewport.mouseClaimed = m_Viewport.mouseClaimed || m_RectGizmo.IsInteracting();
return;
}
m_TransformGizmo.Manipulate(*selected, m_Camera.GetCamera(), m_Viewport.min, m_Viewport.size);
m_Viewport.mouseClaimed = m_Viewport.mouseClaimed || m_TransformGizmo.IsOver() || m_TransformGizmo.IsUsing();
}
void Editor::RequestPick()
{
const bool picking = m_PlayMode.IsEditing() || m_PlayMode.IsPaused();
if (!picking || !m_Viewport.hoverPixel || m_Viewport.mouseClaimed || !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
return;
}
m_App.getRenderer().requestSelectionPick(*m_Viewport.hoverPixel);
m_Viewport.pickSelectsParent = ImGui::GetIO().KeyAlt;
}
void Editor::ApplyPick()
{
const auto picked = m_App.getRenderer().takeSelectionPick();
Scene* scene = CurrentScene();
if (!picked || scene == nullptr) {
return;
}
GameObject* object = scene->FindGameObject(*picked);
if (object != nullptr && m_Viewport.pickSelectsParent) {
if (const Transform* parent = object->GetTransform().GetParent(); parent != nullptr && parent->GetOwner() != nullptr) {
object = parent->GetOwner();
}
}
scene->SetSelectedObject(object != nullptr ? object->GetId() : InvalidObjectId);
}
void Editor::OnAssetDropped(std::string_view path)
{
Scene* scene = CurrentScene();
if (scene == nullptr) {
return;
}
if (AssetTypes::IsScene(path)) {
OpenScene(path);
return;
}
const Camera& view = m_Camera.GetCamera();
const glm::vec3 spawnPosition = view.GetPosition() + view.GetForward() * 5.0f;
const std::string name = std::filesystem::path{path}.stem().string();
try {
if (AssetTypes::IsModel(path)) {
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
GameObject* model = ModelSpawner::SpawnStaticModel(
*scene, m_App.getGfxDevice(), m_App.getResources(), path, options, name);
model->GetTransform().SetWorldPosition(spawnPosition);
scene->SetSelectedObject(model->GetId());
} else if (AssetTypes::IsPrefab(path)) {
GameObject* instance = PrefabSerializer::Instantiate(*scene, AssetFS::GetInstance().GetFullPath(path).string());
if (instance == nullptr) {
SetStatus("Could not instantiate " + std::string{path});
return;
}
instance->GetTransform().SetWorldPosition(spawnPosition);
scene->SetSelectedObject(instance->GetId());
} else if (AssetTypes::IsMaterial(path)) {
GameObject* selected = scene->GetSelectedObject();
if (selected == nullptr) {
SetStatus("Select an object to give it " + std::string{path});
return;
}
const MaterialID material = MaterialSerialization::LoadFile(m_App.getResources(), m_App.getGfxDevice(), path);
std::vector<GameObject*> pending{selected};
while (!pending.empty()) {
GameObject* object = pending.back();
pending.pop_back();
if (auto* meshRenderer = object->GetComponent<MeshRendererComponent>()) {
meshRenderer->SetMaterialID(material);
}
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
}
} else {
return;
}
m_EditPending = true;
} catch (const std::exception& exception) {
spdlog::error("Could not use {}: {}", path, exception.what());
SetStatus("Could not use " + std::string{path});
}
}
void Editor::SubmitColliderLines(Scene& scene) const
{
// Triggers orange, then colored by body type: static blue, dynamic green, kinematic purple
for (const auto& object : scene.GetObjects()) {
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
continue;
}
const auto* rigidbody = object->GetComponent<Rigidbody>();
const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static;
for (const auto& component : object->GetComponents()) {
const auto* collider = dynamic_cast<const Collider*>(component.get());
if (collider == nullptr || collider->IsBeingDestroyed() || !collider->isEnabled()) {
continue;
}
glm::vec4 color{0.1f, 0.8f, 1.0f, 1.0f};
if (collider->IsTrigger()) {
color = {1.0f, 0.75f, 0.1f, 1.0f};
} else if (type == RigidbodyType::Dynamic) {
color = {0.3f, 1.0f, 0.3f, 1.0f};
} else if (type == RigidbodyType::Kinematic) {
color = {0.8f, 0.4f, 1.0f, 1.0f};
}
collider->DrawDebugLines(color);
}
}
}
+118
View File
@@ -0,0 +1,118 @@
#include <editor/PlayMode.h>
#include <algorithm>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h>
PlayMode::PlayMode(App& app)
: m_App(app)
{
}
void PlayMode::EnterEditMode()
{
m_Playing = false;
m_Paused = false;
SceneManager::GetInstance().SetPlaying(false);
SceneManager::GetInstance().SetPaused(false);
InputManager::GetInstance().SetMouseCaptured(false);
m_App.onPlayModeChanged(false);
}
void PlayMode::SetPaused(bool paused)
{
if (!m_Playing || paused == m_Paused) {
return;
}
m_Paused = paused;
SceneManager::GetInstance().SetPaused(paused);
// Hand the mouse to the editor while paused, give it back to the game on resume
auto& input = InputManager::GetInstance();
if (paused) {
m_MouseCapturedBeforePause = input.IsMouseCaptured();
input.SetMouseCaptured(false);
} else {
input.SetMouseCaptured(m_MouseCapturedBeforePause);
}
}
bool PlayMode::ApplyPendingToggle()
{
if (!m_ToggleRequested) {
return false;
}
m_ToggleRequested = false;
if (m_Playing) {
Exit();
return true;
}
return Enter();
}
bool PlayMode::Enter()
{
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
spdlog::error("Cannot enter play mode without an active scene");
return false;
}
const std::shared_ptr<Scene> scene = sceneManager.GetActiveScenes().front();
auto snapshot = SceneSerializer::Capture(*scene);
if (!snapshot) {
spdlog::error("Cannot enter play mode, scene \"{}\" could not be saved", scene->GetName());
return false;
}
if (const GameObject* selected = scene->GetSelectedObject()) {
(*snapshot)["selected"] = selected->GetId();
}
// Play runs the scene as it would load from disk, so nothing the editor did to the live objects
// (started components, unsaved runtime state) leaks into the game
if (!SceneSerializer::LoadFromJson(*scene, *snapshot)) {
spdlog::error("Cannot enter play mode, scene \"{}\" did not load back", scene->GetName());
return false;
}
m_EditSnapshot = std::move(snapshot);
m_EditScene = scene;
m_Playing = true;
sceneManager.SetPlaying(true);
m_App.collectUnusedResources();
m_App.onPlayModeChanged(true);
spdlog::info("Entered play mode");
return true;
}
void PlayMode::Exit()
{
auto& sceneManager = SceneManager::GetInstance();
if (const std::shared_ptr<Scene> scene = m_EditScene.lock(); scene && m_EditSnapshot) {
const auto& scenes = sceneManager.GetScenes();
if (const auto it = std::ranges::find(scenes, scene);
it != scenes.end() && sceneManager.GetActiveScenes().front() != scene) {
sceneManager.SwitchScene(static_cast<int>(std::distance(scenes.begin(), it)));
}
if (!SceneSerializer::LoadFromJson(*scene, *m_EditSnapshot)) {
spdlog::error("Could not restore scene \"{}\" to its state before play mode", scene->GetName());
}
} else {
spdlog::warn("The scene play mode started from no longer exists, nothing to restore");
}
m_EditSnapshot.reset();
m_EditScene.reset();
EnterEditMode();
m_App.collectUnusedResources();
spdlog::info("Exited play mode");
}
+166
View File
@@ -0,0 +1,166 @@
#include <editor/RectGizmo.h>
#include <array>
#include <cmath>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
namespace {
constexpr int MoveHandle = 0;
constexpr float HandleRadius = 5.0f;
constexpr std::array<glm::vec2, 8> HandleDirections{{
{-1.0f, -1.0f}, {0.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 0.0f},
{1.0f, 1.0f}, {0.0f, 1.0f}, {-1.0f, 1.0f}, {-1.0f, 0.0f},
}};
constexpr ImU32 OutlineColor = IM_COL32(60, 200, 255, 255);
constexpr ImU32 ActiveColor = IM_COL32(255, 200, 60, 255);
constexpr ImU32 HiddenColor = IM_COL32(150, 150, 150, 200);
constexpr ImU32 AnchorColor = IM_COL32(255, 255, 255, 200);
[[nodiscard]] ImVec2 ToImVec(const glm::vec2& value) { return ImVec2{value.x, value.y}; }
[[nodiscard]] glm::vec2 ToVec(const ImVec2& value) { return glm::vec2{value.x, value.y}; }
[[nodiscard]] ImGuiMouseCursor CursorFor(int handle)
{
if (handle == MoveHandle) {
return ImGuiMouseCursor_ResizeAll;
}
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(handle - 1)];
if (direction.x == 0.0f) {
return ImGuiMouseCursor_ResizeNS;
}
if (direction.y == 0.0f) {
return ImGuiMouseCursor_ResizeEW;
}
return direction.x == direction.y ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_ResizeNESW;
}
void DrawAnchor(ImDrawList& drawList, const glm::vec2& point, const glm::vec2& outwards)
{
constexpr float size = 8.0f;
const glm::vec2 tip = point;
const glm::vec2 a = point + glm::vec2{outwards.x * size, outwards.y * size * 0.4f};
const glm::vec2 b = point + glm::vec2{outwards.x * size * 0.4f, outwards.y * size};
drawList.AddTriangleFilled(ToImVec(tip), ToImVec(a), ToImVec(b), AnchorColor);
}
}
bool RectGizmo::CanManipulate(GameObject& object)
{
return object.GetComponent<RectTransform>() != nullptr && object.GetComponent<Canvas>() == nullptr &&
Canvas::FindFor(object) != nullptr;
}
bool RectGizmo::Manipulate(GameObject& object, const glm::vec2& drawImageSize, const glm::vec2& viewMin, const glm::vec2& viewSize)
{
m_Hovered = false;
auto* rectTransform = object.GetComponent<RectTransform>();
const Canvas* canvas = Canvas::FindFor(object);
if (!CanManipulate(object) || drawImageSize.x <= 0.0f || drawImageSize.y <= 0.0f) {
m_ActiveHandle = -1;
return false;
}
const glm::vec2 viewScale = viewSize / drawImageSize;
const auto toView = [&](const glm::vec2& point) { return viewMin + point * viewScale; };
const UIRect& rect = rectTransform->GetScreenRect();
const glm::vec2 rectMin = toView(rect.min);
const glm::vec2 rectMax = toView(rect.Max());
const glm::vec2 viewCenter = (rectMin + rectMax) * 0.5f;
const glm::vec2 viewHalfSize = (rectMax - rectMin) * 0.5f;
const glm::vec2 mouse = ToVec(ImGui::GetIO().MousePos);
int hovered = -1;
// Called inside the window that shows the scene, anything on top of it blocks the handles
const bool overImGui = !ImGui::IsWindowHovered() || ImGui::IsAnyItemActive();
if (m_ActiveHandle < 0 && !overImGui) {
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
if (glm::length(mouse - handle) <= HandleRadius + 2.0f) {
hovered = static_cast<int>(i) + 1;
break;
}
}
if (hovered < 0 && mouse.x >= rectMin.x && mouse.y >= rectMin.y && mouse.x <= rectMax.x && mouse.y <= rectMax.y) {
hovered = MoveHandle;
}
if (hovered >= 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
m_ActiveHandle = hovered;
m_DragStartMouse = mouse;
m_DragStartPosition = rectTransform->GetPosition();
m_DragStartSize = rectTransform->GetSize();
}
}
bool changed = false;
if (m_ActiveHandle >= 0) {
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
m_ActiveHandle = -1;
} else {
const float canvasScale = canvas->GetScale(drawImageSize);
glm::vec2 delta = (mouse - m_DragStartMouse) / (viewScale * canvasScale);
if (snap && snapStep > 0.0f) {
delta = glm::round(delta / snapStep) * snapStep;
}
glm::vec2 position = m_DragStartPosition;
glm::vec2 size = m_DragStartSize;
if (m_ActiveHandle == MoveHandle) {
position += delta;
} else {
// The edge opposite the handle stays put, so the pivot moves by the pivot's share of the resize
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(m_ActiveHandle - 1)];
const glm::vec2& pivot = rectTransform->GetPivot();
for (int axis = 0; axis < 2; ++axis) {
if (direction[axis] == 0.0f) {
continue;
}
size[axis] += direction[axis] * delta[axis];
position[axis] += delta[axis] * (direction[axis] > 0.0f ? pivot[axis] : 1.0f - pivot[axis]);
}
}
if (position != rectTransform->GetPosition() || size != rectTransform->GetSize()) {
rectTransform->SetPosition(position);
rectTransform->SetSize(size);
changed = true;
}
}
}
const int highlighted = m_ActiveHandle >= 0 ? m_ActiveHandle : hovered;
m_Hovered = highlighted >= 0;
if (highlighted >= 0) {
ImGui::SetMouseCursor(CursorFor(highlighted));
}
ImDrawList& drawList = *ImGui::GetWindowDrawList();
const ImU32 outlineColor = object.IsActiveInHierarchy() ? OutlineColor : HiddenColor;
const UIRect& parentRect = rectTransform->GetParentRect();
const glm::vec2 anchorMin = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMin());
const glm::vec2 anchorMax = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMax());
DrawAnchor(drawList, {anchorMin.x, anchorMin.y}, {-1.0f, -1.0f});
DrawAnchor(drawList, {anchorMax.x, anchorMin.y}, {1.0f, -1.0f});
DrawAnchor(drawList, {anchorMax.x, anchorMax.y}, {1.0f, 1.0f});
DrawAnchor(drawList, {anchorMin.x, anchorMax.y}, {-1.0f, 1.0f});
drawList.AddRect(ToImVec(rectMin), ToImVec(rectMax), highlighted == MoveHandle ? ActiveColor : outlineColor, 0.0f, 0, 1.5f);
const glm::vec2 pivot = rectMin + (rectMax - rectMin) * rectTransform->GetPivot();
drawList.AddCircle(ToImVec(pivot), 4.0f, outlineColor, 12, 1.5f);
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
const ImU32 color = highlighted == static_cast<int>(i) + 1 ? ActiveColor : outlineColor;
drawList.AddRectFilled(ToImVec(handle - glm::vec2{HandleRadius}), ToImVec(handle + glm::vec2{HandleRadius}), color);
}
return changed;
}
+439
View File
@@ -0,0 +1,439 @@
#include <editor/ResourcesInspector.h>
#include <algorithm>
#include <cctype>
#include <charconv>
#include <system_error>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include <imgui.h>
#include <vulkan/vk_enum_string_helper.h>
#include <destrum/Graphics/Caches/ImageCache.h>
#include <destrum/Graphics/Caches/MaterialCache.h>
#include <destrum/Graphics/Pipelines/ImguiPass.h>
#include <destrum/Graphics/RenderResources.h>
namespace
{
[[nodiscard]] const char* StripPrefix(const char* name, const char* prefix)
{
const std::size_t length = std::strlen(prefix);
return std::strncmp(name, prefix, length) == 0 ? name + length : name;
}
[[nodiscard]] const char* FormatName(VkFormat format)
{
return StripPrefix(string_VkFormat(format), "VK_FORMAT_");
}
[[nodiscard]] const char* LayoutName(VkImageLayout layout)
{
return StripPrefix(string_VkImageLayout(layout), "VK_IMAGE_LAYOUT_");
}
[[nodiscard]] std::string UsageNames(VkImageUsageFlags usage)
{
struct UsageName
{
VkImageUsageFlagBits bit;
const char* name;
};
constexpr UsageName usageNames[] = {
{VK_IMAGE_USAGE_SAMPLED_BIT, "sampled"},
{VK_IMAGE_USAGE_STORAGE_BIT, "storage"},
{VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, "color attachment"},
{VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, "depth attachment"},
{VK_IMAGE_USAGE_TRANSFER_SRC_BIT, "transfer src"},
{VK_IMAGE_USAGE_TRANSFER_DST_BIT, "transfer dst"},
};
std::string result;
for (const UsageName& usageName : usageNames)
{
if ((usage & usageName.bit) == 0)
{
continue;
}
if (!result.empty())
{
result += ", ";
}
result += usageName.name;
}
return result.empty() ? "none" : result;
}
[[nodiscard]] bool IsDepthFormat(VkFormat format)
{
switch (format)
{
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return true;
default:
return false;
}
}
[[nodiscard]] bool ContainsCaseInsensitive(std::string_view haystack, std::string_view needle)
{
const auto it = std::search(
haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
[](char a, char b)
{
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
});
return it != haystack.end();
}
[[nodiscard]] const char* TextureIntentName(TextureIntent intent)
{
switch (intent)
{
case TextureIntent::ColorSrgb: return "color (sRGB)";
case TextureIntent::DataLinear: return "data (linear)";
}
return "unknown";
}
}
ResourcesInspector::PreviewInfo ResourcesInspector::getPreviewInfo(const GPUImage& image)
{
if (image.image == VK_NULL_HANDLE || image.imageView == VK_NULL_HANDLE)
{
return {false, "no image view"};
}
if ((image.usage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0)
{
return {false, "not sampleable"};
}
if (image.isCubemap)
{
return {false, "cubemap"};
}
if (image.numLayers > 1)
{
return {false, "image array"};
}
if (image.getLayout() == VK_IMAGE_LAYOUT_UNDEFINED)
{
return {false, "never written"};
}
// ImGui reads the image when the UI gets rendered at the very end of the frame. Render targets
// like the draw / depth image are in an attachment or transfer layout by then.
if (image.getLayout() != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
{
return {false, "render target"};
}
return {true, ""};
}
void ResourcesInspector::drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const
{
const float width = static_cast<float>(image.extent.width);
const float height = static_cast<float>(image.extent.height);
const float scale = maxSize / std::max({width, height, 1.0f});
const ImVec2 size{std::max(width * scale, 1.0f), std::max(height * scale, 1.0f)};
const PreviewInfo preview = getPreviewInfo(image);
if (preview.previewable)
{
const ImTextureID textureId = imguiPass.getFrameTexture(image.imageView);
if (textureId != ImTextureID{})
{
ImGui::ImageWithBg(textureId, size, ImVec2{0.0f, 0.0f}, ImVec2{1.0f, 1.0f}, ImVec4{0.1f, 0.1f, 0.1f, 1.0f});
return;
}
}
// placeholder box with the reason it can't be shown
const ImVec2 boxSize{maxSize, std::min(maxSize, std::max(size.y, ImGui::GetTextLineHeight() * 2.0f))};
const ImVec2 min = ImGui::GetCursorScreenPos();
const ImVec2 max{min.x + boxSize.x, min.y + boxSize.y};
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_FrameBg));
drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border));
// small thumbnails just get a dash, the reason is in the tooltip
const char* text = ImGui::CalcTextSize(preview.reason).x <= boxSize.x - 8.0f ? preview.reason : "-";
const ImVec2 textSize = ImGui::CalcTextSize(text);
drawList->AddText(
ImVec2{min.x + (boxSize.x - textSize.x) * 0.5f, min.y + (boxSize.y - textSize.y) * 0.5f},
ImGui::GetColorU32(ImGuiCol_TextDisabled),
text);
ImGui::Dummy(boxSize);
}
void ResourcesInspector::drawImageDetails(ImageID id, const RenderResources& resources) const
{
const ImageCache& cache = resources.images();
const GPUImage& image = cache.images[id];
ImGui::Text("Image ID: %u", static_cast<unsigned>(id));
ImGui::TextWrapped("Name: %s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
if (id == resources.getWhiteTextureID())
{
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default white (also metallic/roughness + emissive default)");
}
if (id == resources.getErrorTextureID())
{
ImGui::TextColored(ImVec4{1.0f, 0.4f, 0.8f, 1.0f}, "Error texture (used when a load fails)");
}
if (id == resources.getDefaultNormalTextureID())
{
ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default normal map");
}
ImGui::Separator();
ImGui::Text("Size: %u x %u", image.extent.width, image.extent.height);
ImGui::Text("Format: %s%s", FormatName(image.format), IsDepthFormat(image.format) ? " (shown as red)" : "");
ImGui::Text("Mips: %u Layers: %u%s", image.mipLevels, image.numLayers, image.isCubemap ? " cubemap" : "");
ImGui::Text("Layout: %s", LayoutName(image.getLayout()));
if (const PreviewInfo preview = getPreviewInfo(image); !preview.previewable)
{
ImGui::TextDisabled("No preview: %s", preview.reason);
}
ImGui::TextWrapped("Usage: %s", UsageNames(image.usage).c_str());
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
ImGui::Separator();
ImGui::TextWrapped("File: %s", it->second.path.generic_string().c_str());
ImGui::Text("Intent: %s%s", TextureIntentName(it->second.intent), it->second.mipMap ? ", mipmapped" : "");
}
const MaterialCache& materials = resources.materials();
std::vector<std::size_t> users;
for (std::size_t materialId = 0; materialId < materials.materials.size(); ++materialId)
{
if (materials.materials[materialId].UsesTexture(id))
{
users.push_back(materialId);
}
}
if (!users.empty())
{
ImGui::Separator();
ImGui::Text("Used by %zu material(s):", users.size());
constexpr std::size_t maxListed = 8;
for (std::size_t i = 0; i < std::min(users.size(), maxListed); ++i)
{
const Material& material = materials.materials[users[i]];
ImGui::BulletText(
"#%zu %s",
users[i],
material.name.empty() ? "<unnamed>" : material.name.c_str());
}
if (users.size() > maxListed)
{
ImGui::TextDisabled("... and %zu more", users.size() - maxListed);
}
}
}
bool ResourcesInspector::matchesFilter(ImageID id, const RenderResources& resources) const
{
const std::string_view filterText{filter};
if (filterText.empty())
{
return true;
}
// a plain number matches the image id exactly
unsigned filterId = 0;
const auto [end, error] = std::from_chars(filterText.data(), filterText.data() + filterText.size(), filterId);
if (error == std::errc{} && end == filterText.data() + filterText.size())
{
return filterId == id;
}
const ImageCache& cache = resources.images();
if (ContainsCaseInsensitive(cache.images[id].debugName, filterText))
{
return true;
}
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
return ContainsCaseInsensitive(it->second.path.generic_string(), filterText);
}
return false;
}
void ResourcesInspector::drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass)
{
if (!ImGui::Begin("Image Cache", open))
{
ImGui::End();
return;
}
if (!resources.isInitialized())
{
ImGui::TextDisabled("Render resources aren't initialized yet");
ImGui::End();
return;
}
const ImageCache& cache = resources.images();
std::vector<ImageID> visibleIds;
std::size_t imageCount = 0;
for (std::size_t i = 0; i < cache.images.size(); ++i)
{
const auto id = static_cast<ImageID>(i);
const GPUImage& image = cache.images[i];
if (image.image == VK_NULL_HANDLE)
{
continue;
}
++imageCount;
if (!matchesFilter(id, resources))
{
continue;
}
if (onlyPreviewable && !getPreviewInfo(image).previewable)
{
continue;
}
visibleIds.push_back(id);
}
ImGui::Text("%zu images (bindless capacity %u)", imageCount, cache.getMaxImageCount());
ImGui::SetNextItemWidth(220.0f);
ImGui::InputTextWithHint("##filter", "Filter by name, path or ID", filter, sizeof(filter));
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::SliderFloat("Thumbnail", &thumbnailSize, 24.0f, 256.0f, "%.0f px");
ImGui::SameLine();
ImGui::Checkbox("Previewable only", &onlyPreviewable);
if (selectedImageId != NULL_IMAGE_ID &&
(selectedImageId >= cache.images.size() || cache.images[selectedImageId].image == VK_NULL_HANDLE))
{
selectedImageId = NULL_IMAGE_ID;
}
if (selectedImageId != NULL_IMAGE_ID)
{
if (ImGui::CollapsingHeader("Selected Image", ImGuiTreeNodeFlags_DefaultOpen))
{
drawImagePreview(cache.images[selectedImageId], 256.0f, imguiPass);
ImGui::SameLine();
ImGui::BeginGroup();
drawImageDetails(selectedImageId, resources);
if (ImGui::SmallButton("Clear selection"))
{
selectedImageId = NULL_IMAGE_ID;
}
ImGui::EndGroup();
}
}
constexpr ImGuiTableFlags tableFlags =
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_ScrollY |
ImGuiTableFlags_Resizable |
ImGuiTableFlags_SizingStretchProp;
if (ImGui::BeginTable("##images", 6, tableFlags))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthFixed, thumbnailSize + 4.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 3.0f);
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90.0f);
ImGui::TableSetupColumn("Format", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableHeadersRow();
// only the visible rows get a texture descriptor
const float rowHeight = thumbnailSize + ImGui::GetStyle().CellPadding.y * 2.0f;
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(visibleIds.size()), rowHeight);
while (clipper.Step())
{
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
{
const ImageID id = visibleIds[static_cast<std::size_t>(row)];
const GPUImage& image = cache.images[id];
ImGui::PushID(static_cast<int>(id));
ImGui::TableNextRow(ImGuiTableRowFlags_None, thumbnailSize);
ImGui::TableNextColumn();
const std::string idLabel = std::to_string(id);
if (ImGui::Selectable(
idLabel.c_str(),
selectedImageId == id,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap,
ImVec2{0.0f, thumbnailSize}))
{
selectedImageId = selectedImageId == id ? NULL_IMAGE_ID : id;
}
if (ImGui::BeginItemTooltip())
{
drawImagePreview(image, 256.0f, imguiPass);
drawImageDetails(id, resources);
ImGui::EndTooltip();
}
ImGui::TableNextColumn();
drawImagePreview(image, thumbnailSize, imguiPass);
ImGui::TableNextColumn();
if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end())
{
// loaded images are named after their full path, the file name is enough here
ImGui::TextWrapped("%s", it->second.path.filename().generic_string().c_str());
}
else
{
ImGui::TextWrapped("%s", image.debugName.empty() ? "<unnamed>" : image.debugName.c_str());
}
ImGui::TableNextColumn();
ImGui::Text("%u x %u", image.extent.width, image.extent.height);
if (image.mipLevels > 1)
{
ImGui::TextDisabled("%u mips", image.mipLevels);
}
ImGui::TableNextColumn();
ImGui::TextUnformatted(FormatName(image.format));
ImGui::TableNextColumn();
ImGui::TextUnformatted(LayoutName(image.getLayout()));
ImGui::PopID();
}
}
ImGui::EndTable();
}
ImGui::End();
}
+748
View File
@@ -0,0 +1,748 @@
#include <editor/SceneInspector.h>
#include <algorithm>
#include <cctype>
#include <cfloat>
#include <utility>
#include <vector>
#include <imgui.h>
#include <spdlog/spdlog.h>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Components/UI/UIImage.h>
#include <destrum/Components/UI/UIText.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] bool IsInspectable(const GameObject* object)
{
return object != nullptr && !object->IsBeingDestroyed();
}
[[nodiscard]] GameObject* ParentOf(const GameObject& object)
{
const Transform* parent = object.GetTransform().GetParent();
return parent != nullptr ? parent->GetOwner() : nullptr;
}
[[nodiscard]] bool HasInspectableChildren(const GameObject& object)
{
return std::ranges::any_of(object.GetTransform().GetChildren(), [](const Transform* child) {
return child != nullptr && IsInspectable(child->GetOwner());
});
}
[[nodiscard]] std::vector<GameObject*> CollectObjects(const Scene& scene)
{
std::vector<GameObject*> objects;
objects.reserve(scene.GetObjects().size() + scene.GetPendingAdditions().size());
for (const auto* source : {&scene.GetObjects(), &scene.GetPendingAdditions()}) {
for (const auto& object : *source) {
if (IsInspectable(object.get())) {
objects.push_back(object.get());
}
}
}
return objects;
}
[[nodiscard]] bool IsRoot(const GameObject& object)
{
return !IsInspectable(ParentOf(object));
}
[[nodiscard]] GameObject* NextSibling(const Scene& scene, const GameObject& object)
{
if (const Transform* parent = object.GetTransform().GetParent()) {
const auto& siblings = parent->GetChildren();
const auto it = std::ranges::find(siblings, &object.GetTransform());
for (auto next = it == siblings.end() ? it : it + 1; next != siblings.end(); ++next) {
if (IsInspectable((*next)->GetOwner())) {
return (*next)->GetOwner();
}
}
return nullptr;
}
const auto objects = CollectObjects(scene);
const auto it = std::ranges::find(objects, &object);
for (auto next = it == objects.end() ? it : it + 1; next != objects.end(); ++next) {
if (IsRoot(**next)) {
return *next;
}
}
return nullptr;
}
[[nodiscard]] bool ContainsIgnoreCase(std::string_view text, std::string_view query)
{
const auto it = std::search(text.begin(), text.end(), query.begin(), query.end(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
});
return it != text.end();
}
[[nodiscard]] const char* DisplayName(const GameObject& object)
{
return object.GetName().empty() ? "<unnamed>" : object.GetName().c_str();
}
GameObject* CreateChild(Scene& scene, GameObject* parent, const std::string& name)
{
GameObject* object = scene.CreateGameObject(name);
if (parent != nullptr) {
object->GetTransform().SetParent(&parent->GetTransform(), false);
}
return object;
}
GameObject* CreateUIChild(Scene& scene, GameObject* parent, const std::string& name)
{
if (parent == nullptr || Canvas::FindFor(*parent) == nullptr) {
parent = CreateChild(scene, parent, "Canvas");
parent->AddComponent<Canvas>();
}
GameObject* object = CreateChild(scene, parent, name);
object->AddComponent<RectTransform>();
return object;
}
// Returns the created object so the caller can select and rename it
GameObject* DrawCreateMenu(Scene& scene, GameObject* parent)
{
GameObject* created = nullptr;
if (ImGui::MenuItem("Empty")) {
created = CreateChild(scene, parent, "GameObject");
}
if (ImGui::BeginMenu("UI")) {
if (ImGui::MenuItem("Canvas")) {
created = CreateChild(scene, parent, "Canvas");
created->AddComponent<Canvas>();
}
if (ImGui::MenuItem("Rect")) {
created = CreateUIChild(scene, parent, "Rect");
}
if (ImGui::MenuItem("Image")) {
created = CreateUIChild(scene, parent, "Image");
created->AddComponent<UIImage>();
}
if (ImGui::MenuItem("Text")) {
created = CreateUIChild(scene, parent, "Text");
created->GetComponent<RectTransform>()->SetSize({300.0f, 60.0f});
created->AddComponent<UIText>("Text", 32.0f);
}
ImGui::EndMenu();
}
return created;
}
// Right aligned controls drawn over a collapsing header, x is from the start of the line
[[nodiscard]] float RightAlignedX(float lineStartX, float lineWidth, float itemsWidth)
{
return lineStartX + lineWidth - itemsWidth;
}
}
void SceneInspector::Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open)
{
RegisterEngineComponents();
if (scene.FindGameObject(selectedObjectId) == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SetNextWindowSize(ImVec2(720.0f, 560.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Inspector", open)) {
ImGui::End();
return;
}
HandleShortcuts(scene, selectedObjectId);
if (ImGui::BeginChild("Hierarchy", ImVec2(std::max(220.0f, ImGui::GetContentRegionAvail().x * 0.4f), 0.0f),
ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX)) {
DrawHierarchy(scene, selectedObjectId);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("Properties", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) {
if (GameObject* selected = scene.FindGameObject(selectedObjectId)) {
DrawObject(*selected);
} else if (drawSceneSettings) {
drawSceneSettings();
}
}
ImGui::EndChild();
ImGui::End();
ApplyPendingActions(scene, selectedObjectId);
}
void SceneInspector::HandleShortcuts(Scene& scene, ObjectId& selectedObjectId)
{
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || ImGui::GetIO().WantTextInput ||
selectedObjectId == InvalidObjectId) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
m_PendingDelete = selectedObjectId;
}
if (ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D)) {
m_PendingDuplicate = selectedObjectId;
}
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
if (const GameObject* selected = scene.FindGameObject(selectedObjectId)) {
BeginRename(*selected);
}
}
}
void SceneInspector::DrawHierarchy(Scene& scene, ObjectId& selectedObjectId)
{
if (ImGui::Button("+")) {
ImGui::OpenPopup("CreateObject");
}
if (ImGui::BeginPopup("CreateObject")) {
if (GameObject* created = DrawCreateMenu(scene, nullptr)) {
selectedObjectId = created->GetId();
BeginRename(*created);
}
ImGui::EndPopup();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
ImGuiUtils::InputString("##Search", m_Filter, 0, "Search");
if (!ImGui::BeginChild("Tree")) {
ImGui::EndChild();
return;
}
const std::vector<GameObject*> objects = CollectObjects(scene);
std::unordered_set<const GameObject*> visited;
visited.reserve(objects.size());
if (m_Filter.empty()) {
for (GameObject* object : objects) {
if (IsRoot(*object)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
// Anything not reachable from a root is stuck in a parent cycle, still show it
std::unordered_set<const GameObject*> reachable;
std::vector<const GameObject*> pending;
for (const GameObject* object : objects) {
if (IsRoot(*object)) {
pending.push_back(object);
}
}
while (!pending.empty()) {
const GameObject* object = pending.back();
pending.pop_back();
if (!reachable.insert(object).second) {
continue;
}
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && IsInspectable(child->GetOwner())) {
pending.push_back(child->GetOwner());
}
}
}
for (GameObject* object : objects) {
if (!reachable.contains(object)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
} else {
for (GameObject* object : objects) {
if (ContainsIgnoreCase(object->GetName(), m_Filter)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
}
// The empty space below the tree: click to deselect, drop to move to the root, right click to create
const ImVec2 remaining = ImGui::GetContentRegionAvail();
ImGui::InvisibleButton("##Background", ImVec2(std::max(remaining.x, 1.0f), std::max(remaining.y, ImGui::GetFrameHeight())));
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
selectedObjectId = InvalidObjectId;
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
m_PendingMove = {.object = *static_cast<const ObjectId*>(payload->Data)};
}
ImGui::EndDragDropTarget();
}
if (ImGui::BeginPopupContextItem("BackgroundMenu")) {
DrawObjectMenu(scene, nullptr, selectedObjectId);
ImGui::EndPopup();
}
ImGui::EndChild();
}
void SceneInspector::DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId,
std::unordered_set<const GameObject*>& visited)
{
if (!IsInspectable(&object) || !visited.insert(&object).second) {
return;
}
const bool flat = !m_Filter.empty();
const bool hasChildren = !flat && HasInspectableChildren(object);
const bool renaming = m_RenamingId == object.GetId();
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth |
ImGuiTreeNodeFlags_DrawLinesToNodes;
if (object.GetId() == selectedObjectId) {
flags |= ImGuiTreeNodeFlags_Selected;
}
if (!hasChildren) {
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
}
if (m_OpenNextFrame == object.GetId()) {
ImGui::SetNextItemOpen(true);
m_OpenNextFrame = InvalidObjectId;
}
const bool inactive = !object.IsActiveInHierarchy();
if (inactive) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
const bool open = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())), flags, "%s",
renaming ? "" : DisplayName(object));
if (inactive) {
ImGui::PopStyleColor();
}
if ((ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) &&
!ImGui::IsItemToggledOpen()) {
selectedObjectId = object.GetId();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && !ImGui::IsItemToggledOpen()) {
BeginRename(object);
}
DrawNodeDragDrop(object);
// Leaf nodes push no ID, without this every sibling shares one popup and draws it again
ImGui::PushID(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())));
if (ImGui::BeginPopupContextItem("ObjectMenu")) {
DrawObjectMenu(scene, &object, selectedObjectId);
ImGui::EndPopup();
}
ImGui::PopID();
if (renaming) {
ImGui::SameLine();
DrawRenameField(object);
}
if (open && hasChildren) {
for (Transform* child : object.GetTransform().GetChildren()) {
if (child != nullptr && IsInspectable(child->GetOwner())) {
DrawNode(scene, *child->GetOwner(), selectedObjectId, visited);
}
}
ImGui::TreePop();
}
}
void SceneInspector::DrawNodeDragDrop(GameObject& object)
{
const ObjectId id = object.GetId();
if (ImGui::BeginDragDropSource()) {
ImGui::SetDragDropPayload(ImGuiUtils::GameObjectPayload, &id, sizeof(id));
ImGui::TextUnformatted(DisplayName(object));
ImGui::EndDragDropSource();
}
if (!ImGui::BeginDragDropTarget()) {
return;
}
const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
ImGuiUtils::GameObjectPayload,
ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect);
if (payload != nullptr) {
const ObjectId dragged = *static_cast<const ObjectId*>(payload->Data);
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const float zone = (max.y - min.y) * 0.25f;
const float mouseY = ImGui::GetMousePos().y;
const bool before = mouseY < min.y + zone;
const bool after = mouseY > max.y - zone;
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImU32 color = ImGui::GetColorU32(ImGuiCol_DragDropTarget);
if (before) {
drawList->AddLine(min, ImVec2(max.x, min.y), color, 2.0f);
} else if (after) {
drawList->AddLine(ImVec2(min.x, max.y), max, color, 2.0f);
} else {
drawList->AddRect(min, max, color, 0.0f, 0, 2.0f);
}
if (payload->IsDelivery() && dragged != id) {
const GameObject* parent = ParentOf(object);
const ObjectId parentId = IsInspectable(parent) ? parent->GetId() : InvalidObjectId;
if (before || after) {
m_PendingMove = {.object = dragged, .parent = parentId, .anchor = id, .after = after};
} else {
m_PendingMove = {.object = dragged, .parent = id};
}
}
}
ImGui::EndDragDropTarget();
}
void SceneInspector::DrawRenameField(GameObject& object)
{
if (m_FocusRename) {
ImGui::SetKeyboardFocusHere();
m_FocusRename = false;
}
ImGui::SetNextItemWidth(-FLT_MIN);
const bool submitted = ImGuiUtils::InputString("##Rename", m_RenameBuffer,
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
m_RenamingId = InvalidObjectId;
return;
}
if (submitted || ImGui::IsItemDeactivated()) {
if (!m_RenameBuffer.empty()) {
object.SetName(m_RenameBuffer);
}
m_RenamingId = InvalidObjectId;
}
}
void SceneInspector::BeginRename(const GameObject& object)
{
m_RenamingId = object.GetId();
m_RenameBuffer = object.GetName();
m_FocusRename = true;
}
void SceneInspector::DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId)
{
if (ImGui::BeginMenu(object != nullptr ? "Create Child" : "Create")) {
if (GameObject* created = DrawCreateMenu(scene, object)) {
selectedObjectId = created->GetId();
if (object != nullptr) {
m_OpenNextFrame = object->GetId();
}
BeginRename(*created);
}
ImGui::EndMenu();
}
if (object == nullptr) {
return;
}
ImGui::Separator();
if (ImGui::MenuItem("Rename", "F2")) {
BeginRename(*object);
}
if (ImGui::MenuItem("Duplicate", "Ctrl+D")) {
m_PendingDuplicate = object->GetId();
}
if (ImGui::MenuItem("Unparent", nullptr, false, !IsRoot(*object))) {
m_PendingMove = {.object = object->GetId()};
}
bool active = object->IsActive();
if (ImGui::MenuItem("Active", nullptr, &active)) {
object->SetActive(active);
}
ImGui::Separator();
if (ImGui::MenuItem("Delete", "Del")) {
m_PendingDelete = object->GetId();
}
}
void SceneInspector::ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId)
{
if (m_PendingMove.object != InvalidObjectId) {
GameObject* object = scene.FindGameObject(m_PendingMove.object);
GameObject* parent = scene.FindGameObject(m_PendingMove.parent);
GameObject* insertBefore = scene.FindGameObject(m_PendingMove.anchor);
if (insertBefore != nullptr && m_PendingMove.after) {
insertBefore = NextSibling(scene, *insertBefore);
}
if (insertBefore == object && object != nullptr) {
insertBefore = NextSibling(scene, *object);
}
if (object != nullptr && scene.MoveInHierarchy(*object, parent, insertBefore) && parent != nullptr) {
m_OpenNextFrame = parent->GetId();
}
m_PendingMove = {};
}
if (m_PendingDuplicate != InvalidObjectId) {
if (GameObject* object = scene.FindGameObject(m_PendingDuplicate)) {
if (const GameObject* duplicate = PrefabSerializer::Duplicate(*object)) {
selectedObjectId = duplicate->GetId();
}
}
m_PendingDuplicate = InvalidObjectId;
}
if (m_PendingDelete != InvalidObjectId) {
if (GameObject* object = scene.FindGameObject(m_PendingDelete)) {
object->Destroy();
}
if (selectedObjectId == m_PendingDelete) {
selectedObjectId = InvalidObjectId;
}
m_PendingDelete = InvalidObjectId;
}
}
void SceneInspector::DrawObject(GameObject& object)
{
bool active = object.IsActive();
if (ImGui::Checkbox("##Active", &active)) {
object.SetActive(active);
}
ImGui::SetItemTooltip("Active");
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
std::string name = object.GetName();
if (ImGuiUtils::InputString("##Name", name)) {
object.SetName(name);
}
ImGui::Spacing();
DrawTransform(object);
DrawComponents(object);
DrawAddComponent(object);
}
void SceneInspector::DrawTransform(GameObject& object)
{
auto* rectTransform = object.GetComponent<RectTransform>();
// Everything under a canvas has a RectTransform, a stretched one keeps the old layout
if (rectTransform == nullptr && object.GetComponent<Canvas>() == nullptr && Canvas::FindFor(object) != nullptr) {
rectTransform = object.AddComponent<RectTransform>();
rectTransform->Stretch();
}
if (rectTransform != nullptr) {
const bool open = ImGui::CollapsingHeader("Rect Transform", ImGuiTreeNodeFlags_DefaultOpen);
if (ImGui::BeginPopupContextItem("RectTransformMenu")) {
if (ImGui::MenuItem("Stretch")) {
rectTransform->Stretch();
}
ImGui::EndPopup();
}
if (open) {
ImGui::PushID(rectTransform);
rectTransform->ImGuiInspector();
ImGui::PopID();
}
return;
}
Transform& transform = object.GetTransform();
const bool open = ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen);
bool changed = false;
if (ImGui::BeginPopupContextItem("TransformMenu")) {
if (ImGui::MenuItem("Reset Position")) {
transform.SetLocalPosition(glm::vec3{0.0f});
changed = true;
}
if (ImGui::MenuItem("Reset Rotation")) {
transform.SetLocalRotation(glm::quat{1.0f, 0.0f, 0.0f, 0.0f});
changed = true;
}
if (ImGui::MenuItem("Reset Scale")) {
transform.SetLocalScale(glm::vec3{1.0f});
changed = true;
}
ImGui::EndPopup();
}
if (open) {
glm::vec3 position = transform.GetLocalPosition();
if (ImGuiUtils::DrawVec3Control("Position", position, 0.05f)) {
transform.SetLocalPosition(position);
changed = true;
}
glm::vec3 rotation = glm::degrees(glm::eulerAngles(transform.GetLocalRotation()));
if (ImGuiUtils::DrawVec3Control("Rotation", rotation, 0.5f)) {
transform.SetLocalRotation(rotation);
changed = true;
}
glm::vec3 scale = transform.GetLocalScale();
if (ImGuiUtils::DrawVec3Control("Scale", scale, 0.05f)) {
transform.SetLocalScale(scale);
changed = true;
}
}
if (changed) {
object.RefreshPhysicsInHierarchy();
}
}
void SceneInspector::DrawComponents(GameObject& object)
{
auto& components = object.GetComponents();
const auto* rectTransform = object.GetComponent<RectTransform>();
const bool underCanvas = Canvas::FindFor(object) != nullptr;
for (std::size_t i = 0; i < components.size(); ++i) {
Component* component = components[i].get();
if (component == nullptr || component->IsBeingDestroyed() || component == rectTransform) {
continue;
}
ImGui::PushID(component);
const float lineStart = ImGui::GetCursorPosX();
const float lineWidth = ImGui::GetContentRegionAvail().x;
const bool enabled = component->isEnabled();
if (!enabled) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
const std::string typeName = component->GetTypeName();
const bool open = ImGui::CollapsingHeader(typeName.c_str(),
ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_AllowOverlap);
if (!enabled) {
ImGui::PopStyleColor();
}
const bool removable = !(underCanvas && dynamic_cast<RectTransform*>(component) != nullptr);
if (ImGui::BeginPopupContextItem("ComponentMenu")) {
DrawComponentMenu(object, *component, i, removable);
ImGui::EndPopup();
}
const ImGuiStyle& style = ImGui::GetStyle();
const float buttonWidth = ImGui::CalcTextSize("...").x + style.FramePadding.x * 2.0f;
const float controlsWidth = ImGui::GetFrameHeight() + style.ItemSpacing.x + buttonWidth + style.FramePadding.x;
ImGui::SameLine(RightAlignedX(lineStart, lineWidth, controlsWidth));
bool enabledToggle = enabled;
if (ImGui::Checkbox("##Enabled", &enabledToggle)) {
component->SetEnabled(enabledToggle);
}
ImGui::SetItemTooltip("Enabled");
ImGui::SameLine();
if (ImGui::Button("...")) {
ImGui::OpenPopup("ComponentMenu");
}
if (open) {
component->ImGuiInspector();
ImGui::Spacing();
}
ImGui::PopID();
// A move or remove from the menu changed the list, the rest is drawn next frame
if (components.size() <= i || components[i].get() != component) {
break;
}
}
}
void SceneInspector::DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable)
{
auto& components = object.GetComponents();
if (ImGui::MenuItem("Move Up", nullptr, false, index > 0)) {
std::swap(components[index], components[index - 1]);
}
if (ImGui::MenuItem("Move Down", nullptr, false, index + 1 < components.size())) {
std::swap(components[index], components[index + 1]);
}
ImGui::Separator();
const std::string typeName = component.GetTypeName();
if (ImGui::MenuItem("Copy Values")) {
m_ClipboardType = typeName;
m_Clipboard = component.Serialize();
}
if (ImGui::MenuItem("Paste Values", nullptr, false, m_ClipboardType == typeName)) {
try {
component.Deserialize(m_Clipboard);
if (component.AffectsPhysics()) {
object.RefreshPhysics();
}
} catch (const std::exception& exception) {
spdlog::warn("Pasting {} values failed: {}", typeName, exception.what());
}
}
ImGui::Separator();
if (ImGui::MenuItem("Remove", nullptr, false, removable)) {
const bool affectsPhysics = component.AffectsPhysics();
component.Destroy();
if (affectsPhysics) {
object.RefreshPhysics();
}
}
}
void SceneInspector::DrawAddComponent(GameObject& object)
{
ImGui::Spacing();
if (ImGui::Button("Add Component", ImVec2(-FLT_MIN, 0.0f))) {
m_ComponentFilter.clear();
ImGui::OpenPopup("AddComponent");
}
ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetItemRectSize().x, 0.0f), ImVec2(FLT_MAX, 320.0f));
if (!ImGui::BeginPopup("AddComponent")) {
return;
}
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
ImGui::SetNextItemWidth(-FLT_MIN);
const bool submitted = ImGuiUtils::InputString("##ComponentSearch", m_ComponentFilter,
ImGuiInputTextFlags_EnterReturnsTrue, "Search");
std::vector<std::string> types = ComponentFactory::GetRegisteredTypeNames();
std::ranges::sort(types);
std::erase_if(types, [&](const std::string& type) {
const bool added = std::ranges::any_of(object.GetComponents(), [&](const auto& component) {
return component != nullptr && !component->IsBeingDestroyed() && component->GetTypeName() == type;
});
return added || !ContainsIgnoreCase(type, m_ComponentFilter);
});
std::string chosen = submitted && !types.empty() ? types.front() : std::string{};
for (const std::string& type : types) {
if (ImGui::Selectable(type.c_str())) {
chosen = type;
}
}
if (!chosen.empty()) {
try {
if (ComponentFactory::Create(chosen, object) == nullptr) {
spdlog::warn("Component type is not registered: {}", chosen);
}
} catch (const std::exception& exception) {
spdlog::warn("Adding {} failed: {}", chosen, exception.what());
}
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
+83
View File
@@ -0,0 +1,83 @@
#include <editor/ShaderHotReload.h>
#include <chrono>
#include <cstdlib>
#include <utility>
#include <spdlog/spdlog.h>
namespace {
constexpr float PollInterval = 0.5f;
[[nodiscard]] bool IsShaderSource(const std::filesystem::path& path)
{
const std::string extension = path.extension().string();
return extension == ".vert" || extension == ".frag" || extension == ".comp" || extension == ".glsl";
}
}
void ShaderHotReload::Configure(std::filesystem::path cooker, std::filesystem::path sourceRoot, std::filesystem::path runtimeRoot)
{
m_Cooker = std::move(cooker);
m_SourceRoot = std::move(sourceRoot);
m_RuntimeRoot = std::move(runtimeRoot);
m_Stamps.clear();
(void)SourcesChanged();
}
bool ShaderHotReload::SourcesChanged()
{
bool changed = false;
std::error_code error;
for (const auto& entry : std::filesystem::recursive_directory_iterator(m_SourceRoot / "shaders", error)) {
if (!entry.is_regular_file(error) || !IsShaderSource(entry.path())) {
continue;
}
const auto stamp = entry.last_write_time(error);
auto [it, inserted] = m_Stamps.try_emplace(entry.path().generic_string(), stamp);
if (!inserted && it->second != stamp) {
it->second = stamp;
changed = true;
}
}
return changed;
}
void ShaderHotReload::StartCook()
{
// cmd.exe strips the outer quotes, the inner ones keep paths with spaces intact
const std::string command = "\"\"" + m_Cooker.string() + "\" --input \"" + m_SourceRoot.string() +
"\" --output \"" + m_RuntimeRoot.string() + "\"\"";
m_Cook = std::async(std::launch::async, [command] { return std::system(command.c_str()); });
spdlog::info("Shader sources changed, cooking engine assets");
}
bool ShaderHotReload::Poll(float dt)
{
if (!IsConfigured()) {
return false;
}
if (m_Cook.valid()) {
if (m_Cook.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
return false;
}
const int exitCode = m_Cook.get();
m_LastResult = exitCode == 0 ? "Shaders cooked" : "Shader cook failed, see the log";
if (exitCode != 0) {
spdlog::error("Cooking the engine assets failed with exit code {}", exitCode);
}
return exitCode == 0;
}
m_PollTimer -= dt;
if (m_PollTimer > 0.0f && !m_CookRequested) {
return false;
}
m_PollTimer = PollInterval;
if (SourcesChanged() || std::exchange(m_CookRequested, false)) {
StartCook();
}
return false;
}
+162
View File
@@ -0,0 +1,162 @@
#include <editor/TransformGizmo.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/ObjectModel/GameObject.h>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/matrix_decompose.hpp>
#include <imgui.h>
#include <ImGuizmo.h>
namespace {
ImGuizmo::OPERATION toImGuizmo(GizmoOperation operation)
{
switch (operation) {
case GizmoOperation::Rotate:
return ImGuizmo::ROTATE;
case GizmoOperation::Scale:
return ImGuizmo::SCALE;
case GizmoOperation::Translate:
break;
}
return ImGuizmo::TRANSLATE;
}
}
void TransformGizmo::HandleShortcuts()
{
if (ImGui::GetIO().WantTextInput || ImGuizmo::IsUsing()) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_T, false)) {
operation = GizmoOperation::Translate;
}
if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
operation = GizmoOperation::Rotate;
}
if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) {
operation = GizmoOperation::Scale;
}
if (ImGui::IsKeyPressed(ImGuiKey_L, false)) {
space = space == GizmoSpace::World ? GizmoSpace::Local : GizmoSpace::World;
}
}
void TransformGizmo::DrawToolbar()
{
if (!ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
const auto operationButton = [this](const char* label, GizmoOperation buttonOperation) {
if (ImGui::RadioButton(label, operation == buttonOperation)) {
operation = buttonOperation;
}
};
operationButton("Move (T)", GizmoOperation::Translate);
ImGui::SameLine();
operationButton("Rotate (R)", GizmoOperation::Rotate);
ImGui::SameLine();
operationButton("Scale (Y)", GizmoOperation::Scale);
if (operation != GizmoOperation::Scale) {
bool local = space == GizmoSpace::Local;
if (ImGui::Checkbox("Local (L)", &local)) {
space = local ? GizmoSpace::Local : GizmoSpace::World;
}
ImGui::SameLine();
}
ImGui::Checkbox("Snap", &snap);
if (snap) {
ImGui::SetNextItemWidth(80.0f);
switch (operation) {
case GizmoOperation::Translate:
ImGui::DragFloat("Step", &translationSnap, 0.05f, 0.01f, 10.0f, "%.2f m");
break;
case GizmoOperation::Rotate:
ImGui::DragFloat("Step", &rotationSnapDegrees, 1.0f, 1.0f, 90.0f, "%.0f deg");
break;
case GizmoOperation::Scale:
ImGui::DragFloat("Step", &scaleSnap, 0.01f, 0.01f, 1.0f, "%.2f");
break;
}
}
ImGui::End();
}
bool TransformGizmo::Manipulate(GameObject& object, const Camera& camera, const glm::vec2& viewMin, const glm::vec2& viewSize)
{
ImGuizmo::BeginFrame();
// Drawn into the window that shows the scene, so it stays on top of it and hovers with it
ImGuizmo::SetDrawlist();
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetRect(viewMin.x, viewMin.y, viewSize.x, viewSize.y);
const glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// ImGuizmo expects OpenGL's y up clip space, the engine flips y for Vulkan
projection[1][1] *= -1.0f;
Transform& transform = object.GetTransform();
glm::mat4 worldMatrix = transform.GetWorldMatrix();
const float snapStep = operation == GizmoOperation::Translate ? translationSnap
: operation == GizmoOperation::Rotate ? rotationSnapDegrees
: scaleSnap;
const float snapValues[3] = {snapStep, snapStep, snapStep};
const ImGuizmo::MODE mode =
operation == GizmoOperation::Scale || space == GizmoSpace::Local ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const bool changed = ImGuizmo::Manipulate(
glm::value_ptr(view),
glm::value_ptr(projection),
toImGuizmo(operation),
mode,
glm::value_ptr(worldMatrix),
nullptr,
snap ? snapValues : nullptr);
if (!changed) {
return false;
}
glm::vec3 position;
glm::quat rotation;
glm::vec3 scale;
glm::vec3 skew;
glm::vec4 perspective;
if (!glm::decompose(worldMatrix, scale, rotation, position, skew, perspective)) {
return false;
}
switch (operation) {
case GizmoOperation::Translate:
transform.SetWorldPosition(position);
break;
case GizmoOperation::Rotate:
transform.SetWorldRotation(rotation);
break;
case GizmoOperation::Scale:
transform.SetWorldScale(scale);
break;
}
object.RefreshPhysicsInHierarchy();
return true;
}
bool TransformGizmo::IsUsing() const
{
return ImGuizmo::IsUsing();
}
bool TransformGizmo::IsOver() const
{
return ImGuizmo::IsOver();
}
+95
View File
@@ -0,0 +1,95 @@
#include <editor/UndoStack.h>
#include <utility>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/SceneSerializer.h>
std::optional<UndoStack::Snapshot> UndoStack::Capture(Scene& scene)
{
const auto root = SceneSerializer::Capture(scene);
if (!root) {
return std::nullopt;
}
return Snapshot{.scene = root->dump(), .selected = scene.GetSelectedObjectId()};
}
void UndoStack::Reset(Scene& scene)
{
m_Undo.clear();
m_Redo.clear();
m_Current = Capture(scene);
}
void UndoStack::Rebase(Scene& scene)
{
m_Current = Capture(scene);
}
bool UndoStack::Record(Scene& scene)
{
auto now = Capture(scene);
if (!now) {
return false;
}
if (!m_Current || now->scene == m_Current->scene) {
m_Current = std::move(now);
return false;
}
// Undo keeps what is selected now, the ids are still the same because nothing was reloaded since
m_Current->selected = now->selected;
m_Undo.push_back(std::move(*m_Current));
if (m_Undo.size() > MaxSteps) {
m_Undo.pop_front();
}
m_Redo.clear();
m_Current = std::move(now);
return true;
}
bool UndoStack::Undo(Scene& scene)
{
Record(scene);
if (m_Undo.empty() || !m_Current) {
return false;
}
Snapshot target = std::move(m_Undo.back());
m_Undo.pop_back();
m_Redo.push_back(std::move(*m_Current));
return Restore(scene, target);
}
bool UndoStack::Redo(Scene& scene)
{
Record(scene);
if (m_Redo.empty() || !m_Current) {
return false;
}
Snapshot target = std::move(m_Redo.back());
m_Redo.pop_back();
m_Undo.push_back(std::move(*m_Current));
return Restore(scene, target);
}
bool UndoStack::Restore(Scene& scene, const Snapshot& snapshot)
{
nlohmann::json root = nlohmann::json::parse(snapshot.scene);
if (snapshot.selected != InvalidObjectId) {
root["selected"] = snapshot.selected;
}
if (!SceneSerializer::LoadFromJson(scene, root)) {
spdlog::error("Could not restore the scene from the undo history");
m_Current = Capture(scene);
return false;
}
// Loading gives every object a new id, the next comparison has to start from the new ids
m_Current = Capture(scene);
return true;
}