1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Transform tool - Mostly implemented resizing

This commit is contained in:
Robert 2022-02-10 23:38:27 +01:00
parent 12e91b8c81
commit 1716eba8ec
3 changed files with 302 additions and 63 deletions

View File

@ -45,30 +45,14 @@ public static class LayerExtensions
/// <summary>
/// Returns an absolute and scaled rectangular path for the given layer in real coordinates.
/// </summary>
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
}
/// <summary>
/// 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
/// </summary>
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);
}
}

View File

@ -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<TransformToolViewModel>
{
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<Ellipse>("RotateTopLeft");
_rotateTopRight = this.Get<Ellipse>("RotateTopRight");
_rotateBottomRight = this.Get<Ellipse>("RotateBottomRight");
_rotateBottomLeft = this.Get<Ellipse>("RotateBottomLeft");
_resizeTopCenter = this.Get<Rectangle>("ResizeTopCenter");
_resizeRightCenter = this.Get<Rectangle>("ResizeRightCenter");
_resizeBottomCenter = this.Get<Rectangle>("ResizeBottomCenter");
_resizeLeftCenter = this.Get<Rectangle>("ResizeLeftCenter");
_resizeTopLeft = this.Get<Rectangle>("ResizeTopLeft");
_resizeTopRight = this.Get<Rectangle>("ResizeTopRight");
_resizeBottomRight = this.Get<Rectangle>("ResizeBottomRight");
_resizeBottomLeft = this.Get<Rectangle>("ResizeBottomLeft");
_anchorPoint = this.Get<Ellipse>("AnchorPoint");
}
private void InitializeComponent()
@ -30,7 +70,7 @@ public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
/// <inheritdoc />
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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<TransformToolViewModel>
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");
}
}

View File

@ -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<bool> _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;
/// <inheritdoc />
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<SKSize>(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<SKSize>(Layer.Transform.Scale, size, _time));
}
}
else
{
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<SKSize>(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,
}
}