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