Browse Source

Zoombox rotate/flip

Equbuxu 3 years ago
parent
commit
972b38b6ad

+ 1 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -415,7 +415,7 @@ namespace ChunkyImageLib
         public bool CheckIfCommittedIsEmpty()
         {
             FindAndDeleteEmptyCommittedChunks();
-            return committedChunks.Count == 0;
+            return committedChunks[ChunkResolution.Full].Count == 0;
         }
 
         private HashSet<Vector2i> FindAllChunksOutsideBounds(Vector2i size)

+ 9 - 0
src/ChunkyImageLib/DataHolders/Vector2d.cs

@@ -64,6 +64,10 @@ namespace ChunkyImageLib.DataHolders
         {
             return new Vector2d(X / Length, Y / Length);
         }
+        public Vector2d Abs()
+        {
+            return new Vector2d(Math.Abs(X), Math.Abs(Y));
+        }
         public Vector2d Signs()
         {
             return new Vector2d(X >= 0 ? 1 : -1, Y >= 0 ? 1 : -1);
@@ -134,6 +138,11 @@ namespace ChunkyImageLib.DataHolders
             return new SKSize((float)vec.X, (float)vec.Y);
         }
 
+        public bool IsNaNOrInfinity()
+        {
+            return double.IsNaN(X) || double.IsNaN(Y) || double.IsInfinity(X) || double.IsInfinity(Y);
+        }
+
         public override string ToString()
         {
             return $"({X},{Y})";

+ 5 - 2
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -35,6 +35,9 @@ namespace ChunkyImageLib.Operations
                 FindChunksAlongLine(corners.Item1, corners.Item4, chunkSize)
             };
 
+            if (lines[0].Count == 0 || lines[1].Count == 0 || lines[2].Count == 0 || lines[3].Count == 0)
+                return new HashSet<Vector2i>();
+
             //find min and max X for each Y in lines
             var ySel = (Vector2i vec) => vec.Y;
             int minY = Math.Min(lines[0].Min(ySel), lines[2].Min(ySel));
