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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user