فهرست منبع

Apply transform button and shortcut, fix zoombox movement with middle mouse click

Equbuxu 3 سال پیش
والد
کامیت
29eea8856f

+ 2 - 2
src/ChunkyImageLibTest/ClearRegionOperationTests.cs

@@ -14,7 +14,7 @@ public class ClearRegionOperationTests
     {
     {
         ClearRegionOperation operation = new(new(new(chunkSize, chunkSize), new(chunkSize, chunkSize)));
         ClearRegionOperation operation = new(new(new(chunkSize, chunkSize), new(chunkSize, chunkSize)));
         var expected = new HashSet<VecI>() { new(1, 1) };
         var expected = new HashSet<VecI>() { new(1, 1) };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
 
 
@@ -33,7 +33,7 @@ public class ClearRegionOperationTests
             new(-2, -0), new(-1, -0), new(0, -0), new(1, -0),
             new(-2, -0), new(-1, -0), new(0, -0), new(1, -0),
             new(-2,  1), new(-1,  1), new(0,  1), new(1,  1),
             new(-2,  1), new(-1,  1), new(0,  1), new(1,  1),
         };
         };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
 #pragma warning restore format
 #pragma warning restore format

+ 1 - 1
src/ChunkyImageLibTest/ImageOperationTests.cs

@@ -12,7 +12,7 @@ public class ImageOperationTests
     {
     {
         using Surface testImage = new Surface((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         using Surface testImage = new Surface((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         using ImageOperation operation = new((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), testImage);
         using ImageOperation operation = new((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), testImage);
-        var chunks = operation.FindAffectedChunks();
+        var chunks = operation.FindAffectedChunks(new(ChunkyImage.FullChunkSize));
         Assert.Equal(new HashSet<VecI>() { new(1, 1) }, chunks);
         Assert.Equal(new HashSet<VecI>() { new(1, 1) }, chunks);
     }
     }
 }
 }

+ 7 - 7
src/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -19,7 +19,7 @@ public class RectangleOperationTests
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, SKColors.Black, SKColors.Transparent));
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, SKColors.Black, SKColors.Transparent));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -31,7 +31,7 @@ public class RectangleOperationTests
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, SKColors.Black, SKColors.Transparent));
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, SKColors.Black, SKColors.Transparent));
 
 
         HashSet<VecI> expected = new() { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) };
         HashSet<VecI> expected = new() { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -48,7 +48,7 @@ public class RectangleOperationTests
             new(1, 2),            new(3, 2),
             new(1, 2),            new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
             new(1, 3), new(2, 3), new(3, 3),
         };
         };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -65,7 +65,7 @@ public class RectangleOperationTests
             new(-4, -3),              new(-2, -3),
             new(-4, -3),              new(-2, -3),
             new(-4, -2), new(-3, -2), new(-2, -2),
             new(-4, -2), new(-3, -2), new(-2, -2),
         };
         };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -82,7 +82,7 @@ public class RectangleOperationTests
             new(1, 2), new(2, 2), new(3, 2),
             new(1, 2), new(2, 2), new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
             new(1, 3), new(2, 3), new(3, 3),
         };
         };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -101,7 +101,7 @@ public class RectangleOperationTests
             new(0, 3), new(1, 3), new(2, 3), new(3, 3), new(4, 3),
             new(0, 3), new(1, 3), new(2, 3), new(3, 3), new(4, 3),
             new(0, 4), new(1, 4), new(2, 4), new(3, 4), new(4, 4),
             new(0, 4), new(1, 4), new(2, 4), new(3, 4), new(4, 4),
         };
         };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
@@ -113,7 +113,7 @@ public class RectangleOperationTests
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, SKColors.Black, SKColors.White));
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, SKColors.Black, SKColors.White));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks();
+        var actual = operation.FindAffectedChunks(new(chunkSize));
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }

+ 2 - 0
src/PixiEditor.Zoombox/Operations/MoveDragOperation.cs

@@ -16,6 +16,7 @@ internal class MoveDragOperation : IDragOperation
     public void Start(MouseButtonEventArgs e)
     public void Start(MouseButtonEventArgs e)
     {
     {
         prevMousePos = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
         prevMousePos = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
+        parent.mainGrid.CaptureMouse();
     }
     }
 
 
     public void Update(MouseEventArgs e)
     public void Update(MouseEventArgs e)
@@ -27,5 +28,6 @@ internal class MoveDragOperation : IDragOperation
 
 
     public void Terminate()
     public void Terminate()
     {
     {
+        parent.mainGrid.ReleaseMouseCapture();
     }
     }
 }
 }

+ 2 - 0
src/PixiEditor.Zoombox/Operations/RotateDragOperation.cs