@@ -84,7 +87,7 @@ namespace ChunkyImageLib.Operations
 
         public static HashSet<Vector2i> FindChunksFullyInsideRectangle(Vector2d center, Vector2d size, double angle, int chunkSize)
         {
-            if (size.X < chunkSize || size.Y < chunkSize)
+            if (size.X < chunkSize || size.Y < chunkSize || center.IsNaNOrInfinity() || size.IsNaNOrInfinity() || double.IsNaN(angle) || double.IsInfinity(angle))
                 return new HashSet<Vector2i>();
             // draw a line on the inside of each side
             var corners = FindRectangleCorners(center, size, angle);
@@ -157,7 +160,7 @@ namespace ChunkyImageLib.Operations
         /// </summary>
         public static List<Vector2i> FindChunksAlongLine(Vector2d p1, Vector2d p2, int chunkSize)
         {
-            if (p1 == p2)
+            if (p1 == p2 || p1.IsNaNOrInfinity() || p2.IsNaNOrInfinity())
                 return new List<Vector2i>();
 
             //rotate the line into the first quadrant of the coordinate plane

+ 21 - 0
src/PixiEditor.Zoombox/BoolToIntConverter.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace PixiEditor.Zoombox
+{
+    internal class BoolToIntConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value == null || value is not bool converted)
+                return 1;
+            return converted ? -1 : 1;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 5 - 5
src/PixiEditor.Zoombox/MoveDragOperation.cs

@@ -1,4 +1,4 @@
-using System.Windows;
+using ChunkyImageLib.DataHolders;
 using System.Windows.Input;
 
 namespace PixiEditor.Zoombox
@@ -6,7 +6,7 @@ namespace PixiEditor.Zoombox
     internal class MoveDragOperation : IDragOperation
     {
         private Zoombox parent;
-        private Point prevMousePos;
+        private Vector2d prevMousePos;
 
         public MoveDragOperation(Zoombox zoomBox)
         {
@@ -14,14 +14,14 @@ namespace PixiEditor.Zoombox
         }
         public void Start(MouseButtonEventArgs e)
         {
-            prevMousePos = e.GetPosition(parent.mainCanvas);
+            prevMousePos = Zoombox.ToVector2d(e.GetPosition(parent.mainCanvas));
             parent.mainCanvas.CaptureMouse();
         }
 
         public void Update(MouseEventArgs e)
         {
-            var curMousePos = e.GetPosition(parent.mainCanvas);
-            parent.SpaceOriginPos += curMousePos - prevMousePos;
+            var curMousePos = Zoombox.ToVector2d(e.GetPosition(parent.mainCanvas));
+            parent.Center += parent.ToZoomboxSpace(prevMousePos) - parent.ToZoomboxSpace(curMousePos);
             prevMousePos = curMousePos;
         }
 

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

@@ -0,0 +1,50 @@
+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(Vector2d point)
+        {
+            Vector2d 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();
+        }
+    }
+}

+ 13 - 15
src/PixiEditor.Zoombox/ZoomDragOperation.cs

@@ -1,4 +1,5 @@
-using System.Windows;
+using ChunkyImageLib.DataHolders;
+using System;
 using System.Windows.Input;
 
 namespace PixiEditor.Zoombox
@@ -7,11 +8,10 @@ namespace PixiEditor.Zoombox
     {
         private Zoombox parent;
 
-        private double initZoomPower;
-        private Point initSpaceOriginPos;
+        private double initScale;
 
-        private Point zoomOrigin;
-        private Point screenZoomOrigin;
+        private Vector2d scaleOrigin;
+        private Vector2d screenScaleOrigin;
 
         public ZoomDragOperation(Zoombox zoomBox)
         {
@@ -19,24 +19,22 @@ namespace PixiEditor.Zoombox
         }
         public void Start(MouseButtonEventArgs e)
         {
-            screenZoomOrigin = e.GetPosition(parent.mainCanvas);
-            zoomOrigin = parent.ToZoomboxSpace(screenZoomOrigin);
-            initZoomPower = parent.ZoomPowerClamped;
-            initSpaceOriginPos = parent.SpaceOriginPos;
+            screenScaleOrigin = parent.ToZoomboxSpace(Zoombox.ToVector2d(e.GetPosition(parent.mainCanvas)));
+            scaleOrigin = parent.ToZoomboxSpace(screenScaleOrigin);
+            initScale = parent.Scale;
             parent.mainCanvas.CaptureMouse();
         }
 
         public void Update(MouseEventArgs e)
         {
             var curScreenPos = e.GetPosition(parent.mainCanvas);
-            double deltaX = screenZoomOrigin.X - curScreenPos.X;
+            double deltaX = screenScaleOrigin.X - curScreenPos.X;
             double deltaPower = deltaX / 10.0;
-            parent.ZoomPowerClamped = initZoomPower - deltaPower;
 
-            parent.SpaceOriginPos = initSpaceOriginPos;
-            var shiftedOriginPos = parent.ToScreenSpace(zoomOrigin);
-            var deltaOriginPos = shiftedOriginPos - screenZoomOrigin;
-            parent.SpaceOriginPos = initSpaceOriginPos - deltaOriginPos;
+            parent.Scale *= Math.Pow(Zoombox.ScaleFactor, deltaPower);
+
+            var shiftedOrigin = parent.ToZoomboxSpace(screenScaleOrigin);
+            parent.Center += scaleOrigin - shiftedOrigin;
         }
 
         public void Terminate()

+ 21 - 5
src/PixiEditor.Zoombox/Zoombox.xaml

@@ -7,13 +7,29 @@
              mc:Ignorable="d"
              x:Name="uc"
              d:DesignHeight="450" d:DesignWidth="800">
+    <ContentControl.Resources>
+        <ResourceDictionary>
+            <local:BoolToIntConverter x:Key="BoolToIntConverter"/>
+        </ResourceDictionary>
+    </ContentControl.Resources>
     <Canvas MouseDown="OnMouseDown" MouseUp="OnMouseUp" MouseMove="OnMouseMove" MouseWheel="OnScroll" ClipToBounds="True"
             IsManipulationEnabled="{Binding UseTouchGestures, ElementName=uc}" ManipulationDelta="OnManipulationDelta"
-            x:Name="mainCanvas" Background="Transparent">
-        <Grid x:Name="mainGrid" SizeChanged="RecalculateMinZoomLevel">
-            <Grid.LayoutTransform>
-                <ScaleTransform x:Name="scaleTransform"/>
-            </Grid.LayoutTransform>
+            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}"/>
+                </TransformGroup>
+            </Grid.RenderTransform>
             <ContentPresenter Content="{Binding AdditionalContent, ElementName=uc}"/>
         </Grid>
     </Canvas>

+ 168 - 110
src/PixiEditor.Zoombox/Zoombox.xaml.cs

@@ -1,3 +1,4 @@
+using ChunkyImageLib.DataHolders;
 using System;
 using System.ComponentModel;
 using System.Windows;
@@ -25,14 +26,28 @@ namespace PixiEditor.Zoombox
         public static readonly DependencyProperty UseTouchGesturesProperty =
             DependencyProperty.Register(nameof(UseTouchGestures), typeof(bool), typeof(Zoombox));
 
+
+        public static readonly DependencyProperty ScaleProperty =
+            DependencyProperty.Register(nameof(Scale), typeof(double), typeof(Zoombox), new(1.0, OnPropertyChange));
+
+        public static readonly DependencyProperty CenterProperty =
+            DependencyProperty.Register(nameof(Center), typeof(Vector2d), typeof(Zoombox), new(new Vector2d(0, 0), OnPropertyChange));
+
+        public static readonly DependencyProperty DimensionsProperty =
+            DependencyProperty.Register(nameof(Dimensions), typeof(Vector2d), typeof(Zoombox));
+
+        public static readonly DependencyProperty AngleProperty =
+            DependencyProperty.Register(nameof(Angle), typeof(double), typeof(Zoombox), new(0.0, OnPropertyChange));
+
+        public static readonly DependencyProperty FlipXProperty =
+            DependencyProperty.Register(nameof(FlipX), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
+
+        public static readonly DependencyProperty FlipYProperty =
+            DependencyProperty.Register(nameof(FlipY), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
+
         public static readonly RoutedEvent ViewportMovedEvent = EventManager.RegisterRoutedEvent(
             nameof(ViewportMoved), RoutingStrategy.Bubble, typeof(EventHandler<ViewportRoutedEventArgs>), typeof(Zoombox));
 
-        private const double zoomFactor = 1.09050773267; //2^(1/8)
-        private const double maxZoom = 50;
-        private double minZoom = -28;
-
-        private double[] roundZoomValues = new double[] { .01, .02, .03, .04, .05, .06, .07, .08, .1, .13, .17, .2, .25, .33, .5, .67, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64 };
         public object? AdditionalContent
         {
             get => GetValue(AdditionalContentProperty);
@@ -56,60 +71,92 @@ namespace PixiEditor.Zoombox
             set => SetValue(UseTouchGesturesProperty, value);
         }
 
+        public bool FlipX
+        {
+            get => (bool)GetValue(FlipXProperty);
+            set => SetValue(FlipXProperty, value);
+        }
+
+        public bool FlipY
+        {
+            get => (bool)GetValue(FlipYProperty);
+            set => SetValue(FlipYProperty, value);
+        }
+
+        public double Scale
+        {
+            get => (double)GetValue(ScaleProperty);
+            set => SetValue(ScaleProperty, value);
+        }
+
+        public double Angle
+        {
+            get => (double)GetValue(AngleProperty);
+            set => SetValue(AngleProperty, value);
+        }
+
+        public Vector2d Center
+        {
+            get => (Vector2d)GetValue(CenterProperty);
+            set => SetValue(CenterProperty, value);
+        }
+
+        public Vector2d Dimensions
+        {
+            get => (Vector2d)GetValue(DimensionsProperty);
+            set => SetValue(DimensionsProperty, value);
+        }
+
         public event EventHandler<ViewportRoutedEventArgs> ViewportMoved
         {
             add => AddHandler(ViewportMovedEvent, value);
             remove => RemoveHandler(ViewportMovedEvent, value);
         }
 
-        public double Zoom => Math.Pow(zoomFactor, zoomPower);
+        public double CanvasX => ToScreenSpace(new(0, 0)).X;
+        public double CanvasY => ToScreenSpace(new(0, 0)).Y;
 
-        private Point spaceOriginPos;
-        internal Point SpaceOriginPos
+        public double ScaleTransformXY => Scale;
+        public double FlipTransformX => FlipX ? -1 : 1;
+        public double FlipTransformY => FlipY ? -1 : 1;
+        public double RotateTransformAngle => Angle * 180 / Math.PI;
+        internal const double MaxScale = 70;
+        internal double MinScale
         {
-            get => spaceOriginPos;
-            set
+            get
             {
-                spaceOriginPos = value;
-                Canvas.SetLeft(mainGrid, spaceOriginPos.X);
-                Canvas.SetTop(mainGrid, spaceOriginPos.Y);
-                RaiseViewportEvent();
+                double fraction = Math.Max(
+                    mainCanvas.ActualWidth / mainGrid.ActualWidth,
+                    mainCanvas.ActualHeight / mainGrid.ActualHeight);
+                return Math.Min(fraction / 8, 0.1);
             }
         }
 
-        private double zoomPower;
-        internal double ZoomPowerClamped
+        internal const double ScaleFactor = 1.09050773267; //2^(1/8)
+
+        private double[] roundZoomValues = new double[] { .01, .02, .03, .04, .05, .06, .07, .08, .1, .13, .17, .2, .25, .33, .5, .67, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64 };
+
+        internal Vector2d ToScreenSpace(Vector2d p)
         {
-            get => zoomPower;
-            set
-            {
-                value = Math.Clamp(value, minZoom, maxZoom);
-                if (value == zoomPower)
-                    return;
-                zoomPower = value;
-                var mult = Zoom;
-                scaleTransform.ScaleX = mult;
-                scaleTransform.ScaleY = mult;
-                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Zoom)));
-                RaiseViewportEvent();
-            }
+            Vector2d delta = p - Center;
+            delta = delta.Rotate(Angle) * Scale;
+            if (FlipX)
+                delta.X = -delta.X;
+            if (FlipY)
+                delta.Y = -delta.Y;
+            delta += new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
+            return delta;
         }
