Forráskód Böngészése

Ellipse tool, better viewport, layer tree improvements

Equbuxu 3 éve
szülő
commit
40bd8a6885

+ 30 - 4
src/PixiEditor/Helpers/Behaviours/SliderUpdateBehavior.cs

@@ -8,6 +8,16 @@ namespace PixiEditor.Helpers.Behaviours;
 #nullable enable
 internal class SliderUpdateBehavior : Behavior<Slider>
 {
+    public static readonly DependencyProperty BindingProperty =
+        DependencyProperty.Register(nameof(Binding), typeof(double), typeof(SliderUpdateBehavior), new(0.0, OnBindingValuePropertyChange));
+
+    public double Binding
+    {
+        get => (double)GetValue(BindingProperty);
+        set => SetValue(BindingProperty, value);
+    }
+
+
     public static DependencyProperty DragValueChangedProperty = DependencyProperty.Register(nameof(DragValueChanged), typeof(ICommand), typeof(SliderUpdateBehavior));
     public ICommand DragValueChanged
     {
@@ -41,7 +51,10 @@ internal class SliderUpdateBehavior : Behavior<Slider>
 
     private bool attached = false;
     private bool dragging = false;
-    private bool valueChangedWhileDragging = false;
+
+    private bool bindingValueChangedWhileDragging = false;
+    private double bindingValueWhileDragging = 0.0;
+
     protected override void OnAttached()
     {
         AssociatedObject.Loaded += AssociatedObject_Loaded;
@@ -92,16 +105,29 @@ internal class SliderUpdateBehavior : Behavior<Slider>
         {
             if (obj.DragValueChanged is not null && obj.DragValueChanged.CanExecute(e.NewValue))
                 obj.DragValueChanged.Execute(e.NewValue);
-            obj.valueChangedWhileDragging = true;
         }
     }
 
+    private static void OnBindingValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
+    {
+        SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        if (obj.dragging)
+        {
+            obj.bindingValueChangedWhileDragging = true;
+            obj.bindingValueWhileDragging = (double)e.NewValue;
+            return;
+        }
+        obj.ValueFromSlider = (double)e.NewValue;
+    }
+
     private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
     {
         dragging = false;
-        if (valueChangedWhileDragging && DragEnded is not null && DragEnded.CanExecute(null))
+        if (DragEnded is not null && DragEnded.CanExecute(null))
             DragEnded.Execute(null);
-        valueChangedWhileDragging = false;
+        if (bindingValueChangedWhileDragging)
+            ValueFromSlider = bindingValueWhileDragging;
+        bindingValueChangedWhileDragging = false;
     }
 
     private void Thumb_DragStarted(object sender, DragStartedEventArgs e)

+ 1 - 1
src/PixiEditor/Helpers/Converters/MultiplyConverter.cs

@@ -15,7 +15,7 @@ internal class MultiplyConverter : SingleInstanceConverter<MultiplyConverter>
 
     public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
     {
-        return base.ConvertBack(value, targetType, parameter, culture);
+        return 0;
     }
 
     private double? NumberToDouble(object number)

+ 1 - 1
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -51,7 +51,7 @@ internal static class ServiceCollectionHelpers
         .AddSingleton<ToolViewModel, MagicWandToolViewModel>()
         .AddSingleton<ToolViewModel, FloodFillToolViewModel>()
         .AddSingleton<ToolViewModel, LineToolViewModel>()
-        .AddSingleton<ToolViewModel, CircleToolViewModel>()
+        .AddSingleton<ToolViewModel, EllipseToolViewModel>()
         .AddSingleton<ToolViewModel, RectangleToolViewModel>()
         .AddSingleton<ToolViewModel, EraserToolViewModel>()
         .AddSingleton<ToolViewModel, ColorPickerToolViewModel>()

+ 0 - 0
src/PixiEditor/Images/Tools/CircleImage.png → src/PixiEditor/Images/Tools/EllipseImage.png


+ 9 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -8,6 +8,7 @@ namespace PixiEditor.Models.DocumentModels;
 internal class ChangeExecutionController
 {
     public MouseButtonState LeftMouseState { get; private set; }
+    public ShapeCorners LastTransformState { get; private set; }
     public VecI LastPixelPosition => lastPixelPos;
     public VecD LastPrecisePosition => lastPrecisePos;
     public float LastOpacityValue = 1f;
@@ -127,4 +128,12 @@ internal class ChangeExecutionController
         //call session events
         currentSession?.OnLeftMouseButtonUp();
     }
+
+    public void OnTransformMoved(ShapeCorners corners)
+    {
+        LastTransformState = corners;
+        currentSession?.OnTransformMoved(corners);
+    }
+
+    public void OnTransformApplied() => currentSession?.OnTransformApplied();
 }

+ 100 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs

@@ -0,0 +1,100 @@
+using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+#nullable enable
+internal class EllipseToolExecutor : UpdateableChangeExecutor
+{
+    private int strokeWidth;
+    private SKColor fillColor;
+    private SKColor strokeColor;
+    private Guid memberGuid;
+    private bool drawOnMask;
+
+    private bool transforming = false;
+    private EllipseToolViewModel? ellipseTool;
+    private VecI startPos;
+    private RectI lastRect;
+
+    public override OneOf<Success, Error> Start()
+    {
+        ColorsViewModel? colorsVM = ViewModelMain.Current?.ColorsSubViewModel;
+        ellipseTool = (EllipseToolViewModel?)(ViewModelMain.Current?.ToolsSubViewModel.GetTool<EllipseToolViewModel>());
+        BasicShapeToolbar? toolbar = (BasicShapeToolbar?)ellipseTool?.Toolbar;
+        ViewModels.SubViewModels.Document.StructureMemberViewModel? member = document?.SelectedStructureMember;
+        if (colorsVM is null || toolbar is null || member is null || ellipseTool is null)
+            return new Error();
+
+        fillColor = toolbar.Fill ? toolbar.FillColor.ToSKColor() : SKColors.Transparent;
+        startPos = controller!.LastPixelPosition;
+        strokeColor = colorsVM.PrimaryColor;
+        strokeWidth = toolbar.ToolSize;
+        memberGuid = member.GuidValue;
+        drawOnMask = member.ShouldDrawOnMask;
+
+        DrawEllipseOrCircle(startPos);
+        return new Success();
+    }
+
+    private void DrawEllipseOrCircle(VecI curPos)
+    {
+        RectI rect = RectI.FromTwoPoints(startPos, curPos);
+        if (rect.Width == 0)
+            rect.Width = 1;
+        if (rect.Height == 0)
+            rect.Height = 1;
+
+        if (ellipseTool!.DrawCircle)
+            rect.Width = rect.Height = Math.Min(rect.Width, rect.Height);
+        lastRect = rect;
+
+        helpers!.ActionAccumulator.AddActions(new DrawEllipse_Action(memberGuid, rect, strokeColor, fillColor, strokeWidth, drawOnMask));
+    }
+
+    public override void OnTransformMoved(ShapeCorners corners)
+    {
+        if (!transforming)
+            return;
+
+        helpers!.ActionAccumulator.AddActions(
+            new DrawEllipse_Action(memberGuid, (RectI)RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize), strokeColor, fillColor, strokeWidth, drawOnMask));
+    }
+
+    public override void OnTransformApplied()
+    {
+        helpers!.ActionAccumulator.AddFinishedActions(new EndDrawEllipse_Action());
+        document!.TransformViewModel.HideTransform();
+        onEnded?.Invoke(this);
+    }
+
+    public override void OnPixelPositionChange(VecI pos)
+    {
+        if (transforming)
+            return;
+        DrawEllipseOrCircle(pos);
+    }
+
+    public override void OnLeftMouseButtonUp()
+    {
+        if (transforming)
+            return;
+        transforming = true;
+        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()
+    {
+        if (transforming)
+            document!.TransformViewModel.HideTransform();
+        helpers!.ActionAccumulator.AddFinishedActions(new EndDrawEllipse_Action());
+    }
+}

+ 27 - 17
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineBasedPenExecutor.cs → src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -1,16 +1,18 @@
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class LineBasedPenExecutor : UpdateableChangeExecutor
+internal class PenToolExecutor : UpdateableChangeExecutor
 {
     private Guid guidValue;
     private SKColor color;
     private int toolSize;
     private bool drawOnMask;
+    private bool pixelPerfect;
 
     public override OneOf<Success, Error> Start()
     {
@@ -25,14 +27,13 @@ internal class LineBasedPenExecutor : UpdateableChangeExecutor
         color = vm.ColorsSubViewModel.PrimaryColor;
         toolSize = toolbar.ToolSize;
         drawOnMask = member.ShouldDrawOnMask;
+        pixelPerfect = toolbar.PixelPerfectEnabled;
 
-        LineBasedPen_Action? action = new(
-            guidValue,
-            color,
-            controller!.LastPixelPosition,
-            toolSize,
-            false,
-            drawOnMask);
+        IAction? action = pixelPerfect switch
+        {
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, toolSize, false, drawOnMask),
+            true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask)
+        };
         helpers!.ActionAccumulator.AddActions(action);
 
         return new Success();
@@ -40,24 +41,33 @@ internal class LineBasedPenExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        LineBasedPen_Action? action = new(
-            guidValue,
-            color,
-            pos,
-            toolSize,
-            false,
-            drawOnMask);
+        IAction? action = pixelPerfect switch
+        {
+            false => new LineBasedPen_Action(guidValue, color, pos, toolSize, false, drawOnMask),
+            true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask)
+        };
         helpers!.ActionAccumulator.AddActions(action);
     }
 
     public override void OnLeftMouseButtonUp()
     {
-        helpers!.ActionAccumulator.AddFinishedActions(new EndLineBasedPen_Action());
+        IAction? action = pixelPerfect switch
+        {
+            false => new EndLineBasedPen_Action(),
+            true => new EndPixelPerfectPen_Action()
+        };
+
+        helpers!.ActionAccumulator.AddFinishedActions(action);
         onEnded?.Invoke(this);
     }
 
     public override void ForceStop()
     {
-        helpers!.ActionAccumulator.AddFinishedActions(new EndLineBasedPen_Action());
+        IAction? action = pixelPerfect switch
+        {
+            false => new EndLineBasedPen_Action(),
+            true => new EndPixelPerfectPen_Action()
+        };
+        helpers!.ActionAccumulator.AddFinishedActions(action);
     }
 }