@@ -22,6 +22,7 @@ internal class RotateDragOperation : IDragOperation
         initialClickAngle = GetAngle(new(pointCur.X, pointCur.Y));
         initialClickAngle = GetAngle(new(pointCur.X, pointCur.Y));
         initialZoomboxAngle = owner.Angle;
         initialZoomboxAngle = owner.Angle;
         rotationProcess = new LockingRotationProcess(initialZoomboxAngle);
         rotationProcess = new LockingRotationProcess(initialZoomboxAngle);
+        owner.mainGrid.CaptureMouse();
     }
     }
 
 
     private double GetAngle(VecD point)
     private double GetAngle(VecD point)
@@ -47,5 +48,6 @@ internal class RotateDragOperation : IDragOperation
 
 
     public void Terminate()
     public void Terminate()
     {
     {
+        owner.mainGrid.ReleaseMouseCapture();
     }
     }
 }
 }

+ 2 - 0
src/PixiEditor.Zoombox/Operations/ZoomDragOperation.cs

@@ -23,6 +23,7 @@ internal class ZoomDragOperation : IDragOperation
         screenScaleOrigin = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
         screenScaleOrigin = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));
         scaleOrigin = parent.ToZoomboxSpace(screenScaleOrigin);
         scaleOrigin = parent.ToZoomboxSpace(screenScaleOrigin);
         originalScale = parent.Scale;
         originalScale = parent.Scale;
+        parent.mainGrid.CaptureMouse();
     }
     }
 
 
     public void Update(MouseEventArgs e)
     public void Update(MouseEventArgs e)
@@ -39,5 +40,6 @@ internal class ZoomDragOperation : IDragOperation
 
 
     public void Terminate()
     public void Terminate()
     {
     {
+        parent.mainGrid.ReleaseMouseCapture();
     }
     }
 }
 }

+ 15 - 0
src/PixiEditor/Helpers/Converters/ZoomModeToHitTestVisibleConverter.cs

@@ -0,0 +1,15 @@
+using System.Globalization;
+using System.Windows;
+using PixiEditor.Zoombox;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class ZoomModeToHitTestVisibleConverter : SingleInstanceConverter<ZoomModeToHitTestVisibleConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is not ZoomboxMode zoomboxMode)
+            return DependencyProperty.UnsetValue;
+        return zoomboxMode == ZoomboxMode.Normal;
+    }
+}

+ 1 - 8
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs

@@ -90,14 +90,7 @@ internal class EllipseToolExecutor : UpdateableChangeExecutor
         transforming = true;
         transforming = true;
         document!.TransformViewModel.ShowFixedAngleShapeTransform(new ShapeCorners(lastRect));
         document!.TransformViewModel.ShowFixedAngleShapeTransform(new ShapeCorners(lastRect));
     }
     }
-
-    public override void OnKeyDown(Key key)
-    {
-        if (key is not Key.Enter || !transforming)
-            return;
-        OnTransformApplied();
-    }
-
+    
     public override void ForceStop()
     public override void ForceStop()
     {
     {
         if (transforming)
         if (transforming)

+ 32 - 0
src/PixiEditor/Styles/ThemeStyle.xaml

@@ -62,6 +62,38 @@
         </Style.Triggers>
         </Style.Triggers>
     </Style>
     </Style>
 
 
+    <Style TargetType="Button" x:Key="GrayRoundButton" BasedOn="{StaticResource BaseDarkButton}">
+        <Setter Property="Background" Value="#404040" />
+        <Setter Property="Foreground" Value="White" />
+        <Setter Property="OverridesDefaultStyle" Value="True" />
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="Button">
+                    <Border CornerRadius="4" Background="{TemplateBinding Background}">
+                        <ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center"
+                                          VerticalAlignment="Center" Margin="{TemplateBinding Padding}"/>
+                    </Border>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsEnabled" Value="False">
+                            <Setter Property="Background" Value="Transparent" />
+                            <Setter Property="Foreground" Value="Gray" />
+                            <Setter Property="Cursor" Value="Arrow" />
+                        </Trigger>
+                        <Trigger Property="IsMouseOver" Value="True">
+                            <Setter Property="Background" Value="#FF515151" />
+                            <Setter Property="Foreground" Value="White" />
+                            <Setter Property="Cursor" Value="Hand" />
+                        </Trigger>
+                        <Trigger Property="IsPressed" Value="True">
+                            <Setter Property="Background" Value="#505050" />
+                            <Setter Property="Foreground" Value="White" />
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+    
     <Style TargetType="Button" x:Key="DarkRoundButton" BasedOn="{StaticResource BaseDarkButton}">
     <Style TargetType="Button" x:Key="DarkRoundButton" BasedOn="{StaticResource BaseDarkButton}">
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="Background" Value="#303030" />
         <Setter Property="Background" Value="#303030" />

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -394,5 +394,6 @@ internal class DocumentViewModel : NotifyableObject
     public void OnOpacitySliderDragStarted() => Helpers.ChangeController.OnOpacitySliderDragStarted();
     public void OnOpacitySliderDragStarted() => Helpers.ChangeController.OnOpacitySliderDragStarted();
     public void OnOpacitySliderDragged(float newValue) => Helpers.ChangeController.OnOpacitySliderDragged(newValue);
     public void OnOpacitySliderDragged(float newValue) => Helpers.ChangeController.OnOpacitySliderDragged(newValue);
     public void OnOpacitySliderDragEnded() => Helpers.ChangeController.OnOpacitySliderDragEnded();
     public void OnOpacitySliderDragEnded() => Helpers.ChangeController.OnOpacitySliderDragEnded();
+    public void OnApplyTransform() => Helpers.ChangeController.OnTransformApplied();
     #endregion
     #endregion
 }
 }

