physics: joint constraints (fixed/hinge/ball)

- JointComponent: type, target UUID, per-body local anchors, hinge axis
  + angle limits (data only, zero Jolt deps)
- PhysicsWorld::SyncConstraints: create/remove/rebuild on snapshot
  change; body rebuild contagion via invalidated-bits list; target
  resolved lazily by UUID each sync (no two-pass load needed)
- Jolt FixedConstraint/HingeConstraint/PointConstraint in
  LocalToBodyCOM space (anchors minus shape COM); fixed frames lock
  creation-time relative pose; hinge axis aligned across bodies
- Inspector: Joint section with target entity name dropdown
- Serialization: Joint map with UUID target, as<T> fallbacks
This commit is contained in:
2026-10-03 16:26:20 +08:00
parent a744b332eb
commit 82910c41e8
5 changed files with 296 additions and 0 deletions
+57
View File
@@ -116,6 +116,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
}
if (!reg.all_of<Engine::ECS::JointComponent>(selected) &&
ImGui::MenuItem("Joint"))
{
reg.emplace<Engine::ECS::JointComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
@@ -334,6 +339,58 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
}
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
{
if (ImGui::CollapsingHeader("Joint", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(jc->type);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
{
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
}
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
std::string items;
std::vector<Engine::UUID> uuids;
int current = -1;
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().each(
[&](entt::entity e, const Engine::ECS::IdComponent& id,
const Engine::ECS::NameComponent& n)
{
if (e == selected)
{
return; // 自连无意义
}
if (id.uuid == jc->target)
{
current = static_cast<int>(uuids.size());
}
uuids.push_back(id.uuid);
items.append(n.name);
items.push_back('\0');
});
items.push_back('\0'); // ImGui Combo 双 \0 结尾
if (ImGui::Combo("target", &current, items.c_str()) && current >= 0)
{
jc->target = uuids[static_cast<size_t>(current)];
}
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
{
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
ImGui::Checkbox("enable limit", &jc->enableLimit);
if (jc->enableLimit)
{
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f);
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
}
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{