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

Transform tool - Finished initial implementation

This commit is contained in:
Robert 2022-02-13 21:19:32 +01:00
parent 1716eba8ec
commit 32ebf5f000
36 changed files with 1094 additions and 499 deletions

View File

@ -32,7 +32,7 @@ namespace Artemis.Core
/// <summary>
/// The full path to the Artemis data folder
/// </summary>
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis.Avalonia");
/// <summary>
/// The full path to the Artemis logs folder

View File

@ -579,10 +579,8 @@ namespace Artemis.Core
SKRect bounds = customBounds ?? Bounds;
SKPoint positionProperty = Transform.Position.CurrentValue;
// Start at the center of the shape
SKPoint position = zeroBased
? new SKPoint(bounds.MidX - bounds.Left, bounds.MidY - Bounds.Top)
: new SKPoint(bounds.MidX, bounds.MidY);
// Start at the top left of the shape
SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top);
// Apply translation
if (applyTranslation)
@ -649,9 +647,9 @@ namespace Artemis.Core
SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds);
SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue;
// Translation originates from the unscaled center of the shape and is tied to the anchor
float x = anchorPosition.X - (zeroBased ? bounds.MidX - bounds.Left : bounds.MidX) - anchorProperty.X * bounds.Width;
float y = anchorPosition.Y - (zeroBased ? bounds.MidY - bounds.Top : bounds.MidY) - anchorProperty.Y * bounds.Height;
// Translation originates from the top left of the shape and is tied to the anchor
float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width;
float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height;
SKMatrix transform = SKMatrix.Empty;

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Artemis.Storage.Entities.Profile;
@ -201,33 +202,33 @@ namespace Artemis.Core
/// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new
/// or existing keyframe.
/// </param>
/// <returns>The new keyframe if one was created.</returns>
/// <returns>The keyframe if one was created or updated.</returns>
public LayerPropertyKeyframe<T>? SetCurrentValue(T value, TimeSpan? time)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
LayerPropertyKeyframe<T>? newKeyframe = null;
LayerPropertyKeyframe<T>? keyframe = null;
if (time == null || !KeyframesEnabled || !KeyframesSupported)
BaseValue = value;
else
{
// If on a keyframe, update the keyframe
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
keyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
// Create a new keyframe if none found
if (currentKeyframe == null)
if (keyframe == null)
{
newKeyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
AddKeyframe(newKeyframe);
keyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
AddKeyframe(keyframe);
}
else
currentKeyframe.Value = value;
keyframe.Value = value;
}
// Force an update so that the base value is applied to the current value and
// keyframes/data bindings are applied using the new base value
ReapplyUpdate();
return newKeyframe;
return keyframe;
}
/// <inheritdoc />

View File

@ -0,0 +1,93 @@
using System;
using System.Linq;
namespace Artemis.Core;
/// <summary>
/// Represents a container for a preview value of a <see cref="LayerProperty{T}" /> that can be used to update and
/// discard a temporary value.
/// </summary>
/// <typeparam name="T">The value type of the layer property.</typeparam>
public sealed class LayerPropertyPreview<T> : IDisposable
{
/// <summary>
/// Creates a new instance of the <see cref="LayerPropertyPreview{T}" /> class.
/// </summary>
/// <param name="layerProperty">The layer property to apply the preview value to.</param>
/// <param name="time">The time in the timeline at which the preview is applied.</param>
public LayerPropertyPreview(LayerProperty<T> layerProperty, TimeSpan time)
{
Property = layerProperty;
Time = time;
OriginalKeyframe = layerProperty.Keyframes.FirstOrDefault(k => k.Position == time);
OriginalValue = OriginalKeyframe != null ? OriginalKeyframe.Value : layerProperty.CurrentValue;
PreviewValue = OriginalValue;
}
/// <summary>
/// Gets the property this preview applies to.
/// </summary>
public LayerProperty<T> Property { get; }
/// <summary>
/// Gets the original keyframe of the property at the time the preview was created.
/// </summary>
public LayerPropertyKeyframe<T>? OriginalKeyframe { get; }
/// <summary>
/// Gets the original value of the property at the time the preview was created.
/// </summary>
public T OriginalValue { get; }
/// <summary>
/// Gets the time in the timeline at which the preview is applied.
/// </summary>
public TimeSpan Time { get; }
/// <summary>
/// Gets the keyframe that was created to preview the value.
/// </summary>
public LayerPropertyKeyframe<T>? PreviewKeyframe { get; private set; }
/// <summary>
/// Gets the preview value.
/// </summary>
public T? PreviewValue { get; private set; }
/// <summary>
/// Updates the layer property to the given <paramref name="value" />, keeping track of the original state of the
/// property.
/// </summary>
/// <param name="value">The value to preview.</param>
public void Preview(T value)
{
PreviewValue = value;
PreviewKeyframe = Property.SetCurrentValue(value, Time);
}
/// <summary>
/// Discard the preview value and restores the original state of the property. The returned boolean can be used to
/// determine whether the preview value was different from the original value.
/// </summary>
/// <returns><see langword="true" /> if any changes where discarded; otherwise <see langword="false" />.</returns>
public bool DiscardPreview()
{
if (PreviewKeyframe != null && OriginalKeyframe == null)
{
Property.RemoveKeyframe(PreviewKeyframe);
return true;
}
Property.SetCurrentValue(OriginalValue, Time);
return !Equals(OriginalValue, PreviewValue); ;
}
/// <summary>
/// Discard the preview value and restores the original state of the property. The returned boolean can be used to
/// determine whether the preview value was different from the original value.
/// </summary>
public void Dispose()
{
DiscardPreview();
}
}

View File

@ -12,13 +12,13 @@ namespace Artemis.Core
/// <summary>
/// The point at which the shape is attached to its position
/// </summary>
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputStepSize = 0.001f)]
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%")]
public SKPointLayerProperty AnchorPoint { get; set; }
/// <summary>
/// The position of the shape
/// </summary>
[PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)]
[PropertyDescription(Description = "The position of the shape", InputAffix = "%")]
public SKPointLayerProperty Position { get; set; }
/// <summary>
@ -43,6 +43,8 @@ namespace Artemis.Core
protected override void PopulateDefaults()
{
Scale.DefaultValue = new SKSize(100, 100);
AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f);
Position.DefaultValue = new SKPoint(0.5f, 0.5f);
Opacity.DefaultValue = 100;
}

View File

@ -420,7 +420,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
private static SKPoint RoundPoint(SKPoint point, int decimals)
{
return new((float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero), (float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero));
return new SKPoint(
(float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero),
(float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero)
);
}
private static SKPoint[] UnTransformPoints(SKPoint[] skPoints, Layer layer, SKPoint pivot, bool includeScale)