-        private double ZoomPowerTopCapped
+
+        internal Vector2d ToZoomboxSpace(Vector2d mousePos)
         {
-            get => zoomPower;
-            set
-            {
-                if (value > maxZoom)
-                    value = maxZoom;
-                if (value == zoomPower)
-                    return;
-                zoomPower = value;
-                var mult = Zoom;
-                scaleTransform.ScaleX = mult;
-                scaleTransform.ScaleY = mult;
-                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Zoom)));
-                RaiseViewportEvent();
-            }
+            Vector2d delta = mousePos - new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
+            if (FlipX)
+                delta.X = -delta.X;
+            if (FlipY)
+                delta.Y = -delta.Y;
+            delta = (delta / Scale).Rotate(-Angle);
+            return delta + Center;
         }
 
         private IDragOperation? activeDragOperation = null;
@@ -133,62 +180,55 @@ namespace PixiEditor.Zoombox
 
         private void RaiseViewportEvent()
         {
-            Point center = ToZoomboxSpace(new(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2));
-            Point topLeft = ToZoomboxSpace(new(0, 0));
-            Point bottomRight = ToZoomboxSpace(new(mainCanvas.ActualWidth, mainCanvas.ActualHeight));
-
             RaiseEvent(new ViewportRoutedEventArgs(
                 ViewportMovedEvent,
-                new(center.X, center.Y),
-                new(bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y),
+                Center,
+                Dimensions,
                 new(mainCanvas.ActualWidth, mainCanvas.ActualHeight),
-                0));
+                Angle));
         }
 