+ 2 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -36,4 +36,6 @@ internal abstract class UpdateableChangeExecutor
     public virtual void OnOpacitySliderDragEnded() { }
     public virtual void OnKeyDown(Key key) { }
     public virtual void OnKeyUp(Key key) { }
+    public virtual void OnTransformMoved(ShapeCorners corners) { }
+    public virtual void OnTransformApplied() { }
 }

+ 2 - 2
src/PixiEditor/PixiEditor.csproj

@@ -177,8 +177,8 @@
 		<None Remove="Images\SocialMedia\WebsiteIcon.png" />
 		<None Remove="Images\SocialMedia\YouTubeIcon.png" />
 		<None Remove="Images\Tools\BrightnessImage.png" />
-		<None Remove="Images\Tools\CircleImage.png" />
 		<None Remove="Images\Tools\ColorPickerImage.png" />
+		<None Remove="Images\Tools\EllipseImage.png" />
 		<None Remove="Images\Tools\EraserImage.png" />
 		<None Remove="Images\Tools\FloodFillImage.png" />
 		<None Remove="Images\Tools\LineImage.png" />
@@ -270,7 +270,7 @@
 		<Resource Include="Images\SocialMedia\WebsiteIcon.png" />
 		<Resource Include="Images\SocialMedia\YouTubeIcon.png" />
 		<Resource Include="Images\Tools\BrightnessImage.png" />
