1041 lines
32 KiB
C++
1041 lines
32 KiB
C++
#include "ProjectManager.h"
|
||
#include "../../core/utils/EngineConfig.h"
|
||
#include "../../core/utils/Logging.h"
|
||
#include "../../Engine.h"
|
||
#include "../../core/utils/FileDialog.h"
|
||
#include "../../core/utils/utils.h"
|
||
#include "../../core/utils/AssetManager.h"
|
||
|
||
#include "SceneSerializer.h"
|
||
|
||
#include <yaml-cpp/yaml.h>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <chrono>
|
||
#include <imgui.h>
|
||
#include <string>
|
||
#include <cstring>
|
||
using namespace std::chrono;
|
||
|
||
|
||
#define ASSET_MANEFEST_NAME "assets.capk"
|
||
|
||
|
||
namespace {
|
||
struct CachedEntry {
|
||
fs::path path;
|
||
bool isDirectory;
|
||
AssetType type;
|
||
ImTextureID thumbID;
|
||
std::string label;
|
||
};
|
||
static std::vector<CachedEntry> cacheEntries;
|
||
|
||
struct TreeNode {
|
||
fs::path path;
|
||
std::vector<TreeNode> children;
|
||
};
|
||
static bool rebuild = true;
|
||
static TreeNode cachedTreeRoot;
|
||
static fs::path lastTreeRoot;
|
||
static bool treeDirty = true;
|
||
|
||
// Recursively build the TreeNode children
|
||
void BuildTreeNode(const fs::path& p, TreeNode& node) {
|
||
for (auto& e : fs::directory_iterator(p)) {
|
||
if (e.is_directory()) {
|
||
node.children.push_back({ e.path(), {} });
|
||
BuildTreeNode(e.path(), node.children.back());
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ensure cachedTreeRoot matches s_root
|
||
void EnsureTreeCache(const fs::path& root) {
|
||
if (treeDirty || root != lastTreeRoot) {
|
||
cachedTreeRoot = { root, {} };
|
||
BuildTreeNode(root, cachedTreeRoot);
|
||
lastTreeRoot = root;
|
||
treeDirty = false;
|
||
}
|
||
}
|
||
// Draw from the cached tree
|
||
void DrawCachedTree(const TreeNode& node) {
|
||
// use folder name (or root)
|
||
auto name = node.path == lastTreeRoot
|
||
? "res://"
|
||
: node.path.filename().string().c_str();
|
||
bool open = ImGui::TreeNodeEx(
|
||
node.path.string().c_str(),
|
||
ImGuiTreeNodeFlags_OpenOnArrow,
|
||
"%s",
|
||
name
|
||
);
|
||
if (open) {
|
||
for (auto& child : node.children)
|
||
DrawCachedTree(child);
|
||
ImGui::TreePop();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
bool FileExplorer::s_initialized = false;
|
||
std::string FileExplorer::SelectedPath;
|
||
fs::path FileExplorer::s_root;
|
||
fs::path FileExplorer::s_currentDir;
|
||
std::unordered_set<std::string> FileExplorer::s_knownFiles;
|
||
const char* FileExplorer::ICONS_PATH = "./src/assets/icons/";
|
||
unsigned int FileExplorer::s_folderIcon = 0;
|
||
|
||
std::string FileExplorer::fileSearchQuery = "";
|
||
bool FileExplorer::sortAscending = true;
|
||
int FileExplorer::sortMode = 0;
|
||
bool FileExplorer::showScenes = true;
|
||
bool FileExplorer::showImages = true;
|
||
bool FileExplorer::showAudio = true;
|
||
bool FileExplorer::showScripts = true;
|
||
bool FileExplorer::showOther = true;
|
||
|
||
std::vector<fs::path> FileExplorer::s_loadQueue;
|
||
size_t FileExplorer::s_loadIndex = 0;
|
||
bool FileExplorer::s_loadingInit = false;
|
||
|
||
|
||
std::mutex FileExplorer::s_fileMutex;
|
||
std::atomic<bool> FileExplorer::s_scanInProgress{false};
|
||
steady_clock::time_point FileExplorer::s_lastScan = steady_clock::now();
|
||
const milliseconds FileExplorer::s_scanInterval = milliseconds(1000);
|
||
|
||
|
||
bool FileExplorer::Init()
|
||
{
|
||
if (s_initialized)
|
||
return false;
|
||
|
||
if (!ProjectManager::HasProject())
|
||
return false;
|
||
|
||
// Resolve and verify root…
|
||
s_root = fs::path(ProjectManager::ResolveResPath("res://"));
|
||
s_currentDir = s_root;
|
||
if (!fs::exists(s_root) || !fs::is_directory(s_root)) {
|
||
Logger::LogError("FileExplorer: invalid root path '%s'", s_root.string().c_str());
|
||
return false;
|
||
}
|
||
|
||
s_folderIcon = EngineLoadTextureIfNeeded(std::string(ICONS_PATH) + "folder-outline.png");
|
||
|
||
std::error_code ec;
|
||
fs::recursive_directory_iterator it(s_root, fs::directory_options::skip_permission_denied, ec);
|
||
fs::recursive_directory_iterator end;
|
||
for (; it != end; it.increment(ec)) {
|
||
if (ec) {
|
||
Logger::LogWarning("FileExplorer: skipping '%s': %s",
|
||
it->path().string().c_str(),
|
||
ec.message().c_str());
|
||
ec.clear();
|
||
continue;
|
||
}
|
||
if (it->is_regular_file())
|
||
s_loadQueue.push_back(it->path());
|
||
}
|
||
|
||
s_initialized = true;
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
void FileExplorer::Show(bool* p_open)
|
||
{
|
||
if (!s_initialized) return;
|
||
|
||
// rebuild tree if the root changed
|
||
if (s_root != lastTreeRoot) treeDirty = true;
|
||
EnsureTreeCache(s_root);
|
||
|
||
if (!ImGui::Begin("File Explorer", p_open,
|
||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_HorizontalScrollbar))
|
||
{
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// LEFT PANE: interactive folder‐tree with icons
|
||
// -------------------------------------------------------------------------
|
||
ImGui::BeginChild("##FolderPane", ImVec2(250,0), true);
|
||
{
|
||
// recursive lambda to draw each node
|
||
std::function<void(const TreeNode&)> drawNode = [&](const TreeNode& node) {
|
||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
|
||
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||
if (node.children.empty())
|
||
flags |= ImGuiTreeNodeFlags_Leaf;
|
||
if (node.path == s_currentDir)
|
||
flags |= ImGuiTreeNodeFlags_Selected;
|
||
|
||
ImGui::Image((ImTextureID)s_folderIcon, ImVec2(16,16));
|
||
ImGui::SameLine();
|
||
|
||
const char* label = (node.path == lastTreeRoot)
|
||
? "res://"
|
||
: node.path.filename().string().c_str();
|
||
bool open = ImGui::TreeNodeEx(
|
||
node.path.string().c_str(),
|
||
flags,
|
||
"%s",
|
||
label
|
||
);
|
||
|
||
// 3) click to change directory
|
||
if (ImGui::IsItemClicked()) {
|
||
s_currentDir = node.path;
|
||
treeDirty = true; // also force tree repaint?
|
||
rebuild = true; // file‐grid rebuild
|
||
}
|
||
|
||
// 4) children
|
||
if (open) {
|
||
for (auto& child : node.children)
|
||
drawNode(child);
|
||
ImGui::TreePop();
|
||
}
|
||
};
|
||
|
||
drawNode(cachedTreeRoot);
|
||
}
|
||
ImGui::EndChild();
|
||
|
||
ImGui::SameLine();
|
||
|
||
// -------------------------------------------------------------------------
|
||
// RIGHT PANE: toolbar + file‐grid
|
||
// -------------------------------------------------------------------------
|
||
ImGui::BeginChild("##ContentPane", ImVec2(0,0), false);
|
||
|
||
// --- Filters / Sort / Search state (static to persist frame-to-frame) ---
|
||
static char searchBuf[256] = "";
|
||
static fs::path lastDir;
|
||
static int lastSortMode = -1;
|
||
static bool lastSortAsc = false;
|
||
static bool lastShowScenes = true;
|
||
static bool lastShowImages = true;
|
||
static bool lastShowAudio = true;
|
||
static bool lastShowScripts = true;
|
||
static bool lastShowOther = true;
|
||
static std::string lastSearch;
|
||
static bool rebuild = true;
|
||
|
||
// Filters / Sort popup
|
||
if (ImGui::Button("Filters / Sort")) ImGui::OpenPopup("FileFilterPopup");
|
||
ImGui::SameLine();
|
||
ImGui::SetNextItemWidth(200);
|
||
if (ImGui::InputTextWithHint("##Search","Search...",searchBuf,sizeof(searchBuf))) {
|
||
fileSearchQuery = searchBuf;
|
||
rebuild = true;
|
||
}
|
||
if (ImGui::BeginPopup("FileFilterPopup")) {
|
||
if (ImGui::Selectable("Name", sortMode==0)) { sortMode=0; rebuild=true; }
|
||
if (ImGui::Selectable("Type", sortMode==1)) { sortMode=1; rebuild=true; }
|
||
if (ImGui::Checkbox("Ascending",&sortAscending)) rebuild=true;
|
||
ImGui::Separator();
|
||
if (ImGui::Checkbox("Scenes", &showScenes)) rebuild=true;
|
||
if (ImGui::Checkbox("Images", &showImages)) rebuild=true;
|
||
if (ImGui::Checkbox("Audio", &showAudio)) rebuild=true;
|
||
if (ImGui::Checkbox("Scripts", &showScripts)) rebuild=true;
|
||
if (ImGui::Checkbox("Other", &showOther)) rebuild=true;
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
// “Up” button + current path display
|
||
if (s_currentDir != s_root) {
|
||
if (ImGui::Button("Up")) {
|
||
s_currentDir = s_currentDir.parent_path();
|
||
rebuild = true;
|
||
}
|
||
ImGui::SameLine();
|
||
}
|
||
{
|
||
fs::path rel = fs::relative(s_currentDir, s_root);
|
||
std::string displayPath = "res://"
|
||
+ (rel.empty() ? "" : rel.generic_string()+"/");
|
||
ImGui::TextUnformatted(displayPath.c_str());
|
||
}
|
||
ImGui::Separator();
|
||
|
||
// Detect any state changes → rebuild the file‐grid cache
|
||
if ( lastDir != s_currentDir
|
||
|| lastSortMode != sortMode
|
||
|| lastSortAsc != sortAscending
|
||
|| lastSearch != fileSearchQuery
|
||
|| lastShowScenes != showScenes
|
||
|| lastShowImages != showImages
|
||
|| lastShowAudio != showAudio
|
||
|| lastShowScripts != showScripts
|
||
|| lastShowOther != showOther)
|
||
{
|
||
rebuild = true;
|
||
lastDir = s_currentDir;
|
||
lastSortMode = sortMode;
|
||
lastSortAsc = sortAscending;
|
||
lastSearch = fileSearchQuery;
|
||
lastShowScenes = showScenes;
|
||
lastShowImages = showImages;
|
||
lastShowAudio = showAudio;
|
||
lastShowScripts = showScripts;
|
||
lastShowOther = showOther;
|
||
}
|
||
|
||
if (rebuild) {
|
||
cacheEntries.clear();
|
||
cacheEntries.reserve(128);
|
||
|
||
for (auto& e : fs::directory_iterator(s_currentDir)) {
|
||
bool isDir = e.is_directory();
|
||
AssetType t = AssetManager::AssetTypeFromPath(e.path().string());
|
||
|
||
// type filters
|
||
if (!isDir) {
|
||
if ((t==AssetType::Scene && !showScenes )||
|
||
(t==AssetType::Image && !showImages )||
|
||
(t==AssetType::Audio && !showAudio )||
|
||
(t==AssetType::Script && !showScripts)||
|
||
(t==AssetType::Unknown && !showOther ))
|
||
continue;
|
||
}
|
||
|
||
// search filter
|
||
if (!fileSearchQuery.empty()
|
||
&& e.path().filename().string().find(fileSearchQuery)==std::string::npos)
|
||
continue;
|
||
|
||
CachedEntry ce;
|
||
ce.path = e.path();
|
||
ce.isDirectory = isDir;
|
||
ce.type = t;
|
||
|
||
// thumbnail
|
||
if (isDir) {
|
||
ce.thumbID = (ImTextureID)(intptr_t)s_folderIcon;
|
||
}
|
||
else if (t==AssetType::Image) {
|
||
if (auto* asset = AssetManager::GetAssetByPath(ce.path.string()))
|
||
if (auto* img = dynamic_cast<const ImageAssetInfo*>(asset))
|
||
ce.thumbID = (ImTextureID)(intptr_t)img->textureID;
|
||
}
|
||
if (!ce.thumbID) {
|
||
ce.thumbID = (ImTextureID)(intptr_t)
|
||
EngineLoadTextureIfNeeded(ICONS_PATH + IconFileForPath(ce.path));
|
||
}
|
||
|
||
// label for non‐image files
|
||
if (!isDir && t!=AssetType::Image) {
|
||
static const std::unordered_map<AssetType,const char*> L = {
|
||
{AssetType::Prefab,"Prefab"},
|
||
{AssetType::Scene, "Scene"},
|
||
{AssetType::Audio, "Audio"},
|
||
{AssetType::Script,"Script"},
|
||
{AssetType::Video, "Video"},
|
||
{AssetType::Font, "Font"},
|
||
{AssetType::Shader,"Shader"}
|
||
};
|
||
auto it = L.find(t);
|
||
if (it!=L.end()) ce.label = it->second;
|
||
else {
|
||
auto ext = ce.path.extension().string();
|
||
ce.label = ext.size()>1 ? ext.substr(1) : ext;
|
||
}
|
||
}
|
||
|
||
cacheEntries.push_back(std::move(ce));
|
||
}
|
||
|
||
// sort
|
||
std::sort(cacheEntries.begin(), cacheEntries.end(),
|
||
[&](auto& a, auto& b){
|
||
if (sortMode==1) {
|
||
if (a.isDirectory!=b.isDirectory)
|
||
return sortAscending
|
||
? a.isDirectory>b.isDirectory
|
||
: a.isDirectory<b.isDirectory;
|
||
return sortAscending
|
||
? a.path.extension()<b.path.extension()
|
||
: a.path.extension()>b.path.extension();
|
||
}
|
||
return sortAscending
|
||
? a.path.filename()<b.path.filename()
|
||
: a.path.filename()>b.path.filename();
|
||
});
|
||
|
||
rebuild = false;
|
||
}
|
||
|
||
// Icon‐grid draw pass
|
||
constexpr float iconSize=64.0f, padding=8.0f;
|
||
float avail = ImGui::GetContentRegionAvail().x;
|
||
int cols = std::max(1,int(avail/(iconSize+padding)));
|
||
ImGui::Columns(cols,nullptr,false);
|
||
|
||
for (auto& ce : cacheEntries) {
|
||
PROFILE_ENGINE_SCOPE("entry");
|
||
const auto& p = ce.path;
|
||
const auto pathStr = p.string();
|
||
|
||
ImGui::PushID(pathStr.c_str());
|
||
|
||
bool clicked = false;
|
||
if (ce.isDirectory||ce.type==AssetType::Image) {
|
||
PROFILE_ENGINE_SCOPE("Image");
|
||
clicked = ImGui::ImageButton("##entry",
|
||
ce.thumbID,
|
||
ImVec2(iconSize,iconSize));
|
||
} else {
|
||
clicked = ImGui::Button(
|
||
(ce.label+"##entry").c_str(),
|
||
ImVec2(iconSize,iconSize)
|
||
);
|
||
}
|
||
|
||
if (clicked) {
|
||
if (ce.isDirectory) s_currentDir=p;
|
||
else SelectedPath=pathStr;
|
||
rebuild=true;
|
||
}
|
||
|
||
if (ImGui::BeginPopupContextItem("##entry")) {
|
||
if (ce.isDirectory && ImGui::MenuItem("Open"))
|
||
s_currentDir=p, rebuild=true;
|
||
if (!ce.isDirectory && ImGui::MenuItem("Open File"))
|
||
SelectedPath=pathStr;
|
||
if (ImGui::MenuItem("Copy Path"))
|
||
ImGui::SetClipboardText(pathStr.c_str());
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
if (!ce.isDirectory &&
|
||
ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
|
||
{
|
||
if (auto* asset=AssetManager::GetAssetByPath(pathStr)) {
|
||
const void* data=&asset->uaid;
|
||
const char* typeStr=
|
||
ce.type==AssetType::Image ? ASSET_TEXTURE :
|
||
ce.type==AssetType::Prefab ? ASSET_PREFAB : nullptr;
|
||
if(typeStr)
|
||
ImGui::SetDragDropPayload(typeStr,
|
||
data,
|
||
sizeof(asset->uaid));
|
||
}
|
||
ImGui::TextUnformatted(p.filename().string().c_str());
|
||
ImGui::EndDragDropSource();
|
||
}
|
||
|
||
ImGui::TextWrapped("%s",p.filename().string().c_str());
|
||
ImGui::NextColumn();
|
||
ImGui::PopID();
|
||
}
|
||
|
||
ImGui::Columns(1);
|
||
ImGui::EndChild();
|
||
ImGui::End();
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
void FileExplorer::ScanDirectory()
|
||
{
|
||
std::error_code ec;
|
||
std::unordered_set<std::string> currentFiles;
|
||
|
||
// walk the tree once
|
||
for (auto it = fs::recursive_directory_iterator(s_root,
|
||
fs::directory_options::skip_permission_denied, ec),
|
||
end = fs::recursive_directory_iterator{};
|
||
it != end; it.increment(ec))
|
||
{
|
||
if (ec)
|
||
{
|
||
|
||
ec.clear();
|
||
continue;
|
||
}
|
||
if (!it->is_regular_file()) continue;
|
||
|
||
auto pathStr = it->path().string();
|
||
currentFiles.insert(pathStr);
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lock(s_fileMutex);
|
||
if (!s_knownFiles.count(pathStr))
|
||
{
|
||
s_knownFiles.insert(pathStr);
|
||
s_loadQueue.push_back(it->path());
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lock(s_fileMutex);
|
||
for (auto it = s_knownFiles.begin(); it != s_knownFiles.end(); )
|
||
{
|
||
if (!currentFiles.count(*it))
|
||
{
|
||
|
||
AssetManager::GetAssetByPath(*it);
|
||
it = s_knownFiles.erase(it);
|
||
}
|
||
else ++it;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
void FileExplorer::Update()
|
||
{
|
||
if (!s_initialized)
|
||
{
|
||
Init();
|
||
return;
|
||
}
|
||
|
||
auto now = steady_clock::now();
|
||
|
||
if (!s_scanInProgress.load() && now - s_lastScan >= s_scanInterval)
|
||
{
|
||
s_lastScan = now;
|
||
s_scanInProgress = true;
|
||
// fire‐and‐forget background scan
|
||
std::thread([](){
|
||
ScanDirectory();
|
||
s_scanInProgress = false;
|
||
}).detach();
|
||
}
|
||
}
|
||
|
||
bool FileExplorer::LoadingDone()
|
||
{
|
||
return s_initialized && s_loadIndex >= s_loadQueue.size();
|
||
}
|
||
|
||
void FileExplorer::DrawFolderTree(const fs::path &path)
|
||
{
|
||
PROFILE_ENGINE_SCOPE("DrawTree");
|
||
|
||
for (auto &e : fs::directory_iterator(path)) {
|
||
if (!e.is_directory()) continue;
|
||
auto &d = e.path();
|
||
ImGui::PushID(d.string().c_str());
|
||
ImGui::Image((ImTextureID)(intptr_t)s_folderIcon, ImVec2(16,16)); ImGui::SameLine();
|
||
bool open = ImGui::TreeNodeEx(d.string().c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth, "%s", d.filename().string().c_str());
|
||
if (ImGui::IsItemClicked()) s_currentDir = d;
|
||
if (open) { DrawFolderTree(d); ImGui::TreePop(); }
|
||
ImGui::PopID();
|
||
}
|
||
}
|
||
|
||
void FileExplorer::ProcessLoadQueue()
|
||
{
|
||
while (!s_loadQueue.empty()) {
|
||
fs::path p = s_loadQueue.back();
|
||
s_loadQueue.pop_back();
|
||
|
||
// Load the asset and get the type,... Long way...
|
||
AssetManager::LoadAssetAsync(p.string(), AssetManager::AssetTypeFromExtension(AssetManager::GetFileExtension(p.string())));
|
||
|
||
Logger::LogVerbose("FileExplorer: imported '%s'", p.string().c_str());
|
||
}
|
||
}
|
||
|
||
|
||
std::string FileExplorer::IconFileForPath(const fs::path &path)
|
||
{
|
||
auto ext = path.extension().string();
|
||
if (ext == ".cene") return "movie-open-outline.png";
|
||
return "error.png";
|
||
}
|
||
|
||
|
||
|
||
namespace fs = std::filesystem;
|
||
|
||
std::string ProjectManager::s_currentProjectPath;
|
||
std::string ProjectManager::s_currentProjectName;
|
||
std::string ProjectManager::s_defaultScene;
|
||
|
||
|
||
|
||
|
||
void ProjectManager::ShowCreateProjectPopup()
|
||
{
|
||
static char nameBuf[256] = "";
|
||
static char pathBuf[512] = "";
|
||
|
||
ImGui::SetNextWindowSizeConstraints({ 0, 0 }, { 600, FLT_MAX });
|
||
if (!ImGui::BeginPopupModal(
|
||
"Create New Project",
|
||
nullptr,
|
||
ImGuiWindowFlags_AlwaysAutoResize))
|
||
return;
|
||
|
||
ImGui::Text("Create New Project");
|
||
ImGui::Separator();
|
||
ImGui::Spacing();
|
||
|
||
// Project Name (full width)
|
||
ImGui::TextUnformatted("Project Name");
|
||
ImGui::PushItemWidth(-1);
|
||
ImGui::InputText("##proj_name", nameBuf, IM_ARRAYSIZE(nameBuf));
|
||
ImGui::PopItemWidth();
|
||
ImGui::Spacing();
|
||
|
||
ImGui::TextUnformatted("Project Folder");
|
||
ImGui::PushItemWidth(-85);
|
||
ImGui::InputText("##proj_path", pathBuf, IM_ARRAYSIZE(pathBuf));
|
||
ImGui::PopItemWidth();
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("Browse...", { 80, 0 }))
|
||
{
|
||
if (auto chosen = OpenFolderDialog(); !chosen.empty())
|
||
std::strncpy(pathBuf, chosen.c_str(), sizeof(pathBuf));
|
||
}
|
||
ImGui::Spacing();
|
||
|
||
// Preview path – wrapped so it never overflows
|
||
auto preview = std::filesystem::path(pathBuf) / (std::string(nameBuf) + ".cproj");
|
||
ImGui::Text("Will be created at:");
|
||
ImGui::TextWrapped("%s", preview.string().c_str());
|
||
ImGui::Spacing();
|
||
|
||
// Action buttons centered
|
||
ImGui::Separator();
|
||
float btnW = 80.0f;
|
||
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||
float offsetX = (ImGui::GetWindowWidth() - totalW) * 0.5f;
|
||
ImGui::SetCursorPosX(offsetX);
|
||
|
||
if (ImGui::Button("Cancel", { btnW, 0 }))
|
||
ImGui::CloseCurrentPopup();
|
||
|
||
ImGui::SameLine();
|
||
ImGui::SetItemDefaultFocus();
|
||
if (ImGui::Button("Create", { btnW, 0 }))
|
||
{
|
||
CreateProject(pathBuf, nameBuf);
|
||
nameBuf[0] = pathBuf[0] = '\0';
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
bool CreateDirectories(const fs::path &baseDir)
|
||
{
|
||
static const std::vector<std::string> subdirs = {
|
||
"Prefabs",
|
||
"Scenes",
|
||
"Images",
|
||
"Sounds",
|
||
"Fonts",
|
||
"Shaders",
|
||
"Scripts",
|
||
"Videos",
|
||
|
||
};
|
||
|
||
std::error_code ec;
|
||
bool allOk = true;
|
||
fs::path assetsRoot = baseDir / "Assets";
|
||
|
||
for (const auto &name : subdirs)
|
||
{
|
||
Logger::LogVerbose("Creating Folder: '%s'", name.c_str());
|
||
fs::create_directories(assetsRoot / name, ec);
|
||
if (ec)
|
||
{
|
||
Logger::LogError("Could not ensure %s dir: %s",
|
||
name.c_str(),
|
||
ec.message().c_str());
|
||
allOk = false;
|
||
}
|
||
}
|
||
|
||
return allOk;
|
||
}
|
||
|
||
bool ProjectManager::HasProject() {
|
||
if (s_currentProjectPath.empty() || s_currentProjectName.empty())
|
||
{
|
||
return false;
|
||
|
||
}
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
std::string ProjectManager::ResolveResPath(const std::string &resPath)
|
||
{
|
||
constexpr const char *prefix = "res://";
|
||
// if it’s not a “res://” path, just return it
|
||
if (resPath.rfind(prefix, 0) != 0)
|
||
return resPath;
|
||
|
||
if (s_currentProjectPath.empty())
|
||
{
|
||
Logger::LogError("Cannot resolve res path: project not loaded!");
|
||
return resPath;
|
||
}
|
||
|
||
// start from whatever s_currentProjectPath holds
|
||
fs::path baseDir{s_currentProjectPath};
|
||
|
||
// only append the project name if it isn't already the last path component
|
||
if (!s_currentProjectName.empty() &&
|
||
baseDir.filename().string() != s_currentProjectName)
|
||
{
|
||
baseDir /= s_currentProjectName;
|
||
}
|
||
|
||
// drop the "res://" prefix
|
||
std::string relativePart = resPath.substr(strlen(prefix));
|
||
// build & normalize
|
||
fs::path fullPath = (baseDir / relativePart).lexically_normal();
|
||
|
||
return fullPath.string();
|
||
}
|
||
|
||
|
||
bool ProjectManager::Init()
|
||
{
|
||
s_currentProjectPath.clear();
|
||
s_currentProjectName.clear();
|
||
s_defaultScene.clear();
|
||
Logger::LogOk("Project Core");
|
||
return true;
|
||
}
|
||
|
||
bool ProjectManager::LoadProject(const std::string &projectFilePath)
|
||
{
|
||
fs::path projFile{ projectFilePath };
|
||
if (projFile.extension() != ".cproj" || !fs::exists(projFile)) {
|
||
Logger::LogError("Project file not found or invalid: '%s'",
|
||
projectFilePath.c_str());
|
||
return false;
|
||
}
|
||
|
||
fs::path baseDir = projFile.parent_path();
|
||
std::string projectName = projFile.stem().string();
|
||
|
||
YAML::Node config;
|
||
try {
|
||
config = YAML::LoadFile(projFile.string());
|
||
}
|
||
catch (const YAML::Exception &e) {
|
||
Logger::LogError("Failed to parse project file '%s': %s",
|
||
projFile.string().c_str(), e.what());
|
||
return false;
|
||
}
|
||
|
||
std::string savedName = config["Name"].as<std::string>(projectName);
|
||
std::string defaultScene = config["s_defaultScene"].as<std::string>("");
|
||
|
||
Logger::LogDebug("Loading '%s'",
|
||
savedName.c_str());
|
||
|
||
// update project pointers
|
||
s_currentProjectPath = baseDir.string();
|
||
s_currentProjectName = savedName;
|
||
|
||
// ensure folders
|
||
CreateDirectories(baseDir);
|
||
|
||
// --- Load the asset manifest from Assets/asset_manifest.yaml ---
|
||
{
|
||
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
|
||
if (fs::exists(manifest)) {
|
||
YAML::Node doc = YAML::LoadFile(manifest.string());
|
||
if (doc["Assets"]) {
|
||
AssetManager::Load(doc["Assets"]);
|
||
Logger::LogDebug("Loaded asset manifest: %s",
|
||
manifest.string().c_str());
|
||
}
|
||
} else {
|
||
Logger::LogDebug("No asset manifest found at '%s', skipping.",
|
||
manifest.string().c_str());
|
||
}
|
||
}
|
||
|
||
// finally load the default scene (if any)
|
||
if (!defaultScene.empty()) {
|
||
s_defaultScene = ResolveResPath(defaultScene);
|
||
Logger::LogDebug("Loading Default Scene: %s", s_defaultScene.c_str());
|
||
if (!fs::exists(s_defaultScene)) {
|
||
Logger::LogError("Default scene does not exist: %s", s_defaultScene.c_str());
|
||
return false;
|
||
}
|
||
SceneLoader::LoadScene(s_defaultScene);
|
||
}
|
||
|
||
Logger::LogOk("loaded project '%s'", savedName.c_str());
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
bool ProjectManager::CreateProject(
|
||
const std::string &projectPath,
|
||
const std::string &projectName)
|
||
{
|
||
fs::path baseDir = fs::path(projectPath) / projectName;
|
||
fs::path projFile = baseDir / (projectName + ".cproj");
|
||
|
||
std::error_code ec;
|
||
// 1) create project root
|
||
if (!fs::create_directories(baseDir, ec) && ec) {
|
||
Logger::LogError("Failed to create project directory '%s': %s",
|
||
projectPath.c_str(), ec.message().c_str());
|
||
return false;
|
||
}
|
||
|
||
s_currentProjectPath = baseDir.string();
|
||
s_currentProjectName = projectName;
|
||
|
||
CreateDirectories(baseDir);
|
||
|
||
fs::path sceneDir = baseDir / "Assets" / "Scenes";
|
||
fs::path sceneFile = sceneDir / (projectName + ".cene");
|
||
s_defaultScene = sceneFile.string();
|
||
|
||
{
|
||
YAML::Emitter out;
|
||
out << YAML::BeginMap;
|
||
out << YAML::Key << "Name" << YAML::Value << projectName;
|
||
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
|
||
|
||
fs::path rel = fs::relative(sceneFile, baseDir);
|
||
out << YAML::Key << "s_defaultScene"
|
||
<< YAML::Value << ("res://" + rel.generic_string());
|
||
|
||
auto now = std::chrono::system_clock::now();
|
||
auto t = std::chrono::system_clock::to_time_t(now);
|
||
std::tm tm{};
|
||
#if defined(_WIN32)
|
||
gmtime_s(&tm, &t);
|
||
#else
|
||
gmtime_r(&t, &tm);
|
||
#endif
|
||
char buf[32];
|
||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
|
||
out << YAML::EndMap;
|
||
|
||
std::ofstream fout(projFile);
|
||
if (!fout.is_open()) {
|
||
Logger::LogError("Could not open project file for writing: %s",
|
||
projFile.string().c_str());
|
||
return false;
|
||
}
|
||
fout << out.c_str();
|
||
}
|
||
|
||
Logger::LogOk("Created project '%s' at '%s'",
|
||
projectName.c_str(), projectPath.c_str());
|
||
|
||
SceneLoader::SaveScene(sceneFile.string());
|
||
SceneLoader::LoadScene(sceneFile.string());
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
bool ProjectManager::SaveCurrentProject()
|
||
{
|
||
if (s_currentProjectPath.empty() || s_currentProjectName.empty())
|
||
{
|
||
Logger::LogError("Cannot save project: no project loaded");
|
||
return false;
|
||
}
|
||
|
||
fs::path baseDir = fs::path(s_currentProjectPath);
|
||
fs::path projFile = baseDir / (s_currentProjectName + ".cproj");
|
||
|
||
{
|
||
YAML::Emitter out;
|
||
out << YAML::BeginMap;
|
||
out << YAML::Key << "Name" << YAML::Value << s_currentProjectName;
|
||
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
|
||
|
||
fs::path rel = fs::relative(s_defaultScene, baseDir);
|
||
out << YAML::Key << "s_defaultScene"
|
||
<< YAML::Value << ("res://" + rel.generic_string());
|
||
|
||
auto now = std::chrono::system_clock::now();
|
||
auto t = std::chrono::system_clock::to_time_t(now);
|
||
std::tm tm{};
|
||
#if defined(_WIN32)
|
||
gmtime_s(&tm, &t);
|
||
#else
|
||
gmtime_r(&t, &tm);
|
||
#endif
|
||
char buf[32];
|
||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
|
||
out << YAML::EndMap;
|
||
|
||
std::ofstream fout(projFile);
|
||
if (!fout.is_open())
|
||
{
|
||
Logger::LogError("Could not open project file for writing: %s",
|
||
projFile.string().c_str());
|
||
return false;
|
||
}
|
||
fout << out.c_str();
|
||
}
|
||
|
||
Logger::LogOk("Saved project");
|
||
|
||
CreateDirectories(baseDir);
|
||
|
||
{
|
||
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
|
||
YAML::Emitter manOut;
|
||
manOut << YAML::BeginMap;
|
||
AssetManager::Save(manOut);
|
||
manOut << YAML::EndMap;
|
||
|
||
std::ofstream mf(manifest);
|
||
if (!mf.is_open())
|
||
{
|
||
Logger::LogError("Failed to write asset manifest: %s",
|
||
manifest.string().c_str());
|
||
}
|
||
else
|
||
{
|
||
mf << manOut.c_str();
|
||
Logger::LogDebug("Wrote asset manifest: %s",
|
||
manifest.string().c_str());
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
bool ProjectManager::SaveProject(
|
||
const std::string &projectPath,
|
||
const std::string &projectName)
|
||
{
|
||
fs::path baseDir = fs::path(projectPath) / projectName;
|
||
fs::path projFile = baseDir / (projectName + ".cproj");
|
||
|
||
std::error_code ec;
|
||
if (!fs::create_directories(baseDir, ec) && ec) {
|
||
Logger::LogError("Failed to create project directory '%s': %s",
|
||
projectPath.c_str(), ec.message().c_str());
|
||
return false;
|
||
}
|
||
|
||
YAML::Emitter out;
|
||
out << YAML::BeginMap;
|
||
out << YAML::Key << "Name" << YAML::Value << projectName;
|
||
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
|
||
|
||
fs::path sceneRel = fs::relative(s_defaultScene,
|
||
fs::path(s_currentProjectPath) / s_currentProjectName);
|
||
out << YAML::Key << "s_defaultScene"
|
||
<< YAML::Value << ("res://" + sceneRel.generic_string());
|
||
|
||
// timestamp
|
||
auto now = std::chrono::system_clock::now();
|
||
auto t = std::chrono::system_clock::to_time_t(now);
|
||
std::tm tm{};
|
||
#if defined(_WIN32)
|
||
gmtime_s(&tm, &t);
|
||
#else
|
||
gmtime_r(&t, &tm);
|
||
#endif
|
||
char buf[32];
|
||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
|
||
out << YAML::EndMap;
|
||
|
||
std::ofstream fout(projFile);
|
||
if (!fout.is_open()) {
|
||
Logger::LogError("Could not open project file for writing: %s",
|
||
projFile.string().c_str());
|
||
return false;
|
||
}
|
||
fout << out.c_str();
|
||
fout.close();
|
||
Logger::LogOk("Saved project '%s'",
|
||
projectName.c_str());
|
||
|
||
CreateDirectories(baseDir);
|
||
|
||
{
|
||
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
|
||
YAML::Emitter assetOut;
|
||
assetOut << YAML::BeginMap;
|
||
AssetManager::Save(assetOut);
|
||
assetOut << YAML::EndMap;
|
||
|
||
std::ofstream mf(manifest);
|
||
if (!mf.is_open()) {
|
||
Logger::LogError("Failed to write asset manifest: %s",
|
||
manifest.string().c_str());
|
||
} else {
|
||
mf << assetOut.c_str();
|
||
mf.close();
|
||
Logger::LogDebug("Wrote asset manifest: %s",
|
||
manifest.string().c_str());
|
||
}
|
||
}
|
||
|
||
// update our in-memory project pointers
|
||
s_currentProjectPath = projectPath;
|
||
s_currentProjectName = projectName;
|
||
return true;
|
||
}
|
||
|
||
|
||
const std::string &ProjectManager::GetCurrentProjectPath()
|
||
{
|
||
return s_currentProjectPath;
|
||
}
|
||
|
||
const std::string &ProjectManager::GetCurrentProjectName()
|
||
{
|
||
return s_currentProjectName;
|
||
}
|
||
|
||
const std::string &ProjectManager::GetCurrentAssetsPath()
|
||
{
|
||
static std::string assetsPath;
|
||
fs::path baseDir{s_currentProjectPath};
|
||
|
||
if (!s_currentProjectName.empty() &&
|
||
baseDir.filename().string() != s_currentProjectName)
|
||
{
|
||
baseDir /= s_currentProjectName;
|
||
}
|
||
|
||
fs::path assetDir = (baseDir / "Assets").lexically_normal();
|
||
assetsPath = assetDir.string();
|
||
return assetsPath;
|
||
}
|
||
|