View File

@ -14,6 +14,9 @@ public static class LayerExtensions
/// </summary>
public static SKRect GetLayerBounds(this Layer layer)
{
if (!layer.Leds.Any())
return SKRect.Empty;
return new SKRect(
layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.X),
layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.Y),
@ -32,8 +35,8 @@ public static class LayerExtensions
if (positionOverride != null)
positionProperty = positionOverride.Value;
// Start at the center of the shape
SKPoint position = new(layerBounds.MidX, layerBounds.MidY);
// Start at the top left of the shape
SKPoint position = new(layerBounds.Left, layerBounds.Top);
// Apply translation
position.X += positionProperty.X * layerBounds.Width;
@ -59,7 +62,7 @@ public static class LayerExtensions
/// <summary>
/// Returns a new point normalized to 0.0-1.0
/// </summary>
public static SKPoint GetScaledPoint(this Layer layer, SKPoint point, bool absolute)
public static SKPoint GetNormalizedPoint(this Layer layer, SKPoint point, bool absolute)
{
SKRect bounds = GetLayerBounds(layer);
if (absolute)

View File

@ -0,0 +1,24 @@
using Avalonia.Input;
namespace Artemis.UI.Shared.Providers;
/// <summary>
/// Represents a provider for custom cursors.
/// </summary>
public interface ICursorProvider
{
/// <summary>
/// A cursor that indicates a rotating.
/// </summary>
public Cursor Rotate { get; }
/// <summary>
/// A cursor that indicates dragging.
/// </summary>
public Cursor Drag { get; }
/// <summary>
/// A cursor that indicates dragging horizontally.
/// </summary>
public Cursor DragHorizontal { get; }
}

View File

@ -0,0 +1,52 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to reset a layer property to it's default value.
/// </summary>
public class ResetLayerProperty<T> : IProfileEditorCommand
{
private readonly LayerProperty<T> _layerProperty;
private readonly T _originalBaseValue;
private readonly bool _keyframesEnabled;
/// <summary>
/// Creates a new instance of the <see cref="ResetLayerProperty{T}" /> class.
/// </summary>
public ResetLayerProperty(LayerProperty<T> layerProperty)
{
if (layerProperty.DefaultValue == null)
throw new ArtemisSharedUIException("Can't reset a layer property without a default value.");
_layerProperty = layerProperty;
_originalBaseValue = _layerProperty.BaseValue;
_keyframesEnabled = _layerProperty.KeyframesEnabled;
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName => "Reset layer property";
/// <inheritdoc />
public void Execute()
{
string json = CoreJson.SerializeObject(_layerProperty.DefaultValue, true);
if (_keyframesEnabled)
_layerProperty.KeyframesEnabled = false;
_layerProperty.SetCurrentValue(CoreJson.DeserializeObject<T>(json)!, null);
}
/// <inheritdoc />
public void Undo()
{
_layerProperty.SetCurrentValue(_originalBaseValue, null);
if (_keyframesEnabled)
_layerProperty.KeyframesEnabled = true;
}
#endregion
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
@ -12,6 +13,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
private readonly T _newValue;
private readonly T _originalValue;
private readonly TimeSpan? _time;
private readonly bool _hasKeyframe;
private LayerPropertyKeyframe<T>? _newKeyframe;
/// <summary>
@ -23,6 +25,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
_originalValue = layerProperty.CurrentValue;
_newValue = newValue;
_time = time;
_hasKeyframe = _layerProperty.Keyframes.Any(k => k.Position == time);
DisplayName = $"Update {_layerProperty.PropertyDescription.Name ?? "property"}";
}
@ -50,13 +53,19 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
{
// If there was already a keyframe from a previous execute that was undone, put it back
if (_newKeyframe != null)
{
_layerProperty.AddKeyframe(_newKeyframe);
else
return;
}
// If the layer had no keyframe yet but keyframes are enabled, a new keyframe will be returned
if (!_hasKeyframe && _layerProperty.KeyframesEnabled)
{
_newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
if (_newKeyframe != null)
DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe";
DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe";
}
else
_layerProperty.SetCurrentValue(_newValue, _time);
}
/// <inheritdoc />

View File

@ -1,4 +1,7 @@
using Artemis.Core.Services;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Windows.Ninject;
using Artemis.UI.Windows.Providers;
using Artemis.UI.Windows.Providers.Input;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
@ -13,7 +16,7 @@ namespace Artemis.UI.Windows
{
public override void Initialize()
{
_kernel = ArtemisBootstrapper.Bootstrap(this);
_kernel = ArtemisBootstrapper.Bootstrap(this, new WindowsModule());
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
AvaloniaXamlLoader.Load(this);
}

View File

@ -55,7 +55,7 @@ namespace Artemis.UI.Windows
public bool FocusExistingInstance()
{
_artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew);
_artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535f", out bool createdNew);
if (createdNew)
return false;

View File

@ -8,6 +8,19 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\Cursors\aero_crosshair.cur" />
<None Remove="Assets\Cursors\aero_crosshair_minus.cur" />
<None Remove="Assets\Cursors\aero_crosshair_plus.cur" />
<None Remove="Assets\Cursors\aero_drag.cur" />
<None Remove="Assets\Cursors\aero_drag_ew.cur" />
<None Remove="Assets\Cursors\aero_fill.cur" />
<None Remove="Assets\Cursors\aero_pen_min.cur" />
<None Remove="Assets\Cursors\aero_pen_plus.cur" />
<None Remove="Assets\Cursors\aero_rotate.cur" />
<None Remove="Assets\Cursors\aero_rotate_bl.cur" />
<None Remove="Assets\Cursors\aero_rotate_br.cur" />
<None Remove="Assets\Cursors\aero_rotate_tl.cur" />
<None Remove="Assets\Cursors\aero_rotate_tr.cur" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.12" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

View File

@ -0,0 +1,18 @@
using Artemis.UI.Shared.Providers;
using Artemis.UI.Windows.Providers;
using Ninject.Modules;
namespace Artemis.UI.Windows.Ninject;
public class WindowsModule : NinjectModule
{
#region Overrides of NinjectModule
/// <inheritdoc />
public override void Load()
{
Kernel!.Bind<ICursorProvider>().To<CursorProvider>().InSingletonScope();
}
#endregion
}

View File

@ -0,0 +1,22 @@
using System;
using Artemis.UI.Shared.Providers;
using Avalonia;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Artemis.UI.Windows.Providers;
public class CursorProvider : ICursorProvider
{
public CursorProvider(IAssetLoader assetLoader)
{
Rotate = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_rotate.png"))), new PixelPoint(21, 10));
Drag = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag.png"))), new PixelPoint(11, 3));
DragHorizontal = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png"))), new PixelPoint(16, 5));
}
public Cursor Rotate { get; }
public Cursor Drag { get; }
public Cursor DragHorizontal { get; }
}

View File

@ -11,6 +11,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Ninject;
using Ninject.Modules;
using ReactiveUI;
using Splat.Ninject;
@ -21,7 +22,7 @@ namespace Artemis.UI
private static StandardKernel? _kernel;
private static Application? _application;
public static StandardKernel Bootstrap(Application application)
public static StandardKernel Bootstrap(Application application, params INinjectModule[] modules)
{
if (_application != null || _kernel != null)
throw new ArtemisUIException("UI already bootstrapped");
@ -35,6 +36,7 @@ namespace Artemis.UI
_kernel.Load<CoreModule>();
_kernel.Load<UIModule>();
_kernel.Load<SharedUIModule>();
_kernel.Load(modules);
_kernel.UseNinjectDependencyResolver();

View File

@ -1,4 +1,6 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
@ -27,11 +29,19 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
_profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d);
});
ResetToDefault = ReactiveCommand.Create(ExecuteResetToDefault, Observable.Return(LayerProperty.DefaultValue != null));
}
private void ExecuteResetToDefault()
{
_profileEditorService.ExecuteCommand(new ResetLayerProperty<T>(LayerProperty));
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
public ReactiveCommand<Unit, Unit> ResetToDefault { get; }
public bool KeyframesEnabled
{
@ -62,4 +72,5 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
return depth;
}
}
}

