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:
2026-10-07 09:52:17 +08:00
parent cc837bc605
commit f73e52926f
55 changed files with 23 additions and 83 deletions
+412
View File
@@ -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();
}
}