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