fix(editor): rotation drag accumulates without wrap (UE session semantics)

Dragging rotation hit a wall at the wrap point: each frame re-read
euler from the quat and wrapped to [0,360), so dragging past 360
snapped the display back to 0 (and backwards dragging wrapped up to
360) -- values "jumped around" instead of accumulating.

Port UE TransformDetails session semantics: the rotation row shows a
local mirror value while the user edits it (accumulating freely in
both directions, never wrapping), and re-syncs from the quat only on
foreign changes (gizmo drag, physics writeback, entity switch).
Detection is exact component-wise quat equality: the mirror quat comes
from the same QuatFromEuler used by SetRotation, so an untouched pair
compares bit-equal, while any foreign writeback -- including sub-0.05
degree gizmo steps that a dot-product threshold would miss -- differs
in at least one float. Edit frames skip the check (mirror leads the
quat by one drag delta), gated on the previous frame's item-active
state.
This commit is contained in:
2026-10-04 02:00:42 +08:00
parent 4498bc7535
commit 90097a756e
2 changed files with 65 additions and 9 deletions
+56 -7
View File
@@ -78,7 +78,7 @@ namespace
// TransformComponent::GetRotation 注释) // TransformComponent::GetRotation 注释)
bool DragVec3Row(const char* label, glm::vec3& v, float speed, bool DragVec3Row(const char* label, glm::vec3& v, float speed,
float mn = -3.4e38f, float mx = 3.4e38f, float mn = -3.4e38f, float mx = 3.4e38f,
const char* fmt = "%.2f") const char* fmt = "%.2f", bool* editing = nullptr)
{ {
ImGui::TextUnformatted(label); ImGui::TextUnformatted(label);
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐 ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
@@ -101,6 +101,11 @@ namespace
changed |= ImGui::DragFloat( changed |= ImGui::DragFloat(
i == 0 ? "##x" : i == 1 ? "##y" : "##z", i == 0 ? "##x" : i == 1 ? "##y" : "##z",
&v[i], speed, mn, mx, fmt); &v[i], speed, mn, mx, fmt);
if (editing != nullptr)
{
// 拖拽中/输入框打开中(含无拖动的按住帧)
*editing |= ImGui::IsItemActive();
}
ImGui::PopStyleColor(); ImGui::PopStyleColor();
if (i < 2) ImGui::SameLine(); if (i < 2) ImGui::SameLine();
} }
@@ -215,17 +220,61 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
t->SetPosition(pos); t->SetPosition(pos);
} }
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度) // ---- rotation 行:编辑 mirror(UE TransformDetails 会话语义)----
for (int i = 0; i < 3; ++i) // 每帧回读 quat 反解 + wrap 会让拖动乱跳(360 → 0 折返)。
// UE 做法 = 编辑会话内显示本地累计值(正拖无限增大、反拖
// 无限减小、永不折返),只有"非编辑来源"的姿态改动(Gizmo、
// 物理、重选)才刷新显示。ImGui 无状态 → static mirror:
// 切换实体重置;非编辑态用 quat 位级等价检测判定外部改动
static unsigned s_MirrorEntity = 0;
static glm::vec3 s_RotMirror(0.0f);
static bool s_MirrorValid = false;
static bool s_RotEditingLast = false; // 上一帧是否在编辑
const unsigned entityId =
static_cast<unsigned>(entt::to_integral(selected));
if (entityId != s_MirrorEntity)
{ {
if (rot[i] == 0.0f) rot[i] = 0.0f; // 清 -0.0(atan2 副产物) s_MirrorEntity = entityId;
s_MirrorValid = false;
} }
// "%.1f" = UE Details 默认显示精度(反解噪声 ~0.02° @ pitch 90°, bool editingRot = false;
// 存储端 quat 精确;手输 Ctrl+Click 仍全精度写入) if (s_MirrorValid)
if (DragVec3Row("rotation", rot, 0.5f, -3.4e38f, 3.4e38f, "%.1f")) {
if (!s_RotEditingLast)
{
// 非编辑帧才检测外部改动(编辑帧 mirror 领先 quat 一个
// 拖动增量,等价检测必触发误重置)。编辑态取上一帧
// ——检测先于本帧 DragVec3Row 执行。
// 判据 = 逐分量相等:QuatFromEuler 与 SetRotation 是
// 同一函数,mirror 未被外部动过时位级相同(== 无浮点
// 噪声问题,比 dot 阈值严——Gizmo 极慢拖 0.05°/帧
// 的 quat 差 ~1e-7,dot 阈值法会漏检导致显示不跟手)
const glm::quat mq =
Engine::ECS::TransformComponent::QuatFromEuler(s_RotMirror);
const glm::quat& aq = t->GetRotationQuat();
const bool untouched = mq.x == aq.x && mq.y == aq.y &&
mq.z == aq.z && mq.w == aq.w;
if (!untouched)
{
s_RotMirror = t->GetRotation(); // 外部改动 → 刷新
}
}
}
else
{
s_RotMirror = t->GetRotation();
s_MirrorValid = true;
}
glm::vec3 rot = s_RotMirror;
// "%.1f" = UE Details 默认显示精度。mirror 值直显(无 wrap、
// 无限累计);Ctrl+Click 手输全精度写入
if (DragVec3Row("rotation", rot, 0.5f, -3.4e38f, 3.4e38f,
"%.1f", &editingRot))
{ {
t->SetRotation(rot); t->SetRotation(rot);
s_RotMirror = rot;
} }
s_RotEditingLast = editingRot;
glm::vec3 scl = t->GetScale(); glm::vec3 scl = t->GetScale();
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f)) if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
{ {
+9 -2
View File
@@ -98,13 +98,20 @@ namespace Engine::ECS {
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; } TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz, // 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor—— // 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
// 那是 ZYX 航向序,语义不同) // 那是 ZYX 航向序,语义不同)。输入域无限制:三角函数周期性
// 自动归一(720 ≡ -45 ≡ 315),编辑器拖动累计值直接喂
TransformComponent& SetRotation(const glm::vec3& r) TransformComponent& SetRotation(const glm::vec3& r)
{
return SetRotationQuat(QuatFromEuler(r));
}
// 欧拉(度)→ quat 的唯一转换点(qy·qx·qz)。static 供编辑器
// mirror 的旋转等价检测复用(同一公式构造 → dot 判定严格)
static glm::quat QuatFromEuler(const glm::vec3& r)
{ {
const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0)); const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0));
const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0)); const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0));
const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1)); const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1));
return SetRotationQuat(qy * qx * qz); return qy * qx * qz;
} }
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用, // 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
// 不经欧拉往返 → 零漂移、无万向锁 // 不经欧拉往返 → 零漂移、无万向锁