View File

@ -30,14 +30,21 @@ public class StatusBarViewModel : ActivatableViewModelBase
this.WhenAnyValue(vm => vm.History)
.Select(h => h?.Undo ?? Observable.Never<IProfileEditorCommand?>())
.Switch()
.Subscribe(c => StatusMessage = c != null ? $"Undid '{c.DisplayName}'." : "Nothing to undo.");
.Subscribe(c =>
{
StatusMessage = c != null ? $"Undid '{c.DisplayName}'." : "Nothing to undo.";
ShowStatusMessage = true;
});
this.WhenAnyValue(vm => vm.History)
.Select(h => h?.Redo ?? Observable.Never<IProfileEditorCommand?>())
.Switch()
.Subscribe(c => StatusMessage = c != null ? $"Redid '{c.DisplayName}'." : "Nothing to redo.");
.Subscribe(c =>
{
StatusMessage = c != null ? $"Redid '{c.DisplayName}'." : "Nothing to redo.";
ShowStatusMessage = true;
});
this.WhenAnyValue(vm => vm.StatusMessage).Subscribe(_ => ShowStatusMessage = true);
this.WhenAnyValue(vm => vm.StatusMessage).Throttle(TimeSpan.FromSeconds(3)).Subscribe(_ => ShowStatusMessage = false);
this.WhenAnyValue(vm => vm.ShowStatusMessage).Where(v => v).Throttle(TimeSpan.FromSeconds(3)).Subscribe(_ => ShowStatusMessage = false);
}
public RenderProfileElement? ProfileElement => _profileElement?.Value;

View File

