From 90097a756e3b08039af7e357e3332e8bb5dc3a1d Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sun, 4 Oct 2026 02:00:42 +0800 Subject: [PATCH] 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. --- App/editor/InspectorPanel.cpp | 63 +++++++++++++++++++++++--- Engine/include/Engine/ECS/Components.h | 11 ++++- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index c022e2a..7e2eb6f 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -78,7 +78,7 @@ namespace // TransformComponent::GetRotation 注释) bool DragVec3Row(const char* label, glm::vec3& v, float speed, float mn = -3.4e38f, float mx = 3.4e38f, - const char* fmt = "%.2f") + const char* fmt = "%.2f", bool* editing = nullptr) { ImGui::TextUnformatted(label); ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐 @@ -101,6 +101,11 @@ namespace changed |= ImGui::DragFloat( i == 0 ? "##x" : i == 1 ? "##y" : "##z", &v[i], speed, mn, mx, fmt); + if (editing != nullptr) + { + // 拖拽中/输入框打开中(含无拖动的按住帧) + *editing |= ImGui::IsItemActive(); + } ImGui::PopStyleColor(); if (i < 2) ImGui::SameLine(); } @@ -215,17 +220,61 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { t->SetPosition(pos); } - glm::vec3 rot = t->GetRotation(); // 派生欧拉(度) - for (int i = 0; i < 3; ++i) + // ---- rotation 行:编辑 mirror(UE TransformDetails 会话语义)---- + // 每帧回读 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(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°, - // 存储端 quat 精确;手输 Ctrl+Click 仍全精度写入) - if (DragVec3Row("rotation", rot, 0.5f, -3.4e38f, 3.4e38f, "%.1f")) + bool editingRot = false; + if (s_MirrorValid) + { + 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); + s_RotMirror = rot; } + s_RotEditingLast = editingRot; glm::vec3 scl = t->GetScale(); if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f)) { diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 1ede9e8..737338a 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -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; } // 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz, // 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor—— - // 那是 ZYX 航向序,语义不同) + // 那是 ZYX 航向序,语义不同)。输入域无限制:三角函数周期性 + // 自动归一(720 ≡ -45 ≡ 315),编辑器拖动累计值直接喂 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 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)); - return SetRotationQuat(qy * qx * qz); + return qy * qx * qz; } // 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用, // 不经欧拉往返 → 零漂移、无万向锁