-        public void CenterContent() => CenterContent(new Size(mainGrid.ActualWidth, mainGrid.ActualHeight));
+        public void CenterContent() => CenterContent(new(mainGrid.ActualWidth, mainGrid.ActualHeight));
 
-        public void CenterContent(Size newSize)
+        public void CenterContent(Vector2d newSize)
         {
+
             const double marginFactor = 1.1;
             double scaleFactor = Math.Max(
-                newSize.Width * marginFactor / mainCanvas.ActualWidth,
-                newSize.Height * marginFactor / mainCanvas.ActualHeight);
-            ZoomPowerTopCapped = -Math.Log(scaleFactor, zoomFactor);
-            SpaceOriginPos = new Point(
-                mainCanvas.ActualWidth / 2 - newSize.Width * Zoom / 2,
-                mainCanvas.ActualHeight / 2 - newSize.Height * Zoom / 2);
+                newSize.X * marginFactor / mainCanvas.ActualWidth,
+                newSize.Y * marginFactor / mainCanvas.ActualHeight);
+
+            Angle = 0;
+            FlipX = false;
+            FlipY = false;
+            Scale = scaleFactor;
+            Center = newSize / 2;
         }
 
-        public void ZoomIntoCenter(double delta, bool round)
+        public void ZoomIntoCenter(double delta)
         {
-            ZoomInto(new Point(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2), delta, round);
+            ZoomInto(new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2), delta);
         }
 
