Quellcode durchsuchen

Zoombox improvements

Equbuxu vor 3 Jahren
Ursprung
Commit
9c20558a99

+ 1 - 1
src/PixiEditor.Zoombox/IDragOperation.cs → src/PixiEditor.Zoombox/Operations/IDragOperation.cs

@@ -1,6 +1,6 @@
 using System.Windows.Input;
 
-namespace PixiEditor.Zoombox;
+namespace PixiEditor.Zoombox.Operations;
 
 internal interface IDragOperation
 {

+ 77 - 0
src/PixiEditor.Zoombox/Operations/LockingRotationProcess.cs

@@ -0,0 +1,77 @@
+using System;
+
+namespace PixiEditor.Zoombox.Operations;
+
+internal class LockingRotationProcess
+{
+    private double currentAngleWithoutLock;
+
+    private bool isLocked = false;
+    private double angleThatWeLockedTo = 0;
+    private bool angleWasIncreasingDuringLock = false;
+
+    public LockingRotationProcess(double initialAngle)
+    {
+        this.currentAngleWithoutLock = initialAngle;
+    }
+
+    /// <returns>New rotation angle with locking taken into account</returns>
+    public double UpdateRotation(double newAngle)
+    {
+        return isLocked ? UpdateLockedRotation(newAngle) : UpdateUnlockedRotation(newAngle);
+    }
+
+    private double UpdateUnlockedRotation(double newAngle)
+    {
+        double? lockingAngle = FindAngleToLockOn(currentAngleWithoutLock, newAngle);
+        if (lockingAngle is not null)
+        {
+            isLocked = true;
+            angleThatWeLockedTo = lockingAngle.Value;
+            angleWasIncreasingDuringLock = ZoomboxOperationHelper.SubtractOnCircle(newAngle, currentAngleWithoutLock) > 0;
+            currentAngleWithoutLock = newAngle;
+            return angleThatWeLockedTo;
+        }
+
+        currentAngleWithoutLock = newAngle;
+        return currentAngleWithoutLock;
+    }
+
+    private double UpdateLockedRotation(double newAngle)
+    {
+        currentAngleWithoutLock = newAngle;
+        double deviationFromLocked = ZoomboxOperationHelper.SubtractOnCircle(newAngle, angleThatWeLockedTo);
+
+        if (Math.Abs(deviationFromLocked) > 0.35 ||
+            angleWasIncreasingDuringLock ^ (deviationFromLocked > 0))
+        {
+            isLocked = false;
+            return currentAngleWithoutLock;
+        }
+        return angleThatWeLockedTo;
+    }
+
+    private static bool IsWithin(double point, double rangeStart, double rangeEnd)
+    {
+        double startDist = ZoomboxOperationHelper.SubtractOnCircle(rangeStart, point);
+        double endDist = ZoomboxOperationHelper.SubtractOnCircle(point, rangeEnd);
+        return startDist != 0 && endDist != 0 && Math.Sign(startDist) == Math.Sign(endDist) && Math.Abs(startDist) + Math.Abs(endDist) < Math.PI;
+    }
+
+    private static double? FindAngleToLockOn(double prevAngle, double newAngle)
+    {
+        prevAngle = ZoomboxOperationHelper.Mod(prevAngle, Math.PI * 2);
+        newAngle = ZoomboxOperationHelper.Mod(newAngle, Math.PI * 2);
+
+        if (IsWithin(0, prevAngle, newAngle))
+            return 0;
+        if (IsWithin(Math.PI / 2, prevAngle, newAngle))
+            return Math.PI / 2;
+        if (IsWithin(Math.PI, prevAngle, newAngle))
+            return Math.PI;
+        if (IsWithin(Math.PI * 3 / 2, prevAngle, newAngle))
+            return Math.PI * 3 / 2;
+
+        return null;
+    }
+}

+ 59 - 0
src/PixiEditor.Zoombox/Operations/ManipulationOperation.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditor.Zoombox.Operations;
+
+internal class ManipulationOperation
+{
+    private readonly Zoombox owner;
+    private LockingRotationProcess? rotationProcess;
+
+    private double updatedAngle;
+    private double initialAngle;
+    private bool startedRotating = false;
+
+    public ManipulationOperation(Zoombox owner)
+    {
+        this.owner = owner;
+    }
+
+    public void Start()
+    {
+        updatedAngle = owner.Angle;
+        initialAngle = owner.Angle;
+        rotationProcess = new LockingRotationProcess(owner.Angle);
+    }
+
+    public void Update(ManipulationDeltaEventArgs args)
+    {
+        args.Handled = true;
+        VecD screenTranslation = new(args.DeltaManipulation.Translation.X, args.DeltaManipulation.Translation.Y);
+        VecD screenOrigin = new(args.ManipulationOrigin.X, args.ManipulationOrigin.Y);
+        double deltaAngle = args.DeltaManipulation.Rotation / 180 * Math.PI;
+        if (owner.FlipX ^ owner.FlipY)
+            deltaAngle = -deltaAngle;
+        Manipulate(args.DeltaManipulation.Scale.X, screenTranslation, screenOrigin, deltaAngle);
+    }
+
+    private void Manipulate(double deltaScale, VecD screenTranslation, VecD screenOrigin, double rotation)
+    {
+        double newScale = Math.Clamp(owner.Scale * deltaScale, owner.MinScale, Zoombox.MaxScale);
+
+        updatedAngle += rotation;
+        if (!startedRotating && Math.Abs(ZoomboxOperationHelper.SubtractOnCircle(initialAngle, updatedAngle)) > 0.35)
+            startedRotating = true;
+
+        double newAngle = startedRotating ? rotationProcess!.UpdateRotation(updatedAngle) : initialAngle;
+
+        VecD originalPos = owner.ToZoomboxSpace(screenOrigin);
+        owner.Angle = newAngle;
+        owner.Scale = newScale;
+        VecD newPos = owner.ToZoomboxSpace(screenOrigin);
+        VecD centerTranslation = originalPos - newPos;
+        owner.Center += centerTranslation;
+
+        VecD translatedZoomboxPos = owner.ToZoomboxSpace(screenOrigin + screenTranslation);
+        owner.Center -= translatedZoomboxPos - originalPos;
+    }
+}

+ 2 - 1
src/PixiEditor.Zoombox/MoveDragOperation.cs → src/PixiEditor.Zoombox/Operations/MoveDragOperation.cs

@@ -1,7 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 
-namespace PixiEditor.Zoombox;
+namespace PixiEditor.Zoombox.Operations;
 
 internal class MoveDragOperation : IDragOperation
 {
@@ -12,6 +12,7 @@ internal class MoveDragOperation : IDragOperation
     {
         parent = zoomBox;
     }
+
     public void Start(MouseButtonEventArgs e)
     {
         prevMousePos = Zoombox.ToVecD(e.GetPosition(parent.mainCanvas));

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

@@ -0,0 +1,53 @@
+using System.Windows;
+using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditor.Zoombox.Operations;
+
+internal class RotateDragOperation : IDragOperation
+{
+    private Zoombox owner;
+    private double initialZoomboxAngle;
+    private double initialClickAngle;
+    private LockingRotationProcess? rotationProcess;
+
+    public RotateDragOperation(Zoombox zoomBox)
+    {
+        owner = zoomBox;
+    }
+
+    public void Start(MouseButtonEventArgs e)
+    {
+        Point pointCur = e.GetPosition(owner.mainCanvas);
+        initialClickAngle = GetAngle(new(pointCur.X, pointCur.Y));
+        initialZoomboxAngle = owner.Angle;
+        rotationProcess = new LockingRotationProcess(initialZoomboxAngle);
+        owner.mainCanvas.CaptureMouse();
+    }
+
+    private double GetAngle(VecD point)
+    {
+        VecD center = new(owner.mainCanvas.ActualWidth / 2, owner.mainCanvas.ActualHeight / 2);
+        double angle = (point - center).Angle;
+        if (double.IsNaN(angle) || double.IsInfinity(angle))
+            return 0;
+        return angle;
+    }
+
+    public void Update(MouseEventArgs e)
+    {
+        Point pointCur = e.GetPosition(owner.mainCanvas);
+        double clickAngle = GetAngle(new(pointCur.X, pointCur.Y));
+        double newZoomboxAngle = initialZoomboxAngle;
+        if (owner.FlipX ^ owner.FlipY)
+            newZoomboxAngle += initialClickAngle - clickAngle;
+        else
+            newZoomboxAngle += clickAngle - initialClickAngle;
+        owner.Angle = rotationProcess!.UpdateRotation(newZoomboxAngle);
+    }
+
+    public void Terminate()
+    {
+        owner.mainCanvas.ReleaseMouseCapture();
+    }
+}

+ 4 - 3
src/PixiEditor.Zoombox/ZoomDragOperation.cs → src/PixiEditor.Zoombox/Operations/ZoomDragOperation.cs

@@ -1,8 +1,8 @@
-using ChunkyImageLib.DataHolders;
-using System;
+using System;
 using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
 
-namespace PixiEditor.Zoombox;
+namespace PixiEditor.Zoombox.Operations;
 
 internal class ZoomDragOperation : IDragOperation
 {
@@ -15,6 +15,7 @@ internal class ZoomDragOperation : IDragOperation
     {
         parent = zoomBox;
     }
+
     public void Start(MouseButtonEventArgs e)
     {
         screenScaleOrigin = parent.ToZoomboxSpace(Zoombox.ToVecD(e.GetPosition(parent.mainCanvas)));

+ 21 - 0
src/PixiEditor.Zoombox/Operations/ZoomboxOperationHelper.cs

@@ -0,0 +1,21 @@
+using System;
+
+namespace PixiEditor.Zoombox.Operations;
+
+internal static class ZoomboxOperationHelper
+{
+    public static double Mod(double x, double m)
+    {
+        return (x % m + m) % m;
+    }
+
+    public static double SubtractOnCircle(double angle1, double angle2)
+    {
+        angle1 = Mod(angle1, Math.PI * 2);
+        angle2 = Mod(angle2, Math.PI * 2);
+        double diff = Mod(angle1 - angle2, Math.PI * 2);
+        if (diff > Math.PI)
+            diff -= Math.PI * 2;
+        return diff;
+    }
+}

+ 0 - 49
src/PixiEditor.Zoombox/RotateDragOperation.cs

@@ -1,49 +0,0 @@
-using ChunkyImageLib.DataHolders;
-using System.Windows;
-using System.Windows.Input;
-
-namespace PixiEditor.Zoombox;
-
-internal class RotateDragOperation : IDragOperation
-{
-    private Zoombox parent;
-    private double prevAngle;
-
-
-    public RotateDragOperation(Zoombox zoomBox)
-    {
-        parent = zoomBox;
-    }
-    public void Start(MouseButtonEventArgs e)
-    {
-        Point pointCur = e.GetPosition(parent.mainCanvas);
-        prevAngle = GetAngle(new(pointCur.X, pointCur.Y));
-
-        parent.mainCanvas.CaptureMouse();
-    }
-
-    private double GetAngle(VecD point)
-    {
-        VecD center = new(parent.mainCanvas.ActualWidth / 2, parent.mainCanvas.ActualHeight / 2);
-        double angle = (point - center).Angle;
-        if (double.IsNaN(angle) || double.IsInfinity(angle))
-            return 0;
-        return angle;
-    }
-
-    public void Update(MouseEventArgs e)
-    {
-        Point pointCur = e.GetPosition(parent.mainCanvas);
-        double curAngle = GetAngle(new(pointCur.X, pointCur.Y));
-        double delta = curAngle - prevAngle;
-        if (parent.FlipX ^ parent.FlipY)
-            delta = -delta;
-        prevAngle = curAngle;
-        parent.Angle += delta;
-    }
-
-    public void Terminate()
-    {
-        parent.mainCanvas.ReleaseMouseCapture();
-    }
-}

+ 43 - 25
src/PixiEditor.Zoombox/Zoombox.xaml

@@ -1,31 +1,49 @@
-<ContentControl x:Class="PixiEditor.Zoombox.Zoombox"
-             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
-             xmlns:local="clr-namespace:PixiEditor.Zoombox"
-             mc:Ignorable="d"
-             x:Name="uc"
-             d:DesignHeight="450" d:DesignWidth="800">
-    <Canvas MouseDown="OnMouseDown" MouseUp="OnMouseUp" MouseMove="OnMouseMove" MouseWheel="OnScroll" ClipToBounds="True"
-            IsManipulationEnabled="{Binding UseTouchGestures, ElementName=uc}" ManipulationDelta="OnManipulationDelta"
-            x:Name="mainCanvas" Background="Transparent" SizeChanged="OnMainCanvasSizeChanged">
-        <Grid x:Name="mainGrid" SizeChanged="OnGridSizeChanged"
-              Canvas.Left="{Binding ElementName=uc, Path=CanvasX}"
-              Canvas.Top="{Binding ElementName=uc, Path=CanvasY}">
+<ContentControl
+    x:Class="PixiEditor.Zoombox.Zoombox"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:local="clr-namespace:PixiEditor.Zoombox"
+    mc:Ignorable="d"
+    x:Name="uc"
+    d:DesignHeight="450"
+    d:DesignWidth="800">
+    <Canvas
+        MouseDown="OnMouseDown"
+        MouseUp="OnMouseUp"
+        MouseMove="OnMouseMove"
+        MouseWheel="OnScroll"
+        ClipToBounds="True"
+        IsManipulationEnabled="{Binding UseTouchGestures, ElementName=uc}"
+        ManipulationDelta="OnManipulationDelta"
+        ManipulationStarted="OnManipulationStarted"
+        ManipulationCompleted="OnManipulationCompleted"
+        x:Name="mainCanvas"
+        Background="Transparent"
+        SizeChanged="OnMainCanvasSizeChanged">
+        <Grid
+            x:Name="mainGrid"
+            SizeChanged="OnGridSizeChanged"
+            Canvas.Left="{Binding ElementName=uc, Path=CanvasX}"
+            Canvas.Top="{Binding ElementName=uc, Path=CanvasY}">
             <Grid.RenderTransform>
                 <TransformGroup>
-                    <ScaleTransform x:Name="scaleTransform" 
-                                    ScaleX="{Binding ElementName=uc, Path=ScaleTransformXY}"
-                                    ScaleY="{Binding ElementName=uc, Path=ScaleTransformXY}"/>
-                    <RotateTransform x:Name="rotateTransform"
-                                     Angle="{Binding ElementName=uc, Path=RotateTransformAngle}"/>
-                    <ScaleTransform x:Name="flipTransform"
-                                    ScaleX="{Binding ElementName=uc, Path=FlipTransformX}"
-                                    ScaleY="{Binding ElementName=uc, Path=FlipTransformY}"/>
+                    <ScaleTransform
+                        x:Name="scaleTransform"
+                        ScaleX="{Binding ElementName=uc, Path=ScaleTransformXY}"
+                        ScaleY="{Binding ElementName=uc, Path=ScaleTransformXY}" />
+                    <RotateTransform
+                        x:Name="rotateTransform"
+                        Angle="{Binding ElementName=uc, Path=RotateTransformAngle}" />
+                    <ScaleTransform
+                        x:Name="flipTransform"
+                        ScaleX="{Binding ElementName=uc, Path=FlipTransformX}"
+                        ScaleY="{Binding ElementName=uc, Path=FlipTransformY}" />
                 </TransformGroup>
             </Grid.RenderTransform>
-            <ContentPresenter Content="{Binding AdditionalContent, ElementName=uc}"/>
+            <ContentPresenter
+                Content="{Binding AdditionalContent, ElementName=uc}" />
         </Grid>
     </Canvas>
-</ContentControl>
+</ContentControl>

+ 29 - 26
src/PixiEditor.Zoombox/Zoombox.xaml.cs

@@ -5,6 +5,7 @@ using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Markup;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.Zoombox.Operations;
 
 namespace PixiEditor.Zoombox;
 
@@ -13,15 +14,15 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 {
     public static readonly DependencyProperty AdditionalContentProperty =
         DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Zoombox),
-          new PropertyMetadata(null));
+            new PropertyMetadata(null));
 
     public static readonly DependencyProperty ZoomModeProperty =
         DependencyProperty.Register(nameof(ZoomMode), typeof(ZoomboxMode), typeof(Zoombox),
-          new PropertyMetadata(ZoomboxMode.Normal, ZoomModeChanged));
+            new PropertyMetadata(ZoomboxMode.Normal, ZoomModeChanged));
 
     public static readonly DependencyProperty ZoomOutOnClickProperty =
         DependencyProperty.Register(nameof(ZoomOutOnClick), typeof(bool), typeof(Zoombox),
-          new PropertyMetadata(false));
+            new PropertyMetadata(false));
 
     public static readonly DependencyProperty UseTouchGesturesProperty =
         DependencyProperty.Register(nameof(UseTouchGestures), typeof(bool), typeof(Zoombox));
@@ -56,6 +57,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         get => GetValue(AdditionalContentProperty);
         set => SetValue(AdditionalContentProperty, value);
     }