@ -5,155 +5,193 @@
xmlns:tools="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools.TransformToolView"
x:DataType="tools:TransformToolViewModel">
x:DataType="tools:TransformToolViewModel"
ClipToBounds="False">
<UserControl.Styles>
<Style Selector="Ellipse.rotation-handle">
<Setter Property="Margin" Value="-15"></Setter>
<Setter Property="Width" Value="30"></Setter>
<Setter Property="Height" Value="30"></Setter>
<Setter Property="Fill" Value="Green"></Setter>
<Setter Property="Cursor" Value="Hand"></Setter>
</Style>
<Style Selector="Ellipse.anchor-handle">
<Setter Property="Margin" Value="-15"></Setter>
<Setter Property="Width" Value="30"></Setter>
<Setter Property="Height" Value="30"></Setter>
<Setter Property="Fill" Value="Red"></Setter>
<Setter Property="Cursor" Value="SizeAll"></Setter>
</Style>
<Style Selector="Rectangle.resize-handle">
<Setter Property="Margin" Value="-6"></Setter>
<Setter Property="Width" Value="12"></Setter>
<Setter Property="Height" Value="12"></Setter>
<Setter Property="Fill" Value="Blue"></Setter>
<Setter Property="Cursor" Value="Hand"></Setter>
<Setter Property="RenderTransform">
<Style Selector=":is(Control).unscaled">
<Setter Property="Transitions">
<Setter.Value>
<RotateTransform Angle="{CompiledBinding InverseRotation}"></RotateTransform>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" Easing="CubicEaseOut" />
</Transitions>
</Setter.Value>
</Setter>
</Style>
<!-- Container -->
<Style Selector="Border#TransformationContainer">
<Setter Property="ClipToBounds" Value="False" />
</Style>
<!-- Resize -->
<Style Selector="Rectangle.transform-resize-visual">
<Setter Property="Margin" Value="-5" />
<Setter Property="Width" Value="10" />
<Setter Property="Height" Value="10" />
<Setter Property="Fill" Value="White" />
<Setter Property="Stroke" Value="{DynamicResource SystemAccentColorLight2}" />
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="StrokeJoin" Value="Round" />
</Style>
<Style Selector="Ellipse.transform-resize-handle">
<Setter Property="Margin" Value="-15" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="Fill" Value="Transparent" />
</Style>
<!-- Rotate -->
<Style Selector="Ellipse.transform-rotation-handle">
<Setter Property="Margin" Value="-30" />
<Setter Property="Width" Value="60" />
<Setter Property="Height" Value="60" />
<Setter Property="Fill" Value="Transparent" />
<Setter Property="Cursor" Value="{DynamicResource RotateCursor}" />
</Style>
<!-- Movement -->
<Style Selector="Rectangle.transform-movement-handle">
<Setter Property="Fill" Value="Transparent" />
<Setter Property="Cursor" Value="{DynamicResource DragCursor}" />
</Style>
<!-- Anchor -->
<Style Selector="Panel.transform-anchor-handle">
<Setter Property="Margin" Value="-18.5" />
<Setter Property="Width" Value="37" />
<Setter Property="Height" Value="37" />
<Setter Property="Cursor" Value="SizeAll" />
</Style>
<Style Selector="Panel.transform-anchor-handle :is(Shape)">
<Setter Property="Fill" Value="White" />
<Setter Property="Stroke" Value="{DynamicResource SystemAccentColorLight2}" />
<Setter Property="StrokeThickness" Value="4" />
</Style>
</UserControl.Styles>
<Canvas ClipToBounds="False" UseLayoutRounding="False">
<Border Name="TransformationContainer"
Width="{CompiledBinding ShapeBounds.Width}"
Height="{CompiledBinding ShapeBounds.Height}"
ClipToBounds="False"
Canvas.Left="{CompiledBinding ShapeBounds.Left}"
Canvas.Top="{CompiledBinding ShapeBounds.Top}"
RenderTransformOrigin="{CompiledBinding RelativeAnchor}">
<Border.RenderTransform>
<RotateTransform Angle="{CompiledBinding Rotation}"></RotateTransform>
</Border.RenderTransform>
<Grid>
<Grid Name="HandleGrid">
<!-- Render these first so that they are always behind the actual shape -->
<Ellipse Name="RotateTopLeft"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Classes="rotation-handle"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
<Ellipse Name="RotateTopRight"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Classes="rotation-handle"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
<Ellipse Name="RotateBottomRight"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Classes="rotation-handle"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
<Ellipse Name="RotateBottomLeft"
VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Classes="rotation-handle"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
<Panel VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse Name="RotateTopLeft"
Classes="transform-rotation-handle unscaled"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Top" HorizontalAlignment="Right">
<Ellipse Name="RotateTopRight"
Classes="transform-rotation-handle unscaled"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Bottom" HorizontalAlignment="Right">
<Ellipse Name="RotateBottomRight"
Classes="transform-rotation-handle unscaled"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Bottom" HorizontalAlignment="Left">
<Ellipse Name="RotateBottomLeft"
Classes="transform-rotation-handle unscaled"
PointerPressed="RotationOnPointerPressed"
PointerReleased="RotationOnPointerReleased"
PointerMoved="RotationOnPointerMoved" />
</Panel>
<Rectangle Classes="transform-movement-handle"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
PointerPressed="MoveOnPointerPressed"
PointerReleased="MoveOnPointerReleased"
PointerMoved="MoveOnPointerMoved" />
<!-- Mutation points -->
<Rectangle Name="ResizeTopCenter"
VerticalAlignment="Top"
HorizontalAlignment="Center"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeRightCenter"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeBottomCenter"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeLeftCenter"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Panel VerticalAlignment="Top" HorizontalAlignment="Center" Name="ResizeTopCenter">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Center" HorizontalAlignment="Right" Name="ResizeRightCenter">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Bottom" HorizontalAlignment="Center" Name="ResizeBottomCenter">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Center" HorizontalAlignment="Left" Name="ResizeLeftCenter">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Top" HorizontalAlignment="Left" Name="ResizeTopLeft">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Top" HorizontalAlignment="Right" Name="ResizeTopRight">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Bottom" HorizontalAlignment="Right" Name="ResizeBottomRight">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Panel VerticalAlignment="Bottom" HorizontalAlignment="Left" Name="ResizeBottomLeft">
<Rectangle Classes="transform-resize-visual unscaled" />
<Ellipse Classes="transform-resize-handle unscaled"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved" />
</Panel>
<Rectangle Name="ResizeTopLeft"
Classes="resize-handle"
VerticalAlignment="Top"
HorizontalAlignment="Left"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeTopRight"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeBottomRight"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Rectangle Name="ResizeBottomLeft"
VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Classes="resize-handle"
PointerPressed="ResizeOnPointerPressed"
PointerReleased="ResizeOnPointerReleased"
PointerMoved="ResizeOnPointerMoved">
</Rectangle>
<Canvas>
<Ellipse Name="AnchorPoint"
Canvas.Left="{CompiledBinding Anchor.X}"
Canvas.Top="{CompiledBinding Anchor.Y}"
Classes="anchor-handle"
PointerPressed="MoveOnPointerPressed"
PointerReleased="MoveOnPointerReleased"
PointerMoved="MoveOnPointerMoved" />
<Canvas Name="AnchorCanvas">
<Panel Background="Transparent"
Name="AnchorPoint"
Classes="transform-anchor-handle unscaled"
Canvas.Left="{CompiledBinding Anchor.X}"
Canvas.Top="{CompiledBinding Anchor.Y}"
PointerPressed="AnchorOnPointerPressed"
PointerReleased="AnchorOnPointerReleased"
PointerMoved="AnchorOnPointerMoved">
<Ellipse StrokeThickness="4" Width="14" Height="14" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Line StartPoint="0,0" EndPoint="0,10" StrokeThickness="2" VerticalAlignment="Top" HorizontalAlignment="Center" />
<Line StartPoint="0,25" EndPoint="0,35" StrokeThickness="2" VerticalAlignment="Bottom" HorizontalAlignment="Center" />
<Line StartPoint="0,0" EndPoint="10,0" StrokeThickness="2" VerticalAlignment="Center" HorizontalAlignment="Left" />
<Line StartPoint="25,0" EndPoint="35,0" StrokeThickness="2" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Panel>
</Canvas>
</Grid>
</Border>

View File

