From 32ebf5f00092c662c4cc081360643d0299084880 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 13 Feb 2022 21:19:32 +0100 Subject: [PATCH] Transform tool - Finished initial implementation --- src/Artemis.Core/Constants.cs | 2 +- src/Artemis.Core/Models/Profile/Layer.cs | 12 +- .../Profile/LayerProperties/LayerProperty.cs | 17 +- .../LayerProperties/LayerPropertyPreview.cs | 93 ++++ .../Profile/LayerTransformProperties.cs | 6 +- .../Visualization/Tools/EditToolViewModel.cs | 5 +- .../Extensions/LayerExtensions.cs | 9 +- .../Providers/ICursorProvider.cs | 24 + .../Commands/ResetLayerProperty.cs | 52 ++ .../Commands/UpdateLayerProperty.cs | 15 +- src/Avalonia/Artemis.UI.Windows/App.axaml.cs | 5 +- .../ApplicationStateManager.cs | 2 +- .../Artemis.UI.Windows.csproj | 13 + .../Assets/Cursors/aero_crosshair.png | Bin 0 -> 347 bytes .../Assets/Cursors/aero_crosshair_minus.png | Bin 0 -> 356 bytes .../Assets/Cursors/aero_crosshair_plus.png | Bin 0 -> 369 bytes .../Assets/Cursors/aero_drag.png | Bin 0 -> 627 bytes .../Assets/Cursors/aero_drag_horizontal.png | Bin 0 -> 1124 bytes .../Assets/Cursors/aero_pen_min.png | Bin 0 -> 460 bytes .../Assets/Cursors/aero_pen_plus.png | Bin 0 -> 472 bytes .../Assets/Cursors/aero_rotate.png | Bin 0 -> 825 bytes .../Assets/Cursors/aero_rotate_bl.png | Bin 0 -> 673 bytes .../Assets/Cursors/aero_rotate_br.png | Bin 0 -> 678 bytes .../Assets/Cursors/aero_rotate_tl.png | Bin 0 -> 671 bytes .../Assets/Cursors/aero_rotate_tr.png | Bin 0 -> 669 bytes .../Ninject/WindowsModule.cs | 18 + .../Providers/CursorProvider.cs | 22 + .../Artemis.UI/ArtemisBootstrapper.cs | 4 +- .../Properties/Tree/TreePropertyViewModel.cs | 13 +- .../Panels/StatusBar/StatusBarViewModel.cs | 15 +- .../Tools/TransformToolView.axaml | 294 ++++++----- .../Tools/TransformToolView.axaml.cs | 444 ++++++++++------ .../Tools/TransformToolViewModel.cs | 474 ++++++++++++------ .../LayerShapeVisualizerViewModel.cs | 14 +- .../Visualizers/LayerVisualizerViewModel.cs | 21 +- .../Services/RegistrationService.cs | 19 +- 36 files changed, 1094 insertions(+), 499 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_plus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_bl.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index b63d493c2..2422ef063 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -32,7 +32,7 @@ namespace Artemis.Core /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis.Avalonia"); /// /// The full path to the Artemis logs folder diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 11a34940c..1a8a26905 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -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; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 6bdf1fd6d..f671d6a07 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -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. /// - /// The new keyframe if one was created. + /// The keyframe if one was created or updated. public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); - LayerPropertyKeyframe? newKeyframe = null; + LayerPropertyKeyframe? keyframe = null; if (time == null || !KeyframesEnabled || !KeyframesSupported) BaseValue = value; else { // If on a keyframe, update the keyframe - LayerPropertyKeyframe? 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(value, time.Value, Easings.Functions.Linear, this); - AddKeyframe(newKeyframe); + keyframe = new LayerPropertyKeyframe(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; } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs new file mode 100644 index 000000000..906c5058b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; + +namespace Artemis.Core; + +/// +/// Represents a container for a preview value of a that can be used to update and +/// discard a temporary value. +/// +/// The value type of the layer property. +public sealed class LayerPropertyPreview : IDisposable +{ + /// + /// Creates a new instance of the class. + /// + /// The layer property to apply the preview value to. + /// The time in the timeline at which the preview is applied. + public LayerPropertyPreview(LayerProperty layerProperty, TimeSpan time) + { + Property = layerProperty; + Time = time; + OriginalKeyframe = layerProperty.Keyframes.FirstOrDefault(k => k.Position == time); + OriginalValue = OriginalKeyframe != null ? OriginalKeyframe.Value : layerProperty.CurrentValue; + PreviewValue = OriginalValue; + } + + /// + /// Gets the property this preview applies to. + /// + public LayerProperty Property { get; } + + /// + /// Gets the original keyframe of the property at the time the preview was created. + /// + public LayerPropertyKeyframe? OriginalKeyframe { get; } + + /// + /// Gets the original value of the property at the time the preview was created. + /// + public T OriginalValue { get; } + + /// + /// Gets the time in the timeline at which the preview is applied. + /// + public TimeSpan Time { get; } + + /// + /// Gets the keyframe that was created to preview the value. + /// + public LayerPropertyKeyframe? PreviewKeyframe { get; private set; } + + /// + /// Gets the preview value. + /// + public T? PreviewValue { get; private set; } + + /// + /// Updates the layer property to the given , keeping track of the original state of the + /// property. + /// + /// The value to preview. + public void Preview(T value) + { + PreviewValue = value; + PreviewKeyframe = Property.SetCurrentValue(value, Time); + } + + /// + /// 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. + /// + /// if any changes where discarded; otherwise . + public bool DiscardPreview() + { + if (PreviewKeyframe != null && OriginalKeyframe == null) + { + Property.RemoveKeyframe(PreviewKeyframe); + return true; + } + + Property.SetCurrentValue(OriginalValue, Time); + return !Equals(OriginalValue, PreviewValue); ; + } + + /// + /// 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. + /// + public void Dispose() + { + DiscardPreview(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs index 9a78aba1c..2b2ab624b 100644 --- a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -12,13 +12,13 @@ namespace Artemis.Core /// /// The point at which the shape is attached to its position /// - [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; } /// /// The position of the shape /// - [PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)] + [PropertyDescription(Description = "The position of the shape", InputAffix = "%")] public SKPointLayerProperty Position { get; set; } /// @@ -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; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs index 3f0e8fbde..381e3e42d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs @@ -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) diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs index a1d2cd5d9..9e4b874c0 100644 --- a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs @@ -14,6 +14,9 @@ public static class LayerExtensions /// 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 /// /// Returns a new point normalized to 0.0-1.0 /// - 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) diff --git a/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs b/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs new file mode 100644 index 000000000..a8bd9a768 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs @@ -0,0 +1,24 @@ +using Avalonia.Input; + +namespace Artemis.UI.Shared.Providers; + +/// +/// Represents a provider for custom cursors. +/// +public interface ICursorProvider +{ + /// + /// A cursor that indicates a rotating. + /// + public Cursor Rotate { get; } + + /// + /// A cursor that indicates dragging. + /// + public Cursor Drag { get; } + + /// + /// A cursor that indicates dragging horizontally. + /// + public Cursor DragHorizontal { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs new file mode 100644 index 000000000..edc15e9ec --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs @@ -0,0 +1,52 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to reset a layer property to it's default value. +/// +public class ResetLayerProperty : IProfileEditorCommand +{ + private readonly LayerProperty _layerProperty; + private readonly T _originalBaseValue; + private readonly bool _keyframesEnabled; + + /// + /// Creates a new instance of the class. + /// + public ResetLayerProperty(LayerProperty 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 + + /// + public string DisplayName => "Reset layer property"; + + /// + public void Execute() + { + string json = CoreJson.SerializeObject(_layerProperty.DefaultValue, true); + + if (_keyframesEnabled) + _layerProperty.KeyframesEnabled = false; + + _layerProperty.SetCurrentValue(CoreJson.DeserializeObject(json)!, null); + } + + /// + public void Undo() + { + _layerProperty.SetCurrentValue(_originalBaseValue, null); + if (_keyframesEnabled) + _layerProperty.KeyframesEnabled = true; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs index d6179bb4e..794a64358 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs @@ -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 : IProfileEditorCommand private readonly T _newValue; private readonly T _originalValue; private readonly TimeSpan? _time; + private readonly bool _hasKeyframe; private LayerPropertyKeyframe? _newKeyframe; /// @@ -23,6 +25,7 @@ public class UpdateLayerProperty : 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 : 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); } /// diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs index 6d5b42adc..d527e4d2d 100644 --- a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -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); } diff --git a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs index a3c1be22d..34c509005 100644 --- a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs @@ -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; diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 69e2d4bd1..436ab8302 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -8,6 +8,19 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png new file mode 100644 index 0000000000000000000000000000000000000000..9330d74cac38eb1cd979b88915afe83ce5c71a76 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(Kp`7X z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+CgznR zFXQOpq`=IeKbLc|wo+>|&>Yng*NBpo#FA92 ou5DmoWnd82(1)raH$NpatrE8e^}A;FKpPl5UHx3vIVCg!0C-(lk^lez literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png new file mode 100644 index 0000000000000000000000000000000000000000..2845e62887f9314f53fb5f42a0b1ccb262310d9f GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(Kp|I8 z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+Cgzp1 zJdnXq$(l!pq0Guo)P>E6p<0@2>CL;Z76b-rgDVb@NxHYKXHM0k5VDNPHb6Mw<&;$S_16=h0 literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b0e2e7841c9491b0630a287235a3906e4e6d3e GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(K%pQ{ z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+Cgzo+ zer~(N>j2gk4h{n@F6A?9+zoU4{8{Uq6|)-OF)(bt%Ux`!FT4n7x@w7QL`h0wNvc(H zQ7VvPFfuSQ(ls#BH8ct_G_W!@vobW-HZZUk4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKpuOE zr>`sfGbSD`6_Hz2e6JW77^^*9978;gzn#2S-#JjCeSUGY`QBU(*A<;5lV{90l3=_c z!oY*0^KL@oV}1q2i5;yGtj)Zg2OT^NBxX%$D^cVK-SYkMR*r?G)zRGt-|IYoGUxu} z?|bAwc>mlt=g$r#+X?d{r!$_t-lPP5f>>k?egxmJ6> z#MIyX#XfAZcbDy-Fsr3TyK%Q&@inRKw~UYRo^JUZxrtk3QM-Yp-pvx}$Mt_-=fC7# z6tn+s)tV|$q^p*=MwFx^mZVxG7o`Fz1|tI_BV7X{T|=V~Ljx;gGb=+2Z36=<1A_%8 g{$-(P$jwj5OsmAL;o?-cYd{SQp00i_>zopr0MmT!i~s-t literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..e222d8e91a85eab3dfbee897b05dcdd2ad039f94 GIT binary patch literal 1124 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LL6ppAc7|0%X9&#l^|V z2^V2!XP1(aQc_al=jX4hs{=~O%gf8k%CfPs!PUsf$n^L3Z{NP%#Kh#{#f#F?(npRQ zS-yNZP?x2prH+n{sHiAVv6`CN-o1OHqN4Wg+c$agWOH+KpcW@5C!myyii)$dGmrr^ z7N~vJtXV*|y1IH#Pftil2s1M?Q1X$BwOC zyVlm$R#{oOxVX5crbbm&Ra;wo+O%m`u3P~sV`XIps<*bbUa(+6Q&W?KgapJcpc|^I zs}CPOoR*db)B+R)dS6mf66oga?Ciyh7k74c1_uWNT`njn2$Y2=;N#=t<>dv243PWl z*RStizWn(5_4n`JKyFb{(bK0-=gpf33U<=FJ;mH~<|F^m}=E`R&`c=gyrA zbQRF!SFc`u`}Qr+u9q)g9yoBIzP|p>ojV^sd^mXU;QROQzkmO}XV0E*-@bkN^y$x^ zKhe?AU%q@ zbUl4^``7(1&IQRxFn9d@_Xh~SpF6%uS!CkBKYxD*|IT2Un5I7U;6LFbU%tBh`1kLh z-#^>$Jcs}NPVspAE2!u5zsCmh^M3zc!Kk|6+rPittR$Ns{`&Lh`;7_(*7&MIciukb zA5B188I!!-U8Zs*R5b%R>?NMQuI$g4c(_zVZdLKU0!nLox;TbdoK8+yAavr?DL3Jh zC(pW0JbyS~Li-XXjrr4AnQj`KbpUht83R}i^kNuYECrrCFkoDw#6DlzLCZj7!3k#u zWnp7(ZE0(Eb>(jx5}rJI_VDTH=i&k)LSlkP7d5#0#5e`5a)@#Z^NVv7>}FDZdT7z3 zNtZSiiH4e%^19h@X=$xC*}7$;#)XyZR<2#0pOKT5ck#;6kOj;06K>qPcJJcNs}a|G zT^lEE>|B}1&yx_Ckd~L2+3Md|njin4We-8C(bf!f5aCia3QMJT1q9i4;B-JXp zC>2OC7#SED=^7a68XAQd8dw>dSs9vY8yHv_7#x(JzY0Y|ZhlH;S|x4`8=Ta|fEpM) MUHx3vIVCg!06CPe^8f$< literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png new file mode 100644 index 0000000000000000000000000000000000000000..b876e71d66cfafa6d753bc1c3e4697f128cec296 GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10!aKkx#1%;60oSfw`}gl( zXlUrehYx>!|88YvB_t$t;=~D7R@RLhH*VRorKqTA+qP{xcI^27|3Al1>uR7`oFzei z!3=&HueEM+^DEak- zar*7K+k6cQ91V$9kuJ|S-TUu5IkNj~=iTe|&wsEnr7f7gJ>msZrcs#@Y2QIe8al4_M)lnSI6j0}v7bPbGj4UIw!4XljKtPD-H4GgRd j3=YcAUxlI}H$NpatrE9}4NmG}Kn)C@u6{1-oD!M!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10!Y{xl#1%;60oSfw`}O_% zzkmNiLqmW6`0?=JLn|vQAt9j?Cr+@kvToeCam$u1MMXv1wr$(7W5@sh|D_yCj{uG1 zED7=pX7JN^t#y-|m+@lpb`PK+W0JSKi{zWUtsOuPdx@v7EBiAh9xfG;TUC6ofI>x{ zE{-7yG-OazVr1Cfqpjk!;^Fl(yg}vr zz8V|Zy_gXHSxkTNb~gS>jf3wBdT;ZF^ynBriPtewdHTgQxz0drarzp!`2OC7#SED=^7a68XAQd w8dw>dSs9vY8yHv_7#x(JzY0Y|ZhlH;S|x4`8=Ta|fEpM)UHx3vIVCg!0H>CkdjJ3c literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..91f5728a5324ed771b6fa24b4e9fa95c20104fea GIT binary patch literal 825 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKpuOE zr>`sfGbSD`6_Hz2e6N7=j-D=#As)x~PVx1a;wW*v{{8OTyWUNDGHZvnYpX!X$_09! zAuOsZ!gZBDDQm3YD}50V`|Dh=ya;Pxh?=Bh)P(7$6c#pz1;}!>9N}3wsmH{){Qlf} z_UTK*uGhvcO6NOl@cFml=QGC4|4nkr9-owpZz?+}c6#&Szm`_Z4c}fgDxAI5SUl&B z!sHBHuFh36x%SA-y|Ca^sjr*tDZzg;wVo9h&CV(OyQAloYjbr8WA7E_fbRCnN~ufC z7vIHQsPHO%79>Tp{zc(sFc?YEA6`-G)jhbpV|7SCQ4xL5Afi@uzPiK(7% zy3ZZsp1WRZf_~ZgFRLpKls8V7@eR%4e;u}n>sV=2-9|YL?Wq@&MPKf26S?3n8EPf^ zc=4*9jRv6`+r(M~0?%!fXxJ1wnME{f>cy)XPdh((a#_7xa;)}Vp_*0ouh7B+{(53& z#UGh|YO=oaZD&)ucKOEv{lux4xEIumo54W62SEb|*}jp5Oc}Z+khxYhQbbWkJ$xJ?odO z>l1Vy-+S{;`egj4_hze_=8L4-$R7MN?|Lv-pX6@kU|xS06^l=4Lid*62`t-jkg@Ce zmv@)9tK3;^^|*WURb4)TFDAd+&5ob5W}jPn@s8sxj=z??)FK#IZ0z{p6~z)087D8$gf%Gk`x&`jIFz{!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF zV`Sn5av7O;fFv0JXabrpMn)c>Y5)KK2l{}OO%Nz6Bqa3q?OS_$`vV6K^!4=>78ZW~ z{8>Umf`^Ca`Sa&XmoANpifU+R`0(L#eSQ7n#fyc7h2Ok+7ZVfn=FOY;@83_JJlWON zm7PO~iJAAsi&p{y0y#N3yLRn*_Uzfcu-?r;3lvL&{DK+G_g;A~^y{ttfxbc;o~7b- zi>;~v+LR4pTQ~rGl7a3lf2zs6p!TaiUB$7C7!;n?9Z5ZxKu=LRq?$7 z3a#*TaSX9I{dVGUp+g2dE>BsU)-NpYyjR^G@bCX(gBgq0-m%!IetwU+^r94i&2J?6taRG>%6_&5td~z~J&_=h+Il0yrnA^7 z$!fNG@T-i!2`^dojBkGL4y!sG`#5d(Y4NTW>$QPJ-`gL*tl2qD(6-k7X<5C^)%6v> zuCCr%wfLryd5yE3%!&5dWj~&aS2Fgd@!R$KU*!b4S+&G9q9i4;B-JXpC>2OC7#SED z=^7a68XAQd8dw>dSs9vX8yHv_7<~BNYL22IH$NpatrE9}##Of6Kn)C@u6{1-oD!M< D{NvYu literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png new file mode 100644 index 0000000000000000000000000000000000000000..31275d9301b0c4db5bec7a3b82ff98064d76776e GIT binary patch literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF z1F{*Jco~^^fDBRrL_1IyP&*?diV6S!{|EYjl}%7cNC>FF-roN0+qZpveFqL4kdTo0 z{P}ZXVd2uHOP@b~&cnm=;lt;KhK8u9s5fuk2@4A^Uc9)zzW&9FS4_;j>>NU_uC9|O zPk#UY{hK#$Vq#*RJ$trm*RGtL9037=zLH1cK+_dVg8YIR%=caqdjHG*ZQlW#Lh+?M zR*UP{BwZ&@yDQfG^Y61=IRZ63i@t!|n&j>7qIe{SR}9EuFY)wsWq-!R!=)l}tBUUx zP-v~Ei(`nz>9-S)^Bpn}aCxiJa?krk;`Dpg3%CFOuOXwOsF?fhRO`+kALi+tkLXzM z;moCXiQ!s+jK$-#md?egELjnYMv}m4U&B@2%!28glbfGSez?YiL|$+YQvf;OXk; Jvd$@?2><~d+g1Po literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd314a7a853af33ac5c1ea12d7ac694b25502d0 GIT binary patch literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0WD;Ov6$3Kh zfRT}hkqLQT~t(5Lqo%d51)B>c%DChzI5r*!otGOpFc}TNE|qD zps%m*?c2BZ_Vz%B2?+_YvI#c7O%eri6ib5qf*JOj3*_v2_V?%AX0d6LT_xG-7F+Qw z6)&_o(D&9}=-2xzLN^8afQlKDyxm=twUTnIfE@M`PhVH|XG}a?Dk8V4_+9~pmU+53 zhFF|_JIP(>kO7Zt?q$U<_nbs7R=@xIuf3@-jA!DW*IahK2LN0%irzgVqd_nm+L? zW4o1*{mG2SG-#R9zn`4?8BzR}@1|=A09~wF;u=wsl30>zm0Xkxq!^40jEr;*jC2i+ xLJSS8jLobJ&9n^+tPBi3d~Y>J(U6;;l9^VCTSMb2+isu+22WQ%mvv4FO#qn4)^z{? literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png new file mode 100644 index 0000000000000000000000000000000000000000..6c218cf7cc2b7e1897ceb22856a202a12734a9f5 GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF zV`SoCWaI&gPz8WYW8wuG`{Knbpb!54|NrdSvt7G(<>cfD2naAS^RjaYxw^Veo;>;e z`}c3&yorg4dGqF-u(0sr#f$6f>py(>+|bYv6&1B~>C)%VpY!nWNJvP0{`|SHu&}SM z@4$fr_V)H~-@X+R5(2uGl}*swnmT>9QSvOW)U;>6D28&dW1%^0qGPO@4Rdm-91j>s#Vy+x9uH ztg)D&;Jq7eo*M5GXKS2H7oZ!g*j$8 zb0zS${boBo_2B3Fx*7V$m-wysta+UbbgycOYeY#(Vo9o1a#1RfVlXl=GSW3L(ls;+ wF*L9;HnTD`*ETS)GB5~h=xavNkei>9nO2EggZf=Fd!PmePgg&ebxsLQ065gwzW@LL literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs b/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs new file mode 100644 index 000000000..3a79ea6c3 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs @@ -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 + + /// + public override void Load() + { + Kernel!.Bind().To().InSingletonScope(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs b/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs new file mode 100644 index 000000000..97a53f032 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs index be40ac024..a72152b46 100644 --- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs @@ -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(); _kernel.Load(); _kernel.Load(); + _kernel.Load(modules); _kernel.UseNinjectDependencyResolver(); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs index 5a5eb64c6..1f95bd0dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs @@ -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 : 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(LayerProperty)); } public LayerProperty LayerProperty { get; } public PropertyViewModel PropertyViewModel { get; } public PropertyInputViewModel? PropertyInputViewModel { get; } + public ReactiveCommand ResetToDefault { get; } public bool KeyframesEnabled { @@ -62,4 +72,5 @@ internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropert return depth; } -} \ No newline at end of file +} + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs index e409816dd..802c407a8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs @@ -30,14 +30,21 @@ public class StatusBarViewModel : ActivatableViewModelBase this.WhenAnyValue(vm => vm.History) .Select(h => h?.Undo ?? Observable.Never()) .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()) .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; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml index f0b7c4bdf..8a6ef6906 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml @@ -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"> - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + 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 cf78734c3..4799b39cf 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,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 { - private ZoomBorder? _zoomBorder; - private SKPoint _dragStart; + private readonly List _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("RotateTopLeft"); - _rotateTopRight = this.Get("RotateTopRight"); - _rotateBottomRight = this.Get("RotateBottomRight"); - _rotateBottomLeft = this.Get("RotateBottomLeft"); + _handleGrid = this.Get("HandleGrid"); - _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"); + _handles.Add(this.Get("RotateTopLeft")); + _handles.Add(this.Get("RotateTopRight")); + _handles.Add(this.Get("RotateBottomRight")); + _handles.Add(this.Get("RotateBottomLeft")); - _anchorPoint = this.Get("AnchorPoint"); + _resizeTopCenter = this.Get("ResizeTopCenter"); + _handles.Add(_resizeTopCenter); + _resizeRightCenter = this.Get("ResizeRightCenter"); + _handles.Add(_resizeRightCenter); + _resizeBottomCenter = this.Get("ResizeBottomCenter"); + _handles.Add(_resizeBottomCenter); + _resizeLeftCenter = this.Get("ResizeLeftCenter"); + _handles.Add(_resizeLeftCenter); + _resizeTopLeft = this.Get("ResizeTopLeft"); + _handles.Add(_resizeTopLeft); + _resizeTopRight = this.Get("ResizeTopRight"); + _handles.Add(_resizeTopRight); + _resizeBottomRight = this.Get("ResizeBottomRight"); + _handles.Add(_resizeBottomRight); + _resizeBottomLeft = this.Get("ResizeBottomLeft"); + _handles.Add(_resizeBottomLeft); + + _handles.Add(this.Get("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.RenderTransform = counterRotate; + + foreach (Control control in _handleGrid.GetVisualDescendants().Where(d => d is Control c && c.Classes.Contains("unscaled")).Cast()) + control.RenderTransform = counterScale; } private void InitializeComponent() @@ -65,6 +96,48 @@ public class TransformToolView : ReactiveUserControl 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 /// @@ -89,43 +162,7 @@ public class TransformToolView : ReactiveUserControl 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 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 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 } \ 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 d78c51f2b..b19d90311 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,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 _isEnabled; - private RelativePoint _relativeAnchor; - private double _inverseRotation; + private readonly IProfileEditorService _profileEditorService; + private Point _anchor; private ObservableAsPropertyHelper? _layer; + private RelativePoint _relativeAnchor; private double _rotation; private Rect _shapeBounds; - private Point _anchor; private TimeSpan _time; /// @@ -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(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, + Left = 8 } + + #region Movement + + private LayerPropertyPreview? _movementPreview; + + public void StartMovement() + { + if (Layer == null) + return; + + _movementPreview?.DiscardPreview(); + _movementPreview = new LayerPropertyPreview(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(Layer.Transform.Position, _movementPreview.PreviewValue, _time)); + _movementPreview = null; + } + + #endregion + + #region Anchor movement + + private SKPoint _dragOffset; + private SKPoint _dragStartAnchor; + + private LayerPropertyPreview? _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(Layer.Transform.Position, _time); + _anchorMovementPreview = new LayerPropertyPreview(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(Layer.Transform.Position, _movementPreview.PreviewValue, _time)); + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.AnchorPoint, _anchorMovementPreview.PreviewValue, _time)); + } + + _movementPreview = null; + _anchorMovementPreview = null; + } + + #endregion + + #region Resizing + + private LayerPropertyPreview? _resizePreview; + + public void StartResize() + { + if (Layer == null) + return; + + _resizePreview?.DiscardPreview(); + _resizePreview = new LayerPropertyPreview(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(Layer.Transform.Scale, _resizePreview.PreviewValue, _time)); + _resizePreview = null; + } + + #endregion + + #region Rotating + + private LayerPropertyPreview? _rotatePreview; + + public void StartRotation() + { + if (Layer == null) + return; + + _rotatePreview?.DiscardPreview(); + _rotatePreview = new LayerPropertyPreview(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(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 } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs index c180f00da..31315ae25 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -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; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs index 32fd84f96..f7e934dc6 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs @@ -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; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs index 2d629a2bf..f6f8e6c53 100644 --- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs @@ -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 toolViewModels) + public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable toolViewModels) { + _kernel = kernel; _inputService = inputService; _propertyInputService = propertyInputService; profileEditorService.Tools.AddRange(toolViewModels); + CreateCursorResources(); + } + + private void CreateCursorResources() + { + ICursorProvider? cursorProvider = _kernel.TryGet(); + 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()