-		<Resource Include="Images\Tools\CircleImage.png" />
+		<Resource Include="Images\Tools\EllipseImage.png" />
 		<Resource Include="Images\Tools\ColorPickerImage.png" />
 		<Resource Include="Images\Tools\EraserImage.png" />
 		<Resource Include="Images\Tools\FloodFillImage.png" />

+ 23 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentTransformViewModel.cs

@@ -39,6 +39,17 @@ internal class DocumentTransformViewModel : INotifyPropertyChanged
         }
     }
 
+    private bool lockRotation;
+    public bool LockRotation
+    {
+        get => lockRotation;
+        set
+        {
+            lockRotation = value;
+            PropertyChanged?.Invoke(this, new(nameof(LockRotation)));
+        }
+    }
+
     private bool transformActive;
     public bool TransformActive
     {
@@ -80,11 +91,21 @@ internal class DocumentTransformViewModel : INotifyPropertyChanged
         TransformActive = false;
     }
 
-    public void ShowShapeTransform(ShapeCorners initPos)
+    public void ShowFixedAngleShapeTransform(ShapeCorners initPos)
+    {
+        CornerFreedom = TransformCornerFreedom.Scale;
+        SideFreedom = TransformSideFreedom.ScaleProportionally;
+        LockRotation = true;
+        RequestedCorners = initPos;
+        TransformActive = true;
+    }
+
+    public void ShowRotatingShapeTransform(ShapeCorners initPos)
     {
         CornerFreedom = TransformCornerFreedom.Scale;
         SideFreedom = TransformSideFreedom.ScaleProportionally;
         RequestedCorners = initPos;
+        LockRotation = false;
         TransformActive = true;
     }
 
