feat(editor): multi-script entities + asset browser panel
ScriptComponent now holds a list of {name, active} items (Unity
semantics: one entity, many scripts, each with its own enable bit;
component-level active stays as the master switch). ScriptRuntime
reconciles instances by name per entity -- rename/remove destroys
only the affected instances, untouched ones keep ticking. Scenes
persist Script.Items; old single-script key still loads.
AssetBrowserPanel (lightweight Unity Project window): fixed roots
(assets/scenes) as tabs, recursive directory tree, file list with
type filter (models/textures/scenes) and name substring filter,
double-click preloads models/HDRs into the asset caches (path-
deduped). Scene files point to the File menu for now; drag & drop
is a later iteration. Docked with Stats by default in fresh layouts.
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#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"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
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]);
|
||||
|
||||
// ---- 左:目录树 | 右:文件列表 ----
|
||||
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);
|
||||
DrawDirTree(ctx, root);
|
||||
}
|
||||
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(root))
|
||||
{
|
||||
DrawFileList(ctx, root);
|
||||
}
|
||||
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();
|
||||
if (ImGui::TreeNode(name.c_str()))
|
||||
{
|
||||
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())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// tooltip:完整相对路径(面板窄时文件名截断仍可辨识)
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("%s", assetPath.c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user