using System; using System.Collections.Generic; using System.Linq; using RosettaUI.Builder; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UIElements; namespace RosettaUI.UIToolkit.AnimationCurveEditor { /// /// 選択中のすべてのControlPointを囲むRect /// public class SelectedControlPointsRect : VisualElement { #region RectSide enum and helper methods private enum RectSide { Top, Bottom, Left, Right } private static float GetSideValue(Rect rect, RectSide side) { return side switch { RectSide.Top => rect.yMin, RectSide.Bottom => rect.yMax, RectSide.Left => rect.xMin, RectSide.Right => rect.xMax, _ => throw new ArgumentOutOfRangeException(nameof(side), side, null) }; } private static Rect SetSideValue(Rect rect, RectSide side, float value) { return side switch { RectSide.Top => Rect.MinMaxRect(rect.xMin, value, rect.xMax, rect.yMax), RectSide.Bottom => Rect.MinMaxRect(rect.xMin, rect.yMin, rect.xMax, value), RectSide.Left => Rect.MinMaxRect(value, rect.yMin, rect.xMax, rect.yMax), RectSide.Right => Rect.MinMaxRect(rect.xMin, rect.yMin, value, rect.yMax), _ => throw new ArgumentOutOfRangeException(nameof(side), side, null) }; } // 1辺を移動して形を変えられるRect private class EdgeMovableRect { private Rect _originalRect; private float _sideValue; public RectSide Side { get; private set; } public void Setup(RectSide side, Rect rect) { Side = side; _originalRect = rect; _sideValue = GetSideValue(rect, side); } public Rect MoveEdge(float delta) { _sideValue += delta; return SetSideValue(_originalRect, Side, _sideValue); } } #endregion public static float ThicknessMin { get; set; } = 15f; private const string UssClassName = "rosettaui-animation-curve-editor__selected-control-points-rect"; private const string HandleClassName = UssClassName + "__handle"; private const string HandleHorizontalClassName = HandleClassName + "--horizontal"; private const string HandleVerticalClassName = HandleClassName + "--vertical"; private const string HandleTopClassName = HandleClassName + "--top"; private const string HandleBottomClassName = HandleClassName + "--bottom"; private const string HandleLeftClassName = HandleClassName + "--left"; private const string HandleRightClassName = HandleClassName + "--right"; private const string HandleLineClassName = HandleClassName + "__center-line"; private readonly CurveController _curveController; private readonly PreviewTransform _previewTransform; private readonly CurveSnapshot _curveSnapshot; private readonly Dictionary _normalizedPositionsOnDragStart = new(); private readonly EdgeMovableRect _edgeMovableRect = new(); private readonly VisualElement _topHandle; private readonly VisualElement _bottomHandle; private readonly VisualElement _leftHandle; private readonly VisualElement _rightHandle; private CurveController.RecordUndoSnapshotScope _undoScopeOnHandleDragStarting; private Rect ControlPointsRect { get { using var _ = ListPool.Get(out var list); list.AddRange(_curveController.SelectedControlPoints); if (list.Count == 0) { return default; } var firstPos = list[0].LocalPosition; var min = firstPos; var max = firstPos; foreach (var pos in list.Skip(1).Select(cp => cp.LocalPosition)) { min = Vector2.Min(min, pos); max = Vector2.Max(max, pos); } return Rect.MinMaxRect(min.x, min.y, max.x, max.y); } } public SelectedControlPointsRect(CurveController curveController, PreviewTransform previewTransform) { _curveController = curveController; _previewTransform = previewTransform; _curveSnapshot = new CurveSnapshot(curveController); AddToClassList(UssClassName); _topHandle = CreateHandle(RectSide.Top, HandleTopClassName, HandleHorizontalClassName); Add(_topHandle); _bottomHandle = CreateHandle(RectSide.Bottom, HandleBottomClassName, HandleHorizontalClassName); Add(_bottomHandle); _leftHandle = CreateHandle(RectSide.Left, HandleLeftClassName, HandleVerticalClassName); Add(_leftHandle); _rightHandle = CreateHandle(RectSide.Right, HandleRightClassName, HandleVerticalClassName); Add(_rightHandle); return; VisualElement CreateHandle(RectSide side, string classNameDir, string classNameHv) { var handle = new VisualElement(); handle.AddToClassList(HandleClassName); handle.AddToClassList(classNameDir); handle.AddToClassList(classNameHv); // handle.style.cursor = CursorHolder.Get(cursorType); var dragManipulator = new DragManipulator( onDragStarting: evt => OnHandleDragStarting(evt, side), onDrag: OnHandleDrag, onDragEnd: OnHandleDragEnd ); handle.AddManipulator(dragManipulator); var line = new VisualElement { pickingMode = PickingMode.Ignore }; line.AddToClassList(HandleLineClassName); handle.Add(line); return handle; } } public void UpdateView() { var rect = ControlPointsRect; if (rect is { width: 0, height: 0 }) { this.Hide(); return; } // ハンドルの表示/非表示を設定 var topBottomEnable = rect.height >= float.Epsilon; var leftRightEnable = rect.width >= float.Epsilon; _topHandle.SetShow(topBottomEnable); _bottomHandle.SetShow(topBottomEnable); _leftHandle.SetShow(leftRightEnable); _rightHandle.SetShow(leftRightEnable); // 厚みがThicknessMin以下にならないように制限(中心位置は変えない) var center = rect.center; rect.width = Mathf.Max(rect.width, ThicknessMin); rect.height = Mathf.Max(rect.height, ThicknessMin); rect.center = center; style.left = rect.x; style.top = rect.y; style.width = rect.width; style.height = rect.height; this.Show(); } #region Drag callbacks private bool OnHandleDragStarting(PointerDownEvent evt, RectSide side) { if (evt.button != 0) return false; _undoScopeOnHandleDragStarting = _curveController.RecordUndoSnapshot(); var rectOnParent = ControlPointsRect; var rectOnCurve = new Rect() { min = _previewTransform.GetCurvePosFromScreenPos(rectOnParent.min), max = _previewTransform.GetCurvePosFromScreenPos(rectOnParent.max) }; _edgeMovableRect.Setup(side, rectOnCurve); _normalizedPositionsOnDragStart.Clear(); foreach (var cp in _curveController.SelectedControlPoints) { var normalized = Rect.PointToNormalized(rectOnCurve, cp.KeyframePosition); _normalizedPositionsOnDragStart[cp] = normalized; } // 選択されていないKeyframeを保存しておく // ドラッグ中に選択中のKeyframeが同一timeに来ると上書きされて消えるが、 // さらにドラッグしてtimeがずれたら復活したい _curveSnapshot.TakeSnapshot(); evt.StopPropagation(); return true; } // 表示されるRectとは別に論理的にControlPointsのBoundingRectを計算する // 表示されるRectは厚みがThicknessMin以下にならないように制限しているが、論理Rectは制限なし // ドラッグ中にZoomされてもいいように論理RectはCurve座標系 private void OnHandleDrag(PointerMoveEvent evt) { // 未選択Keyframeの追加を試みる // ドラッグ中に選択中のKeyframeが同一timeに来ると上書きされて消えるがさらにドラッグしてtimeがずれたら復活させる // すでに同一timeにKeyframeがある場合はAddKeyframeで無視されるので問題ない _curveSnapshot.ApplySnapshotWithoutSelection(); var delta = _previewTransform.GetCurvePosFromScreenPos(evt.deltaPosition) - _previewTransform.GetCurvePosFromScreenPos(Vector2.zero); var deltaValue = _edgeMovableRect.Side is RectSide.Left or RectSide.Right ? delta.x : delta.y; var rect = _edgeMovableRect.MoveEdge(deltaValue); _curveController.UpdateSelectedControlPointKeyframes(cp => { if (!_normalizedPositionsOnDragStart.TryGetValue(cp, out var normalized)) { return cp.Keyframe; } var keyframePosition = Rect.NormalizedToPoint(rect, normalized); keyframePosition = _previewTransform.SnapCurvePositionIfEnabled(keyframePosition); var keyframe = cp.Keyframe; keyframe.SetPosition(keyframePosition); return keyframe; }); evt.StopPropagation(); } private void OnHandleDragEnd(EventBase obj) { _undoScopeOnHandleDragStarting.Dispose(); _undoScopeOnHandleDragStarting = default; } #endregion } }