feat(editor): manual mesh renderer setup + asset hover info card

Mesh Renderer was missing from the Add Component menu and its model
reference was read-only, so a mesh could only be attached by loading
a saved scene. Add it to the menu (Unity Add Component semantics) and
give the block a UE-Details-style asset slot: a dropdown listing all
model files under assets/, selecting applies LoadModel (cache deduped).

Asset browser files now show a UE Content Browser style hover card:
kind, size, modified time, and loaded state (new AssetCache::Contains
query, read-only -- hovering never triggers loading).

Fix latent FATALs unlocked by manual assignment: AssetCache::Get
aborts on invalid handles, so rendering, animation sampling, and mesh
collider build now guard mr.model.Valid() before calling it.
This commit is contained in:
2026-10-04 01:07:38 +08:00
parent 7d02d344e5
commit 69671cf4eb
5 changed files with 156 additions and 14 deletions
+76 -10
View File
@@ -13,7 +13,9 @@
#include <imgui.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <vector>
#include <type_traits>
@@ -148,6 +150,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::EndDisabled();
}
};
menuItem(static_cast<Engine::ECS::MeshRendererComponent*>(nullptr),
"Mesh Renderer");
menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
@@ -194,19 +198,79 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
}
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
const std::string cur = ctx.assets->Models().Path(mr->model);
// 模型资产槽(UE Details 资产槽语义):下拉列 assets/ 下全部
// 模型文件,选中即 LoadModel 应用;已加载路径缓存去重复用
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
namespace fs = std::filesystem;
static std::vector<std::string> s_ModelFiles;
s_ModelFiles.clear();
if (fs::exists("assets"))
{
for (const auto& entry :
fs::recursive_directory_iterator("assets"))
{
if (!entry.is_regular_file())
{
continue;
}
std::string ext = entry.path().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")
{
s_ModelFiles.push_back(
entry.path().generic_string());
}
}
std::sort(s_ModelFiles.begin(), s_ModelFiles.end());
}
std::vector<const char*> opts;
opts.push_back("<none>");
for (const auto& f : s_ModelFiles)
{
opts.push_back(f.c_str());
}
int sel = 0;
for (size_t k = 0; k < s_ModelFiles.size(); ++k)
{
if (s_ModelFiles[k] == cur)
{
sel = static_cast<int>(k) + 1;
}
}
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::Combo("##model_slot", &sel, opts.data(),
static_cast<int>(opts.size())))
{
mr->model = (sel <= 0)
? Engine::ModelRef{}
: ctx.assets->LoadModel(s_ModelFiles[static_cast<size_t>(sel) - 1]);
}
ImGui::Spacing();
}
// 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号)
if (mr->model.Valid())
{
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
ImGui::TextDisabled("no model assigned");
}
if (!mr->active)
{
@@ -677,8 +741,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::BeginDisabled(true);
}
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
// 句柄无效不进 GetModel(缓存对无效句柄 FATAL;手选 <none> 即无效)
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model)
: nullptr;
// 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
if (mr == nullptr)