+ 10 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
@@ -71,6 +72,15 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
         SetActiveTool(typeof(T));
         SetActiveTool(typeof(T));
     }
     }
 
 
+    [Command.Basic("PixiEditor.Tools.ApplyTransform", "Apply transform", "", Key = Key.Enter)]
+    public void ApplyTransform()
+    {
+        DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+        doc.OnApplyTransform();
+    }
+
     [Command.Internal("PixiEditor.Tools.SelectTool", CanExecute = "PixiEditor.HasDocument")]
     [Command.Internal("PixiEditor.Tools.SelectTool", CanExecute = "PixiEditor.HasDocument")]
     public void SetActiveTool(ToolViewModel tool)
     public void SetActiveTool(ToolViewModel tool)
     {
     {

+ 5 - 1
src/PixiEditor/Views/UserControls/SymmetryOverlay/SymmetryOverlay.cs

@@ -148,11 +148,13 @@ internal class SymmetryOverlay : Control
 
 
     private VecD ToVecD(Point pos) => new VecD(pos.X, pos.Y);
     private VecD ToVecD(Point pos) => new VecD(pos.X, pos.Y);
 
 
-    public SymmetryAxisDirection? capturedDirection;
+    private SymmetryAxisDirection? capturedDirection;
 
 
     protected override void OnMouseDown(MouseButtonEventArgs e)
     protected override void OnMouseDown(MouseButtonEventArgs e)
     {
     {
         base.OnMouseDown(e);
         base.OnMouseDown(e);
+        if (e.ChangedButton != MouseButton.Left)
+            return;
 
 
         var pos = ToVecD(e.GetPosition(this));
         var pos = ToVecD(e.GetPosition(this));
         var dir = IsTouchingHandle(pos);
         var dir = IsTouchingHandle(pos);
@@ -178,6 +180,8 @@ internal class SymmetryOverlay : Control
     protected override void OnMouseUp(MouseButtonEventArgs e)
     protected override void OnMouseUp(MouseButtonEventArgs e)
     {
     {
         base.OnMouseUp(e);
         base.OnMouseUp(e);
+        if (e.ChangedButton != MouseButton.Left)
+            return;
 
 
         if (capturedDirection is null)
         if (capturedDirection is null)
             return;
             return;

+ 7 - 2
src/PixiEditor/Views/UserControls/TransformOverlay/TransformOverlay.cs

@@ -1,12 +1,13 @@
 using System.Windows;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 
 
 namespace PixiEditor.Views.UserControls.TransformOverlay;
 namespace PixiEditor.Views.UserControls.TransformOverlay;
 #nullable enable
 #nullable enable
-internal class TransformOverlay : Control
+internal class TransformOverlay : Decorator
 {
 {
     public static readonly DependencyProperty RequestedCornersProperty =
     public static readonly DependencyProperty RequestedCornersProperty =
         DependencyProperty.Register(nameof(RequestedCorners), typeof(ShapeCorners), typeof(TransformOverlay),
         DependencyProperty.Register(nameof(RequestedCorners), typeof(ShapeCorners), typeof(TransformOverlay),
@@ -181,7 +182,9 @@ internal class TransformOverlay : Control
     protected override void OnMouseDown(MouseButtonEventArgs e)
     protected override void OnMouseDown(MouseButtonEventArgs e)
     {
     {
         base.OnMouseDown(e);
         base.OnMouseDown(e);
-
+        if (e.ChangedButton != MouseButton.Left)
+            return;
+        
         e.Handled = true;
         e.Handled = true;
         VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
         VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
         Anchor? anchor = TransformHelper.GetAnchorInPosition(pos, Corners, InternalState.Origin, ZoomboxScale);
         Anchor? anchor = TransformHelper.GetAnchorInPosition(pos, Corners, InternalState.Origin, ZoomboxScale);
@@ -297,6 +300,8 @@ internal class TransformOverlay : Control
     protected override void OnMouseUp(MouseButtonEventArgs e)
     protected override void OnMouseUp(MouseButtonEventArgs e)
     {
     {
         base.OnMouseUp(e);
         base.OnMouseUp(e);
+        if (e.ChangedButton != MouseButton.Left)
+            return;
         if (ReleaseAnchor())
         if (ReleaseAnchor())
             e.Handled = true;
             e.Handled = true;
         else if (isMoving)
         else if (isMoving)

+ 17 - 1
src/PixiEditor/Views/UserControls/Viewport.xaml

@@ -13,6 +13,7 @@
     xmlns:sym="clr-namespace:PixiEditor.Views.UserControls.SymmetryOverlay"
     xmlns:sym="clr-namespace:PixiEditor.Views.UserControls.SymmetryOverlay"
     xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
     xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+    xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
     mc:Ignorable="d"
     mc:Ignorable="d"
     x:Name="vpUc"
     x:Name="vpUc"
     d:DesignHeight="450"
     d:DesignHeight="450"
@@ -93,6 +94,7 @@
                         Source="{Binding TargetBitmap}"
                         Source="{Binding TargetBitmap}"
                         RenderOptions.BitmapScalingMode="{Binding Zoombox.Scale, Converter={converters:ScaleToBitmapScalingModeConverter}}"/>
                         RenderOptions.BitmapScalingMode="{Binding Zoombox.Scale, Converter={converters:ScaleToBitmapScalingModeConverter}}"/>
                     <sym:SymmetryOverlay
                     <sym:SymmetryOverlay
+                        IsHitTestVisible="{Binding ZoomMode, Converter={converters:ZoomModeToHitTestVisibleConverter}}"
                         ZoomboxScale="{Binding Zoombox.Scale}"
                         ZoomboxScale="{Binding Zoombox.Scale}"
                         HorizontalAxisVisible="{Binding Document.HorizontalSymmetryAxisEnabledBindable}"
                         HorizontalAxisVisible="{Binding Document.HorizontalSymmetryAxisEnabledBindable}"
                         VerticalAxisVisible="{Binding Document.VerticalSymmetryAxisEnabledBindable}"
                         VerticalAxisVisible="{Binding Document.VerticalSymmetryAxisEnabledBindable}"
@@ -104,6 +106,8 @@
                         Path="{Binding Document.SelectionPathBindable}"
                         Path="{Binding Document.SelectionPathBindable}"
                         ZoomboxScale="{Binding Zoombox.Scale}" />
                         ZoomboxScale="{Binding Zoombox.Scale}" />
                     <to:TransformOverlay
                     <to:TransformOverlay
+                        Cursor="Arrow"
+                        IsHitTestVisible="{Binding ZoomMode, Converter={converters:ZoomModeToHitTestVisibleConverter}}"
                         HorizontalAlignment="Stretch"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:BoolToVisibilityConverter}}"
                         Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:BoolToVisibilityConverter}}"
@@ -113,7 +117,7 @@
                         SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
                         SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
                         LockRotation="{Binding Document.TransformViewModel.LockRotation}"
                         LockRotation="{Binding Document.TransformViewModel.LockRotation}"
                         InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
                         InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
-                        ZoomboxScale="{Binding Zoombox.Scale}" />
+                        ZoomboxScale="{Binding Zoombox.Scale}"/>
                     <Grid IsHitTestVisible="False" SnapsToDevicePixels="True"
                     <Grid IsHitTestVisible="False" SnapsToDevicePixels="True"
                         ShowGridLines="True" Width="{Binding Document.Width}" Height="{Binding Document.Height}" Panel.ZIndex="10" 
                         ShowGridLines="True" Width="{Binding Document.Width}" Height="{Binding Document.Height}" Panel.ZIndex="10" 
                         Visibility="{Binding GridLinesVisible, Converter={converters:BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
                         Visibility="{Binding GridLinesVisible, Converter={converters:BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
@@ -142,5 +146,17 @@
                 </Grid>
                 </Grid>
             </Border>
             </Border>
         </zoombox:Zoombox>
         </zoombox:Zoombox>
+        <Button 
+            Panel.ZIndex="99999"
+            DockPanel.Dock="Bottom" 
+            Width="140" 
+            Height="28" 
+            Margin="5" 
+            VerticalAlignment="Bottom" 
+            Style="{StaticResource GrayRoundButton}"
+            Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:BoolToVisibilityConverter}, ElementName=vpUc}"
+            Command="{cmds:Command PixiEditor.Tools.ApplyTransform}">
+            Apply transform
+        </Button>
     </Grid>
     </Grid>
 </UserControl>
 </UserControl>