-        public void ZoomInto(Point mousePos, double delta, bool round = false)
+        public void ZoomInto(Vector2d mousePos, double delta)
         {
             if (delta == 0)
                 return;
             var oldZoomboxMousePos = ToZoomboxSpace(mousePos);
 
-            if (round)
-            {
-                int curIndex = GetClosestRoundZoomValueIndex(Zoom);
-                if (curIndex == 0 && delta < 0 || curIndex == roundZoomValues.Length - 1 && delta > 0)
-                    return;
-                int nextIndex = delta < 0 ? curIndex - 1 : curIndex + 1;
-                double newZoom = roundZoomValues[nextIndex];
-                ZoomPowerClamped = Math.Log(newZoom, zoomFactor);
-            }
-            else
-            {
-                ZoomPowerClamped += delta;
-            }
+            int curIndex = GetClosestRoundZoomValueIndex(Scale);
+            int nextIndex = curIndex;
+            if (!(curIndex == 0 && delta < 0 || curIndex == roundZoomValues.Length - 1 && delta > 0))
+                nextIndex = delta < 0 ? curIndex - 1 : curIndex + 1;
+            double newScale = roundZoomValues[nextIndex];
 
-            if (Math.Abs(ZoomPowerClamped) < 1) ZoomPowerClamped = 0;
+            if (Math.Abs(newScale - 1) < 0.1) newScale = 1;
+            newScale = Math.Clamp(newScale, MinScale, MaxScale);
+            Scale = newScale;
 
-            var shiftedMousePos = ToScreenSpace(oldZoomboxMousePos);
-            var deltaMousePos = mousePos - shiftedMousePos;
-            SpaceOriginPos = SpaceOriginPos + deltaMousePos;
+            var newZoomboxMousePos = ToZoomboxSpace(mousePos);
+            var deltaCenter = oldZoomboxMousePos - newZoomboxMousePos;
+            Center += deltaCenter;
         }
 
         private int GetClosestRoundZoomValueIndex(double value)
@@ -207,32 +247,6 @@ namespace PixiEditor.Zoombox
             return index;
         }
 
-        private void RecalculateMinZoomLevel(object sender, SizeChangedEventArgs args)
-        {
-            double fraction = Math.Max(
-                mainCanvas.ActualWidth / mainGrid.ActualWidth,
-                mainCanvas.ActualHeight / mainGrid.ActualHeight);
-            minZoom = Math.Min(0, Math.Log(fraction / 8, zoomFactor));
-        }
-
-        internal Point ToScreenSpace(Point p)
-        {
-            double zoom = Zoom;
-            p.X *= zoom;
-            p.Y *= zoom;
-            p += (Vector)SpaceOriginPos;
-            return p;
-        }
-
-        internal Point ToZoomboxSpace(Point mousePos)
-        {
-            double zoom = Zoom;
-            mousePos -= (Vector)SpaceOriginPos;
-            mousePos.X /= zoom;
-            mousePos.Y /= zoom;
-            return mousePos;
-        }
-
         private void OnMouseDown(object sender, MouseButtonEventArgs e)
         {
             if (e.ChangedButton == MouseButton.Right)
@@ -253,6 +267,8 @@ namespace PixiEditor.Zoombox
                 activeDragOperation = new MoveDragOperation(this);
             else if (ZoomMode == ZoomboxMode.Zoom)
                 activeDragOperation = new ZoomDragOperation(this);
+            else if (ZoomMode == ZoomboxMode.Rotate)
+                activeDragOperation = new RotateDragOperation(this);
             else
                 throw new InvalidOperationException("Unknown zoombox mode");
 
@@ -271,7 +287,7 @@ namespace PixiEditor.Zoombox
             else
             {
                 if (ZoomMode == ZoomboxMode.Zoom && e.ChangedButton == MouseButton.Left)
-                    ZoomInto(e.GetPosition(mainCanvas), ZoomOutOnClick ? -1 : 1, true);
+                    ZoomInto(ToVector2d(e.GetPosition(mainCanvas)), ZoomOutOnClick ? -1 : 1);
             }
             activeMouseDownEventArgs = null;
         }