@@ -93,6 +114,7 @@ internal class DocumentTransformViewModel : INotifyPropertyChanged
         CornerFreedom = TransformCornerFreedom.Free;
         SideFreedom = TransformSideFreedom.Free;
         RequestedCorners = initPos;
+        LockRotation = false;
         TransformActive = true;
     }
 }

+ 5 - 4
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -109,12 +109,12 @@ internal class DocumentViewModel : NotifyableObject
     public DocumentViewModel(string name)
     {
         //Name = name;
-        TransformViewModel = new();
-        //TransformViewModel.TransformMoved += OnTransformUpdate;
-
         Helpers = new DocumentHelpers(this);
         StructureRoot = new FolderViewModel(this, Helpers, Helpers.Tracker.Document.StructureRoot.GuidValue);
 
+        TransformViewModel = new();
+        TransformViewModel.TransformMoved += (_, args) => Helpers.ChangeController.OnTransformMoved(args);
+
         /*UndoCommand = new RelayCommand(Undo);
         RedoCommand = new RelayCommand(Redo);
         ClearSelectionCommand = new RelayCommand(ClearSelection);
@@ -231,7 +231,8 @@ internal class DocumentViewModel : NotifyableObject
 
     public void UseOpacitySlider() => Helpers.ChangeController.TryStartUpdateableChange<StructureMemberOpacityExecutor>();
 
-    public void UsePenTool() => Helpers.ChangeController.TryStartUpdateableChange<LineBasedPenExecutor>();
+    public void UsePenTool() => Helpers.ChangeController.TryStartUpdateableChange<PenToolExecutor>();
+    public void UseEllipseTool() => Helpers.ChangeController.TryStartUpdateableChange<EllipseToolExecutor>();
 
     public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
     {

+ 0 - 25
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/CircleToolViewModel.cs

@@ -1,25 +0,0 @@
-using System.Windows.Input;
-using PixiEditor.Models.Commands.Attributes.Commands;
-
-namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
-
-[Command.Tool(Key = Key.C)]
-internal class CircleToolViewModel : ShapeTool
-{
-    private string defaultActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
-
-    public CircleToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-    }
-
-    public override string Tooltip => $"Draws circle on canvas ({Shortcut}). Hold Shift to draw even circle.";
-
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
-    {
-        if (shiftIsDown)
-            ActionDisplay = "Click and move mouse to draw an even circle.";
-        else
-            ActionDisplay = defaultActionDisplay;
-    }
-}

+ 38 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EllipseToolViewModel.cs

@@ -0,0 +1,38 @@
+using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Models.Commands.Attributes.Commands;
+
+namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+
+[Command.Tool(Key = Key.C)]
+internal class EllipseToolViewModel : ShapeTool
+{
+    private string defaultActionDisplay = "Click and move mouse to draw an ellipse. Hold Shift to draw a circle.";
+
+    public EllipseToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+    }
+
+    public override string Tooltip => $"Draws an ellipse on canvas ({Shortcut}). Hold Shift to draw a circle.";
+    public bool DrawCircle { get; private set; }
+
+    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    {
+        if (shiftIsDown)
+        {
+            ActionDisplay = "Click and move mouse to draw a circle.";
+            DrawCircle = true;
+        }
+        else
+        {
+            ActionDisplay = defaultActionDisplay;
+            DrawCircle = false;
+        }
+    }
+
+    public override void OnLeftMouseButtonDown(VecD pos)
+    {
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.UseEllipseTool();
+    }
+}

+ 1 - 0
src/PixiEditor/Views/UserControls/DrawingViewPort.xaml

@@ -62,6 +62,7 @@
 
         <local:Zoombox.AdditionalContent>
             <Canvas Width="{Binding Width}" Height="{Binding Height}"
+                    d:Width="400" d:Height="400"
                     Loaded="OnCanvasLoaded"
                     VerticalAlignment="Center" HorizontalAlignment="Center"
                     RenderOptions.BitmapScalingMode="NearestNeighbor">

+ 6 - 6
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml

@@ -59,14 +59,14 @@
                     IsSnapToTickEnabled="False"
                     x:Name="opacitySlider"
                     VerticalAlignment="Center"
-                    HorizontalAlignment="Stretch"
-                    Value="{Binding ElementName=layersManager, Path=ActiveDocument.SelectedStructureMember.OpacityBindable, Mode=OneWay}">
+                    HorizontalAlignment="Stretch">
                         <i:Interaction.Behaviors>
                             <beh:SliderUpdateBehavior
-                            DragStarted="{commands:Command PixiEditor.Layer.OpacitySliderDragStarted}"
-                            DragValueChanged="{commands:Command PixiEditor.Layer.OpacitySliderDragged, UseProvided=True}"
-                            DragEnded="{commands:Command PixiEditor.Layer.OpacitySliderDragEnded}"
-                            ValueFromSlider="{Binding ElementName=opacitySlider, Path=Value}" />
+                                Binding="{Binding ElementName=layersManager, Path=ActiveDocument.SelectedStructureMember.OpacityBindable, Mode=OneWay}"
+                                DragStarted="{commands:Command PixiEditor.Layer.OpacitySliderDragStarted}"
+                                DragValueChanged="{commands:Command PixiEditor.Layer.OpacitySliderDragged, UseProvided=True}"
+                                DragEnded="{commands:Command PixiEditor.Layer.OpacitySliderDragEnded}"
+                                ValueFromSlider="{Binding ElementName=opacitySlider, Path=Value, Mode=TwoWay}" />
                         </i:Interaction.Behaviors>
                     </Slider>
                     <userControls:NumberInput

+ 27 - 14
src/PixiEditor/Views/UserControls/TransformOverlay/TransformOverlay.cs

@@ -28,6 +28,9 @@ internal class TransformOverlay : Control
         DependencyProperty.Register(nameof(CornerFreedom), typeof(TransformCornerFreedom), typeof(TransformOverlay),
             new PropertyMetadata(TransformCornerFreedom.Locked));
 
+    public static readonly DependencyProperty LockRotationProperty =
+        DependencyProperty.Register(nameof(LockRotation), typeof(bool), typeof(TransformOverlay), new(false));
+
     public static readonly DependencyProperty SnapToAnglesProperty =
         DependencyProperty.Register(nameof(SnapToAngles), typeof(bool), typeof(TransformOverlay), new PropertyMetadata(false));
 
@@ -77,6 +80,12 @@ internal class TransformOverlay : Control
         set => SetValue(ZoomboxScaleProperty, value);
     }
 
+    public bool LockRotation
+    {
+        get => (bool)GetValue(LockRotationProperty);
+        set => SetValue(LockRotationProperty, value);
+    }
+
     private bool isResettingRequestedCorners = false;
     private bool isMoving = false;
     private VecD mousePosOnStartMove = new();
@@ -174,8 +183,8 @@ internal class TransformOverlay : Control
         base.OnMouseDown(e);
 
         e.Handled = true;
-        var pos = TransformHelper.ToVecD(e.GetPosition(this));
-        var anchor = TransformHelper.GetAnchorInPosition(pos, Corners, InternalState.Origin, ZoomboxScale);
+        VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
+        Anchor? anchor = TransformHelper.GetAnchorInPosition(pos, Corners, InternalState.Origin, ZoomboxScale);
         if (anchor is not null)
         {
             capturedAnchor = anchor;
@@ -190,7 +199,7 @@ internal class TransformOverlay : Control
             originOnStartMove = InternalState.Origin;
             cornersOnStartMove = Corners;
         }
-        else
+        else if (!LockRotation)
         {
             isRotating = true;
             mousePosOnStartRotate = TransformHelper.ToVecD(e.GetPosition(this));
@@ -198,6 +207,10 @@ internal class TransformOverlay : Control
             propAngle1OnStartRotate = InternalState.ProportionalAngle1;
             propAngle2OnStartRotate = InternalState.ProportionalAngle2;
         }
+        else
+        {
+            return;
+        }
         CaptureMouse();
     }
 
@@ -211,8 +224,8 @@ internal class TransformOverlay : Control
 
         if (isMoving)
         {
-            var pos = TransformHelper.ToVecD(e.GetPosition(this));
-            var delta = pos - mousePosOnStartMove;
+            VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
+            VecD delta = pos - mousePosOnStartMove;
 
             if (Corners.IsSnappedToPixels)
                 delta = delta.Round();
@@ -229,8 +242,8 @@ internal class TransformOverlay : Control
         }
         else if (isRotating)
         {
-            var pos = TransformHelper.ToVecD(e.GetPosition(this));
-            var angle = (mousePosOnStartRotate - InternalState.Origin).CCWAngleTo(pos - InternalState.Origin);
+            VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
+            double angle = (mousePosOnStartRotate - InternalState.Origin).CCWAngleTo(pos - InternalState.Origin);
             if (SnapToAngles)
                 angle = TransformHelper.FindSnappingAngle(cornersOnStartRotate, angle);
             InternalState = InternalState with { ProportionalAngle1 = propAngle1OnStartRotate + angle, ProportionalAngle2 = propAngle2OnStartRotate + angle, };
@@ -243,16 +256,16 @@ internal class TransformOverlay : Control
         if (capturedAnchor is null)
             throw new InvalidOperationException("No anchor is captured");
         e.Handled = true;
-        if (TransformHelper.IsCorner((Anchor)capturedAnchor) && CornerFreedom == TransformCornerFreedom.Locked ||
-            TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked)
+        if ((TransformHelper.IsCorner((Anchor)capturedAnchor) && CornerFreedom == TransformCornerFreedom.Locked) ||
+            (TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked))
             return;
 
-        var pos = TransformHelper.ToVecD(e.GetPosition(this));
+        VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
 
         if (TransformHelper.IsCorner((Anchor)capturedAnchor))
         {
-            var targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
-            var newCorners = TransformUpdateHelper.UpdateShapeFromCorner
+            VecD targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
+            ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
                 ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1, InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos);
             if (newCorners is not null)
             {
@@ -264,8 +277,8 @@ internal class TransformOverlay : Control
         }
         else if (TransformHelper.IsSide((Anchor)capturedAnchor))
         {
-            var targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
-            var newCorners = TransformUpdateHelper.UpdateShapeFromSide
+            VecD targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
+            ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
                 ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1, InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos);
             if (newCorners is not null)
             {

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

@@ -7,6 +7,7 @@
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
     xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
+    xmlns:sys="clr-namespace:System;assembly=System.Runtime"
     xmlns:to="clr-namespace:PixiEditor.Views.UserControls.TransformOverlay"
     xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
     xmlns:sym="clr-namespace:PixiEditor.Views.UserControls.SymmetryOverlay"
@@ -29,12 +30,23 @@
             FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
             FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
             <Border
-                BorderThickness="1"
-                Background="White"
-                BorderBrush="Black"
+                d:Width="64"
+                d:Height="64"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
-                DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+                DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
+                RenderOptions.BitmapScalingMode="NearestNeighbor">
+                <Border.Background>
+                    <ImageBrush ImageSource="/Images/CheckerTile.png" TileMode="Tile" ViewportUnits="Absolute">
+                        <ImageBrush.Viewport>
+                            <Binding Path="Scale" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type zoombox:Zoombox}}" Converter="{converters:ZoomToViewportConverter}">
+                                <Binding.ConverterParameter>
+                                    <sys:Double>16</sys:Double>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </ImageBrush.Viewport>
+                    </ImageBrush>
+                </Border.Background>
                 <Grid>
                     <Canvas>
                         <Image
@@ -79,6 +91,7 @@
                         RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners, Mode=TwoWay}"
                         CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
                         SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
+                        LockRotation="{Binding Document.TransformViewModel.LockRotation}"
                         InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
                         ZoomboxScale="{Binding Zoombox.Scale}" />
                 </Grid>
@@ -102,11 +115,6 @@
                 <RowDefinition
                     Height="1*" />
             </Grid.RowDefinitions>
-            <Border
-                BorderBrush="Red"
-                Grid.Row="1"
-                Grid.Column="1"
-                BorderThickness="1" />
         </Grid>
     </Grid>
 </UserControl>

+ 12 - 12
src/PixiEditor/Views/UserControls/Viewport.xaml.cs

@@ -142,9 +142,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         get => realDimensions;
         set
         {
-            var oldRes = CalculateResolution();
+            ChunkResolution oldRes = CalculateResolution();
             realDimensions = value;
-            var newRes = CalculateResolution();
+            ChunkResolution newRes = CalculateResolution();
 
             PropertyChanged?.Invoke(this, new(nameof(RealDimensions)));
             Document?.AddOrUpdateViewport(GetLocation());
@@ -161,9 +161,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         get => dimensions;
         set
         {
-            var oldRes = CalculateResolution();
+            ChunkResolution oldRes = CalculateResolution();
             dimensions = value;
-            var newRes = CalculateResolution();
+            ChunkResolution newRes = CalculateResolution();
 
             PropertyChanged?.Invoke(this, new(nameof(Dimensions)));
             Document?.AddOrUpdateViewport(GetLocation());
@@ -177,7 +177,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         get
         {
-            return Document?.Bitmaps.TryGetValue(CalculateResolution(), out var value) == true ? value : null;
+            return Document?.Bitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
         }
     }
 
@@ -220,9 +220,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private static void OnDocumentChange(DependencyObject viewportObj, DependencyPropertyChangedEventArgs args)
     {
-        var oldDoc = (DocumentViewModel?)args.OldValue;
-        var newDoc = (DocumentViewModel?)args.NewValue;
-        var viewport = (Viewport)viewportObj;
+        DocumentViewModel? oldDoc = (DocumentViewModel?)args.OldValue;
+        DocumentViewModel? newDoc = (DocumentViewModel?)args.NewValue;
+        Viewport? viewport = (Viewport)viewportObj;
         oldDoc?.RemoveViewport(viewport.GuidValue);
         newDoc?.AddOrUpdateViewport(viewport.GetLocation());
     }
@@ -247,7 +247,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private ViewportInfo GetLocation()
     {
-        return new(Angle, Center, RealDimensions / 2, Dimensions / 2, CalculateResolution(), GuidValue, Delayed, ForceRefreshFinalImage);
+        return new(Angle, Center, RealDimensions, Dimensions, CalculateResolution(), GuidValue, Delayed, ForceRefreshFinalImage);
     }
 
     private void OnReferenceImageSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -259,9 +259,9 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         if (MouseDownCommand is null)
             return;
-        var pos = e.GetPosition((IInputElement)sender);
+        Point pos = e.GetPosition((IInputElement)sender);
         VecD conv = new VecD(pos.X, pos.Y);
-        var parameter = new MouseOnCanvasEventArgs(e.ChangedButton, conv);
+        MouseOnCanvasEventArgs? parameter = new MouseOnCanvasEventArgs(e.ChangedButton, conv);
 
         if (MouseDownCommand.CanExecute(parameter))
             MouseDownCommand.Execute(parameter);
@@ -271,7 +271,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         if (MouseMoveCommand is null)
             return;
-        var pos = e.GetPosition((IInputElement)sender);
+        Point pos = e.GetPosition((IInputElement)sender);
         VecD conv = new VecD(pos.X, pos.Y);
 
         if (MouseMoveCommand.CanExecute(conv))