// // AssetBrowserPanel.cpp:项目资产浏览器实现。 // #include "AssetBrowserPanel.h" #include #include #include #include #include #include #include #include #include namespace { // 固定根:下标与 Editor 无耦合,仅面板内部使用 constexpr const char* kRootNames[] = {"Assets", "Scenes"}; constexpr const char* kRootPaths[] = {"assets", "scenes"}; // 类型过滤(下标即身份):0 = All constexpr const char* kTypeNames[] = {"All", "Models", "Textures", "Scenes"}; // 资产类别:按扩展名分派(过滤用) enum class Kind { Model, Texture, Scene, Other }; Kind KindOf(const std::filesystem::path& p) { std::string ext = p.extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); }); if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj") { return Kind::Model; } if (ext == ".hdr" || ext == ".png" || ext == ".jpg" || ext == ".jpeg") { return Kind::Texture; } if (ext == ".scene") { return Kind::Scene; } return Kind::Other; } bool PassFilter(int typeFilter, Kind kind) { switch (typeFilter) { case 1: return kind == Kind::Model; case 2: return kind == Kind::Texture; case 3: return kind == Kind::Scene; default: return true; } } // 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签) const char* KindTag(Kind kind) { switch (kind) { case Kind::Model: return "[M]"; case Kind::Texture: return "[T]"; case Kind::Scene: return "[S]"; default: return "[ ]"; } } // 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格) std::string ToAssetPath(const std::filesystem::path& p) { std::string s = p.generic_string(); std::replace(s.begin(), s.end(), '\\', '/'); return s; } } // namespace void AssetBrowserPanel::Draw(const EditorContext& ctx) { if (!ImGui::Begin("Asset Browser")) { ImGui::End(); return; } // ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ---- if (ImGui::BeginTabBar("##roots")) { for (int i = 0; i < 2; ++i) { if (ImGui::BeginTabItem(kRootNames[i])) { m_Root = i; ImGui::EndTabItem(); } } ImGui::EndTabBar(); } ImGui::SameLine(); ImGui::SetNextItemWidth(110.0f); ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 4); ImGui::SameLine(); ImGui::SetNextItemWidth(140.0f); ImGui::InputTextWithHint("##name", "filter", m_NameFilter, sizeof(m_NameFilter)); ImGui::Separator(); const std::filesystem::path root(kRootPaths[m_Root]); // 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with // 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验 // 防 "assets_x" 之类误匹配 const std::string rootStr = root.generic_string(); const std::string curStr = m_CurrentDir.generic_string(); const bool under = curStr == rootStr || (curStr.size() > rootStr.size() && curStr.compare(0, rootStr.size(), rootStr) == 0 && curStr[rootStr.size()] == '/'); if (m_CurrentDir.empty() || !under) { m_CurrentDir = root; } // ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)---- if (ImGui::Button("Root")) { m_CurrentDir = root; } ImGui::SameLine(); std::string rel = std::filesystem::relative(m_CurrentDir, root) .generic_string(); ImGui::TextDisabled("%s/%s", kRootNames[m_Root], rel.empty() ? "" : rel.c_str()); ImGui::Separator(); // ---- 左:目录树 | 右:当前目录文件列表 ---- const float treeWidth = 180.0f; ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f), ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders); if (std::filesystem::exists(root)) { // 根节点也画进树(可点击选中) ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNodeEx(kRootNames[m_Root], ImGuiTreeNodeFlags_OpenOnArrow)) { if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { m_CurrentDir = root; } DrawDirTree(ctx, root); ImGui::TreePop(); } } else { ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]); } ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders); if (std::filesystem::exists(m_CurrentDir)) { DrawFileList(ctx, m_CurrentDir); } ImGui::EndChild(); ImGui::End(); } void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx, const std::filesystem::path& dir) { for (const auto& entry : std::filesystem::directory_iterator(dir)) { if (!entry.is_directory()) { continue; } const std::string name = entry.path().filename().string(); // OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠 const bool open = ImGui::TreeNodeEx( name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { m_CurrentDir = entry.path(); } if (open) { DrawDirTree(ctx, entry.path()); ImGui::TreePop(); } } } void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, const std::filesystem::path& dir) { // 收集 + 过滤 + 排序(名字序;目录迭代序不稳定) struct Item { std::filesystem::path path; Kind kind; }; std::vector items; const std::string filter = m_NameFilter; for (const auto& entry : std::filesystem::directory_iterator(dir)) { if (entry.is_directory()) { // 子目录也进列表:单行点击进入(树/列表双向导航) const std::string dname = entry.path().filename().string(); ImGui::PushID(dname.c_str()); ImGui::TextUnformatted("[D]"); ImGui::SameLine(); if (ImGui::Selectable(dname.c_str())) { m_CurrentDir = entry.path(); } ImGui::PopID(); continue; } const Kind kind = KindOf(entry.path()); if (!PassFilter(m_TypeFilter, kind)) { continue; } if (!filter.empty()) { const std::string name = entry.path().filename().string(); if (name.find(filter) == std::string::npos) { continue; } } items.push_back({entry.path(), kind}); } std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) { return a.path.filename().string() < b.path.filename().string(); }); for (const auto& item : items) { const std::string name = item.path.filename().string(); const std::string assetPath = ToAssetPath(item.path); ImGui::PushID(name.c_str()); ImGui::TextUnformatted(KindTag(item.kind)); ImGui::SameLine(); ImGui::Selectable(name.c_str()); // 双击预热:model/HDR 进缓存(路径去重,重复双击零成本); // 场景加载含环境恢复/选中清理流程,提示走 File 菜单 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { if (item.kind == Kind::Model && ctx.assets) { const auto ref = ctx.assets->LoadModel(assetPath); ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})", assetPath, ref.Valid()); } else if (item.kind == Kind::Texture && ctx.assets) { const auto ref = ctx.assets->LoadEnvironment(assetPath); ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})", assetPath, ref.Valid()); } else if (item.kind == Kind::Scene) { ENGINE_INFO("AssetBrowser: load scenes via File menu ({})", assetPath); } } // hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态 if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextUnformatted(name.c_str()); ImGui::Separator(); static const char* kKindNames[] = {"Model", "Texture/Environment", "Scene", "File"}; ImGui::Text("type: %s", kKindNames[static_cast(item.kind)]); namespace fs = std::filesystem; std::error_code ec; const auto sz = fs::file_size(item.path, ec); if (!ec) { char szBuf[32]; if (sz >= 1024ull * 1024ull) { std::snprintf(szBuf, sizeof(szBuf), "%.1f MB", static_cast(sz) / (1024.0 * 1024.0)); } else { std::snprintf(szBuf, sizeof(szBuf), "%.1f KB", static_cast(sz) / 1024.0); } ImGui::Text("size: %s", szBuf); } const auto ftime = fs::last_write_time(item.path, ec); if (!ec) { // file_clock → system_clock 近似转换(C++17 无官方转换) const auto sctp = std::chrono::time_point_cast< std::chrono::system_clock::duration>( ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); const std::time_t tt = std::chrono::system_clock::to_time_t(sctp); char tmBuf[32]; std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M", std::localtime(&tt)); ImGui::Text("modified: %s", tmBuf); } // 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded) if (ctx.assets) { const char* state = "not loaded"; if (item.kind == Kind::Model && ctx.assets->IsModelLoaded(assetPath)) { state = "loaded"; } else if (item.kind == Kind::Texture && ctx.assets->IsEnvironmentLoaded(assetPath)) { state = "loaded"; } ImGui::Text("state: %s", state); // 已加载的 model 展开资产数据概览(meshes=0 即纯动画 // 容器;未加载不显示——查询不触发加载) if (item.kind == Kind::Model) { if (const Engine::Model* m = ctx.assets->GetModelByPath(assetPath); m != nullptr) { ImGui::Text("meshes: %u, bones: %u, clips: %u", static_cast(m->GetMeshes().size()), m->GetBoneCount(), static_cast(m->GetClips().size())); } } } ImGui::TextDisabled("%s", assetPath.c_str()); ImGui::EndTooltip(); } ImGui::PopID(); } }