@@ -292,17 +308,59 @@ namespace PixiEditor.Zoombox
         {
             for (int i = 0; i < Math.Abs(e.Delta / 100); i++)
             {
-                ZoomInto(e.GetPosition(mainCanvas), e.Delta / 100, true);
+                ZoomInto(ToVector2d(e.GetPosition(mainCanvas)), e.Delta / 100);
             }
         }
 
         private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
         {
-            if (e.Handled = UseTouchGestures)
+            if (UseTouchGestures)
             {
-                ZoomInto(e.ManipulationOrigin, e.DeltaManipulation.Expansion.X / 5.0);
-                SpaceOriginPos += e.DeltaManipulation.Translation;
+                e.Handled = true;
+                double newScale = Math.Clamp(Scale * e.DeltaManipulation.Scale.X, MinScale, MaxScale);
+                double newAngle = Angle + e.DeltaManipulation.Rotation;
+                Vector2d screenTranslation = new(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
+                Vector2d screenOrigin = new(e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
+
+                Vector2d originalPos = ToZoomboxSpace(screenOrigin);
+                Angle = newAngle;
+                Scale = newScale;
+                Vector2d newPos = ToZoomboxSpace(screenOrigin);
+                Vector2d centerTranslation = originalPos - newPos;
+                Center += centerTranslation;
+
+                Vector2d translatedZoomboxPos = ToZoomboxSpace(screenOrigin + screenTranslation);
+                Center -= translatedZoomboxPos - originalPos;
             }
         }
+
+        internal static Vector2d ToVector2d(Point point) => new Vector2d(point.X, point.Y);
+
+        private static void OnPropertyChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+        {
+            var zoombox = (Zoombox)obj;
+
+            Vector2d topLeft = zoombox.ToZoomboxSpace(new(0, 0)).Rotate(zoombox.Angle);
+            Vector2d bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.ActualWidth, zoombox.mainCanvas.ActualHeight)).Rotate(zoombox.Angle);
+
+            zoombox.Dimensions = (bottomRight - topLeft).Abs();
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.ScaleTransformXY)));
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.RotateTransformAngle)));
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.FlipTransformX)));
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.FlipTransformY)));
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasX)));
+            zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasY)));
+            zoombox.RaiseViewportEvent();
+        }
+
+        private void OnMainCanvasSizeChanged(object sender, SizeChangedEventArgs e)
+        {
+            RaiseViewportEvent();
+        }
+
+        private void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
+        {
+            RaiseViewportEvent();
+        }
     }
 }

+ 1 - 0
src/PixiEditor.Zoombox/ZoomboxMode.cs

@@ -4,6 +4,7 @@
     {
         Normal,
         Move,
+        Rotate,
         Zoom
     }
 }

+ 1 - 1
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -102,7 +102,7 @@ namespace PixiEditorPrototype.Models.Rendering
             var chunksOnScreen = OperationHelper.FindChunksTouchingRectangle(
                 helpers.State.ViewportCenter,
                 helpers.State.ViewportSize,
-                helpers.State.ViewportAngle,
+                -helpers.State.ViewportAngle,
                 ChunkResolution.Full.PixelSize());
 
             chunksOnScreen.IntersectWith(postponedChunks[resolution]);

+ 3 - 4
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -83,11 +83,11 @@ namespace PixiEditorPrototype.ViewModels
         {
             Vector2d densityVec = size.Divide(realSize);
             double density = Math.Min(densityVec.X, densityVec.Y);
-            if (density > 8)
+            if (density > 8.01)
                 return ChunkResolution.Eighth;
-            else if (density > 4)
+            else if (density > 4.01)
                 return ChunkResolution.Quarter;
-            else if (density > 2)
+            else if (density > 2.01)
                 return ChunkResolution.Half;
             return ChunkResolution.Full;
         }
@@ -108,7 +108,6 @@ namespace PixiEditorPrototype.ViewModels
         public int ResizeHeight { get; set; }
 
         private DocumentHelpers Helpers { get; }
-        public object MoveViewportEventArgs { get; private set; }
 
         private ViewModelMain owner;
 

