Reorganize directory layout: editor panels/, grouped shaders, drop stale header
- 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
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user