Transform tool - Finished initial implementation
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
24
src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs
Normal 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; }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
|
After Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 369 B |
BIN
src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag.png
Normal file
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_plus.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png
Normal file
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 669 B |
18
src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs
Normal 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
|
||||
}
|
||||
22
src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs
Normal 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; }
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||