- App/editor: move 7 panels into panels/ (Editor core stays at root) - shaders/: group into pbr/ post/ debug/ env/ (runtime load paths updated) - Delete outdated duplicate Engine/src/Renderer/VulkanAssetUploader.h
413 lines
14 KiB
C++
413 lines
14 KiB
C++
//
|
||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||
//
|
||
|
||
#include "AssetBrowserPanel.h"
|
||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||
|
||
#include <Engine/Core/Log.h>
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <algorithm>
|
||
#include <cctype>
|
||
#include <chrono>
|
||
#include <cstdio>
|
||
#include <ctime>
|
||
#include <filesystem>
|
||
#include <vector>
|
||
|
||
namespace {
|
||
|
||
// 固定根:下标与 Editor 无耦合,仅面板内部使用
|
||
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||
|
||
// 类型过滤(下标即身份):0 = All
|
||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||
"Textures", "Scenes",
|
||
"Materials", "Controllers"};
|
||
|
||
// 资产类别:按扩展名分派(过滤用)
|
||
enum class Kind { Model, Texture, Scene, Material, AnimClip, 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;
|
||
}
|
||
if (ext == ".material")
|
||
{
|
||
return Kind::Material;
|
||
}
|
||
if (ext == ".animctrl")
|
||
{
|
||
return Kind::AnimClip;
|
||
}
|
||
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;
|
||
case 4: return kind == Kind::Material;
|
||
case 5: return kind == Kind::AnimClip;
|
||
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]";
|
||
case Kind::Material: return "[Ma]";
|
||
case Kind::AnimClip: return "[An]";
|
||
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("Content 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, 6);
|
||
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<Item> 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());
|
||
|
||
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
|
||
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
|
||
{
|
||
ImGui::SetDragDropPayload(kMaterialDragPayload,
|
||
assetPath.c_str(), assetPath.size() + 1);
|
||
ImGui::TextUnformatted(name.c_str());
|
||
ImGui::EndDragDropSource();
|
||
}
|
||
|
||
// .animctrl 拖拽源:拖到 Details Controller 槽指派
|
||
//(kControllerDragPayload,对齐材质槽交互)
|
||
if (item.kind == Kind::AnimClip && ImGui::BeginDragDropSource())
|
||
{
|
||
ImGui::SetDragDropPayload(kControllerDragPayload,
|
||
assetPath.c_str(), assetPath.size() + 1);
|
||
ImGui::TextUnformatted(name.c_str());
|
||
ImGui::EndDragDropSource();
|
||
}
|
||
|
||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||
// File 菜单
|
||
if (ImGui::IsItemHovered() &&
|
||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||
{
|
||
if (item.kind == Kind::Material && ctx.materialEditor)
|
||
{
|
||
ctx.materialEditor->OpenAsset(assetPath);
|
||
}
|
||
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
|
||
{
|
||
ctx.controllerEditor->Open(assetPath);
|
||
}
|
||
else 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();
|
||
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
|
||
// 数组越界读
|
||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||
"Scene", "Material",
|
||
"Controller", "File"};
|
||
ImGui::Text("type: %s", kKindNames[static_cast<int>(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<double>(sz) / (1024.0 * 1024.0));
|
||
}
|
||
else
|
||
{
|
||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||
static_cast<double>(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<unsigned>(m->GetMeshes().size()),
|
||
m->GetBoneCount(),
|
||
static_cast<unsigned>(m->GetClips().size()));
|
||
}
|
||
}
|
||
}
|
||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||
ImGui::EndTooltip();
|
||
}
|
||
ImGui::PopID();
|
||
}
|
||
}
|