@ -1,17 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Providers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Transformation;
using Avalonia.ReactiveUI;
using Avalonia.Skia;
using Avalonia.Styling;
using Avalonia.VisualTree;
using ReactiveUI;
using SkiaSharp;
@ -19,45 +24,71 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
{
private ZoomBorder? _zoomBorder;
private SKPoint _dragStart;
private readonly List<Control> _handles = new();
private readonly Panel _resizeBottomCenter;
private readonly Panel _resizeBottomLeft;
private readonly Panel _resizeBottomRight;
private readonly Panel _resizeLeftCenter;
private readonly Panel _resizeRightCenter;
private readonly Panel _resizeTopCenter;
private readonly Panel _resizeTopLeft;
private readonly Panel _resizeTopRight;
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;
private ZoomBorder? _zoomBorder;
private readonly Grid _handleGrid;
public TransformToolView()
{
InitializeComponent();
_rotateTopLeft = this.Get<Ellipse>("RotateTopLeft");
_rotateTopRight = this.Get<Ellipse>("RotateTopRight");
_rotateBottomRight = this.Get<Ellipse>("RotateBottomRight");
_rotateBottomLeft = this.Get<Ellipse>("RotateBottomLeft");
_handleGrid = this.Get<Grid>("HandleGrid");
_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");
_handles.Add(this.Get<Ellipse>("RotateTopLeft"));
_handles.Add(this.Get<Ellipse>("RotateTopRight"));
_handles.Add(this.Get<Ellipse>("RotateBottomRight"));
_handles.Add(this.Get<Ellipse>("RotateBottomLeft"));
_anchorPoint = this.Get<Ellipse>("AnchorPoint");
_resizeTopCenter = this.Get<Panel>("ResizeTopCenter");
_handles.Add(_resizeTopCenter);
_resizeRightCenter = this.Get<Panel>("ResizeRightCenter");
_handles.Add(_resizeRightCenter);
_resizeBottomCenter = this.Get<Panel>("ResizeBottomCenter");
_handles.Add(_resizeBottomCenter);
_resizeLeftCenter = this.Get<Panel>("ResizeLeftCenter");
_handles.Add(_resizeLeftCenter);
_resizeTopLeft = this.Get<Panel>("ResizeTopLeft");
_handles.Add(_resizeTopLeft);
_resizeTopRight = this.Get<Panel>("ResizeTopRight");
_handles.Add(_resizeTopRight);
_resizeBottomRight = this.Get<Panel>("ResizeBottomRight");
_handles.Add(_resizeBottomRight);
_resizeBottomLeft = this.Get<Panel>("ResizeBottomLeft");
_handles.Add(_resizeBottomLeft);
_handles.Add(this.Get<Panel>("AnchorPoint"));
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Rotation).Subscribe(_ => UpdateTransforms()).DisposeWith(d));
}
private void UpdateTransforms()
{
if (_zoomBorder == null || ViewModel == null)
return;
double resizeSize = Math.Clamp(1 / _zoomBorder.ZoomX, 0.2, 2);
TransformOperations.Builder builder = TransformOperations.CreateBuilder(2);
builder.AppendScale(resizeSize, resizeSize);
TransformOperations counterScale = builder.Build();
RotateTransform counterRotate = new(ViewModel.Rotation * -1);
// Apply the counter rotation to the containers
foreach (Panel panel in _handleGrid.Children.Where(c => c is Panel and not Canvas).Cast<Panel>())
panel.RenderTransform = counterRotate;
foreach (Control control in _handleGrid.GetVisualDescendants().Where(d => d is Control c && c.Classes.Contains("unscaled")).Cast<Control>())
control.RenderTransform = counterScale;
}
private void InitializeComponent()
@ -65,6 +96,48 @@ public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
AvaloniaXamlLoader.Load(this);
}
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(Ellipse element)
{
if (ReferenceEquals(element.Parent, _resizeTopLeft))
return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Left;
if (ReferenceEquals(element.Parent, _resizeTopRight))
return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Right;
if (ReferenceEquals(element.Parent, _resizeBottomRight))
return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Right;
if (ReferenceEquals(element.Parent, _resizeBottomLeft))
return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Left;
if (ReferenceEquals(element.Parent, _resizeTopCenter))
return TransformToolViewModel.ResizeSide.Top;
if (ReferenceEquals(element.Parent, _resizeRightCenter))
return TransformToolViewModel.ResizeSide.Right;
if (ReferenceEquals(element.Parent, _resizeBottomCenter))
return TransformToolViewModel.ResizeSide.Bottom;
if (ReferenceEquals(element.Parent, _resizeLeftCenter))
return TransformToolViewModel.ResizeSide.Left;
throw new ArgumentException("Given element is not a child of a resize container");
}
#region Zoom
/// <inheritdoc />
@ -89,43 +162,7 @@ public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null)
return;
// TODO
}
#endregion
#region Rotation
private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
_dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint();
_dragOffset = ViewModel.Layer.GetDragOffset(_dragStart);
e.Pointer.Capture(element);
e.Handled = true;
}
private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(null);
e.Handled = true;
}
private void RotationOnPointerMoved(object? sender, PointerEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Handled = true;
UpdateTransforms();
}
#endregion
@ -134,33 +171,87 @@ public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
private void MoveOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
_dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint();
_dragOffset = ViewModel.Layer.GetDragOffset(_dragStart);
SKPoint dragStart = e.GetCurrentPoint(this).Position.ToSKPoint();
SKRect shapeBounds = ViewModel.Layer.GetLayerPath(true, true, false).Bounds;
_dragOffset = new SKPoint(dragStart.X - shapeBounds.Left, dragStart.Y - shapeBounds.Top);
e.Pointer.Capture(element);
e.Handled = true;
}
ViewModel.StartMovement();
ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.Position.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.Position.CurrentValue.Y:F3}%");
ToolTip.SetIsOpen((Control) sender, true);
private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(null);
e.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
}
private void MoveOnPointerMoved(object? sender, PointerEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (!ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
SKPoint position = e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset;
ViewModel.UpdateMovement(position, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.Position.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.Position.CurrentValue.Y:F3}%");
e.Handled = true;
}
private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null)
return;
ViewModel.FinishMovement();
ToolTip.SetTip((Control) sender, null);
ToolTip.SetIsOpen((Control) sender, false);
e.Pointer.Capture(null);
e.Handled = true;
}
#endregion
#region Anchor movement
private void AnchorOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
SKPoint dragStart = e.GetCurrentPoint(this).Position.ToSKPoint();
_dragOffset = dragStart - ViewModel.Layer.GetLayerAnchorPosition();
ViewModel.StartAnchorMovement(e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset);
ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.Y:F3}%");
ToolTip.SetIsOpen((Control) sender, true);
e.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
}
private void AnchorOnPointerMoved(object? sender, PointerEventArgs e)
{
if (!ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
SKPoint position = e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset;
ViewModel.UpdateAnchorMovement(position, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.Y:F3}%");
e.Handled = true;
}
private void AnchorOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null)
return;
ViewModel.FinishAnchorMovement();
ToolTip.SetTip((Control) sender, null);
ToolTip.SetIsOpen((Control) sender, false);
e.Pointer.Capture(null);
e.Handled = true;
}
@ -170,86 +261,151 @@ public class TransformToolView : ReactiveUserControl<TransformToolViewModel>
private void ResizeOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
_dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer);
_dragOffset = ViewModel.Layer.GetDragOffset(_dragStart);
SKPoint dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer);
_dragOffset = ViewModel.Layer.GetDragOffset(dragStart);
ToolTip.SetTip((Control) sender, $"Width: {ViewModel.Layer.Transform.Scale.CurrentValue.Width:F3}% Height: {ViewModel.Layer.Transform.Scale.CurrentValue.Height:F3}%");
ToolTip.SetIsOpen((Control) sender, true);
SKPoint position = GetPositionForViewModel(e);
ViewModel.StartResize(position);
ViewModel.StartResize();
e.Pointer.Capture(element);
e.Handled = true;
}
private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
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.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
}
private void ResizeOnPointerMoved(object? sender, PointerEventArgs e)
{
Shape? element = (Shape?) sender;
if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
UpdateCursors();
if (sender is not Ellipse element || !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));
ViewModel.UpdateResize(
position,
GetResizeDirection(element),
e.KeyModifiers.HasFlag(KeyModifiers.Alt),
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
e.KeyModifiers.HasFlag(KeyModifiers.Control)
);
ToolTip.SetTip((Control) sender, $"Width: {ViewModel.Layer.Transform.Scale.CurrentValue.Width:F3}% Height: {ViewModel.Layer.Transform.Scale.CurrentValue.Height:F3}%");
e.Handled = true;
}
private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null)
return;
ViewModel.FinishResize();
ToolTip.SetTip((Control) sender, null);
ToolTip.SetIsOpen((Control) sender, false);
e.Pointer.Capture(null);
e.Handled = true;
}
private void UpdateCursors()
{
_resizeTopCenter.Cursor = GetCursorAtAngle(0f);
_resizeTopRight.Cursor = GetCursorAtAngle(45f);
_resizeRightCenter.Cursor = GetCursorAtAngle(90f);
_resizeBottomRight.Cursor = GetCursorAtAngle(135f);
_resizeBottomCenter.Cursor = GetCursorAtAngle(180f);
_resizeBottomLeft.Cursor = GetCursorAtAngle(225f);
_resizeLeftCenter.Cursor = GetCursorAtAngle(270f);
_resizeTopLeft.Cursor = GetCursorAtAngle(315f);
}
private Cursor GetCursorAtAngle(float angle, bool includeLayerRotation = true)
{
if (includeLayerRotation && ViewModel?.Layer != null)
angle = (angle + ViewModel.Layer.Transform.Rotation.CurrentValue) % 360;
if (angle is > 330 and <= 360 or >= 0 and <= 30)
return new Cursor(StandardCursorType.TopSide);
if (angle is > 30 and <= 60)
return new Cursor(StandardCursorType.TopRightCorner);
if (angle is > 60 and <= 120)
return new Cursor(StandardCursorType.RightSide);
if (angle is > 120 and <= 150)
return new Cursor(StandardCursorType.BottomRightCorner);
if (angle is > 150 and <= 210)
return new Cursor(StandardCursorType.BottomSide);
if (angle is > 210 and <= 240)
return new Cursor(StandardCursorType.BottomLeftCorner);
if (angle is > 240 and <= 300)
return new Cursor(StandardCursorType.LeftSide);
if (angle is > 300 and <= 330)
return new Cursor(StandardCursorType.TopLeftCorner);
return Cursor.Default;
}
#endregion
private SKPoint GetPositionForViewModel(PointerEventArgs e)
#region Rotation
private float _rotationDragOffset;
private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null)
return;
float startAngle = CalculateAngleToAnchor(e);
_rotationDragOffset = startAngle - ViewModel.Layer.Transform.Rotation;
ViewModel.StartRotation();
ToolTip.SetTip((Control)sender, $"{ViewModel.Layer.Transform.Rotation.CurrentValue:F3}°");
ToolTip.SetIsOpen((Control)sender, true);
e.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
}
private void RotationOnPointerMoved(object? sender, PointerEventArgs e)
{
if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
float angle = CalculateAngleToAnchor(e);
angle -= _rotationDragOffset;
if (angle < 0)
angle += 360;
ViewModel?.UpdateRotation(angle, e.KeyModifiers.HasFlag(KeyModifiers.Control));
ToolTip.SetTip((Control)sender, $"{ViewModel.Layer.Transform.Rotation.CurrentValue:F3}°");
e.Handled = true;
}
private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left)
return;
ViewModel?.FinishRotation();
ToolTip.SetTip((Control)sender, null);
ToolTip.SetIsOpen((Control)sender, false);
e.Pointer.Capture(null);
e.Handled = true;
}
private float CalculateAngleToAnchor(PointerEventArgs e)
{
if (ViewModel?.Layer == null)
return SKPoint.Empty;
return 0;
SKPoint point = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer);
return point + _dragOffset;
SKPoint start = ViewModel.Layer.GetLayerAnchorPosition();
SKPoint arrival = e.GetCurrentPoint(this).Position.ToSKPoint();
float radian = (float) Math.Atan2(start.Y - arrival.Y, start.X - arrival.X);
float angle = radian * (180f / (float) Math.PI);
return angle;
}
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");
}
#endregion
}

