using System; using System.Collections.Generic; using RosettaUI.Builder; using UnityEngine; using UnityEngine.UIElements; namespace RosettaUI.UIToolkit.AnimationCurveEditor { /// /// A visual element that allows you to edit an . /// public class AnimationCurveEditor : VisualElement, IDisposable { #region Static Window Management private static ModalWindow _window; private static AnimationCurveEditor _animationCurveEditorInstance; static AnimationCurveEditor() { StaticResourceUtility.AddResetStaticResourceCallback(ReleaseStaticResource); void ReleaseStaticResource() { _window?.Hide(); _window = null; _animationCurveEditorInstance?.Dispose(); _animationCurveEditorInstance = null; } } public static void Show(Vector2 position, VisualElement target, AnimationCurve initialCurve, Action onCurveChanged) { if (_window == null) { _window = new ModalWindow(true) { style = { width = 800, height = 500, minWidth = 500 } }; _animationCurveEditorInstance = new AnimationCurveEditor(); _window.Add(_animationCurveEditorInstance); _window.RegisterCallback(_ => _window.Hide()); _window.RegisterCallback(_ => { onCurveChanged?.Invoke(initialCurve); _window.Hide(); }); } _window.Show(position, target); // Preventing Overflow if (!float.IsNaN(_window.resolvedStyle.width) && !float.IsNaN(_window.resolvedStyle.height)) { VisualElementExtension.CheckOutOfScreen(position, _window); } // Panel is not set before Show(), so callback registration and other operations are performed after Show(). _animationCurveEditorInstance.SetCurve(initialCurve); _animationCurveEditorInstance.OnCurveChanged += onCurveChanged; _animationCurveEditorInstance.RegisterCallback(OnDetach); return; void OnDetach(DetachFromPanelEvent _) { _animationCurveEditorInstance.OnCurveChanged -= onCurveChanged; _animationCurveEditorInstance.UnregisterCallback(OnDetach); target?.Focus(); } } #endregion public event Action OnCurveChanged; private CurvePointContainer _curvePointContainer; private CurvePreview _curvePreview; private RenderTexture _curveEditorTexture = null; private VisualElement _curvePreviewElement; private VisualElementKeyEventHelper _keyEventHelper; private ToggleButton _snapXButton; private ToggleButton _snapYButton; private ScrollerController _scrollerController; private AxisLabelController _axisLabelController; private PropertyFieldController _propertyFieldController; private PreviewTransform _previewTransform; private int _selectedControlPointIndex = -1; private int _mouseButton; private Vector2 _prevPointerPosition; private long _lastPointerDownTime; private bool _prevSnapX = false; private bool _prevSnapY = false; private static readonly string USSClassName = "rosettaui-animation-curve-editor"; private static readonly Vector2 ZoomRange = new(0.0001f, 10000f); private static readonly float FitViewPadding = 0.05f; public AnimationCurveEditor() { var visualTreeAsset = Resources.Load("RosettaUI_AnimationCurveEditor"); visualTreeAsset.CloneTree(this); AddToClassList(USSClassName); InitUI(); _curvePointContainer = new CurvePointContainer(_curvePreviewElement, () => new ControlPoint(_previewTransform, OnControlPointSelected, OnControlPointMoved, RemoveControlPoint)); } /// /// Set the curve to be edited. /// public void SetCurve(AnimationCurve curve) { _curvePointContainer.SetCurve(curve); FitViewToCurve(); UpdateView(); UnselectAllControlPoint(); } public void Dispose() { if (_curvePreview != null) { _curvePreview.Dispose(); _curvePreview = null; } if (_curveEditorTexture != null) { UnityEngine.Object.DestroyImmediate(_curveEditorTexture); _curveEditorTexture = null; } } private void MoveKey(Keyframe key) { if (_curvePointContainer.IsEmpty || _selectedControlPointIndex < 0) return; var gridViewport = new GridViewport(_previewTransform.GetPreviewRect()); if (_snapXButton.value) { key.time = gridViewport.RoundX(key.time, 0.05f); } if (_snapYButton.value) { key.value = gridViewport.RoundY(key.value, 0.05f); } _selectedControlPointIndex = _curvePointContainer.MoveKey(_selectedControlPointIndex, key); } #region Initialization private void InitUI() { // Key Bind _keyEventHelper = new VisualElementKeyEventHelper(this); _keyEventHelper.RegisterKeyAction(new [] { KeyCode.LeftControl, KeyCode.LeftCommand, KeyCode.RightControl, KeyCode.RightCommand }, evt => { switch (evt) { case KeyEventType.KeyDown: _prevSnapX = _snapXButton.value; _prevSnapY = _snapYButton.value; _snapXButton.SetValueWithoutNotify(true); _snapYButton.SetValueWithoutNotify(true); break; case KeyEventType.KeyUp: _snapXButton.SetValueWithoutNotify(_prevSnapX); _snapYButton.SetValueWithoutNotify(_prevSnapY); break; case KeyEventType.KeyPress: default: break; } }); _keyEventHelper.RegisterKeyAction(KeyCode.A, evt => { if (evt == KeyEventType.KeyUp) { FitViewToCurve(); } }); _keyEventHelper.RegisterKeyAction(KeyCode.Delete, evt => { if (evt != KeyEventType.KeyUp || _selectedControlPointIndex < 0) return; RemoveControlPoint(_curvePointContainer.ControlPoints[_selectedControlPointIndex]); }); // Buttons var fitButton = this.Q