- ColliderType + Capsule/Cylinder/Compound; CompoundChild list (Box/ Sphere/Capsule, local pos/quat, non-recursive) - CreateBody: CapsuleShape/CylinderShape (ctor param orders differ) + StaticCompoundShape; empty/failed compound falls back to Box - BodyRecord caches radius/halfHeight/compound; SyncScene rebuilds on change (per-collider param extraction) - Inspector: combo entries + shape params + compound child list editor - Serialization: new fields with as<T> fallback, compound seq round-trip
383 lines
15 KiB
C++
383 lines
15 KiB
C++
//
|
||
// InspectorPanel.cpp:组件编辑实现。
|
||
//
|
||
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
|
||
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
|
||
//
|
||
|
||
#include "InspectorPanel.h"
|
||
#include "Engine/ECS/Components.h"
|
||
#include "Engine/Renderer/Model.h"
|
||
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <cstdio>
|
||
#include <vector>
|
||
|
||
namespace
|
||
{
|
||
// InputText 需要 char 缓冲;提交时写回 std::string
|
||
bool InputTextString(const char* label, std::string& str)
|
||
{
|
||
char buf[256];
|
||
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
|
||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||
{
|
||
str = buf;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
|
||
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
|
||
// 任一轴变化返回 true
|
||
bool DragVec3Row(const char* label, glm::vec3& v, float speed,
|
||
float mn = -3.4e38f, float mx = 3.4e38f)
|
||
{
|
||
ImGui::TextUnformatted(label);
|
||
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
|
||
ImGui::PushID(label);
|
||
|
||
constexpr ImVec4 axisCol[3] = {
|
||
{0.72f, 0.24f, 0.24f, 1.00f}, // X 红
|
||
{0.30f, 0.68f, 0.34f, 1.00f}, // Y 绿
|
||
{0.24f, 0.40f, 0.88f, 1.00f}, // Z 蓝
|
||
};
|
||
const float boxW =
|
||
(ImGui::GetContentRegionAvail().x
|
||
- 2.0f * ImGui::GetStyle().ItemInnerSpacing.x) / 3.0f;
|
||
|
||
bool changed = false;
|
||
for (int i = 0; i < 3; ++i)
|
||
{
|
||
ImGui::PushStyleColor(ImGuiCol_FrameBg, axisCol[i]);
|
||
ImGui::SetNextItemWidth(boxW);
|
||
changed |= ImGui::DragFloat(
|
||
i == 0 ? "##x" : i == 1 ? "##y" : "##z",
|
||
&v[i], speed, mn, mx, "%.2f");
|
||
ImGui::PopStyleColor();
|
||
if (i < 2) ImGui::SameLine();
|
||
}
|
||
ImGui::PopID();
|
||
return changed;
|
||
}
|
||
}
|
||
|
||
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||
{
|
||
ImGui::Begin("Inspector");
|
||
|
||
auto& reg = ctx.scene->GetRegistry();
|
||
if (selected == entt::null || !reg.valid(selected))
|
||
{
|
||
ImGui::TextDisabled("(no entity selected)");
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
// ---- Name ----
|
||
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
|
||
{
|
||
InputTextString("Name", name->name);
|
||
}
|
||
ImGui::Separator();
|
||
|
||
// ---- Add Component ----
|
||
if (ImGui::Button("Add Component"))
|
||
{
|
||
ImGui::OpenPopup("add_component");
|
||
}
|
||
if (ImGui::BeginPopup("add_component"))
|
||
{
|
||
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
|
||
ImGui::MenuItem("Light"))
|
||
{
|
||
reg.emplace<Engine::ECS::LightComponent>(selected);
|
||
}
|
||
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
|
||
ImGui::MenuItem("Camera"))
|
||
{
|
||
reg.emplace<Engine::ECS::CameraComponent>(selected);
|
||
}
|
||
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
|
||
ImGui::MenuItem("Animation"))
|
||
{
|
||
reg.emplace<Engine::ECS::AnimationComponent>(selected);
|
||
}
|
||
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
|
||
ImGui::MenuItem("Name"))
|
||
{
|
||
reg.emplace<Engine::ECS::NameComponent>(selected);
|
||
}
|
||
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) &&
|
||
ImGui::MenuItem("RigidBody"))
|
||
{
|
||
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
ImGui::Separator();
|
||
|
||
// ---- Transform ----
|
||
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
glm::vec3 pos = t->GetPosition();
|
||
if (DragVec3Row("position", pos, 0.05f))
|
||
{
|
||
t->SetPosition(pos);
|
||
}
|
||
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度)
|
||
if (DragVec3Row("rotation", rot, 0.5f))
|
||
{
|
||
t->SetRotation(rot);
|
||
}
|
||
glm::vec3 scl = t->GetScale();
|
||
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
|
||
{
|
||
t->SetScale(scl);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- MeshRenderer ----
|
||
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("MeshRenderer",
|
||
ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
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()));
|
||
ImGui::TextDisabled("(mesh asset is read-only here)");
|
||
}
|
||
else
|
||
{
|
||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||
"model: INVALID (dangling ref)");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Light ----
|
||
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
int type = static_cast<int>(light->type);
|
||
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
|
||
{
|
||
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
|
||
}
|
||
ImGui::ColorEdit3("color", &light->color.x);
|
||
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
|
||
if (light->type == Engine::ECS::LightComponent::Type::Directional)
|
||
{
|
||
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
|
||
ImGui::Checkbox("castShadow", &light->castShadow);
|
||
}
|
||
else
|
||
{
|
||
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
|
||
ImGui::TextDisabled("(position = entity Transform)");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Camera ----
|
||
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
|
||
{
|
||
// primary 互斥:启用本相机时清掉其余实体的 primary
|
||
//(引擎取第一个 primary 作为游戏形态渲染视角)
|
||
reg.view<Engine::ECS::CameraComponent>().each(
|
||
[&](entt::entity e, Engine::ECS::CameraComponent& c)
|
||
{
|
||
if (e != selected)
|
||
{
|
||
c.primary = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
|
||
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
|
||
// SceneRenderer,编辑器形态恒用编辑器相机)
|
||
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
|
||
{
|
||
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||
selected))
|
||
{
|
||
ctx.editorCamera->AlignTo(*tr);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- RigidBody(Jolt 刚体:动态体 Transform 由物理每帧写回)----
|
||
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
|
||
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
int type = static_cast<int>(rb->type);
|
||
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
|
||
{
|
||
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
|
||
}
|
||
int collider = static_cast<int>(rb->collider);
|
||
if (ImGui::Combo("collider", &collider,
|
||
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
|
||
{
|
||
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
|
||
}
|
||
if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box)
|
||
{
|
||
ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
|
||
}
|
||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Sphere)
|
||
{
|
||
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
|
||
}
|
||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
|
||
{
|
||
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
|
||
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
|
||
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||
}
|
||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
|
||
{
|
||
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
|
||
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
|
||
}
|
||
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
|
||
{
|
||
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
|
||
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("+ add"))
|
||
{
|
||
rb->compound.emplace_back();
|
||
}
|
||
int remove = -1;
|
||
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
|
||
{
|
||
auto& c = rb->compound[i];
|
||
ImGui::PushID(i);
|
||
ImGui::Separator();
|
||
int sh = static_cast<int>(c.shape);
|
||
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
|
||
{
|
||
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
|
||
}
|
||
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
|
||
{
|
||
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
|
||
}
|
||
else
|
||
{
|
||
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
|
||
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
|
||
{
|
||
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||
}
|
||
}
|
||
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
|
||
if (ImGui::Button("remove"))
|
||
{
|
||
remove = i;
|
||
}
|
||
ImGui::PopID();
|
||
}
|
||
if (remove >= 0)
|
||
{
|
||
rb->compound.erase(rb->compound.begin() + remove);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
|
||
// Dynamic=凸包近似),无尺寸可调
|
||
ImGui::TextDisabled("follows MeshRenderer geometry");
|
||
}
|
||
ImGui::DragFloat("mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f");
|
||
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
|
||
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
|
||
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
|
||
ImGui::Checkbox("isSensor", &rb->isSensor);
|
||
if (rb->bodyValid &&
|
||
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
|
||
{
|
||
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
|
||
rb->velocity.x, rb->velocity.y, rb->velocity.z);
|
||
|
||
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
|
||
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
|
||
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
|
||
if (ImGui::Button("Apply Impulse"))
|
||
{
|
||
ctx.app->GetPhysics().AddImpulse(
|
||
static_cast<uint32_t>(entt::to_integral(selected)),
|
||
s_debugImpulse);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Animation ----
|
||
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
||
{
|
||
if (ImGui::CollapsingHeader("Animation",
|
||
ImGuiTreeNodeFlags_DefaultOpen))
|
||
{
|
||
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
||
const Engine::Model* model =
|
||
mr ? ctx.assets->GetModel(mr->model) : nullptr;
|
||
|
||
if (model != nullptr && model->HasAnimations())
|
||
{
|
||
const auto& clips = model->GetClips();
|
||
std::vector<const char*> clipNames;
|
||
clipNames.reserve(clips.size());
|
||
for (const auto& c : clips)
|
||
{
|
||
clipNames.push_back(c.name.c_str());
|
||
}
|
||
int cur = anim->clipIndex;
|
||
if (ImGui::Combo("clip", &cur, clipNames.data(),
|
||
static_cast<int>(clipNames.size())))
|
||
{
|
||
anim->Play(cur); // 触发 crossfade(fade 秒)
|
||
}
|
||
ImGui::Checkbox("playing", &anim->playing);
|
||
ImGui::SameLine();
|
||
ImGui::Checkbox("loop", &anim->loop);
|
||
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
|
||
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
|
||
if (anim->clipIndex >= 0 &&
|
||
anim->clipIndex < static_cast<int>(clips.size()))
|
||
{
|
||
ImGui::Text("t: %.2f / %.2f s", anim->time,
|
||
clips[anim->clipIndex].duration);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ImGui::TextDisabled("(no animation clips on model)");
|
||
}
|
||
}
|
||
}
|
||
|
||
ImGui::End();
|
||
}
|