+
     public ZoomboxMode ZoomMode
     {
         get => (ZoomboxMode)GetValue(ZoomModeProperty);
@@ -130,6 +132,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
     public double FlipTransformY => FlipY ? -1 : 1;
     public double RotateTransformAngle => Angle * 180 / Math.PI;
     internal const double MaxScale = 70;
+
     internal double MinScale
     {
         get
@@ -170,7 +173,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     private IDragOperation? activeDragOperation = null;
     private MouseButtonEventArgs? activeMouseDownEventArgs = null;
-    private Point activeMouseDownPos;
+    private VecD activeMouseDownPos;
 
     public event PropertyChangedEventHandler? PropertyChanged;
 
@@ -204,7 +207,6 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     public void CenterContent(VecD newSize)
     {
-
         const double marginFactor = 1.1;
         double scaleFactor = Math.Max(
             newSize.X * marginFactor / mainCanvas.ActualWidth,
@@ -264,7 +266,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         if (e.ChangedButton == MouseButton.Right)
             return;
         activeMouseDownEventArgs = e;
-        activeMouseDownPos = e.GetPosition(mainCanvas);
+        activeMouseDownPos = ToVecD(e.GetPosition(mainCanvas));
         Keyboard.Focus(this);
     }
 
@@ -308,9 +310,9 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
     {
         if (activeDragOperation is null && activeMouseDownEventArgs is not null)
         {
-            var cur = e.GetPosition(mainCanvas);
+            var cur = ToVecD(e.GetPosition(mainCanvas));
 
-            if (Math.Abs(cur.X - activeMouseDownPos.X) > 3)
+            if ((cur - activeMouseDownPos).TaxicabLength > 3)
                 InitiateDrag(activeMouseDownEventArgs);
         }
         activeDragOperation?.Update(e);
@@ -325,30 +327,29 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         }
     }
 
-    private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
+
+    private ManipulationOperation? activeManipulationOperation;
+
+    private void OnManipulationStarted(object? sender, ManipulationStartedEventArgs e)
     {
-        if (!UseTouchGestures)
+        if (!UseTouchGestures || activeManipulationOperation is not null)
             return;
-        e.Handled = true;
-        VecD screenTranslation = new(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
-        VecD screenOrigin = new(e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
-        Manipulate(e.DeltaManipulation.Scale.X, screenTranslation, screenOrigin, e.DeltaManipulation.Rotation / 180 * Math.PI);
+        activeManipulationOperation = new ManipulationOperation(this);
+        activeManipulationOperation.Start();
     }
 
-    private void Manipulate(double deltaScale, VecD screenTranslation, VecD screenOrigin, double rotation)
+    private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
     {
-        double newScale = Math.Clamp(Scale * deltaScale, MinScale, MaxScale);
-        double newAngle = Angle + rotation;
-
-        VecD originalPos = ToZoomboxSpace(screenOrigin);
-        Angle = newAngle;
-        Scale = newScale;
-        VecD newPos = ToZoomboxSpace(screenOrigin);
-        VecD centerTranslation = originalPos - newPos;
-        Center += centerTranslation;
+        if (!UseTouchGestures || activeManipulationOperation is null)
+            return;
+        activeManipulationOperation.Update(e);
+    }
 
-        VecD translatedZoomboxPos = ToZoomboxSpace(screenOrigin + screenTranslation);
-        Center -= translatedZoomboxPos - originalPos;
+    private void OnManipulationCompleted(object? sender, ManipulationCompletedEventArgs e)
+    {
+        if (!UseTouchGestures || activeManipulationOperation is null)
+            return;
+        activeManipulationOperation = null;
     }
 
     internal static VecD ToVecD(Point point) => new VecD(point.X, point.Y);
@@ -372,11 +373,13 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
 
     private void OnMainCanvasSizeChanged(object sender, SizeChangedEventArgs e)
     {
+        OnPropertyChange(this, new DependencyPropertyChangedEventArgs());
         RaiseViewportEvent();
     }
 
     private void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
     {
+        OnPropertyChange(this, new DependencyPropertyChangedEventArgs());
         RaiseViewportEvent();
     }
 }

+ 3 - 3
src/README.md

@@ -13,7 +13,7 @@ Decouples the state of a document from the UI.
     - [x] ChunkPool multithreading support
     - [x] Dispose that returns borrowed chunks
     - [x] ChunkyImage finalizer that returns borrowed chunks
-    - [ ] Get Committed Pixel
+    - [x] Get Committed Pixel
     - [x] GetLatestChunk resolution parameter
         - [x] Support for different chunk sizes in the chunk pool
         - [x] Rendering for different chunk sizes
@@ -93,7 +93,7 @@ Decouples the state of a document from the UI.
         - [x] Paste image with transformation
         - [x] Rectangle
         - [x] Ellipse
-        - [ ] Line
+        - [x] Line
         - [x] Path-based pen
         - [x] Regular pen
         - [x] Pixel-perfect pen
@@ -119,7 +119,7 @@ Decouples the state of a document from the UI.
     - [x] Selection overlay
     - [x] Viewport system
     - [ ] New zoombox (touch fixes left)
-    - [ ] Pipette tool
+    - [x] Pipette tool
 
 ## Included cs projects (all wip, most features not implemented yet):