+ 30 - 9
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -32,19 +32,40 @@ namespace PixiEditorPrototype.ViewModels
 
         public event PropertyChangedEventHandler? PropertyChanged;
 
-        private bool enableViewportDragging;
-        public bool EnableViewportDragging
+        public bool NormalZoombox
         {
-            get => enableViewportDragging;
             set
             {
-                enableViewportDragging = value;
-                PropertyChanged?.Invoke(this, new(nameof(EnableViewportDragging)));
+                if (!value)
+                    return;
+                ZoomboxMode = ZoomboxMode.Normal;
                 PropertyChanged?.Invoke(this, new(nameof(ZoomboxMode)));
             }
         }
 
-        public ZoomboxMode ZoomboxMode => enableViewportDragging ? ZoomboxMode.Move : ZoomboxMode.Normal;
+        public bool MoveZoombox
+        {
+            set
+            {
+                if (!value)
+                    return;
+                ZoomboxMode = ZoomboxMode.Move;
+                PropertyChanged?.Invoke(this, new(nameof(ZoomboxMode)));
+            }
+        }
+
+        public bool RotateZoombox
+        {
+            set
+            {
+                if (!value)
+                    return;
+                ZoomboxMode = ZoomboxMode.Rotate;
+                PropertyChanged?.Invoke(this, new(nameof(ZoomboxMode)));
+            }
+        }
+
+        public ZoomboxMode ZoomboxMode { get; set; }
 
         public ViewModelMain()
         {
@@ -58,7 +79,7 @@ namespace PixiEditorPrototype.ViewModels
 
         private void MouseDown(object? param)
         {
-            if (ActiveDocument is null || EnableViewportDragging)
+            if (ActiveDocument is null || ZoomboxMode != ZoomboxMode.Normal)
                 return;
             mouseIsDown = true;
             var args = (MouseButtonEventArgs)(param!);
@@ -70,7 +91,7 @@ namespace PixiEditorPrototype.ViewModels
 
         private void MouseMove(object? param)
         {
-            if (ActiveDocument is null || !mouseIsDown || EnableViewportDragging)
+            if (ActiveDocument is null || !mouseIsDown || ZoomboxMode != ZoomboxMode.Normal)
                 return;
             var args = (MouseEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
@@ -104,7 +125,7 @@ namespace PixiEditorPrototype.ViewModels
 
         private void MouseUp(object? param)
         {
-            if (ActiveDocument is null || !mouseIsDown || EnableViewportDragging)
+            if (ActiveDocument is null || !mouseIsDown || ZoomboxMode != ZoomboxMode.Normal)
                 return;
             mouseIsDown = false;
             ProcessToolMouseUp();

+ 9 - 2
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -115,12 +115,19 @@
                 <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Rectangle}">Rect</Button>
                 <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Select}">Select</Button>
                 <colorpicker:PortableColorPicker Margin="5" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="30" Height="30"/>
-                <CheckBox Margin="5" x:Name="viewportMoveCheckbox" IsChecked="{Binding EnableViewportDragging}">Move</CheckBox>
+                <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding NormalZoombox, Mode=OneWayToSource}">Normal</RadioButton>
+                <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding MoveZoombox, Mode=OneWayToSource}">Move</RadioButton>
+                <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding RotateZoombox, Mode=OneWayToSource}">Rotate</RadioButton>
+                <CheckBox x:Name="flipXCheckbox" Margin="5, 0">Flip X</CheckBox>
+                <CheckBox x:Name="flipYCheckbox" Margin="5, 0">Flip Y</CheckBox>
             </StackPanel>
         </Border>
 
         <Grid>
-            <zoombox:Zoombox x:Name="zoombox" ZoomMode="{Binding ZoomboxMode}">
+            <zoombox:Zoombox x:Name="zoombox" UseTouchGestures="True"
+                             ZoomMode="{Binding ZoomboxMode}" 
+                             FlipX="{Binding ElementName=flipXCheckbox, Path=IsChecked}"
+                             FlipY="{Binding ElementName=flipYCheckbox, Path=IsChecked}">
                 <i:Interaction.Triggers>
                     <i:EventTrigger EventName="ViewportMoved">
                         <i:InvokeCommandAction Command="{Binding ActiveDocument.MoveViewportCommand}" PassEventArgsToCommand="True"/>