View File

@ -1,9 +1,8 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
@ -17,14 +16,13 @@ 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;
private readonly IProfileEditorService _profileEditorService;
private Point _anchor;
private ObservableAsPropertyHelper<Layer?>? _layer;
private RelativePoint _relativeAnchor;
private double _rotation;
private Rect _shapeBounds;
private Point _anchor;
private TimeSpan _time;
/// <inheritdoc />
@ -116,12 +114,6 @@ public class TransformToolViewModel : ToolViewModel
set => RaiseAndSetIfChanged(ref _rotation, value);
}
public double InverseRotation
{
get => _inverseRotation;
set => RaiseAndSetIfChanged(ref _inverseRotation, value);
}
public RelativePoint RelativeAnchor
{
get => _relativeAnchor;
@ -149,172 +141,324 @@ public class TransformToolViewModel : ToolViewModel
return;
SKPath path = new();
path.AddRect(Layer.GetLayerBounds());
path.Transform(Layer.GetTransformMatrix(false, true, true, false));
SKRect layerBounds = Layer.GetLayerBounds();
path.AddRect(layerBounds);
path.Transform(Layer.GetTransformMatrix(false, true, true, false, layerBounds));
ShapeBounds = path.Bounds.ToRect();
Rotation = Layer.Transform.Rotation.CurrentValue;
InverseRotation = Rotation * -1;
// The middle of the element is 0.5/0.5 in Avalonia, in Artemis it's 0.0/0.0 so compensate for that below
SKPoint layerAnchor = Layer.Transform.AnchorPoint.CurrentValue;
RelativeAnchor = new RelativePoint(layerAnchor.X + 0.5, layerAnchor.Y + 0.5, RelativeUnit.Relative);
RelativeAnchor = new RelativePoint(layerAnchor.X, layerAnchor.Y, RelativeUnit.Relative);
Anchor = new Point(ShapeBounds.Width * RelativeAnchor.Point.X, ShapeBounds.Height * RelativeAnchor.Point.Y);
}
#region Resizing
private SKSize _resizeStartScale;
private bool _hadKeyframe;
public void StartResize(SKPoint position)
{
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,
Left = 8
}
#region Movement
private LayerPropertyPreview<SKPoint>? _movementPreview;
public void StartMovement()
{
if (Layer == null)
return;
_movementPreview?.DiscardPreview();
_movementPreview = new LayerPropertyPreview<SKPoint>(Layer.Transform.Position, _time);
}
public void UpdateMovement(SKPoint position, bool stickToAxis, bool round)
{
if (Layer == null)
return;
if (_movementPreview == null)
throw new ArtemisUIException("Can't update movement without a preview having been started");
// Get a normalized point
SKPoint scaled = Layer.GetNormalizedPoint(position, true);
// Compensate for the anchor
scaled.X += ((Layer.Transform.AnchorPoint.CurrentValue.X) * (Layer.Transform.Scale.CurrentValue.Width/100f));
scaled.Y += ((Layer.Transform.AnchorPoint.CurrentValue.Y) * (Layer.Transform.Scale.CurrentValue.Height/100f));
_movementPreview.Preview(scaled);
}
public void FinishMovement()
{
if (Layer == null)
return;
if (_movementPreview == null)
throw new ArtemisUIException("Can't update movement without a preview having been started");
if (_movementPreview.DiscardPreview())
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<SKPoint>(Layer.Transform.Position, _movementPreview.PreviewValue, _time));
_movementPreview = null;
}
#endregion
#region Anchor movement
private SKPoint _dragOffset;
private SKPoint _dragStartAnchor;
private LayerPropertyPreview<SKPoint>? _anchorMovementPreview;
public void StartAnchorMovement(SKPoint position)
{
if (Layer == null)
return;
// Mouse doesn't care about rotation so get the layer path without rotation
SKPath path = Layer.GetLayerPath(true, true, false);
SKPoint topLeft = path.Points[0];
// Measure from the top-left of the shape (without rotation)
_dragOffset = topLeft + (position - topLeft);
// Get the absolute layer anchor and make it relative to the unrotated shape
_dragStartAnchor = Layer.GetLayerAnchorPosition() - topLeft;
_movementPreview?.DiscardPreview();
_anchorMovementPreview?.DiscardPreview();
_movementPreview = new LayerPropertyPreview<SKPoint>(Layer.Transform.Position, _time);
_anchorMovementPreview = new LayerPropertyPreview<SKPoint>(Layer.Transform.AnchorPoint, _time);
}
public void UpdateAnchorMovement(SKPoint position, bool stickToAxis, bool round)
{
if (Layer == null)
return;
if (_movementPreview == null || _anchorMovementPreview == null)
throw new ArtemisUIException("Can't update movement without a preview having been started");
SKPoint topLeft = Layer.GetLayerPath(true, true, true)[0];
// The start anchor is relative to an unrotated version of the shape
SKPoint start = _dragStartAnchor;
// Add the current position to the start anchor to determine the new position
SKPoint current = start + (position - _dragOffset);
// In order to keep the mouse movement unrotated, counter-act the active rotation
SKPoint[] countered = UnTransformPoints(new[] {start, current}, Layer, start, true);
// If shift is held down, round down to 1 decimal to allow moving the anchor in big increments
int decimals = round ? 1 : 3;
SKPoint scaled = RoundPoint(Layer.GetNormalizedPoint(countered[1], false), decimals);
// Update the anchor point, this causes the shape to move
_anchorMovementPreview.Preview(scaled);
// TopLeft is not updated yet and acts as a snapshot of the top-left before changing the anchor
SKPath path = Layer.GetLayerPath(true, true, true);
// Calculate the (scaled) difference between the old and now position
SKPoint difference = Layer.GetNormalizedPoint(topLeft - path.Points[0], false);
// Apply the difference so that the shape effectively stays in place
_movementPreview.Preview(Layer.Transform.Position.CurrentValue + difference);
}
public void FinishAnchorMovement()
{
if (Layer == null)
return;
if (_movementPreview == null || _anchorMovementPreview == null)
throw new ArtemisUIException("Can't update movement without a preview having been started");
// Not interested in this one's return value but we do need to discard it
_movementPreview.DiscardPreview();
if (_anchorMovementPreview.DiscardPreview())
{
using ProfileEditorCommandScope commandScope = _profileEditorService.CreateCommandScope("Update anchor point");
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<SKPoint>(Layer.Transform.Position, _movementPreview.PreviewValue, _time));
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<SKPoint>(Layer.Transform.AnchorPoint, _anchorMovementPreview.PreviewValue, _time));
}
_movementPreview = null;
_anchorMovementPreview = null;
}
#endregion
#region Resizing
private LayerPropertyPreview<SKSize>? _resizePreview;
public void StartResize()
{
if (Layer == null)
return;
_resizePreview?.DiscardPreview();
_resizePreview = new LayerPropertyPreview<SKSize>(Layer.Transform.Scale, _time);
}
public void UpdateResize(SKPoint position, ResizeSide side, bool evenPercentages, bool evenPixels, bool round)
{
if (Layer == null)
return;
if (_resizePreview == null)
throw new ArtemisUIException("Can't update size without a preview having been started");
SKPoint normalizedAnchor = Layer.Transform.AnchorPoint;
normalizedAnchor = new SKPoint(normalizedAnchor.X, normalizedAnchor.Y);
// The anchor is used to ensure a side can't shrink past the anchor
SKPoint anchor = Layer.GetLayerAnchorPosition();
SKRect bounds = Layer.GetLayerBounds();
float width = Layer.Transform.Scale.CurrentValue.Width;
float height = Layer.Transform.Scale.CurrentValue.Height;
// Resize each side as requested, the sides of each axis are mutually exclusive
if (side.HasFlag(ResizeSide.Left) && normalizedAnchor.X != 0)
{
if (position.X > anchor.X)
position.X = anchor.X;
float anchorWeight = normalizedAnchor.X;
float requiredDistance = anchor.X - position.X;
float requiredSize = requiredDistance / anchorWeight;
width = requiredSize / bounds.Width * 100f;
if (round)
width = MathF.Round(width / 5f) * 5f;
}
else if (side.HasFlag(ResizeSide.Right) && Math.Abs(normalizedAnchor.X - 1) > 0.001)
{
if (position.X < anchor.X)
position.X = anchor.X;
float anchorWeight = 1f - normalizedAnchor.X;
float requiredDistance = position.X - anchor.X;
float requiredSize = requiredDistance / anchorWeight;
width = requiredSize / bounds.Width * 100f;
if (round)
width = MathF.Round(width / 5f) * 5f;
}
if (side.HasFlag(ResizeSide.Top) && normalizedAnchor.Y != 0)
{
if (position.Y > anchor.Y)
position.Y = anchor.Y;
float anchorWeight = normalizedAnchor.Y;
float requiredDistance = anchor.Y - position.Y;
float requiredSize = requiredDistance / anchorWeight;
height = requiredSize / bounds.Height * 100f;
if (round)
height = MathF.Round(height / 5f) * 5f;
}
else if (side.HasFlag(ResizeSide.Bottom) && Math.Abs(normalizedAnchor.Y - 1) > 0.001)
{
if (position.Y < anchor.Y)
position.Y = anchor.Y;
float anchorWeight = 1f - normalizedAnchor.Y;
float requiredDistance = position.Y - anchor.Y;
float requiredSize = requiredDistance / anchorWeight;
height = requiredSize / bounds.Height * 100f;
if (round)
height = MathF.Round(height / 5f) * 5f;
}
// Apply side evening but only when resizing on a corner
bool resizingCorner = (side.HasFlag(ResizeSide.Left) || side.HasFlag(ResizeSide.Right)) && (side.HasFlag(ResizeSide.Top) || side.HasFlag(ResizeSide.Bottom));
if (evenPercentages && resizingCorner)
{
if (width > height)
width = height;
else
height = width;
}
else if (evenPixels && resizingCorner)
{
if (width * bounds.Width > height * bounds.Height)
height = width * bounds.Width / bounds.Height;
else
width = height * bounds.Height / bounds.Width;
}
_resizePreview.Preview(new SKSize(width, height));
}
public void FinishResize()
{
if (Layer == null)
return;
if (_resizePreview == null)
throw new ArtemisUIException("Can't update size without a preview having been started");
if (_resizePreview.DiscardPreview())
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<SKSize>(Layer.Transform.Scale, _resizePreview.PreviewValue, _time));
_resizePreview = null;
}
#endregion
#region Rotating
private LayerPropertyPreview<float>? _rotatePreview;
public void StartRotation()
{
if (Layer == null)
return;
_rotatePreview?.DiscardPreview();
_rotatePreview = new LayerPropertyPreview<float>(Layer.Transform.Rotation, _time);
}
public void UpdateRotation(float rotation, bool round)
{
if (_rotatePreview == null)
throw new ArtemisUIException("Can't update rotation without a preview having been started");
if (round)
rotation = MathF.Round(rotation / 5f) * 5f;
_rotatePreview.Preview(rotation);
}
public void FinishRotation()
{
if (Layer == null)
return;
if (_rotatePreview == null)
throw new ArtemisUIException("Can't update rotation without a preview having been started");
if (_rotatePreview.DiscardPreview())
_profileEditorService.ExecuteCommand(new UpdateLayerProperty<float>(Layer.Transform.Rotation, _rotatePreview.PreviewValue, _time));
_rotatePreview = null;
}
#endregion
#region Utilities
private static SKPoint RoundPoint(SKPoint point, int decimals)
{
return new SKPoint(
(float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero),
(float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero)
);
}
private static SKPoint[] UnTransformPoints(SKPoint[] skPoints, Layer layer, SKPoint pivot, bool includeScale)
{
using SKPath counterRotatePath = new();
counterRotatePath.AddPoly(skPoints, false);
counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y));
if (includeScale)
counterRotatePath.Transform(SKMatrix.CreateScale(1f / (layer.Transform.Scale.CurrentValue.Width / 100f), 1f / (layer.Transform.Scale.CurrentValue.Height / 100f)));
return counterRotatePath.Points;
}
#endregion
}

