From 1716eba8ec5d2eef1dc87a908311ff417d536311 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 10 Feb 2022 23:38:27 +0100 Subject: [PATCH] Transform tool - Mostly implemented resizing --- .../Extensions/LayerExtensions.cs | 48 ++--- .../Tools/TransformToolView.axaml.cs | 140 +++++++++++--- .../Tools/TransformToolViewModel.cs | 177 ++++++++++++++++-- 3 files changed, 302 insertions(+), 63 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs index 2de45c517..a1d2cd5d9 100644 --- a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs @@ -45,30 +45,14 @@ public static class LayerExtensions /// /// Returns an absolute and scaled rectangular path for the given layer in real coordinates. /// - public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation, SKPoint? anchorOverride = null) + public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation) { SKRect layerBounds = GetLayerBounds(layer); - // Apply transformation like done by the core during layer rendering (same differences apply as in GetLayerTransformGroup) - SKPoint anchorPosition = GetLayerAnchorPosition(layer); - if (anchorOverride != null) - anchorPosition = anchorOverride.Value; - - SKPoint anchorProperty = layer.Transform.AnchorPoint.CurrentValue; - - // Translation originates from the unscaled center of the shape and is tied to the anchor - float x = anchorPosition.X - layerBounds.MidX - anchorProperty.X * layerBounds.Width; - float y = anchorPosition.Y - layerBounds.MidY - anchorProperty.Y * layerBounds.Height; - + SKMatrix transform = layer.GetTransformMatrix(false, includeTranslation, includeScale, includeRotation, layerBounds); SKPath path = new(); path.AddRect(layerBounds); - if (includeTranslation) - path.Transform(SKMatrix.CreateTranslation(x, y)); - if (includeScale) - path.Transform(SKMatrix.CreateScale(layer.Transform.Scale.CurrentValue.Width / 100f, layer.Transform.Scale.CurrentValue.Height / 100f, anchorPosition.X, anchorPosition.Y)); - if (includeRotation) - path.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue, anchorPosition.X, anchorPosition.Y)); - + path.Transform(transform); return path; } @@ -91,19 +75,27 @@ public static class LayerExtensions } /// - /// Returns the offset from the given point to the top-left of the layer + /// Returns the offset from the given point to the closest sides of the layer's shape bounds /// public static SKPoint GetDragOffset(this Layer layer, SKPoint dragStart) { - // Figure out what the top left will be if the shape moves to the current cursor position - SKPoint scaledDragStart = GetScaledPoint(layer, dragStart, true); - SKPoint tempAnchor = GetLayerAnchorPosition(layer, scaledDragStart); - SKPoint tempTopLeft = GetLayerPath(layer, true, true, true, tempAnchor)[0]; + SKRect bounds = layer.GetLayerPath(true, true, false).Bounds; + SKPoint anchor = layer.GetLayerAnchorPosition(); - // Get the shape's position - SKPoint topLeft = GetLayerPath(layer, true, true, true)[0]; + float xOffset = 0f, yOffset = 0f; - // The difference between the two is the offset - return topLeft - tempTopLeft; + // X offset + if (dragStart.X < anchor.X) + xOffset = bounds.Left - dragStart.X; + else if (dragStart.X > anchor.X) + xOffset = bounds.Right - dragStart.X; + + // Y offset + if (dragStart.Y < anchor.Y) + yOffset = bounds.Top - dragStart.Y; + else if (dragStart.Y > anchor.Y) + yOffset = bounds.Bottom - dragStart.Y; + + return new SKPoint(xOffset, yOffset); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs index 67562ca14..cf78734c3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs @@ -1,23 +1,63 @@ using System; using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Extensions; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; +using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Skia; +using Avalonia.Styling; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolView : ReactiveUserControl { private ZoomBorder? _zoomBorder; - private PointerPoint _dragOffset; + private SKPoint _dragStart; + private SKPoint _dragOffset; + + private readonly Ellipse _rotateTopLeft; + private readonly Ellipse _rotateTopRight; + private readonly Ellipse _rotateBottomRight; + private readonly Ellipse _rotateBottomLeft; + + private readonly Rectangle _resizeTopCenter; + private readonly Rectangle _resizeRightCenter; + private readonly Rectangle _resizeBottomCenter; + private readonly Rectangle _resizeLeftCenter; + private readonly Rectangle _resizeTopLeft; + private readonly Rectangle _resizeTopRight; + private readonly Rectangle _resizeBottomRight; + private readonly Rectangle _resizeBottomLeft; + + private readonly Ellipse _anchorPoint; public TransformToolView() { InitializeComponent(); + + _rotateTopLeft = this.Get("RotateTopLeft"); + _rotateTopRight = this.Get("RotateTopRight"); + _rotateBottomRight = this.Get("RotateBottomRight"); + _rotateBottomLeft = this.Get("RotateBottomLeft"); + + _resizeTopCenter = this.Get("ResizeTopCenter"); + _resizeRightCenter = this.Get("ResizeRightCenter"); + _resizeBottomCenter = this.Get("ResizeBottomCenter"); + _resizeLeftCenter = this.Get("ResizeLeftCenter"); + _resizeTopLeft = this.Get("ResizeTopLeft"); + _resizeTopRight = this.Get("ResizeTopRight"); + _resizeBottomRight = this.Get("ResizeBottomRight"); + _resizeBottomLeft = this.Get("ResizeBottomLeft"); + + _anchorPoint = this.Get("AnchorPoint"); } private void InitializeComponent() @@ -30,7 +70,7 @@ public class TransformToolView : ReactiveUserControl /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - _zoomBorder = (ZoomBorder?)this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); if (_zoomBorder != null) _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; base.OnAttachedToLogicalTree(e); @@ -58,11 +98,12 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); e.Pointer.Capture(element); e.Handled = true; @@ -70,8 +111,8 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Pointer.Capture(null); @@ -80,8 +121,8 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?) sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Handled = true; @@ -93,11 +134,12 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); e.Pointer.Capture(element); e.Handled = true; @@ -105,8 +147,8 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Pointer.Capture(null); @@ -115,8 +157,8 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Handled = true; @@ -128,11 +170,15 @@ public class TransformToolView : ReactiveUserControl private void ResizeOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); + + SKPoint position = GetPositionForViewModel(e); + ViewModel.StartResize(position); e.Pointer.Capture(element); e.Handled = true; @@ -140,22 +186,70 @@ public class TransformToolView : ReactiveUserControl private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) return; + SKPoint position = GetPositionForViewModel(e); + ViewModel.FinishResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); + e.Pointer.Capture(null); e.Handled = true; } private void ResizeOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; + SKPoint position = GetPositionForViewModel(e); + ViewModel.UpdateResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); + e.Handled = true; } #endregion + + private SKPoint GetPositionForViewModel(PointerEventArgs e) + { + if (ViewModel?.Layer == null) + return SKPoint.Empty; + + SKPoint point = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + return point + _dragOffset; + } + + private static SKPoint CounteractLayerRotation(SKPoint point, Layer layer) + { + SKPoint pivot = layer.GetLayerAnchorPosition(); + + using SKPath counterRotatePath = new(); + counterRotatePath.AddPoly(new[] {SKPoint.Empty, point}, false); + counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y)); + + return counterRotatePath.Points[1]; + } + + private TransformToolViewModel.ResizeSide GetResizeDirection(Shape shape) + { + if (ReferenceEquals(shape, _resizeTopLeft)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(shape, _resizeTopRight)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomRight)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomLeft)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(shape, _resizeTopCenter)) + return TransformToolViewModel.ResizeSide.Top; + if (ReferenceEquals(shape, _resizeRightCenter)) + return TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomCenter)) + return TransformToolViewModel.ResizeSide.Bottom; + if (ReferenceEquals(shape, _resizeLeftCenter)) + return TransformToolViewModel.ResizeSide.Left; + + throw new ArgumentException("Given shape isn't a resize shape"); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs index e1b53e1e9..d78c51f2b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -1,9 +1,12 @@ using System; +using System.Diagnostics; +using System.Linq; using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; using Material.Icons; @@ -14,6 +17,7 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolViewModel : ToolViewModel { + private readonly IProfileEditorService _profileEditorService; private readonly ObservableAsPropertyHelper _isEnabled; private RelativePoint _relativeAnchor; private double _inverseRotation; @@ -21,10 +25,13 @@ public class TransformToolViewModel : ToolViewModel private double _rotation; private Rect _shapeBounds; private Point _anchor; + private TimeSpan _time; /// public TransformToolViewModel(IProfileEditorService profileEditorService) { + _profileEditorService = profileEditorService; + // Not disposed when deactivated but when really disposed _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); @@ -69,7 +76,11 @@ public class TransformToolViewModel : ToolViewModel .DisposeWith(d); this.WhenAnyValue(vm => vm.Layer).Subscribe(_ => Update()).DisposeWith(d); - profileEditorService.Time.Subscribe(_ => Update()).DisposeWith(d); + profileEditorService.Time.Subscribe(t => + { + _time = t; + Update(); + }).DisposeWith(d); }); } @@ -151,17 +162,159 @@ public class TransformToolViewModel : ToolViewModel Anchor = new Point(ShapeBounds.Width * RelativeAnchor.Point.X, ShapeBounds.Height * RelativeAnchor.Point.Y); } - public enum ShapeControlPoint + #region Resizing + + private SKSize _resizeStartScale; + private bool _hadKeyframe; + + public void StartResize(SKPoint position) { - TopLeft, - TopRight, - BottomRight, - BottomLeft, - TopCenter, - RightCenter, - BottomCenter, - LeftCenter, - LayerShape, - Anchor + if (Layer == null) + return; + + _resizeStartScale = Layer.Transform.Scale.CurrentValue; + _hadKeyframe = Layer.Transform.Scale.Keyframes.Any(k => k.Position == _time); + } + + public void FinishResize(SKPoint position, ResizeSide side, bool evenSides) + { + if (Layer == null) + return; + + // Grab the size one last time + SKSize size = UpdateResize(position, side, evenSides); + + // If the layer has keyframes, new keyframes may have been added while the user was dragging + if (Layer.Transform.Scale.KeyframesEnabled) + { + // If there was already a keyframe at the old spot, edit that keyframe + if (_hadKeyframe) + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); + // If there was no keyframe yet, remove the keyframe that was created while dragging and create a permanent one + else + { + Layer.Transform.Scale.RemoveKeyframe(Layer.Transform.Scale.Keyframes.First(k => k.Position == _time)); + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _time)); + } + } + else + { + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); + } + } + + public SKSize UpdateResize(SKPoint position, ResizeSide side, bool evenSides) + { + if (Layer == null) + return SKSize.Empty; + + SKPoint normalizedAnchor = Layer.Transform.AnchorPoint; + // TODO Remove when anchor is centralized at 0.5,0.5 + normalizedAnchor = new SKPoint(normalizedAnchor.X + 0.5f, normalizedAnchor.Y + 0.5f); + + // The anchor is used to ensure a side can't shrink past the anchor + SKPoint anchor = Layer.GetLayerAnchorPosition(); + // The bounds are used to determine whether to shrink or grow + SKRect shapeBounds = Layer.GetLayerPath(true, true, false).Bounds; + + float width = shapeBounds.Width; + float height = shapeBounds.Height; + + // Resize each side as requested, the sides of each axis are mutually exclusive + if (side.HasFlag(ResizeSide.Left)) + { + if (position.X > anchor.X) + position.X = anchor.X; + + float anchorOffset = 1f - normalizedAnchor.X; + float difference = MathF.Abs(shapeBounds.Left - position.X); + if (position.X < shapeBounds.Left) + width += difference / anchorOffset; + else + width -= difference / anchorOffset; + } + else if (side.HasFlag(ResizeSide.Right)) + { + if (position.X < anchor.X) + position.X = anchor.X; + + float anchorOffset = normalizedAnchor.X; + float difference = MathF.Abs(shapeBounds.Right - position.X); + if (position.X > shapeBounds.Right) + width += difference / anchorOffset; + else + width -= difference / anchorOffset; + } + + if (side.HasFlag(ResizeSide.Top)) + { + if (position.Y > anchor.Y) + position.Y = anchor.Y; + + float anchorOffset = 1f - normalizedAnchor.Y; + float difference = MathF.Abs(shapeBounds.Top - position.Y); + if (position.Y < shapeBounds.Top) + height += difference / anchorOffset; + else + height -= difference / anchorOffset; + } + else if (side.HasFlag(ResizeSide.Bottom)) + { + if (position.Y < anchor.Y) + position.Y = anchor.Y; + + float anchorOffset = normalizedAnchor.Y; + float difference = MathF.Abs(shapeBounds.Bottom - position.Y); + if (position.Y > shapeBounds.Bottom) + height += difference / anchorOffset; + else + height -= difference / anchorOffset; + } + + // Even out the sides to the size of the longest side + if (evenSides) + { + if (width > height) + width = height; + else + height = width; + } + + // Normalize the scale to a percentage + SKRect bounds = Layer.GetLayerBounds(); + width = width / bounds.Width * 100f; + height = height / bounds.Height * 100f; + + Layer.Transform.Scale.SetCurrentValue(new SKSize(width, height), _time); + return new SKSize(width, height); + } + + #endregion + + #region Rotating + + + + #endregion + + #region Movement + + + + #endregion + + #region Anchor movement + + + + #endregion + + [Flags] + public enum ResizeSide + { + Top = 1, + Right = 2, + Bottom = 4, + Left = 8, } } \ No newline at end of file