diff --git a/common/Path.cpp b/common/Path.cpp new file mode 100644 index 000000000..f6585a125 --- /dev/null +++ b/common/Path.cpp @@ -0,0 +1,56 @@ +//// Path.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 24/07/2025 +// Description: Source file for the path utilities +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Path.hpp" + +namespace nexo { + + const std::filesystem::path& Path::getExecutablePath() + { + if (!m_executablePathCached.empty() && !m_executableRootPathCached.empty()) + return m_executablePathCached; + const boost::dll::fs::path path = boost::dll::program_location(); + m_executablePathCached = path.c_str(); + m_executableRootPathCached = m_executablePathCached.parent_path(); + return m_executablePathCached; + } + + std::filesystem::path Path::resolvePathRelativeToExe(const std::filesystem::path& path) + { + if (m_executableRootPathCached.empty()) + getExecutablePath(); + return (m_executableRootPathCached / path).lexically_normal(); + } + + void Path::resetCache() + { + m_executablePathCached.clear(); + m_executableRootPathCached.clear(); + } + + std::string normalizePathAndRemovePrefixSlash(const std::string &rawPath) + { + namespace fs = std::filesystem; + fs::path p = fs::path(rawPath).lexically_normal(); + + std::string s = p.generic_string(); + + if (s == "/" || s.empty()) + return {}; + + size_t start = s.find_first_not_of('/'); + size_t end = s.find_last_not_of('/'); + return s.substr(start, end - start + 1); + } +} diff --git a/common/Path.hpp b/common/Path.hpp index 85da756a6..636fd7682 100644 --- a/common/Path.hpp +++ b/common/Path.hpp @@ -28,15 +28,7 @@ namespace nexo { * @brief Get the path to the executable (e.g.: nexoEditor) * @return The path to the executable */ - static const std::filesystem::path& getExecutablePath() - { - if (!m_executablePathCached.empty() && !m_executableRootPathCached.empty()) - return m_executablePathCached; - const boost::dll::fs::path path = boost::dll::program_location(); - m_executablePathCached = path.c_str(); - m_executableRootPathCached = m_executablePathCached.parent_path(); - return m_executablePathCached; - } + static const std::filesystem::path& getExecutablePath(); /** * @brief Resolve a path relative to the executable @@ -46,21 +38,12 @@ namespace nexo { * @note Example: if assets is a folder in the same directory as the executable, you can use: resolvePathRelativeToExe("assets") * @example ../editor/src/Editor.cpp */ - static std::filesystem::path resolvePathRelativeToExe(const std::filesystem::path& path) - { - if (m_executableRootPathCached.empty()) - getExecutablePath(); - return (m_executableRootPathCached / path).lexically_normal(); - } + static std::filesystem::path resolvePathRelativeToExe(const std::filesystem::path& path); /** * @brief Reset the cached paths */ - static void resetCache() - { - m_executablePathCached.clear(); - m_executableRootPathCached.clear(); - } + static void resetCache(); private: Path() = default; @@ -69,5 +52,5 @@ namespace nexo { inline static std::filesystem::path m_executableRootPathCached; }; - + std::string normalizePathAndRemovePrefixSlash(const std::string &rawPath); } // namespace nexo diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b1c620009..7d307baa3 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -11,6 +11,7 @@ set(SRCS common/Exception.cpp common/math/Matrix.cpp common/math/Light.cpp + common/Path.cpp editor/main.cpp editor/src/backends/ImGuiBackend.cpp editor/src/backends/opengl/openglImGuiBackend.cpp @@ -47,12 +48,14 @@ set(SRCS editor/src/DocumentWindows/EditorScene/Shutdown.cpp editor/src/DocumentWindows/EditorScene/Toolbar.cpp editor/src/DocumentWindows/EditorScene/Update.cpp + editor/src/DocumentWindows/EditorScene/DragDrop.cpp editor/src/DocumentWindows/AssetManager/Init.cpp editor/src/DocumentWindows/AssetManager/Show.cpp editor/src/DocumentWindows/AssetManager/Shutdown.cpp editor/src/DocumentWindows/AssetManager/Update.cpp editor/src/DocumentWindows/AssetManager/FolderTree.cpp editor/src/DocumentWindows/AssetManager/Thumbnail.cpp + editor/src/DocumentWindows/AssetManager/FileDrop.cpp editor/src/DocumentWindows/ConsoleWindow/Init.cpp editor/src/DocumentWindows/ConsoleWindow/Log.cpp editor/src/DocumentWindows/ConsoleWindow/Show.cpp @@ -77,6 +80,7 @@ set(SRCS editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp editor/src/DocumentWindows/SceneTreeWindow/Update.cpp editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp + editor/src/DocumentWindows/SceneTreeWindow/DragDrop.cpp editor/src/DocumentWindows/PopupManager.cpp editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp diff --git a/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp index e9cf23990..1c4a0d9d6 100644 --- a/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp +++ b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp @@ -18,10 +18,12 @@ #include #include #include "utils/TransparentStringHash.hpp" +#include +#include "assets/Asset.hpp" namespace nexo::editor { - class AssetManagerWindow final : public ADocumentWindow { + class AssetManagerWindow final : public ADocumentWindow, LISTENS_TO(event::EventFileDrop) { public: using ADocumentWindow::ADocumentWindow; @@ -30,6 +32,8 @@ namespace nexo::editor { void show() override; void update() override; + void handleEvent(event::EventFileDrop& event) override; + private: struct LayoutSettings { struct LayoutSizes { @@ -74,7 +78,8 @@ namespace nexo::editor { void handleSelection(int index, bool isSelected); assets::AssetType m_selectedType = assets::AssetType::UNKNOWN; - std::string m_currentFolder; + std::string m_currentFolder; // Currently selected folder + std::string m_hoveredFolder; // Currently hovered folder std::vector> m_folderStructure; // Pairs of (path, name) char m_searchBuffer[256] = ""; @@ -110,5 +115,25 @@ namespace nexo::editor { const ImVec2& itemPos, const ImVec2& itemSize ); + + std::vector m_pendingDroppedFiles; + bool m_showDropIndicator = false; + + void handleDroppedFiles(); + const assets::AssetLocation getAssetLocation(const std::filesystem::path &path) const; + void importDroppedFile(const std::string& filePath); + }; + + /** + * @brief Payload structure for drag and drop operations from asset manager. + * + * Contains information about the asset being dragged. + */ + struct AssetDragDropPayload + { + assets::AssetType type; ///< Type of the asset + assets::AssetID id; ///< ID of the asset + std::string path; ///< Path to the asset + std::string name; ///< Display name of the asset }; } diff --git a/editor/src/DocumentWindows/AssetManager/FileDrop.cpp b/editor/src/DocumentWindows/AssetManager/FileDrop.cpp new file mode 100644 index 000000000..a9df7c8ab --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/FileDrop.cpp @@ -0,0 +1,111 @@ +//// FileDrop.cpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Jean CARDONNE +// Date: 30/06/2025 +// Description: Implementation of file drop handling for asset manager +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" +#include "assets/Asset.hpp" +#include "assets/AssetImporter.hpp" +#include "assets/AssetLocation.hpp" +#include "assets/Assets/Model/Model.hpp" +#include "assets/Assets/Texture/Texture.hpp" +#include "Logger.hpp" +#include +#include + +namespace nexo::editor { + + static assets::AssetType getAssetTypeFromExtension(const std::string &extension) + { + static const std::set imageExtensions = { + ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".gif", ".psd", ".hdr", ".pic", ".pnm", ".ppm", ".pgm" + }; + if (imageExtensions.contains(extension)) + return assets::AssetType::TEXTURE; + static const std::set modelExtensions = { + ".gltf", ".glb", ".fbx", ".obj", ".dae", ".3ds", ".stl", ".ply", ".blend", ".x3d", ".ifc" + }; + if (modelExtensions.contains(extension)) + return assets::AssetType::MODEL; + return assets::AssetType::UNKNOWN; + } + + const assets::AssetLocation AssetManagerWindow::getAssetLocation(const std::filesystem::path &path) const + { + std::string assetName = path.stem().string(); + std::filesystem::path folderPath; + std::string targetFolder = !m_hoveredFolder.empty() ? m_hoveredFolder : m_currentFolder; + + std::string assetPath = targetFolder; + std::string locationString = assetName + "@" + assetPath; + + LOG(NEXO_DEV, + "Creating asset location: {} (current folder: '{}', hovered: '{}')", + locationString, + m_currentFolder, + m_hoveredFolder); + + assets::AssetLocation location(locationString); + return location; + } + + void AssetManagerWindow::handleEvent(event::EventFileDrop& event) + { + m_pendingDroppedFiles.insert(m_pendingDroppedFiles.end(), + event.files.begin(), + event.files.end()); + } + + void AssetManagerWindow::handleDroppedFiles() + { + if (m_pendingDroppedFiles.empty()) + return; + + for (const auto& filePath : m_pendingDroppedFiles) + importDroppedFile(filePath); + m_pendingDroppedFiles.clear(); + + m_folderStructure.clear(); + buildFolderStructure(); + } + + void AssetManagerWindow::importDroppedFile(const std::string& filePath) + { + std::filesystem::path path(filePath); + + if (!std::filesystem::exists(path)) { + LOG(NEXO_WARN, "Dropped file does not exist: {}", filePath); + return; + } + + std::string extension = path.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + + assets::AssetType assetType = getAssetTypeFromExtension(extension); + if (assetType == assets::AssetType::UNKNOWN) { + LOG(NEXO_WARN, "Unsupported file type: {}", extension); + return; + } + + assets::AssetLocation location = getAssetLocation(path); + + assets::AssetImporter importer; + assets::ImporterFileInput fileInput{path}; + try { + auto assetRef = importer.importAssetAuto(location, fileInput); + if (!assetRef) + LOG(NEXO_ERROR, "Failed to import asset: {}", location.getPath().data()); + } catch (const std::exception& e) { + LOG(NEXO_ERROR, "Exception while importing {}: {}", location.getPath().data(), e.what()); + } + } +} diff --git a/editor/src/DocumentWindows/AssetManager/FolderTree.cpp b/editor/src/DocumentWindows/AssetManager/FolderTree.cpp index edff607ff..5b5d38b70 100644 --- a/editor/src/DocumentWindows/AssetManager/FolderTree.cpp +++ b/editor/src/DocumentWindows/AssetManager/FolderTree.cpp @@ -155,51 +155,48 @@ namespace nexo::editor { void AssetManagerWindow::buildFolderStructure() { m_folderStructure.clear(); + // Root entry m_folderStructure.emplace_back("", "Assets"); m_folderChildren.clear(); // Clear the folder children map // First pass: build the folder structure std::set> uniqueFolderPaths; + std::unordered_set seen{""}; + const auto assets = assets::AssetCatalog::getInstance().getAssets(); - for (const auto& asset : assets) { - if (auto assetData = asset.lock()) { - std::string fullPath = assetData->getMetadata().location.getPath(); - std::filesystem::path fsPath(fullPath); - - // Extract all parent directories from the path - while (fsPath.has_parent_path()) { - fsPath = fsPath.parent_path(); - if (!fsPath.empty()) { - uniqueFolderPaths.insert(fsPath.string()); + for (auto& ref : assets) { + if (auto assetData = ref.lock()) { + // normalized path: e.g. "Random/Sub" + std::filesystem::path p{ assetData->getMetadata().location.getPath() }; + std::filesystem::path curr; + for (auto const& part : p) { + // skip empty or “_internal” style parts + auto s = part.string(); + if (s.empty() || s.front() == '_') + continue; + curr /= part; + auto folderPath = curr.string(); + if (seen.emplace(folderPath).second) { + m_folderStructure.emplace_back( + folderPath, + curr.filename().string() + ); } } } } - // Add the unique folder paths to m_folderStructure - for (const auto& folderPath : uniqueFolderPaths) { - std::filesystem::path fsPath(folderPath); - std::string folderName = fsPath.filename().string(); - m_folderStructure.emplace_back(folderPath, folderName); - } - - // Second pass: build the parent-child map - for (const auto& [path, name] : m_folderStructure) { - if (path.empty()) continue; // Skip root - - std::filesystem::path fsPath(path); - std::string parentPath = fsPath.parent_path().string(); - - // If parent path is empty, set it to "" (root) - if (parentPath.empty()) { - parentPath = ""; + std::sort( + m_folderStructure.begin() + 1, + m_folderStructure.end(), + [](auto const& a, auto const& b){ + return a.first < b.first; } - - m_folderChildren[parentPath].push_back(path); - } + ); } + void AssetManagerWindow::drawFolderTree() { handleNewFolderCreation(); diff --git a/editor/src/DocumentWindows/AssetManager/Init.cpp b/editor/src/DocumentWindows/AssetManager/Init.cpp index b49bf81cc..9df8c5ea0 100644 --- a/editor/src/DocumentWindows/AssetManager/Init.cpp +++ b/editor/src/DocumentWindows/AssetManager/Init.cpp @@ -24,13 +24,20 @@ namespace nexo::editor { { auto& catalog = assets::AssetCatalog::getInstance(); auto asset = std::make_unique(); - catalog.registerAsset(assets::AssetLocation("my_package::My_Model@Random/"), std::move(asset)); + assets::AssetLocation location{"my_package::My_Model@Random"}; + catalog.registerAsset(location, std::move(asset)); + { + assets::AssetImporter importer; + std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/models/9mn/scene.gltf"); + assets::ImporterFileInput fileInput{path}; + auto assetRef9mn = importer.importAsset(assets::AssetLocation("my_package::9mn@DefaultScene"), fileInput); + } { assets::AssetImporter importer; std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/textures/logo_nexo.png"); assets::ImporterFileInput fileInput{path}; - auto textureRef = importer.importAsset(assets::AssetLocation("nexo_logo@Random/"), fileInput); + auto textureRef = importer.importAsset(assets::AssetLocation("nexo_logo@Random"), fileInput); } { @@ -39,5 +46,7 @@ namespace nexo::editor { assets::ImporterFileInput fileInput{path}; m_folderIcon = importer.importAsset(assets::AssetLocation("icon_folder@_internal"), fileInput); } + // Register for file drop events + Application::getInstance().getEventManager()->registerListener(this); } } diff --git a/editor/src/DocumentWindows/AssetManager/Show.cpp b/editor/src/DocumentWindows/AssetManager/Show.cpp index 68e81996b..602c1b93e 100644 --- a/editor/src/DocumentWindows/AssetManager/Show.cpp +++ b/editor/src/DocumentWindows/AssetManager/Show.cpp @@ -19,6 +19,8 @@ #include "Path.hpp" #include "assets/Assets/Texture/Texture.hpp" #include "context/ThumbnailCache.hpp" +#include +#include "Logger.hpp" #include namespace nexo::editor { @@ -169,6 +171,31 @@ namespace nexo::editor { if (isHovered) ImGui::SetTooltip("%s", assetData->getMetadata().location.getFullLocation().c_str()); + // Handle drag source for assets + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + AssetDragDropPayload payload; + payload.type = assetData->getType(); + payload.id = assetData->getID(); + payload.path = assetData->getMetadata().location.getFullLocation(); + payload.name = assetName; + + ImGui::SetDragDropPayload("ASSET_DRAG", &payload, sizeof(payload)); + + // Show preview while dragging + //TODO: Add asset preview thanks to thumbnail cache after rebasing + if (assetData->getType() == assets::AssetType::TEXTURE) { + auto textureAsset = asset.as(); + auto textureData = textureAsset.lock(); + if (textureData && textureData->getData() && textureData->getData()->texture) { + ImTextureID textureId = textureData->getData()->texture->getId(); + ImGui::Image(textureId, {64, 64}); + } + } + + ImGui::EndDragDropSource(); + } + ImGui::PopID(); } @@ -199,6 +226,22 @@ namespace nexo::editor { bool clicked = ImGui::InvisibleButton("##folder", itemSize); bool isHovered = ImGui::IsItemHovered(); + if (isHovered) { + m_hoveredFolder = folderPath; + } else if (m_hoveredFolder == folderPath) { + m_hoveredFolder.clear(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) + { + const AssetDragDropPayload* data = (const AssetDragDropPayload*)payload->Data; + assets::AssetCatalog::getInstance().moveAsset(data->id, folderPath); + } + ImGui::EndDragDropTarget(); + } + // Background - use hover color when hovered ImU32 bgColor = isHovered ? m_layout.color.thumbnailBgHovered : IM_COL32(0, 0, 0, 0); drawList->AddRectFilled(itemPos, itemEnd, bgColor, m_layout.size.cornerRadius); @@ -275,100 +318,79 @@ namespace nexo::editor { void AssetManagerWindow::drawAssetsGrid() { ImVec2 startPos = ImGui::GetCursorScreenPos(); - std::vector> subfolders; - // First, collect all immediate subfolders of the current folder - for (const auto& [path, name] : m_folderStructure) { - // Skip the current folder itself - if (path == m_currentFolder) + // 1) Collect immediate subfolders of the current folder + std::vector> subfolders; + for (auto& [path,name] : m_folderStructure) { + if (path.empty() || path.front() == '_') continue; - if (path.find(m_currentFolder) == 0) { - // For root folder (empty current folder) - if (m_currentFolder.empty()) { - if (path.find('/') == std::string::npos) { - subfolders.emplace_back(path, name); - } - } - // For non-root folders - else { - // Check if it's a direct child (only one more / after current folder) - std::string pathAfterCurrent = path.substr(m_currentFolder.length()); - if (pathAfterCurrent[0] == '/') { - pathAfterCurrent = pathAfterCurrent.substr(1); - } - - if (pathAfterCurrent.find('/') == std::string::npos && !pathAfterCurrent.empty()) { - subfolders.emplace_back(path, pathAfterCurrent); - } + if (m_currentFolder.empty()) { + // root level = no slash in path + if (path.find('/') == std::string::npos) + subfolders.emplace_back(path, name); + } else { + std::string prefix = m_currentFolder + "/"; + // immediate child: starts with "curr/" but has no further '/' + if (path.rfind(prefix, 0) == 0 && + path.find('/', prefix.size()) == std::string::npos) + { + subfolders.emplace_back(path, path.substr(prefix.size())); } } } - const std::vector assets = assets::AssetCatalog::getInstance().getAssets(); - - // Filter assets by currently selected folder - std::vector filteredAssets; - for (const auto& asset : assets) { - if (auto assetData = asset.lock()) { - if (assetData->getMetadata().location.getPath() == "_internal") - continue; - - if (m_selectedType != assets::AssetType::UNKNOWN && assetData->getType() != m_selectedType) - continue; - - // Check if asset is in current folder exactly (not in subfolders) - std::string assetPath = assetData->getMetadata().location.getPath(); - std::string assetDir = assetPath.substr(0, assetPath.find_last_of('/')); - - // If there's no slash, asset is in root - if (assetPath.find('/') == std::string::npos) - assetDir = ""; - - if (assetDir == m_currentFolder) - filteredAssets.push_back(asset); + // 2) Collect assets exactly in the current folder + std::vector filtered; + for (auto& ref : assets::AssetCatalog::getInstance().getAssets()) { + if (auto d = ref.lock()) { + const auto& folder = d->getMetadata().location.getPath(); + if (folder == "_internal") continue; + if (m_selectedType != assets::AssetType::UNKNOWN && + d->getType() != m_selectedType) continue; + + // **here’s the fix**: just compare the whole normalized folder + if (folder == m_currentFolder) + filtered.push_back(ref); } } - size_t totalItems = subfolders.size() + filteredAssets.size(); - + // 3) Layout & draw both subfolders and assets + size_t totalItems = subfolders.size() + filtered.size(); ImGuiListClipper clipper; - int rowCount = (static_cast(totalItems) + m_layout.size.columnCount - 1) / m_layout.size.columnCount; - clipper.Begin(rowCount, m_layout.size.itemStep.y); + int rows = int((totalItems + m_layout.size.columnCount - 1) / m_layout.size.columnCount); + clipper.Begin(rows, m_layout.size.itemStep.y); while (clipper.Step()) { - for (int lineIdx = clipper.DisplayStart; lineIdx < clipper.DisplayEnd; ++lineIdx) { - int startIdx = lineIdx * m_layout.size.columnCount; - int endIdx = std::min(startIdx + m_layout.size.columnCount, static_cast(totalItems)); + for (int line = clipper.DisplayStart; line < clipper.DisplayEnd; ++line) { + int startIdx = line * m_layout.size.columnCount; + int endIdx = std::min(startIdx + m_layout.size.columnCount, (int)totalItems); for (int i = startIdx; i < endIdx; ++i) { - auto col = static_cast(i % m_layout.size.columnCount); - auto row = static_cast(i / m_layout.size.columnCount); + float col = float(i % m_layout.size.columnCount); + float row = float(i / m_layout.size.columnCount); ImVec2 itemPos{ startPos.x + col * m_layout.size.itemStep.x, startPos.y + row * m_layout.size.itemStep.y }; - // Draw folder if index is in subfolder range - if (i < static_cast(subfolders.size())) { + if (i < (int)subfolders.size()) { + // draw folder thumbnail drawFolder( subfolders[i].first, subfolders[i].second, itemPos, m_layout.size.itemSize ); - } - // Otherwise draw asset - else { - int assetIdx = i - static_cast(subfolders.size()); - if (assetIdx < static_cast(filteredAssets.size())) { - drawAsset( - filteredAssets[assetIdx], - assetIdx, - itemPos, - m_layout.size.itemSize - ); - } + } else { + // draw asset thumbnail + int assetIdx = i - (int)subfolders.size(); + drawAsset( + filtered[assetIdx], + assetIdx, + itemPos, + m_layout.size.itemSize + ); } } } @@ -378,6 +400,7 @@ namespace nexo::editor { void AssetManagerWindow::show() { + m_hoveredFolder.clear(); if (m_folderStructure.empty()) buildFolderStructure(); @@ -413,50 +436,100 @@ namespace nexo::editor { ImGui::SameLine(); ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); - // Show path breadcrumb - if (m_currentFolder.empty()) { - ImGui::Text("Assets"); - } else { - // Display clickable breadcrumbs - ImGui::Text(ICON_FA_FOLDER " "); - ImGui::SameLine(); + // Handle file drops + if (ImGui::BeginDragDropTarget()) + { + m_showDropIndicator = true; + + // Accept external file drops (this is a placeholder - ImGui doesn't directly support OS file drops) + // The actual files come through the EventFileDrop event + + ImGui::EndDragDropTarget(); + } + else + { + m_showDropIndicator = false; + } - // Start with root level "Assets" + // Draw drop indicator + if (m_showDropIndicator || !m_pendingDroppedFiles.empty()) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 windowSize = ImGui::GetWindowSize(); + + // Draw semi-transparent overlay + drawList->AddRectFilled(windowPos, ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y), + IM_COL32(100, 100, 255, 50)); + + // Draw border + drawList->AddRect(windowPos, ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y), + IM_COL32(100, 100, 255, 200), 0.0f, 0, 3.0f); + + // Draw text + const char* dropText = "Drop files here to import"; + ImVec2 textSize = ImGui::CalcTextSize(dropText); + ImVec2 textPos = ImVec2(windowPos.x + (windowSize.x - textSize.x) * 0.5f, + windowPos.y + (windowSize.y - textSize.y) * 0.5f); + drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 1.5f, textPos, + IM_COL32(255, 255, 255, 255), dropText); + } + + ImGui::Text(ICON_FA_FOLDER " "); + ImGui::SameLine(); + + { + ImGui::PushID("breadcrumb_root"); if (ImGui::Button("Assets")) - m_currentFolder = ""; - - // Split the current path into components - std::string path = m_currentFolder; - size_t pos = 0; - std::string segment; - std::string fullPath = ""; - - while ((pos = path.find('/')) != std::string::npos) { - segment = path.substr(0, pos); - if (!segment.empty()) { - fullPath += (fullPath.empty() ? "" : "/") + segment; - ImGui::SameLine(); - ImGui::Text(" > "); - ImGui::SameLine(); - if (ImGui::Button(segment.c_str())) { - m_currentFolder = fullPath; - } + m_currentFolder.clear(); + + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) + { + const AssetDragDropPayload* data = (const AssetDragDropPayload*)payload->Data; + assets::AssetCatalog::getInstance().moveAsset(data->id, ""); } - path.erase(0, pos + 1); + ImGui::EndDragDropTarget(); } + ImGui::PopID(); + } - // Last segment - if (!path.empty()) { - ImGui::SameLine(); - ImGui::Text(" > "); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", path.c_str()); + // Intermediate breadcrumbs + std::string path = m_currentFolder; + size_t pos = 0; + std::string segment; + std::string fullPath; + while ((pos = path.find('/')) != std::string::npos) { + segment = path.substr(0, pos); + fullPath += (fullPath.empty() ? "" : "/") + segment; + ImGui::SameLine(); ImGui::Text(" > "); ImGui::SameLine(); + + ImGui::PushID(("breadcrumb_" + fullPath).c_str()); + if (ImGui::Button(segment.c_str())) + m_currentFolder = fullPath; + + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) + { + const AssetDragDropPayload* data = (const AssetDragDropPayload*)payload->Data; + assets::AssetCatalog::getInstance().moveAsset(data->id, fullPath); + } + ImGui::EndDragDropTarget(); } + ImGui::PopID(); + + path.erase(0, pos + 1); + } + + if (!path.empty()) { + ImGui::SameLine(); ImGui::Text(" > "); ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", path.c_str()); } ImGui::Separator(); - // Calculate layout for asset grid calculateLayout(ImGui::GetContentRegionAvail().x); drawAssetsGrid(); ImGui::EndChild(); diff --git a/editor/src/DocumentWindows/AssetManager/Update.cpp b/editor/src/DocumentWindows/AssetManager/Update.cpp index 475078433..41442d0a1 100644 --- a/editor/src/DocumentWindows/AssetManager/Update.cpp +++ b/editor/src/DocumentWindows/AssetManager/Update.cpp @@ -18,6 +18,7 @@ namespace nexo::editor { void AssetManagerWindow::update() { + handleDroppedFiles(); // Nothing to do for now } diff --git a/editor/src/DocumentWindows/EditorScene/DragDrop.cpp b/editor/src/DocumentWindows/EditorScene/DragDrop.cpp new file mode 100644 index 000000000..6a0f8b956 --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/DragDrop.cpp @@ -0,0 +1,185 @@ +//// DragDrop.cpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 12/07/2025 +// Description: Source file for the drag and drop feature of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "Definitions.hpp" +#include "EditorScene.hpp" +#include "assets/Asset.hpp" +#include "assets/AssetCatalog.hpp" +#include "components/Editor.hpp" +#include "components/MaterialComponent.hpp" +#include "context/ActionManager.hpp" + +namespace nexo::editor { + + void EditorScene::handleDropModel(const AssetDragDropPayload &payload) + { + auto modelRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (!modelRef) + return; + if (auto model = modelRef.as(); model) + { + auto& sceneManager = Application::getInstance().getSceneManager(); + // Create entity with the model + ecs::Entity newEntity = EntityFactory3D::createModel( + model, + {0.0f, 0.0f, 0.0f}, // position + {1.0f, 1.0f, 1.0f}, // scale + {0.0f, 0.0f, 0.0f} // rotation + ); + + // Add to the scene + auto& scene = sceneManager.getScene(m_sceneId); + scene.addEntity(newEntity); + + // Record action for undo/redo TODO: Fix undo for models, it does not seem to work properly + auto action = std::make_unique(newEntity); + ActionManager::get().recordAction(std::move(action)); + } + } + + void EditorScene::handleDropTexture(const AssetDragDropPayload &payload) + { + auto textureRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (!textureRef) + return; + if (auto texture = textureRef.as(); texture) + { + auto [mx, my] = ImGui::GetMousePos(); + mx -= m_viewportBounds[0].x; + my -= m_viewportBounds[0].y; + + // Flip the y-coordinate to match opengl texture format + my = m_contentSize.y - my; + + // Check if mouse is inside viewport + if (!(mx >= 0 && my >= 0 && mx < m_contentSize.x && my < m_contentSize.y)) + return; + const int entityId = sampleEntityTexture(mx, my); + if (entityId == -1) { + auto& sceneManager = Application::getInstance().getSceneManager(); + components::Material material; + material.albedoTexture = texture; + material.albedoColor = glm::vec4(1.0f); // White to show texture colors + + // Create billboard entity + ecs::Entity newEntity = EntityFactory3D::createBillboard( + {0.0f, 0.0f, 0.0f}, // position + {1.0f, 1.0f, 1.0f}, // size + material + ); + + // Add to the scene + auto& scene = sceneManager.getScene(m_sceneId); + scene.addEntity(newEntity); + + // Record action for undo/redo + auto action = std::make_unique(newEntity); + ActionManager::get().recordAction(std::move(action)); + return; + } + auto matComponent = Application::getInstance().m_coordinator->tryGetComponent(entityId); + if (!matComponent) + return; + auto material = matComponent->get().material.lock(); + if (!material) + return; + material->getData()->albedoTexture = texture; + } + } + + void EditorScene::handleDropMaterial(const AssetDragDropPayload &payload) + { + auto materialRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (!materialRef) + return; + if (auto material = materialRef.as(); material) + { + auto [mx, my] = ImGui::GetMousePos(); + mx -= m_viewportBounds[0].x; + my -= m_viewportBounds[0].y; + + // Flip the y-coordinate to match opengl texture format + my = m_contentSize.y - my; + + // Check if mouse is inside viewport + if (!(mx >= 0 && my >= 0 && mx < m_contentSize.x && my < m_contentSize.y)) + return; + const int entityId = sampleEntityTexture(mx, my); + if (entityId == -1) + return; + auto matComponent = Application::getInstance().m_coordinator->tryGetComponent(entityId); + if (!matComponent) + return; + matComponent->get().material = material; + } + } + + void EditorScene::handleDropTarget() + { + if (ImGui::BeginDragDropTarget()) + { + // Handle drops from asset manager + if (const ImGuiPayload* assetPayload = ImGui::AcceptDragDropPayload("ASSET_DRAG", ImGuiDragDropFlags_AcceptBeforeDelivery)) + { + IM_ASSERT(assetPayload->DataSize == sizeof(AssetDragDropPayload)); + auto [mx, my] = ImGui::GetMousePos(); + mx -= m_viewportBounds[0].x; + my -= m_viewportBounds[0].y; + + // Flip the y-coordinate to match opengl texture format + my = m_contentSize.y - my; + + // Check if mouse is inside viewport + if (!(mx >= 0 && my >= 0 && mx < m_contentSize.x && my < m_contentSize.y)) + return; + const int entityId = sampleEntityTexture(mx, my); + if (entityId != -1 && static_cast(entityId) != m_entityHovered) + { + m_entityHovered = static_cast(entityId); + Application::getInstance().m_coordinator->addComponent(m_entityHovered, components::SelectedTag{}); + } + if (entityId == -1 && m_entityHovered != ecs::INVALID_ENTITY) + { + Application::getInstance().m_coordinator->removeComponent(m_entityHovered); + m_entityHovered = ecs::INVALID_ENTITY; + } + if (!assetPayload->IsDelivery()) + { + return; + } + if (m_entityHovered != ecs::INVALID_ENTITY) + Application::getInstance().m_coordinator->removeComponent(m_entityHovered); + m_entityHovered = ecs::INVALID_ENTITY; + const auto& payload = *static_cast(assetPayload->Data); + + switch(payload.type) + { + case assets::AssetType::MODEL: + handleDropModel(payload); + break; + case assets::AssetType::TEXTURE: + handleDropTexture(payload); + break; + case assets::AssetType::MATERIAL: + handleDropMaterial(payload); + break; + default: + break; + } + } + ImGui::EndDragDropTarget(); + } + } +} diff --git a/editor/src/DocumentWindows/EditorScene/EditorScene.hpp b/editor/src/DocumentWindows/EditorScene/EditorScene.hpp index fa2449626..779ff807f 100644 --- a/editor/src/DocumentWindows/EditorScene/EditorScene.hpp +++ b/editor/src/DocumentWindows/EditorScene/EditorScene.hpp @@ -17,11 +17,13 @@ #include #include "ADocumentWindow.hpp" +#include "Definitions.hpp" #include "inputs/WindowState.hpp" #include "core/scene/SceneManager.hpp" #include "../PopupManager.hpp" #include "ImNexo/Widgets.hpp" #include +#include "DocumentWindows/AssetManager/AssetManagerWindow.hpp" namespace nexo::editor { @@ -103,6 +105,8 @@ namespace nexo::editor bool m_snapToGrid = false; bool m_wireframeEnabled = false; + ecs::Entity m_entityHovered = ecs::INVALID_ENTITY; + int m_sceneId = -1; std::string m_sceneUuid; int m_activeCamera = -1; @@ -157,11 +161,11 @@ namespace nexo::editor void setupShortcuts(); /** - * @brief Populates the scene with default entities. - * - * Creates standard light sources (ambient, directional, point, spot) - * and a simple ground plane in the scene. - */ + * @brief Populates the scene with default entities. + * + * Creates standard light sources (ambient, directional, point, spot) + * and a simple ground plane in the scene. + */ void loadDefaultEntities() const; /** @@ -302,6 +306,10 @@ namespace nexo::editor void renderNewEntityPopup(); void handleSelection(); + void handleDropTarget(); + void handleDropModel(const AssetDragDropPayload &payload); + void handleDropTexture(const AssetDragDropPayload &payload); + void handleDropMaterial(const AssetDragDropPayload &payload); int sampleEntityTexture(float mx, float my) const; ecs::Entity findRootParent(ecs::Entity entityId) const; void selectEntityHierarchy(ecs::Entity entityId, const bool isCtrlPressed); diff --git a/editor/src/DocumentWindows/EditorScene/Init.cpp b/editor/src/DocumentWindows/EditorScene/Init.cpp index 2442e8e37..341bab2b5 100644 --- a/editor/src/DocumentWindows/EditorScene/Init.cpp +++ b/editor/src/DocumentWindows/EditorScene/Init.cpp @@ -110,8 +110,6 @@ namespace nexo::editor const auto light = LightFactory::createPointLight(position, colors[i % colors.size()], 0.01, 0.0010); scene.addEntity(light); } - - assets::AssetImporter importer; } void EditorScene::loadDefaultEntities() const @@ -275,7 +273,6 @@ namespace nexo::editor } } - void EditorScene::setupWindow() { m_contentSize = ImVec2(1280, 720); diff --git a/editor/src/DocumentWindows/EditorScene/Show.cpp b/editor/src/DocumentWindows/EditorScene/Show.cpp index 0ecf2ae23..50495cff4 100644 --- a/editor/src/DocumentWindows/EditorScene/Show.cpp +++ b/editor/src/DocumentWindows/EditorScene/Show.cpp @@ -150,6 +150,7 @@ namespace nexo::editor const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); ImNexo::Image(static_cast(static_cast(textureId)), m_contentSize); + handleDropTarget(); const ImVec2 viewportMin = ImGui::GetItemRectMin(); const ImVec2 viewportMax = ImGui::GetItemRectMax(); m_viewportBounds[0] = viewportMin; diff --git a/editor/src/DocumentWindows/EditorScene/Toolbar.cpp b/editor/src/DocumentWindows/EditorScene/Toolbar.cpp index fa354def5..468e1d7a7 100644 --- a/editor/src/DocumentWindows/EditorScene/Toolbar.cpp +++ b/editor/src/DocumentWindows/EditorScene/Toolbar.cpp @@ -70,8 +70,9 @@ namespace nexo::editor { void EditorScene::initialToolbarSetup(const float buttonWidth) const { ImVec2 toolbarPos = m_windowPos; - toolbarPos.x += 10.0f; - toolbarPos.y += 20.0f; + ImVec2 contentMin = ImGui::GetWindowContentRegionMin(); + toolbarPos.x += contentMin.x + 10.0f; + toolbarPos.y += contentMin.y + 20.0f; ImGui::SetCursorScreenPos(toolbarPos); diff --git a/editor/src/DocumentWindows/SceneTreeWindow/DragDrop.cpp b/editor/src/DocumentWindows/SceneTreeWindow/DragDrop.cpp new file mode 100644 index 000000000..e6550eef2 --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/DragDrop.cpp @@ -0,0 +1,318 @@ +//// DragDrop.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Jean CARDONNE +// Date: 2025-06-30 +// Description: Implementation of drag and drop functionality for the scene tree +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "DocumentWindows/AssetManager/AssetManagerWindow.hpp" +#include "components/Uuid.hpp" +#include "context/ActionManager.hpp" +#include "context/Selector.hpp" +#include "context/actions/EntityActions.hpp" +#include "components/Parent.hpp" +#include "components/Transform.hpp" +#include "components/Render3D.hpp" +#include "EntityFactory3D.hpp" +#include "assets/AssetCatalog.hpp" +#include "assets/Assets/Model/Model.hpp" +#include "assets/Assets/Texture/Texture.hpp" +#include +#include +#include +#define GLM_ENABLE_EXPERIMENTAL +#include + +namespace nexo::editor { + + void SceneTreeWindow::handleDragSource(const SceneObject& object) + { + if (object.type == SelectionType::SCENE || object.type == SelectionType::NONE) + return; + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + SceneTreeDragDropPayload payload{ + object.data.entity, + object.data.sceneProperties.sceneId, + object.type, + object.uuid, + object.uiName + }; + + ImGui::SetDragDropPayload("SCENE_TREE_NODE", &payload, sizeof(payload)); + ImGui::EndDragDropSource(); + } + } + + void SceneTreeWindow::handleDropTarget(const SceneObject& object) + { + if (ImGui::BeginDragDropTarget()) + { + // Handle drops from scene tree nodes + if (const ImGuiPayload* imguiPayload = ImGui::AcceptDragDropPayload("SCENE_TREE_NODE")) + { + IM_ASSERT(imguiPayload->DataSize == sizeof(SceneTreeDragDropPayload)); + const auto& payload = *static_cast(imguiPayload->Data); + + if (canAcceptDrop(object, payload)) + handleDropFromSceneTree(object, payload); + } + + // Handle drops from asset manager + if (const ImGuiPayload* assetPayload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) + { + IM_ASSERT(assetPayload->DataSize == sizeof(AssetDragDropPayload)); + const auto& payload = *static_cast(assetPayload->Data); + + handleDropFromAssetManager(object, payload); + } + + ImGui::EndDragDropTarget(); + } + } + + bool SceneTreeWindow::canAcceptDrop(const SceneObject& dropTarget, const SceneTreeDragDropPayload& payload) + { + // Can't drop on itself + if (dropTarget.type != SelectionType::SCENE && dropTarget.data.entity == payload.entity) + return false; + + // Can't drop a parent onto its child (prevent circular references) + if (dropTarget.type != SelectionType::SCENE) + { + ecs::Entity currentEntity = dropTarget.data.entity; + auto parentComp = Application::m_coordinator->tryGetComponent(currentEntity); + while (parentComp.has_value()) + { + if (parentComp->get().parent == payload.entity) + return false; + currentEntity = parentComp->get().parent; + parentComp = Application::m_coordinator->tryGetComponent(currentEntity); + } + } + + return true; + } + + void SceneTreeWindow::handleDropFromSceneTree(const SceneObject& dropTarget, const SceneTreeDragDropPayload& payload) + { + auto& app = Application::getInstance(); + auto& sceneManager = app.getSceneManager(); + auto& coordinator = *Application::m_coordinator; + + auto& sourceScene = sceneManager.getScene(payload.sourceSceneId); + + if (dropTarget.type == SelectionType::SCENE) + { + // Dropping onto a scene - move entity to that scene + if (payload.sourceSceneId != dropTarget.data.sceneProperties.sceneId) + { + sourceScene.removeEntity(payload.entity); + + auto& targetScene = sceneManager.getScene(dropTarget.data.sceneProperties.sceneId); + targetScene.addEntity(payload.entity); + + // Remove parent relationship if moving to different scene + auto parentComp = coordinator.tryGetComponent(payload.entity); + if (parentComp.has_value()) + { + auto parentTransform = coordinator.tryGetComponent(parentComp->get().parent); + if (parentTransform.has_value()) + parentTransform->get().removeChild(payload.entity); + + coordinator.removeComponent(payload.entity); + } + + // Record action for undo/redo + // TODO: Create a specific action for moving entities between scenes + // For now, we just perform the operation without undo support for scene moves + } + } + else if (dropTarget.type == SelectionType::ENTITY) + { + // Dropping onto an entity - create parent-child relationship + ecs::Entity parentEntity = dropTarget.data.entity; + ecs::Entity childEntity = payload.entity; + + auto childTransformOpt = coordinator.tryGetComponent(childEntity); + if (!childTransformOpt.has_value()) + return; + auto &childTransform = childTransformOpt->get(); + glm::mat4 childWorldMat = childTransform.worldMatrix; + + auto parentTransformOpt = coordinator.tryGetComponent(parentEntity); + if (!parentTransformOpt.has_value()) + return; + auto& parentTransform = parentTransformOpt->get(); + glm::mat4 parentWorldMat = parentTransform.worldMatrix; + + // Compute the new localMatrix so that parentWorldMat * local = old world + glm::mat4 invParent = glm::inverse(parentWorldMat); + glm::mat4 newLocalMat = invParent * childWorldMat; + + glm::vec3 skew, scale, translation; + glm::quat rotation; + glm::vec4 perspective; + glm::decompose( + newLocalMat, + scale, + rotation, + translation, + skew, + perspective + ); + + childTransform.pos = translation; + childTransform.quat = rotation; + childTransform.size = scale; + + ecs::Entity oldParent = ecs::INVALID_ENTITY; + auto oldParentComp = coordinator.tryGetComponent(childEntity); + if (oldParentComp.has_value()) + { + oldParent = oldParentComp->get().parent; + + if (auto oldPT = coordinator.tryGetComponent(oldParent)) { + oldPT->get().removeChild(childEntity); + if (oldPT->get().children.empty() && coordinator.entityHasComponent(oldParent)) + coordinator.removeComponent(oldParent); + } + } + + if (!oldParentComp.has_value()) + coordinator.addComponent(childEntity, components::ParentComponent{parentEntity}); + else + oldParentComp->get().parent = parentEntity; + + if (!coordinator.entityHasComponent(parentEntity)) + coordinator.addComponent(parentEntity, components::TransformComponent{}); + auto& pt = coordinator.getComponent(parentEntity); + pt.addChild(childEntity); + + if (!coordinator.entityHasComponent(parentEntity) && + !coordinator.entityHasComponent(parentEntity)) + { + std::string name = dropTarget.uiName; + auto it = ObjectTypeToIcon.find(dropTarget.type); + if (it != ObjectTypeToIcon.end()) + { + const std::string& icon = it->second; + if (name.rfind(icon, 0) == 0) + name.erase(0, icon.size()); + } + + coordinator.addComponent( + parentEntity, + components::RootComponent{ name, nullptr, 1 } + ); + } + + if (payload.sourceSceneId != dropTarget.data.sceneProperties.sceneId) + { + sourceScene.removeEntity(childEntity); + auto& targetScene = sceneManager.getScene(dropTarget.data.sceneProperties.sceneId); + targetScene.addEntity(childEntity); + + auto& sceneTag = coordinator.getComponent(childEntity); + sceneTag.id = dropTarget.data.sceneProperties.sceneId; + } + + auto action = std::make_unique(childEntity, oldParent, parentEntity); + ActionManager::get().recordAction(std::move(action)); + } + } + + void SceneTreeWindow::handleDropFromAssetManager(const SceneObject& dropTarget, const AssetDragDropPayload& payload) + { + if (dropTarget.type == SelectionType::SCENE) + { + auto& app = Application::getInstance(); + auto& sceneManager = app.getSceneManager(); + + if (payload.type == assets::AssetType::MODEL) + { + auto modelRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (!modelRef) + return; + if (auto model = modelRef.as(); model) + { + ecs::Entity newEntity = EntityFactory3D::createModel( + model, + {0.0f, 0.0f, 0.0f}, + {1.0f, 1.0f, 1.0f}, + {0.0f, 0.0f, 0.0f} + ); + auto& scene = sceneManager.getScene(dropTarget.data.sceneProperties.sceneId); + scene.addEntity(newEntity); + + // Record action for undo/redo TODO: Fix undo for models, it does not seem to work properly + auto action = std::make_unique(newEntity); + ActionManager::get().recordAction(std::move(action)); + } + } + else if (payload.type == assets::AssetType::TEXTURE) + { + auto textureRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (!textureRef) + return; + if (auto texture = textureRef.as(); texture) + { + components::Material material; + material.albedoTexture = texture; + material.albedoColor = glm::vec4(1.0f); + + ecs::Entity newEntity = EntityFactory3D::createBillboard( + {0.0f, 0.0f, 0.0f}, + {1.0f, 1.0f, 1.0f}, + material + ); + + auto& scene = sceneManager.getScene(dropTarget.data.sceneProperties.sceneId); + scene.addEntity(newEntity); + + auto action = std::make_unique(newEntity); + ActionManager::get().recordAction(std::move(action)); + } + } + } + else if (dropTarget.type == SelectionType::ENTITY) + { + auto matCompOpt = Application::m_coordinator->tryGetComponent(dropTarget.data.entity); + if (!matCompOpt) + { ImGui::EndDragDropTarget(); return; } + + auto& matComp = matCompOpt->get(); + + if (payload.type == assets::AssetType::TEXTURE) + { + auto texRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (auto tex = texRef.as(); tex) + { + auto mat = matComp.material.lock(); + if (!mat) + return; + mat->getData()->albedoTexture = tex; + } + } + else if (payload.type == assets::AssetType::MATERIAL) + { + auto matRef = assets::AssetCatalog::getInstance().getAsset(payload.id); + if (auto m = matRef.as(); m) + { + auto oldMat = matComp.material; + matComp.material = m; + } + } + } + } + +} // namespace nexo::editor diff --git a/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp b/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp index 98093ed20..95a87746f 100644 --- a/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp +++ b/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp @@ -425,5 +425,54 @@ namespace nexo::editor static void selectAllCallback(); static void hideSelectedCallback(); static void showAllCallback(); + + /** + * @brief Handles drag source setup for scene objects. + * @param object The scene object being dragged. + */ + void handleDragSource(const SceneObject& object); + + /** + * @brief Handles drop target setup for scene objects. + * @param object The scene object that can receive drops. + */ + void handleDropTarget(const SceneObject& object); + + /** + * @brief Processes the drop operation from the scene tree itself. + * @param dropTarget The target scene object receiving the drop. + * @param payload The drag-and-drop payload data. + */ + void handleDropFromSceneTree(const SceneObject& dropTarget, const struct SceneTreeDragDropPayload& payload); + + /** + * @brief Processes the drop operation from the asset manager. + * @param dropTarget The target scene object receiving the drop. + * @param payload The drag-and-drop payload data. + */ + void handleDropFromAssetManager(const SceneObject& dropTarget, const struct AssetDragDropPayload& payload); + + /** + * @brief Validates if a drop operation is allowed. + * @param dropTarget The target scene object. + * @param payload The drag-and-drop payload data. + * @return true if the drop is valid, false otherwise. + */ + static bool canAcceptDrop(const SceneObject& dropTarget, const struct SceneTreeDragDropPayload& payload); + }; + + /** + * @brief Payload structure for drag and drop operations in the scene tree. + * + * Contains all necessary information to perform entity/scene drag and drop + * operations including validation and hierarchy updates. + */ + struct SceneTreeDragDropPayload + { + ecs::Entity entity; ///< The entity being dragged + scene::SceneId sourceSceneId; ///< The scene the entity originated from + SelectionType type; ///< The type of object being dragged + std::string uuid; ///< UUID of the dragged object + std::string name; ///< Display name of the dragged object }; } diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp index 0cd41baa9..0db1e3d82 100644 --- a/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp +++ b/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp @@ -186,6 +186,9 @@ namespace nexo::editor handleHovering(object); + handleDragSource(object); + handleDropTarget(object); + // Handles the right click on each different type of object if (object.type != SelectionType::NONE && ImGui::BeginPopupContextItem(uniqueLabel.c_str())) { diff --git a/editor/src/context/actions/EntityActions.cpp b/editor/src/context/actions/EntityActions.cpp index 455ec79a4..00c263094 100644 --- a/editor/src/context/actions/EntityActions.cpp +++ b/editor/src/context/actions/EntityActions.cpp @@ -64,4 +64,84 @@ namespace nexo::editor { for (const auto &action : m_componentRestoreActions) action->undo(); } + + void EntityParentChangeAction::redo() + { + auto& coordinator = *Application::m_coordinator; + + // Handle old parent + if (m_oldParent != ecs::INVALID_ENTITY) { + auto oldParentTransform = coordinator.tryGetComponent(m_oldParent); + if (oldParentTransform.has_value()) { + oldParentTransform->get().removeChild(m_entity); + } + } + + // Handle new parent + if (m_newParent != ecs::INVALID_ENTITY) { + // Add or update parent component on entity + auto parentComp = coordinator.tryGetComponent(m_entity); + if (!parentComp.has_value()) { + coordinator.addComponent(m_entity, components::ParentComponent{m_newParent}); + } else { + parentComp->get().parent = m_newParent; + } + + // Add to new parent's children + auto newParentTransform = coordinator.tryGetComponent(m_newParent); + if (!newParentTransform.has_value()) { + coordinator.addComponent(m_newParent, components::TransformComponent{}); + newParentTransform = coordinator.tryGetComponent(m_newParent); + } + if (newParentTransform.has_value()) { + newParentTransform->get().addChild(m_entity); + } + } else { + // Remove parent component (make it a root entity) + auto parentComp = coordinator.tryGetComponent(m_entity); + if (parentComp.has_value()) { + coordinator.removeComponent(m_entity); + } + } + } + + void EntityParentChangeAction::undo() + { + auto& coordinator = *Application::m_coordinator; + + // Handle new parent (undo by removing from it) + if (m_newParent != ecs::INVALID_ENTITY) { + auto newParentTransform = coordinator.tryGetComponent(m_newParent); + if (newParentTransform.has_value()) { + newParentTransform->get().removeChild(m_entity); + } + } + + // Handle old parent (restore to it) + if (m_oldParent != ecs::INVALID_ENTITY) { + // Add or update parent component on entity + auto parentComp = coordinator.tryGetComponent(m_entity); + if (!parentComp.has_value()) { + coordinator.addComponent(m_entity, components::ParentComponent{m_oldParent}); + } else { + parentComp->get().parent = m_oldParent; + } + + // Add back to old parent's children + auto oldParentTransform = coordinator.tryGetComponent(m_oldParent); + if (!oldParentTransform.has_value()) { + coordinator.addComponent(m_oldParent, components::TransformComponent{}); + oldParentTransform = coordinator.tryGetComponent(m_oldParent); + } + if (oldParentTransform.has_value()) { + oldParentTransform->get().addChild(m_entity); + } + } else { + // Remove parent component (restore to root entity) + auto parentComp = coordinator.tryGetComponent(m_entity); + if (parentComp.has_value()) { + coordinator.removeComponent(m_entity); + } + } + } } diff --git a/editor/src/context/actions/EntityActions.hpp b/editor/src/context/actions/EntityActions.hpp index 227d0695e..ed858308c 100644 --- a/editor/src/context/actions/EntityActions.hpp +++ b/editor/src/context/actions/EntityActions.hpp @@ -152,4 +152,21 @@ namespace nexo::editor { std::vector> m_componentRestoreActions; }; + /** + * Stores information needed to undo/redo entity parent changes + * Handles hierarchy component updates + */ + class EntityParentChangeAction final : public Action { + public: + EntityParentChangeAction(ecs::Entity entity, ecs::Entity oldParent, ecs::Entity newParent) + : m_entity(entity), m_oldParent(oldParent), m_newParent(newParent) {} + + void redo() override; + void undo() override; + private: + ecs::Entity m_entity; + ecs::Entity m_oldParent; + ecs::Entity m_newParent; + }; + } diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 84bb280b8..c1df4ebd7 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -12,6 +12,7 @@ set(COMMON_SOURCES common/Exception.cpp common/math/Vector.cpp common/math/Projection.cpp + common/Path.cpp engine/src/Nexo.cpp engine/src/EntityFactory3D.cpp engine/src/LightFactory.cpp diff --git a/engine/src/Application.cpp b/engine/src/Application.cpp index acb032834..c130cdb7b 100644 --- a/engine/src/Application.cpp +++ b/engine/src/Application.cpp @@ -196,6 +196,16 @@ namespace nexo { m_eventManager->emitEvent( std::make_shared(static_cast(xpos), static_cast(ypos))); }); + + m_window->setFileDropCallback([this](const int count, const char** paths) { + std::vector files; + files.reserve(count); + for (int i = 0; i < count; ++i) { + files.emplace_back(paths[i]); + } + m_eventManager->emitEvent( + std::make_shared(files)); + }); } void Application::registerSystems() diff --git a/engine/src/assets/AssetCatalog.cpp b/engine/src/assets/AssetCatalog.cpp index b2736d0ac..1a8186ed9 100644 --- a/engine/src/assets/AssetCatalog.cpp +++ b/engine/src/assets/AssetCatalog.cpp @@ -34,6 +34,20 @@ namespace nexo::assets { } } + void AssetCatalog::moveAsset(const GenericAssetRef &asset, const std::string &path) + { + if (const auto assetData = asset.lock()) + moveAsset(assetData->getID(), path); + } + + void AssetCatalog::moveAsset(AssetID id, const std::string &path) + { + if (!m_assets.contains(id)) + return; + auto asset = m_assets.at(id); + asset->m_metadata.location.setPath(path); + } + GenericAssetRef AssetCatalog::getAsset(AssetID id) const { if (!m_assets.contains(id)) diff --git a/engine/src/assets/AssetCatalog.hpp b/engine/src/assets/AssetCatalog.hpp index cd8f2e003..c54f35878 100644 --- a/engine/src/assets/AssetCatalog.hpp +++ b/engine/src/assets/AssetCatalog.hpp @@ -24,6 +24,7 @@ #include "Assets/Texture/Texture.hpp" #include "Assets/Texture/Texture.hpp" +#include "assets/AssetRef.hpp" namespace nexo::assets { @@ -80,6 +81,21 @@ namespace nexo::assets { */ void deleteAsset(const GenericAssetRef& asset); + /** + * @brief Moves an asset to another location. + * @param asset The asset to move. + * @param path The new location for the asset. + */ + void moveAsset(const GenericAssetRef &asset, const std::string &path); + + /** + * @brief Moves an asset to another location. + * @param id The ID of the asset to move. + * @param path The new location for the asset. + */ + void moveAsset(AssetID id, const std::string &path); + + /** * @brief Get an asset by its ID. * @param id The ID of the asset to get. @@ -180,6 +196,8 @@ namespace nexo::assets { return assetRef.template as(); } + + private: std::unordered_map> m_assets; }; diff --git a/engine/src/assets/AssetImporter.cpp b/engine/src/assets/AssetImporter.cpp index f651bc456..f7cec5aad 100644 --- a/engine/src/assets/AssetImporter.cpp +++ b/engine/src/assets/AssetImporter.cpp @@ -41,12 +41,25 @@ namespace nexo::assets { GenericAssetRef AssetImporter::importAssetAuto(const AssetLocation& location, const ImporterInputVariant& inputVariant) { - for (const auto& importers: m_importers | std::views::values) { - if (importers.empty()) - continue; - if (const auto asset = importAssetTryImporters(location, inputVariant, importers)) - return asset; + // Temp fix to use all importers in priority order + // TODO: change the way we store importers, maybe stop using a map + std::vector allImporters; + std::vector allImportersDetails; + for (const auto& typeIdx: m_importers | std::views::keys) { + const auto& importers = m_importers.at(typeIdx); + const auto& importerDetails = m_importersDetails.at(typeIdx); + for (int importerIdx = 0; importerIdx < static_cast(importers.size()); ++importerIdx) { + const auto& details = importerDetails[importerIdx]; + const int priority = details.priority; + size_t k = 0; + for (; k < allImporters.size() && priority <= allImportersDetails[k].priority ; ++k); + allImporters.insert(allImporters.begin() + static_cast(k), importers[importerIdx]); + allImportersDetails.insert(allImportersDetails.begin() + static_cast(k), details); + } } + + if (const auto asset = importAssetTryImporters(location, inputVariant, allImporters)) + return asset; return GenericAssetRef::null(); } diff --git a/engine/src/assets/AssetLocation.cpp b/engine/src/assets/AssetLocation.cpp index dd47173c7..d7a63d63e 100644 --- a/engine/src/assets/AssetLocation.cpp +++ b/engine/src/assets/AssetLocation.cpp @@ -13,6 +13,7 @@ /////////////////////////////////////////////////////////////////////////////// #include "AssetLocation.hpp" +#include "Path.hpp" namespace nexo::assets { @@ -23,7 +24,7 @@ namespace nexo::assets { ) { _name = name; - _path = path; + _path = normalizePathAndRemovePrefixSlash(path); _packName = packName; } } // namespace nexo::assets diff --git a/engine/src/assets/AssetLocation.hpp b/engine/src/assets/AssetLocation.hpp index b33e5eb47..618b0e6a7 100644 --- a/engine/src/assets/AssetLocation.hpp +++ b/engine/src/assets/AssetLocation.hpp @@ -21,6 +21,7 @@ #include "AssetName.hpp" #include "AssetPackName.hpp" +#include "Path.hpp" namespace nexo::assets { @@ -61,7 +62,7 @@ namespace nexo::assets { */ AssetLocation& setPath(const std::string& path) { - _path = path; + _path = normalizePathAndRemovePrefixSlash(path); return *this; } @@ -122,8 +123,10 @@ namespace nexo::assets { if (_packName) fullLocation += _packName->data() + "::"; fullLocation += _name.data(); - if (!_path.empty()) - fullLocation += "@" + _path; + if (!_path.empty()) { + fullLocation += "@"; + fullLocation += _path; + } return fullLocation; } @@ -152,6 +155,7 @@ namespace nexo::assets { std::string extractedPath; parseFullLocation(fullLocation, extractedAssetName, extractedPath, extractedPackName); + extractedPath = normalizePathAndRemovePrefixSlash(extractedPath); try { _name = AssetName(extractedAssetName); diff --git a/engine/src/core/event/WindowEvent.hpp b/engine/src/core/event/WindowEvent.hpp index b018c9a95..73d8fc66a 100644 --- a/engine/src/core/event/WindowEvent.hpp +++ b/engine/src/core/event/WindowEvent.hpp @@ -163,4 +163,21 @@ namespace nexo::event { return os; } }; + + class EventFileDrop final : public Event { + public: + EventFileDrop(const std::vector& droppedFiles) : files(droppedFiles) {}; + + std::vector files; + + friend std::ostream &operator<<(std::ostream &os, const EventFileDrop &event) + { + os << "[FILE DROP EVENT] " << event.files.size() << " file(s): "; + for (size_t i = 0; i < event.files.size(); ++i) { + if (i > 0) os << ", "; + os << event.files[i]; + } + return os; + } + }; } diff --git a/engine/src/renderer/Window.hpp b/engine/src/renderer/Window.hpp index 976ffed41..115bc1b8e 100644 --- a/engine/src/renderer/Window.hpp +++ b/engine/src/renderer/Window.hpp @@ -29,6 +29,7 @@ namespace nexo::renderer { using MouseClickCallback = std::function; using MouseScrollCallback = std::function; using MouseMoveCallback = std::function; + using FileDropCallback = std::function; struct NxWindowProperty { @@ -43,6 +44,7 @@ namespace nexo::renderer { MouseClickCallback mouseClickCallback; MouseScrollCallback mouseScrollCallback; MouseMoveCallback mouseMoveCallback; + FileDropCallback fileDropCallback; NxWindowProperty(const unsigned int w, const unsigned h, const char * t) : width(w), height(h), title(t) {} }; @@ -108,6 +110,7 @@ namespace nexo::renderer { virtual void setMouseClickCallback(MouseClickCallback callback) = 0; virtual void setMouseScrollCallback(MouseScrollCallback callback) = 0; virtual void setMouseMoveCallback(MouseMoveCallback callback) = 0; + virtual void setFileDropCallback(FileDropCallback callback) = 0; // Linux specific methods #ifdef __linux__ diff --git a/engine/src/renderer/opengl/OpenGlWindow.cpp b/engine/src/renderer/opengl/OpenGlWindow.cpp index e6730e90b..3484ef9ae 100644 --- a/engine/src/renderer/opengl/OpenGlWindow.cpp +++ b/engine/src/renderer/opengl/OpenGlWindow.cpp @@ -81,6 +81,13 @@ namespace nexo::renderer { if (props->mouseMoveCallback) props->mouseMoveCallback(xpos, ypos); }); + + glfwSetDropCallback(_openGlWindow, [](GLFWwindow *window, const int count, const char **paths) + { + const auto *props = static_cast(glfwGetWindowUserPointer(window)); + if (props->fileDropCallback) + props->fileDropCallback(count, paths); + }); } void NxOpenGlWindow::init() diff --git a/engine/src/renderer/opengl/OpenGlWindow.hpp b/engine/src/renderer/opengl/OpenGlWindow.hpp index 73f4c1023..dbeb11eb6 100644 --- a/engine/src/renderer/opengl/OpenGlWindow.hpp +++ b/engine/src/renderer/opengl/OpenGlWindow.hpp @@ -104,6 +104,7 @@ namespace nexo::renderer { void setMouseClickCallback(MouseClickCallback callback) override { _props.mouseClickCallback = std::move(callback); } void setMouseScrollCallback(MouseScrollCallback callback) override { _props.mouseScrollCallback = std::move(callback); } void setMouseMoveCallback(MouseMoveCallback callback) override { _props.mouseMoveCallback = std::move(callback); } + void setFileDropCallback(FileDropCallback callback) override { _props.fileDropCallback = std::move(callback); } // Linux specific method #ifdef __linux__ diff --git a/engine/src/systems/TransformMatrixSystem.cpp b/engine/src/systems/TransformMatrixSystem.cpp index 11dd04b5c..2b0e62969 100644 --- a/engine/src/systems/TransformMatrixSystem.cpp +++ b/engine/src/systems/TransformMatrixSystem.cpp @@ -29,7 +29,7 @@ namespace nexo::system { for (const ecs::Entity entity : entities) { auto &sceneTag = getComponent(entity); - if (!sceneTag.isActive || sceneTag.id != sceneRendered) + if (sceneTag.id != sceneRendered) continue; auto &transform = getComponent(entity); transform.localMatrix = createTransformMatrix(transform); diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index e21b9a298..a77559e34 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -23,6 +23,7 @@ include_directories("./common") # TODO: make common a library and link it to the tests set(COMMON_SOURCES common/Exception.cpp + common/Path.cpp common/math/Matrix.cpp common/math/Vector.cpp common/math/Light.cpp diff --git a/tests/renderer/CMakeLists.txt b/tests/renderer/CMakeLists.txt index f35213488..de2bb675f 100644 --- a/tests/renderer/CMakeLists.txt +++ b/tests/renderer/CMakeLists.txt @@ -25,6 +25,7 @@ include_directories("./engine/src/renderer") set(COMMON_SOURCES common/Exception.cpp common/math/Matrix.cpp + common/Path.cpp ) set(RENDERER_SOURCES