View File

@ -91,19 +91,7 @@ public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualiz
private void UpdateLayerBounds()
{
// Create accurate bounds based on the RgbLeds and not the rounded ArtemisLeds
SKPath path = new();
foreach (ArtemisLed artemisLed in Layer.Leds)
{
path.AddRect(SKRect.Create(
artemisLed.RgbLed.AbsoluteBoundary.Location.X,
artemisLed.RgbLed.AbsoluteBoundary.Location.Y,
artemisLed.RgbLed.AbsoluteBoundary.Size.Width,
artemisLed.RgbLed.AbsoluteBoundary.Size.Height)
);
}
SKRect bounds = path.Bounds;
SKRect bounds = Layer.GetLayerBounds();
LayerBounds = new Rect(0, 0, bounds.Width, bounds.Height);
X = bounds.Left;
Y = bounds.Top;

View File

@ -2,11 +2,13 @@
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia;
using Avalonia.Controls.Mixins;
using ReactiveUI;
using ShimSkiaSharp;
using SKRect = SkiaSharp.SKRect;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
@ -59,20 +61,9 @@ public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerVie
private void Update()
{
// Create accurate bounds based on the RgbLeds and not the rounded ArtemisLeds
SKPath path = new();
foreach (ArtemisLed artemisLed in Layer.Leds)
{
path.AddRect(SKRect.Create(
artemisLed.RgbLed.AbsoluteBoundary.Location.X,
artemisLed.RgbLed.AbsoluteBoundary.Location.Y,
artemisLed.RgbLed.AbsoluteBoundary.Size.Width,
artemisLed.RgbLed.AbsoluteBoundary.Size.Height)
);
}
LayerBounds = new Rect(0, 0, path.Bounds.Width, path.Bounds.Height);
X = path.Bounds.Left;
Y = path.Bounds.Top;
SKRect bounds = Layer.GetLayerBounds();
LayerBounds = new Rect(0, 0, bounds.Width, bounds.Height);
X = bounds.Left;
Y = bounds.Top;
}
}

View File

@ -3,24 +3,41 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.PropertyInput;
using Avalonia;
using DynamicData;
using Ninject;
namespace Artemis.UI.Services;
public class RegistrationService : IRegistrationService
{
private readonly IKernel _kernel;
private readonly IInputService _inputService;
private readonly IPropertyInputService _propertyInputService;
private bool _registeredBuiltInPropertyEditors;
public RegistrationService(IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable<IToolViewModel> toolViewModels)
public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable<IToolViewModel> toolViewModels)
{
_kernel = kernel;
_inputService = inputService;
_propertyInputService = propertyInputService;
profileEditorService.Tools.AddRange(toolViewModels);
CreateCursorResources();
}
private void CreateCursorResources()
{
ICursorProvider? cursorProvider = _kernel.TryGet<ICursorProvider>();
if (cursorProvider == null)
return;
Application.Current?.Resources.Add("RotateCursor", cursorProvider.Rotate);
Application.Current?.Resources.Add("DragCursor", cursorProvider.Drag);
Application.Current?.Resources.Add("DragHorizontalCursor", cursorProvider.DragHorizontal);
}
public void RegisterBuiltInDataModelDisplays()