From 1594a664e900ac6fc24590cf8c39887801a2714b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=83=AD=E5=BF=83=E5=B8=82=E6=B0=91=E7=9F=B3=E5=85=88?= =?UTF-8?q?=E7=94=9F?= <1249467256@qq.com> Date: Sat, 21 Mar 2026 14:44:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=94=AF=E6=8C=81SuperView/Skeleton?= =?UTF-8?q?=E5=B1=8F=E5=B9=95=E7=A9=BA=E9=97=B4=E6=8B=96=E6=8B=BD=E4=B8=8E?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SuperView编辑器新增G/R/X/Y/Z/Shift等快捷键,支持3D拖拽/旋转/撤销,操作体验类Blender - 骨骼编辑器支持屏幕空间拖拽、旋转、撤销(Ctrl+Z)、撤销按钮,自动检测窗口焦点防误操作 - 新增CachedTabControl,Tab切换内容缓存,提升性能 - 删除/上移/下移命令支持多选批量操作 - 动画与骨骼吸附兼容性修复,支持骨骼名/索引,暂停时吸附不丢失 - 细节优化:特效可视化轴向跟随旋转,Tab失焦高亮修复,按钮布局统一 - 代码大量注释,便于维护 --- AssetEditor/Views/CachedTabControl.cs | 85 +++++ AssetEditor/Views/MainWindow.xaml | 86 +++-- .../MetaEditor/Commands/DeleteEntryCommand.cs | 23 +- .../MetaEditor/Commands/MoveEntryCommand.cs | 108 ++++-- .../SuperViewManipulatorComponent.cs | 356 ++++++++++++++++++ .../SuperView/SuperViewViewModel.cs | 215 +++++++++-- .../Visualisation/MetaDataBuilder.cs | 41 +- .../SkeletonEditor/EditorView.xaml | 33 +- .../SkeletonEditor/SkeletonEditorViewModel.cs | 297 ++++++++++++++- GameWorld/View3D/Animation/AnimationPlayer.cs | 6 +- GameWorld/View3D/Animation/GameSkeleton.cs | 5 +- GameWorld/View3D/SceneNodes/Rmv2MeshNode.cs | 30 +- .../View3D/Services/ComplexMeshLoader.cs | 7 +- .../Services/FocusSelectableObjectService.cs | 4 +- .../Utility/SkeletonBoneAnimationResolver.cs | 35 +- 15 files changed, 1186 insertions(+), 145 deletions(-) create mode 100644 AssetEditor/Views/CachedTabControl.cs create mode 100644 Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewManipulatorComponent.cs diff --git a/AssetEditor/Views/CachedTabControl.cs b/AssetEditor/Views/CachedTabControl.cs new file mode 100644 index 000000000..9e1dcbd75 --- /dev/null +++ b/AssetEditor/Views/CachedTabControl.cs @@ -0,0 +1,85 @@ +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; + +namespace AssetEditor.Views +{ + [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))] + public class CachedTabControl : TabControl + { + private Panel _itemsHolderPanel = null; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + _itemsHolderPanel = GetTemplateChild("PART_ItemsHolder") as Panel; + UpdateSelectedItem(); + } + + protected override void OnSelectionChanged(SelectionChangedEventArgs e) + { + base.OnSelectionChanged(e); + UpdateSelectedItem(); + } + + private void UpdateSelectedItem() + { + if (_itemsHolderPanel == null) return; + + object selectedItem = this.SelectedItem; + if (selectedItem == null) return; + + ContentPresenter cp = FindChildContentPresenter(selectedItem); + if (cp == null) + { + cp = new ContentPresenter + { + Content = selectedItem, + ContentTemplate = this.SelectedContentTemplate, + ContentTemplateSelector = this.ContentTemplateSelector, + ContentStringFormat = this.SelectedContentStringFormat, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + _itemsHolderPanel.Children.Add(cp); + } + + foreach (ContentPresenter child in _itemsHolderPanel.Children) + { + child.Visibility = (child.Content == selectedItem) ? Visibility.Visible : Visibility.Collapsed; + } + } + + protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) + { + base.OnItemsChanged(e); + if (_itemsHolderPanel == null) return; + + if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace) + { + if (e.OldItems != null) + { + foreach (var item in e.OldItems) + { + var cp = FindChildContentPresenter(item); + if (cp != null) _itemsHolderPanel.Children.Remove(cp); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + _itemsHolderPanel.Children.Clear(); + } + } + + private ContentPresenter FindChildContentPresenter(object data) + { + if (_itemsHolderPanel == null) return null; + foreach (ContentPresenter cp in _itemsHolderPanel.Children) + { + if (cp.Content == data) return cp; + } + return null; + } + } +} diff --git a/AssetEditor/Views/MainWindow.xaml b/AssetEditor/Views/MainWindow.xaml index 2f657c76b..b02af4eed 100644 --- a/AssetEditor/Views/MainWindow.xaml +++ b/AssetEditor/Views/MainWindow.xaml @@ -158,28 +158,48 @@ - + x:Name="EditorsTabControl" BorderThickness="1, 1, 0, 0" > - - - + + + + + + + + + + + + + + + + + + - - - + + + - + @@ -193,22 +213,12 @@ - + - - - - + + + + @@ -224,18 +234,12 @@ - - - + + diff --git a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs index 269acbaf4..22162941a 100644 --- a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs +++ b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs @@ -1,4 +1,5 @@ -using Editors.AnimationMeta.Presentation; +using System.Linq; +using Editors.AnimationMeta.Presentation; using Shared.Core.Events; namespace Editors.AnimationMeta.MetaEditor.Commands @@ -7,14 +8,24 @@ internal class DeleteEntryCommand : IUiCommand { public void Execute(MetaDataEditorViewModel controller) { - var itemToRemove = controller.SelectedAttribute; - if (itemToRemove == null || controller.ParsedFile == null) - return; + if (controller.ParsedFile == null) return; + + // Get all selected items from the UI tags + var itemsToRemove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToRemove.Any()) return; + + // Batch remove + foreach (var item in itemsToRemove) + { + controller.ParsedFile.Attributes.Remove(item); + } - controller.ParsedFile.Attributes.Remove(itemToRemove); controller.UpdateView(); controller.SelectedTag = controller.Tags.FirstOrDefault(); } - } } diff --git a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs index d111d8223..866a23bce 100644 --- a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs +++ b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs @@ -1,4 +1,5 @@ -using Editors.AnimationMeta.Presentation; +using System.Linq; +using Editors.AnimationMeta.Presentation; using Shared.Core.Events; namespace Editors.AnimationMeta.MetaEditor.Commands @@ -7,39 +8,92 @@ internal class MoveEntryCommand : IUiCommand { public void ExecuteUp(MetaDataEditorViewModel controller) { - var itemToMove = controller.SelectedAttribute; - if (itemToMove == null || controller.ParsedFile == null) - return; - - var currentIndex = controller.ParsedFile.Attributes.IndexOf(itemToMove); - if (currentIndex == 0) - return; - - controller.ParsedFile.Attributes.Remove(itemToMove); - controller.ParsedFile.Attributes.Insert(currentIndex - 1, itemToMove); - controller.UpdateView(); - controller.SelectedTag = controller.Tags - .Where(x => x._input == itemToMove) - .FirstOrDefault(); + if (controller.ParsedFile == null) return; + + var itemsToMove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToMove.Any()) return; + + var attributes = controller.ParsedFile.Attributes; + + // Sort ascending to move top items first, preventing them from jumping over each other + var sortedItems = itemsToMove.OrderBy(x => attributes.IndexOf(x)).ToList(); + bool moved = false; + + foreach (var item in sortedItems) + { + var currentIndex = attributes.IndexOf(item); + if (currentIndex > 0) + { + // If the item above is also in the selection, keep them as a block + var itemAbove = attributes[currentIndex - 1]; + if (!itemsToMove.Contains(itemAbove)) + { + attributes.RemoveAt(currentIndex); + attributes.Insert(currentIndex - 1, item); + moved = true; + } + } + } + + if (moved) + { + controller.UpdateView(); + // Restore selection state for the moved items + foreach (var tag in controller.Tags.Where(t => itemsToMove.Contains(t._input))) + { + tag.IsSelected = true; + } + controller.SelectedTag = controller.Tags.FirstOrDefault(x => x.IsSelected); + } } public void ExecuteDown(MetaDataEditorViewModel controller) { - var itemToMove = controller.SelectedAttribute; - if (itemToMove == null || controller.ParsedFile == null) - return; + if (controller.ParsedFile == null) return; + + var itemsToMove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToMove.Any()) return; + + var attributes = controller.ParsedFile.Attributes; - var currentIndex = controller.ParsedFile.Attributes.IndexOf(itemToMove); - if (currentIndex == controller.ParsedFile.Attributes.Count -1) - return; + // Sort descending to move bottom items first + var sortedItems = itemsToMove.OrderByDescending(x => attributes.IndexOf(x)).ToList(); + bool moved = false; - controller.ParsedFile.Attributes.Remove(itemToMove); - controller.ParsedFile.Attributes.Insert(currentIndex + 1, itemToMove); - controller.UpdateView(); - controller.SelectedTag = controller.Tags - .Where(x => x._input == itemToMove) - .FirstOrDefault(); + foreach (var item in sortedItems) + { + var currentIndex = attributes.IndexOf(item); + if (currentIndex < attributes.Count - 1) + { + // If the item below is also in the selection, keep them as a block + var itemBelow = attributes[currentIndex + 1]; + if (!itemsToMove.Contains(itemBelow)) + { + attributes.RemoveAt(currentIndex); + attributes.Insert(currentIndex + 1, item); + moved = true; + } + } + } + if (moved) + { + controller.UpdateView(); + // Restore selection state for the moved items + foreach (var tag in controller.Tags.Where(t => itemsToMove.Contains(t._input))) + { + tag.IsSelected = true; + } + controller.SelectedTag = controller.Tags.FirstOrDefault(x => x.IsSelected); + } } } } diff --git a/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewManipulatorComponent.cs b/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewManipulatorComponent.cs new file mode 100644 index 000000000..bbd7eaa77 --- /dev/null +++ b/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewManipulatorComponent.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using Shared.Core.Services; +using GameWorld.Core.Services; +using Shared.GameFormats.AnimationMeta.Parsing; +using GameWorld.Core.Components.Input; +using GameWorld.Core.SceneNodes; + +namespace Editors.AnimationMeta.SuperView +{ + public delegate void DragUpdateDelegate(Vector3 worldDeltaPos, Quaternion worldDeltaRot); + + public class SuperViewManipulatorComponent : IGameComponent, IUpdateable + { + public enum ManipulateMode { None, Move, Rotate } + public enum AxisLock { None, X, Y, Z } + + [System.Runtime.InteropServices.DllImport("user32.dll")] + public static extern bool GetCursorPos(out POINT lpPoint); + [System.Runtime.InteropServices.DllImport("user32.dll")] + public static extern bool SetCursorPos(int X, int Y); + + public struct POINT { public int X; public int Y; } + + private class UndoRecord + { + public Vector3? Pos; public Vector4? Rot; + public Vector3? StartPos; public Vector3? EndPos; + } + private readonly Stack _undoStack = new Stack(); + + private readonly IWpfGame _game; + private readonly FocusSelectableObjectService _cameraService; + private readonly IKeyboardComponent _keyboard; + private readonly IMouseComponent _mouse; + + public ParsedMetadataAttribute SelectedAttribute { get; set; } + + public Func GetSelectedNode { get; set; } + public SceneNode SelectedNode { get; private set; } + + public ManipulateMode CurrentMode { get; private set; } = ManipulateMode.None; + public AxisLock CurrentAxis { get; private set; } = AxisLock.None; + + private Vector3 _originalLocalPos; + private Quaternion _originalLocalRot; + private Vector3 _originalLocalStartPos; + private Vector3 _originalLocalEndPos; + private bool _isSplashAttack = false; + + // 【核心解算引擎】 + public Func GetBoneWorldMatrix { get; set; } + private Quaternion _boneWorldRotation; + public Vector3 TrueWorldPivot { get; private set; } + + private Vector3 _startIntersect; + private Vector2 _startMousePos; + private POINT _lockedPhysicalCursorPos; + private Vector2 _virtualMousePos; + + public event Action OnDragStarted; + public event DragUpdateDelegate OnDragUpdate; + public event Action OnDragCompleted; + + public bool Enabled { get; set; } = true; + public int UpdateOrder { get; set; } = 1000; + public event EventHandler EnabledChanged; + public event EventHandler UpdateOrderChanged; + + public SuperViewManipulatorComponent(IWpfGame game, FocusSelectableObjectService cameraService, IKeyboardComponent keyboard, IMouseComponent mouse) + { + _game = game; + _cameraService = cameraService; + _keyboard = keyboard; + _mouse = mouse; + } + + public void Initialize() { } + + private bool HasSpatialProperty(object item) + { + if (item == null) return false; + var type = item.GetType(); + return type.GetProperty("Position") != null || + type.GetProperty("StartPosition") != null || + type.GetProperty("Orientation") != null; + } + + public void Update(GameTime gameTime) + { + if (_mouse.IsMouseButtonPressed(MouseButton.Left) || _mouse.IsMouseButtonPressed(MouseButton.Right)) + _game.GetFocusElement()?.Focus(); + + if (_keyboard.IsKeyDown(Keys.LeftControl) && _keyboard.IsKeyReleased(Keys.Z)) + { + if (CurrentMode == ManipulateMode.None && _undoStack.Count > 0) + { + var record = _undoStack.Pop(); + ApplyRawData(record.Pos, record.Rot, record.StartPos, record.EndPos); + OnDragCompleted?.Invoke(); + } + } + + if (SelectedAttribute == null) return; + if (_mouse.MouseOwner != null && _mouse.MouseOwner != this) return; + + bool shiftDown = _keyboard.IsKeyDown(Keys.LeftShift) || _keyboard.IsKeyDown(Keys.RightShift); + float speedMultiplier = shiftDown ? 0.1f : 1.0f; + + if (CurrentMode == ManipulateMode.None) + { + if (_keyboard.IsKeyDown(Keys.G)) StartManipulation(ManipulateMode.Move, _mouse.Position()); + else if (_keyboard.IsKeyDown(Keys.R)) StartManipulation(ManipulateMode.Rotate, _mouse.Position()); + return; + } + + if (_mouse.MouseOwner == null) _mouse.MouseOwner = this; + + if (_keyboard.IsKeyDown(Keys.X)) CurrentAxis = AxisLock.X; + else if (_keyboard.IsKeyDown(Keys.Y)) CurrentAxis = AxisLock.Y; + else if (_keyboard.IsKeyDown(Keys.Z)) CurrentAxis = AxisLock.Z; + + GetCursorPos(out POINT currentPhysicalPos); + int dx = currentPhysicalPos.X - _lockedPhysicalCursorPos.X; + int dy = currentPhysicalPos.Y - _lockedPhysicalCursorPos.Y; + + if (dx != 0 || dy != 0) + { + _virtualMousePos.X += dx; + _virtualMousePos.Y += dy; + SetCursorPos(_lockedPhysicalCursorPos.X, _lockedPhysicalCursorPos.Y); + } + + if (CurrentMode == ManipulateMode.Move) UpdateMovement(_virtualMousePos, speedMultiplier); + else if (CurrentMode == ManipulateMode.Rotate) UpdateRotation(_virtualMousePos, speedMultiplier); + + if (_mouse.IsMouseButtonPressed(MouseButton.Left)) + { + CurrentMode = ManipulateMode.None; + _mouse.MouseOwner = null; + ShowAndReleaseMouse(); + OnDragCompleted?.Invoke(); + } + else if (_mouse.IsMouseButtonPressed(MouseButton.Right) || _keyboard.IsKeyDown(Keys.Escape)) + { + CurrentMode = ManipulateMode.None; + _mouse.MouseOwner = null; + ShowAndReleaseMouse(); + + if (_undoStack.Count > 0) + { + var record = _undoStack.Pop(); + ApplyRawData(record.Pos, record.Rot, record.StartPos, record.EndPos); + } + + OnDragUpdate?.Invoke(Vector3.Zero, Quaternion.Identity); + OnDragCompleted?.Invoke(); + } + } + + private void StartManipulation(ManipulateMode mode, Vector2 mousePos) + { + if (!HasSpatialProperty(SelectedAttribute)) return; + + CurrentMode = mode; + CurrentAxis = AxisLock.None; + _isSplashAttack = false; + + dynamic meta = SelectedAttribute; + + try { _originalLocalPos = meta.Position; } catch { _originalLocalPos = Vector3.Zero; } + try { _originalLocalRot = new Quaternion(meta.Orientation); } catch { _originalLocalRot = Quaternion.Identity; } + + try + { + _originalLocalStartPos = meta.StartPosition; + _originalLocalEndPos = meta.EndPosition; + _originalLocalPos = _originalLocalStartPos; + _isSplashAttack = true; + } + catch { } + + _undoStack.Push(new UndoRecord + { + Pos = _originalLocalPos, + Rot = new Vector4(_originalLocalRot.X, _originalLocalRot.Y, _originalLocalRot.Z, _originalLocalRot.W), + StartPos = _isSplashAttack ? _originalLocalStartPos : null, + EndPos = _isSplashAttack ? _originalLocalEndPos : null + }); + + SelectedNode = GetSelectedNode?.Invoke(); + + // 【FIX】: Directly get the real bone world matrix to avoid coordinate offset + Matrix boneMatrix = GetBoneWorldMatrix != null ? GetBoneWorldMatrix() : Matrix.Identity; + + if (boneMatrix == Matrix.Identity) + { + TrueWorldPivot = _originalLocalPos; + _boneWorldRotation = Quaternion.Identity; + } + else + { + boneMatrix.Decompose(out _, out Quaternion boneRot, out _); + _boneWorldRotation = boneRot; + TrueWorldPivot = Vector3.Transform(_originalLocalPos, boneMatrix); + } + + _startMousePos = mousePos; + _virtualMousePos = mousePos; + + GetCursorPos(out _lockedPhysicalCursorPos); + HideAndLockMouse(); + + _startIntersect = GetMousePlaneIntersection(_virtualMousePos, TrueWorldPivot, GetWorkPlaneNormal()); + OnDragStarted?.Invoke(); + } + + private void UpdateMovement(Vector2 virtualMousePos, float speed) + { + var planeNormal = GetWorkPlaneNormal(); + var currentIntersect = GetMousePlaneIntersection(virtualMousePos, TrueWorldPivot, planeNormal); + var worldDelta = (currentIntersect - _startIntersect) * speed; + + if (CurrentAxis == AxisLock.X) worldDelta *= new Vector3(1, 0, 0); + if (CurrentAxis == AxisLock.Y) worldDelta *= new Vector3(0, 1, 0); + if (CurrentAxis == AxisLock.Z) worldDelta *= new Vector3(0, 0, 1); + + Vector3 localDelta = Vector3.Transform(worldDelta, Quaternion.Inverse(_boneWorldRotation)); + + if (_isSplashAttack) + ApplyRawData(null, null, _originalLocalStartPos + localDelta, _originalLocalEndPos + localDelta); + else + ApplyRawData(_originalLocalPos + localDelta, null, null, null); + + OnDragUpdate?.Invoke(worldDelta, Quaternion.Identity); + } + + private void UpdateRotation(Vector2 virtualMousePos, float speed) + { + Vector3 projectedVec = _game.GraphicsDevice.Viewport.Project( + TrueWorldPivot, _cameraService.Camera.ProjectionMatrix, _cameraService.Camera.ViewMatrix, Matrix.Identity); + + Vector2 screenCenter = new Vector2(projectedVec.X, projectedVec.Y); + Vector2 startVec = _startMousePos - screenCenter; + Vector2 currentVec = virtualMousePos - screenCenter; + + if (startVec.LengthSquared() < 1 || currentVec.LengthSquared() < 1) return; + + startVec.Normalize(); + currentVec.Normalize(); + + float angle = (float)Math.Atan2( + startVec.X * currentVec.Y - startVec.Y * currentVec.X, + startVec.X * currentVec.X + startVec.Y * currentVec.Y); + + // 【FIX】: Inverted angle calculation to fix reverse rotation mapping + angle *= -speed; + + Vector3 cameraForward = Vector3.Normalize(_cameraService.Camera.LookAt - _cameraService.Camera.Position); + Vector3 rotAxis = cameraForward; + + if (CurrentAxis == AxisLock.X) rotAxis = Vector3.UnitX; + if (CurrentAxis == AxisLock.Y) rotAxis = Vector3.UnitY; + if (CurrentAxis == AxisLock.Z) rotAxis = Vector3.UnitZ; + + Quaternion worldDeltaRot = Quaternion.CreateFromAxisAngle(rotAxis, angle); + Quaternion localDeltaRot = Quaternion.Inverse(_boneWorldRotation) * worldDeltaRot * _boneWorldRotation; + + if (_isSplashAttack) + { + Vector3 newEndLocal = _originalLocalStartPos + Vector3.Transform(_originalLocalEndPos - _originalLocalStartPos, localDeltaRot); + ApplyRawData(null, null, _originalLocalStartPos, newEndLocal); + } + else + { + Quaternion newLocalRot = localDeltaRot * _originalLocalRot; + ApplyRawData(null, new Vector4(newLocalRot.X, newLocalRot.Y, newLocalRot.Z, newLocalRot.W), null, null); + } + + OnDragUpdate?.Invoke(Vector3.Zero, worldDeltaRot); + } + + private void ApplyRawData(Vector3? pos, Vector4? rot, Vector3? startPos, Vector3? endPos) + { + try + { + dynamic meta = SelectedAttribute; + if (pos.HasValue) try { meta.Position = pos.Value; } catch { } + if (rot.HasValue) try { meta.Orientation = rot.Value; } catch { } + if (startPos.HasValue) try { meta.StartPosition = startPos.Value; } catch { } + if (endPos.HasValue) try { meta.EndPosition = endPos.Value; } catch { } + } + catch { } + } + + private void HideAndLockMouse() + { + try + { + System.Windows.Application.Current.Dispatcher.Invoke(() => { + var element = _game.GetFocusElement(); + if (element != null) + { + System.Windows.Input.Mouse.Capture(element); + element.Cursor = System.Windows.Input.Cursors.None; + } + }); + } + catch { } + } + + private void ShowAndReleaseMouse() + { + try + { + System.Windows.Application.Current.Dispatcher.Invoke(() => { + var element = _game.GetFocusElement(); + if (element != null) + { + element.ReleaseMouseCapture(); + element.Cursor = System.Windows.Input.Cursors.Arrow; + } + }); + } + catch { } + } + + private Vector3 GetMousePlaneIntersection(Vector2 mousePos, Vector3 planePoint, Vector3 planeNormal) + { + var viewport = _game.GraphicsDevice.Viewport; + var nearPoint = viewport.Unproject(new Vector3(mousePos.X, mousePos.Y, 0), _cameraService.Camera.ProjectionMatrix, _cameraService.Camera.ViewMatrix, Matrix.Identity); + var farPoint = viewport.Unproject(new Vector3(mousePos.X, mousePos.Y, 1), _cameraService.Camera.ProjectionMatrix, _cameraService.Camera.ViewMatrix, Matrix.Identity); + + var rayDir = farPoint - nearPoint; + rayDir.Normalize(); + + float denominator = Vector3.Dot(planeNormal, rayDir); + if (Math.Abs(denominator) > 0.0001f) + { + float t = Vector3.Dot(planePoint - nearPoint, planeNormal) / denominator; + return nearPoint + rayDir * t; + } + return planePoint; + } + + private Vector3 GetWorkPlaneNormal() + { + if (CurrentAxis == AxisLock.X) return Vector3.UnitZ; + if (CurrentAxis == AxisLock.Y) return Vector3.UnitZ; + if (CurrentAxis == AxisLock.Z) return Vector3.UnitX; + return Vector3.Normalize(_cameraService.Camera.LookAt - _cameraService.Camera.Position); + } + } +} diff --git a/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewViewModel.cs b/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewViewModel.cs index c7e6aec15..881b9973c 100644 --- a/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewViewModel.cs +++ b/Editors/MetaDataEditor/AnimationMeta/SuperView/SuperViewViewModel.cs @@ -1,4 +1,8 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; using Editors.AnimationMeta.Presentation; using Editors.AnimationMeta.SuperView.Visualisation; using Editors.Shared.Core.Common; @@ -10,6 +14,11 @@ using Shared.Core.PackFiles; using Shared.Core.ToolCreation; using Shared.GameFormats.AnimationMeta.Parsing; +using Shared.GameFormats.AnimationMeta.Definitions; +using Shared.Core.Services; +using GameWorld.Core.Services; +using GameWorld.Core.Components.Input; +using GameWorld.Core.SceneNodes; namespace Editors.AnimationMeta.SuperView { @@ -24,26 +33,34 @@ public partial class SuperViewViewModel : EditorHostBase, ISaveableEditor private readonly IEventHub _eventHub; private readonly IUiCommandFactory _uiCommandFactory; + private readonly IWpfGame _wpfGame; + private readonly FocusSelectableObjectService _cameraService; + private readonly IKeyboardComponent _keyboard; + private readonly IMouseComponent _mouse; + private SuperViewManipulatorComponent _manipulator; + + private bool _isHandlingDragComplete = false; + + private Dictionary _initialMatrices = new Dictionary(); + [ObservableProperty] string _persistentMetaFilePath = ""; [ObservableProperty] string _metaFilePath = ""; [ObservableProperty] MetaDataEditorViewModel _persistentMetaEditor; [ObservableProperty] MetaDataEditorViewModel _metaEditor; [ObservableProperty] int _selectedTabControllerIndex = 0; + public override Type EditorViewModelType => typeof(EditorView); + public bool HasUnsavedChanges - { - get - { - return PersistentMetaEditor.HasUnsavedChanges || MetaEditor.HasUnsavedChanges; - } - set + { + get { return PersistentMetaEditor.HasUnsavedChanges || MetaEditor.HasUnsavedChanges; } + set { PersistentMetaEditor.HasUnsavedChanges = value; MetaEditor.HasUnsavedChanges = value; - } + } } - public SuperViewViewModel( IPackFileService packFileService, IEventHub eventHub, @@ -51,7 +68,12 @@ public SuperViewViewModel( SceneObjectEditor sceneObjectBuilder, IEditorHostParameters editorHostParameters, MetaDataFileParser metaDataFileParser, - IMetaDataBuilder metaDataFactory) + IMetaDataBuilder metaDataFactory, + IWpfGame wpfGame, + FocusSelectableObjectService cameraService, + IKeyboardComponent keyboard, + IMouseComponent mouse + ) : base(editorHostParameters) { DisplayName = "Super view"; @@ -61,16 +83,39 @@ public SuperViewViewModel( _sceneObjectBuilder = sceneObjectBuilder; _metaDataFileParser = metaDataFileParser; _metaDataFactory = metaDataFactory; + + _wpfGame = wpfGame; + _cameraService = cameraService; + _keyboard = keyboard; + _mouse = mouse; + Initialize(); + eventHub.Register(this, OnFileSaved); eventHub.Register(this, OnSceneObjectUpdated); eventHub.Register(this, OnMetaDataAttributeChanged); eventHub.Register(this, OnSelectedMetaDataAttributeChanged); } - private void OnSelectedMetaDataAttributeChanged(SelecteMetaDataAttributeChangedEvent @event) => RecreateMetaDataInformation(); - void OnMetaDataAttributeChanged(MetaDataAttributeChangedEvent @event) => RecreateMetaDataInformation(); - void OnMetaDataChanged(SceneObject sceneObject) => RecreateMetaDataInformation(); + private void OnSelectedMetaDataAttributeChanged(SelecteMetaDataAttributeChangedEvent @event) + { + if (_manipulator != null) + _manipulator.SelectedAttribute = MetaEditor.SelectedAttribute ?? PersistentMetaEditor.SelectedAttribute; + + try { System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { _wpfGame.GetFocusElement()?.Focus(); }); } catch { } + + if (!_isHandlingDragComplete) RecreateMetaDataInformation(); + } + + void OnMetaDataAttributeChanged(MetaDataAttributeChangedEvent @event) + { + if (!_isHandlingDragComplete) RecreateMetaDataInformation(); + } + + void OnMetaDataChanged(SceneObject sceneObject) + { + if (!_isHandlingDragComplete) RecreateMetaDataInformation(); + } private void OnFileSaved(ScopedFileSavedEvent evnt) { @@ -83,29 +128,152 @@ private void OnFileSaved(ScopedFileSavedEvent evnt) throw new Exception($"Unable to determine file owner when reciving a file save event in SuperView. Owner:{evnt.FileOwner}, File:{evnt.NewPath}"); } + private int GetTargetInstanceIndex(ParsedMetadataAttribute targetAttr) + { + if (targetAttr == null) return -1; + int index = 0; + + bool ProcessFile(ParsedMetadataFile file) + { + if (file == null) return false; + bool CheckList(IEnumerable items) + { + foreach (var item in items) + { + if (ReferenceEquals(item, targetAttr) || item.Equals(targetAttr)) return true; + index++; + } + return false; + } + + if (CheckList(file.GetItemsOfType())) return true; + if (CheckList(file.GetItemsOfType())) return true; + if (CheckList(file.GetItemsOfType())) return true; + if (CheckList(file.GetItemsOfType())) return true; + if (CheckList(file.GetItemsOfType())) return true; + if (CheckList(file.GetItemsOfType())) return true; + + return false; + } + + if (MetaEditor.ParsedFile == null || MetaEditor.ParsedFile.GetItemsOfType().Count == 0) + { + if (ProcessFile(PersistentMetaEditor.ParsedFile)) return index; + } + if (ProcessFile(MetaEditor.ParsedFile)) return index; + + return -1; + } + void Initialize() { PersistentMetaEditor = new MetaDataEditorViewModel(_uiCommandFactory, _metaDataFileParser, _eventHub); MetaEditor = new MetaDataEditorViewModel(_uiCommandFactory, _metaDataFileParser, _eventHub); - - var assetViewModel = _sceneObjectViewModelBuilder.CreateAsset("SuperViewRoot", true, "Root", Color.Black,null); + + var assetViewModel = _sceneObjectViewModelBuilder.CreateAsset("SuperViewRoot", true, "Root", Color.Black, null); SceneObjects.Add(assetViewModel); assetViewModel.Data.MetaDataChanged += OnMetaDataChanged; - _asset = assetViewModel; OnSceneObjectUpdated(new SceneObjectUpdateEvent(_asset.Data, false, false, false, true)); - } - + _manipulator = new SuperViewManipulatorComponent(_wpfGame, _cameraService, _keyboard, _mouse); + + _manipulator.GetSelectedNode = () => + { + if (_manipulator.SelectedAttribute == null) return null; + int targetIndex = GetTargetInstanceIndex(_manipulator.SelectedAttribute); + if (targetIndex >= 0 && targetIndex < _asset.Data.MetaDataItems.Count) + { + var inst = _asset.Data.MetaDataItems[targetIndex]; + var nodeField = inst.GetType().GetField("_node", BindingFlags.NonPublic | BindingFlags.Instance); + return nodeField?.GetValue(inst) as SceneNode; + } + return null; + }; + + // 【FIX】: Pass the actual bone world matrix to the manipulator to prevent position offset after dragging + _manipulator.GetBoneWorldMatrix = () => + { + if (_manipulator.SelectedAttribute != null && _asset != null && _asset.Data != null) + { + try + { + dynamic meta = _manipulator.SelectedAttribute; + int boneId = -1; + + try { boneId = meta.BoneId; } catch { try { boneId = meta.NodeIndex; } catch { } } + + if (boneId >= 0) + { + dynamic data = _asset.Data; + if (data.Skeleton != null) + { + return data.Skeleton.GetAnimatedWorldTranform(boneId); + } + } + } + catch { } + } + return Matrix.Identity; + }; + + _manipulator.OnDragStarted += () => + { + _initialMatrices.Clear(); + foreach (var inst in _asset.Data.MetaDataItems) + { + var nodeField = inst.GetType().GetField("_node", BindingFlags.NonPublic | BindingFlags.Instance); + if (nodeField != null && nodeField.GetValue(inst) is SceneNode node) + _initialMatrices[node] = node.ModelMatrix; + } + }; + + _manipulator.OnDragUpdate += (worldDeltaPos, worldDeltaRot) => + { + try + { + var node = _manipulator.SelectedNode; + if (node != null && _initialMatrices.TryGetValue(node, out Matrix initialMatrix)) + { + Vector3 pivotWorld = _manipulator.TrueWorldPivot; + + node.ModelMatrix = initialMatrix * + Matrix.CreateTranslation(-pivotWorld) * + Matrix.CreateFromQuaternion(worldDeltaRot) * + Matrix.CreateTranslation(pivotWorld) * + Matrix.CreateTranslation(worldDeltaPos); + } + } + catch { } + }; + + _manipulator.OnDragCompleted += () => + { + _isHandlingDragComplete = true; + HasUnsavedChanges = true; + + var currentActiveEditor = MetaEditor.SelectedAttribute != null ? MetaEditor : PersistentMetaEditor; + if (currentActiveEditor.SelectedTag != null) + { + int selectedIndex = currentActiveEditor.Tags.IndexOf(currentActiveEditor.SelectedTag); + currentActiveEditor.UpdateView(); + if (selectedIndex >= 0 && selectedIndex < currentActiveEditor.Tags.Count) + currentActiveEditor.SelectedTag = currentActiveEditor.Tags[selectedIndex]; + } + + RecreateMetaDataInformation(); + _isHandlingDragComplete = false; + }; + + _wpfGame.AddComponent(_manipulator); + } void RecreateMetaDataInformation() { foreach (var item in SceneObjects) { - foreach (var t in item.Data.MetaDataItems) - t.CleanUp(); - + foreach (var t in item.Data.MetaDataItems) t.CleanUp(); item.Data.MetaDataItems.Clear(); item.Data.Player.AnimationRules.Clear(); } @@ -121,7 +289,6 @@ private void OnSceneObjectUpdated(SceneObjectUpdateEvent e) { PersistentMetaEditor.LoadFile(e.Owner.PersistMetaData); MetaEditor.LoadFile(e.Owner.MetaData); - RecreateMetaDataInformation(); } @@ -129,22 +296,20 @@ public void Load(AnimationToolInput debugDataToLoad) { _sceneObjectBuilder.SetMesh(_asset.Data, debugDataToLoad.Mesh); - // Hack :( if (debugDataToLoad.AnimationSlot != null) { var frag = _asset.FragAndSlotSelection.FragmentList.PossibleValues.FirstOrDefault(x => x.FullPath == debugDataToLoad.FragmentName); _asset.FragAndSlotSelection.FragmentList.SelectedItem = frag; - var slot = _asset.FragAndSlotSelection.FragmentSlotList.PossibleValues.First(x => x.SlotName == debugDataToLoad.AnimationSlot.Value); _asset.FragAndSlotSelection.FragmentSlotList.SelectedItem = slot; } } - public void RefreshAction() => _asset.Data.TriggerMeshChanged(); public override void Close() { + if (_manipulator != null) _wpfGame.RemoveComponent(_manipulator); _eventHub?.UnRegister(this); base.Close(); } diff --git a/Editors/MetaDataEditor/AnimationMeta/SuperView/Visualisation/MetaDataBuilder.cs b/Editors/MetaDataEditor/AnimationMeta/SuperView/Visualisation/MetaDataBuilder.cs index b5c3487c4..884b64f51 100644 --- a/Editors/MetaDataEditor/AnimationMeta/SuperView/Visualisation/MetaDataBuilder.cs +++ b/Editors/MetaDataEditor/AnimationMeta/SuperView/Visualisation/MetaDataBuilder.cs @@ -193,11 +193,23 @@ private IMetaDataInstance CreateAnimatedProp(IAnimatedPropMeta animatedPropMeta, }); loadedNode.ScaleMult = animatedPropMeta.Scale; + // Add the animation rules // Add the animation rules var animationRule = new CopyRootTransform(rootSkeleton, animatedPropMeta.BoneId, animatedPropMeta.Position, new Quaternion(animatedPropMeta.Orientation)); propPlayer.AnimationRules.Add(animationRule); - if(rootPlayer.IsPlaying) + + // [FIX 1]: Force IsEnabled to true. Otherwise, if paused, Refresh() will return immediately and the bone transform won't calculate. + propPlayer.IsEnabled = true; + + // Sync the exact frame to prevent the prop from resetting its own animation. + propPlayer.CurrentFrame = rootPlayer.CurrentFrame; + + // Inherit the play/pause state from the root player. + if (rootPlayer.IsPlaying) propPlayer.Play(); + else + propPlayer.Pause(); + propPlayer.Refresh(); // Add to scene @@ -287,17 +299,36 @@ private IMetaDataInstance CreateEffect(IEffectMeta effect, SceneNode root, ISkel var node = new SimpleDrawableNode("Effect:" + effect.VfxName); var locatorScale = 0.3f; - node.AddItem(LineHelper.AddRgbLocator(effect.Position, locatorScale)); + var textOffset = locatorScale * 0.5f + 0.01f; + + // 获取特效的旋转并生成变换矩阵 (基于四元数) + Quaternion rotationQuat = new Quaternion(effect.Orientation); + Matrix rotMatrix = Matrix.CreateFromQuaternion(rotationQuat); + + // 将全局标准轴乘以旋转矩阵,计算出特效当前的局部坐标轴向 + Vector3 localX = Vector3.Transform(Vector3.UnitX, rotMatrix); + Vector3 localY = Vector3.Transform(Vector3.UnitY, rotMatrix); + Vector3 localZ = Vector3.Transform(Vector3.UnitZ, rotMatrix); + + // 绘制受旋转影响的三根轴线 (红=X, 绿=Y, 蓝=Z) + node.AddItem(LineHelper.AddLine(effect.Position, effect.Position + localX * locatorScale, Color.Red)); + node.AddItem(LineHelper.AddLine(effect.Position, effect.Position + localY * locatorScale, Color.Green)); + node.AddItem(LineHelper.AddLine(effect.Position, effect.Position + localZ * locatorScale, Color.Blue)); + + // 添加特效名称标签(原点) node.AddItem(new WorldTextRenderItem(_resourceLibrary, effect.VfxName, effect.Position, color)); - node.AddItem(new WorldTextRenderItem(_resourceLibrary, "X", effect.Position + new Vector3(locatorScale * .5f + 0.01f,0,0), Color.Red)); - node.AddItem(new WorldTextRenderItem(_resourceLibrary, "Y", effect.Position + new Vector3(0, locatorScale * .5f + 0.01f, 0), Color.Green)); - node.AddItem(new WorldTextRenderItem(_resourceLibrary, "Z", effect.Position + new Vector3(0,0,locatorScale * .5f + 0.01f), Color.Blue)); + + // 添加跟随轴向旋转的 XYZ 文本标签 + node.AddItem(new WorldTextRenderItem(_resourceLibrary, "X", effect.Position + localX * textOffset, Color.Red)); + node.AddItem(new WorldTextRenderItem(_resourceLibrary, "Y", effect.Position + localY * textOffset, Color.Green)); + node.AddItem(new WorldTextRenderItem(_resourceLibrary, "Z", effect.Position + localZ * textOffset, Color.Blue)); root.AddObject(node); var instance = new DrawableMetaInstance(effect.EffectStartTime, effect.EffectEndTime, node.Name, node); if (effect.Tracking) instance.FollowBone(skeleton, effect.NodeIndex); + return instance; } } diff --git a/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/SkeletonEditor/EditorView.xaml b/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/SkeletonEditor/EditorView.xaml index 687f04fd9..0b3ca53e6 100644 --- a/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/SkeletonEditor/EditorView.xaml +++ b/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/SkeletonEditor/EditorView.xaml @@ -9,6 +9,15 @@ xmlns:mathviews="clr-namespace:CommonControls.MathViews;assembly=Shared.Ui" mc:Ignorable="d" d:DesignHeight="850" d:DesignWidth="800"> + + + + + + + + +