Browse Source

Better transform overlay

Equbuxu 3 years ago
parent
commit
2491258bdb

+ 25 - 2
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -1,11 +1,34 @@
 namespace ChunkyImageLib.DataHolders;
 public struct ShapeCorners
 {
+    public ShapeCorners(Vector2d center, Vector2d size, double angle)
+    {
+        TopLeft = center - size / 2;
+        TopRight = center + new Vector2d(size.X / 2, -size.Y / 2);
+        BottomRight = center + size / 2;
+        BottomLeft = center + new Vector2d(-size.X / 2, size.Y / 2);
+    }
     public Vector2d TopLeft { get; set; }
     public Vector2d TopRight { get; set; }
     public Vector2d BottomLeft { get; set; }
     public Vector2d BottomRight { get; set; }
+    public bool IsLegal
+    {
+        get
+        {
+            var top = TopLeft - TopRight;
+            var right = TopRight - BottomRight;
+            var bottom = BottomRight - BottomLeft;
+            var left = BottomLeft - TopLeft;
+            var topRight = Math.Sign(top.Cross(right));
+            return topRight == Math.Sign(right.Cross(bottom)) && topRight == Math.Sign(bottom.Cross(left)) && topRight == Math.Sign(left.Cross(top));
+        }
+    }
     public bool HasNaNOrInfinity => TopLeft.IsNaNOrInfinity() || TopRight.IsNaNOrInfinity() || BottomLeft.IsNaNOrInfinity() || BottomRight.IsNaNOrInfinity();
-    public double TopWidth => (TopRight - TopLeft).Length;
-    public double LeftHeight => (TopLeft - BottomLeft).Length;
+    public Vector2d RectSize => new((TopLeft - TopRight).Length, (TopLeft - BottomLeft).Length);
+    public Vector2d RectCenter => (TopLeft - BottomRight) / 2 + BottomRight;
+    public double RectRotation =>
+        (TopLeft - TopRight).Cross(TopLeft - BottomLeft) > 0 ?
+        RectSize.CCWAngleTo(BottomRight - TopLeft) :
+        RectSize.CCWAngleTo(BottomLeft - TopRight);
 }

+ 30 - 1
src/ChunkyImageLib/DataHolders/Vector2d.cs

@@ -29,6 +29,10 @@ public struct Vector2d
         result.Y = X * Math.Sin(angle) + Y * Math.Cos(angle);
         return result;
     }
+    public Vector2d Rotate(double angle, Vector2d around)
+    {
+        return (this - around).Rotate(angle) + around;
+    }
     public double DistanceToLineSegment(Vector2d pos1, Vector2d pos2)
     {
         Vector2d segment = pos2 - pos1;
@@ -49,17 +53,35 @@ public struct Vector2d
 
         return triangleArea / a * 2;
     }
+    public Vector2d ProjectOntoLine(Vector2d pos1, Vector2d pos2)
+    {
+        Vector2d line = (pos2 - pos1).Normalize();
+        Vector2d point = this - pos1;
+        return (line * point) * line + pos1;
+    }
+
+    public Vector2d ReflectAcrossLine(Vector2d pos1, Vector2d pos2)
+    {
+        var onLine = ProjectOntoLine(pos1, pos2);
+        return onLine - (this - onLine);
+    }
     public double AngleTo(Vector2d other)
     {
         return Math.Acos((this * other) / Length / other.Length);
     }
 
+    /// <summary>
+    /// Returns the angle between two vectors when travelling counterclockwise (assuming Y pointing up) from this vector to passed vector
+    /// </summary>
     public double CCWAngleTo(Vector2d other)
     {
         var rot = other.Rotate(-Angle);
         return rot.Angle;
     }
-
+    public Vector2d Lerp(Vector2d other, double factor)
+    {
+        return (other - this) * factor + this;
+    }
     public Vector2d Normalize()
     {
         return new Vector2d(X / Length, Y / Length);
@@ -72,6 +94,13 @@ public struct Vector2d
     {
         return new Vector2d(X >= 0 ? 1 : -1, Y >= 0 ? 1 : -1);
     }
+    /// <summary>
+    /// Returns the signed magnitude (Z coordinate) of the vector resulting from the cross product
+    /// </summary>
+    public double Cross(Vector2d other)
+    {
+        return (X * other.Y) - (Y * other.X);
+    }
     public Vector2d Multiply(Vector2d other)
     {
         return new Vector2d(X * other.X, Y * other.Y);

+ 1 - 1
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -29,7 +29,7 @@ internal class ClearRegionOperation : IDrawOperation
 
     public HashSet<Vector2i> FindAffectedChunks()
     {
-        return OperationHelper.FindChunksFullyInsideRectangle(pos, size);
+        return OperationHelper.FindChunksFullyInsideRectangle(pos, size, ChunkPool.FullChunkSize);
     }
     public void Dispose() { }
 }

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

@@ -62,7 +62,9 @@ public static class OperationHelper
 
     public static HashSet<Vector2i> FindChunksTouchingQuadrilateral(ShapeCorners corners, int chunkSize)
     {
-        if (corners.HasNaNOrInfinity)
+        if (corners.HasNaNOrInfinity ||
+            (corners.BottomLeft - corners.TopRight).Length > chunkSize * 40 * 20 ||
+            (corners.TopLeft - corners.BottomRight).Length > chunkSize * 40 * 20)
             return new HashSet<Vector2i>();
         List<Vector2i>[] lines = new List<Vector2i>[] {
             FindChunksAlongLine(corners.TopRight, corners.TopLeft, chunkSize),
@@ -75,7 +77,9 @@ public static class OperationHelper
 
     public static HashSet<Vector2i> FindChunksFullyInsideQuadrilateral(ShapeCorners corners, int chunkSize)
     {
-        if (corners.HasNaNOrInfinity)
+        if (corners.HasNaNOrInfinity ||
+            (corners.BottomLeft - corners.TopRight).Length > chunkSize * 40 * 20 ||
+            (corners.TopLeft - corners.BottomRight).Length > chunkSize * 40 * 20)
             return new HashSet<Vector2i>();
         List<Vector2i>[] lines = new List<Vector2i>[] {
             FindChunksAlongLine(corners.TopLeft, corners.TopRight, chunkSize),
@@ -100,7 +104,9 @@ public static class OperationHelper
     /// </summary>
     public static HashSet<Vector2i> FindChunksTouchingRectangle(Vector2d center, Vector2d size, double angle, int chunkSize)
     {
-        if (size.X == 0 || size.Y == 0)
+        if (size.X == 0 || size.Y == 0 || center.IsNaNOrInfinity() || size.IsNaNOrInfinity() || double.IsNaN(angle) || double.IsInfinity(angle))
+            return new HashSet<Vector2i>();
+        if (size.X > chunkSize * 40 * 20 || size.Y > chunkSize * 40 * 20)
             return new HashSet<Vector2i>();
         // draw a line on the outside of each side
         var corners = FindRectangleCorners(center, size, angle);
@@ -157,10 +163,12 @@ public static class OperationHelper
         return output;
     }
 
-    public static HashSet<Vector2i> FindChunksFullyInsideRectangle(Vector2i pos, Vector2i size)
+    public static HashSet<Vector2i> FindChunksFullyInsideRectangle(Vector2i pos, Vector2i size, int chunkSize)
     {
+        if (size.X > chunkSize * 40 * 20 || size.Y > chunkSize * 40 * 20)
+            return new HashSet<Vector2i>();
         Vector2i startChunk = GetChunkPos(pos, ChunkPool.FullChunkSize);
-        Vector2i endChunk = GetChunkPosBiased(pos + size, false, false, ChunkPool.FullChunkSize);
+        Vector2i endChunk = GetChunkPosBiased(pos + size, false, false, chunkSize);
         HashSet<Vector2i> output = new();
         for (int x = startChunk.X; x <= endChunk.X; x++)
         {
@@ -176,6 +184,8 @@ public static class OperationHelper
     {
         if (size.X < chunkSize || size.Y < chunkSize || center.IsNaNOrInfinity() || size.IsNaNOrInfinity() || double.IsNaN(angle) || double.IsInfinity(angle))
             return new HashSet<Vector2i>();
+        if (size.X > chunkSize * 40 * 20 || size.Y > chunkSize * 40 * 20)
+            return new HashSet<Vector2i>();
         // draw a line on the inside of each side
         var corners = FindRectangleCorners(center, size, angle);
         List<Vector2i>[] lines = new List<Vector2i>[] {

+ 76 - 63
src/PixiEditorPrototype/CustomControls/TransformOverlay/AffineMode.cs

@@ -1,56 +1,73 @@
-using System.Windows;
-using System.Windows.Media;
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;/*
 internal class AffineMode : ITransformMode
 {
     private TransformOverlay owner;
 
-    private static Pen blackPen = new Pen(Brushes.Black, 1);
-    private static Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 6 }, 0) };
-
     public AffineMode(TransformOverlay owner)
     {
         this.owner = owner;
     }
 
-    public void OnRender(DrawingContext context)
-    {
-        blackPen.Thickness = 1 / owner.ZoomboxScale;
-        blackDashedPen.Thickness = 1 / owner.ZoomboxScale;
-
-        var trans = owner.AffineTransform;
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(trans.TopLeft), TransformOverlay.ToPoint(trans.TopRight));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(trans.TopLeft), TransformOverlay.ToPoint(trans.BottomLeft));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(trans.BottomRight), TransformOverlay.ToPoint(trans.BottomLeft));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(trans.BottomRight), TransformOverlay.ToPoint(trans.TopRight));
-
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(trans.TopLeft));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(trans.TopRight));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(trans.BottomLeft));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(trans.BottomRight));
-
-        Vector2d rotPos = GetRotPos();
-        double radius = TransformOverlay.SideLength / owner.ZoomboxScale / 2;
-        context.DrawEllipse(Brushes.White, blackPen, new Point(rotPos.X, rotPos.Y), radius, radius);
-    }
-
     public void OnAnchorDrag(Vector2d pos, Anchor anchor)
     {
-        if (anchor == Anchor.Rotation)
+        switch (anchor)
         {
-            var cur = GetRotPos();
-            var angle = (cur - owner.AffineTransform.Center).CCWAngleTo(pos - owner.AffineTransform.Center);
-            owner.AffineTransform = new AffineTransform(owner.AffineTransform.Center, owner.AffineTransform.Size, owner.AffineTransform.Angle + angle);
-            return;
+            case Anchor.Rotation:
+                OnRotationDrag(pos);
+                break;
+            case Anchor.TopLeft:
+            case Anchor.TopRight:
+            case Anchor.BottomLeft:
+            case Anchor.BottomRight:
+                MoveCornerKeepStraight(pos, anchor);
+                break;
+            case Anchor.Top:
+            case Anchor.Left:
+            case Anchor.Right:
+            case Anchor.Bottom:
+                MoveSideKeepStraight(pos, anchor);
+                break;
         }
+    }
 
-        Vector2d curPos = AnchorTransformSpace(anchor);
+    private void MoveSideKeepStraight(Vector2d pos, Anchor anchor)
+    {
+        Vector2d curPos = AnchorInTransformSpace(anchor);
         Vector2d newPos = (pos - owner.AffineTransform.Center).Rotate(-owner.AffineTransform.Angle);
 
-        Anchor opposite = GetOpposite(anchor);
-        Vector2d oppPos = AnchorTransformSpace(opposite);
+        if (anchor is Anchor.Bottom or Anchor.Top)
+            newPos.X = curPos.X;
+        else
+            newPos.Y = curPos.Y;
+
+        Vector2d topLeft = AnchorInTransformSpace(Anchor.TopLeft);
+        Vector2d bottomRight = AnchorInTransformSpace(Anchor.BottomRight);
+        Vector2d delta = newPos - curPos;
+
+        if (anchor is Anchor.Top or Anchor.Left)
+            topLeft += delta;
+        else
+            bottomRight += delta;
+
+        Vector2d newTopLeft = new(Math.Min(topLeft.X, bottomRight.X), Math.Min(topLeft.Y, bottomRight.Y));
+        Vector2d newBottomRight = new(Math.Max(topLeft.X, bottomRight.X), Math.Max(topLeft.Y, bottomRight.Y));
+
+        Vector2d deltaCenter =
+            ((newTopLeft - newBottomRight) / 2 + newBottomRight)
+            .Rotate(owner.AffineTransform.Angle) / 2;
+
+        Vector2d newSize = (newBottomRight - newTopLeft);
+
+        owner.AffineTransform = new AffineTransform(owner.AffineTransform.Center + deltaCenter, newSize, owner.AffineTransform.Angle);
+    }
+
+    private void MoveCornerKeepStraight(Vector2d pos, Anchor anchor)
+    {
+        Vector2d curPos = AnchorInTransformSpace(anchor);
+        Vector2d newPos = (pos - owner.AffineTransform.Center).Rotate(-owner.AffineTransform.Angle);
+
+        Anchor opposite = GetOppositeCorner(anchor);
+        Vector2d oppPos = AnchorInTransformSpace(opposite);
 
         Vector2d oldSize = curPos - oppPos;
         Vector2d newSize = newPos - oppPos;
@@ -59,13 +76,7 @@ internal class AffineMode : ITransformMode
         owner.AffineTransform = new AffineTransform(owner.AffineTransform.Center + deltaCenter, newSize.Abs(), owner.AffineTransform.Angle);
     }
 
-    private Vector2d GetRotPos()
-    {
-        var trans = owner.AffineTransform;
-        return (trans.TopLeft + trans.TopRight) / 2 + (trans.TopLeft - trans.BottomLeft).Normalize() * 10 / owner.ZoomboxScale;
-    }
-
-    private Vector2d AnchorTransformSpace(Anchor anchor)
+    private Vector2d AnchorInTransformSpace(Anchor anchor)
     {
         var halfSize = owner.AffineTransform.Size / 2;
         return anchor switch
@@ -74,34 +85,36 @@ internal class AffineMode : ITransformMode
             Anchor.TopRight => new(halfSize.X, -halfSize.Y),
             Anchor.BottomLeft => new(-halfSize.X, halfSize.Y),
             Anchor.BottomRight => halfSize,
+            Anchor.Left => new(-halfSize.X, 0),
+            Anchor.Right => new(halfSize.X, 0),
+            Anchor.Top => new(0, -halfSize.Y),
+            Anchor.Bottom => new(0, halfSize.Y),
             _ => throw new System.NotImplementedException(),
         };
     }
 
-    private Anchor GetOpposite(Anchor anchor)
+    private Anchor GetOppositeSide(Anchor side)
     {
-        return anchor switch
+        return side switch
         {
-            Anchor.TopLeft => Anchor.BottomRight,
-            Anchor.TopRight => Anchor.BottomLeft,
-            Anchor.BottomLeft => Anchor.TopRight,
-            Anchor.BottomRight => Anchor.TopLeft,
-            _ => throw new System.NotImplementedException(),
+            Anchor.Left => Anchor.Right,
+            Anchor.Right => Anchor.Left,
+            Anchor.Top => Anchor.Bottom,
+            Anchor.Bottom => Anchor.Top,
+            _ => throw new ArgumentException($"{side} is not a side anchor"),
         };
     }
 
-    public Anchor? GetAnchorInPosition(Vector2d pos)
+    private (Anchor,Anchor) GetCornersOnSide(Anchor side)
     {
-        if (owner.IsWithinAnchor(owner.AffineTransform.TopLeft, pos))
-            return Anchor.TopLeft;
-        if (owner.IsWithinAnchor(owner.AffineTransform.TopRight, pos))
-            return Anchor.TopRight;
-        if (owner.IsWithinAnchor(owner.AffineTransform.BottomLeft, pos))
-            return Anchor.BottomLeft;
-        if (owner.IsWithinAnchor(owner.AffineTransform.BottomRight, pos))
-            return Anchor.BottomRight;
-        if (owner.IsWithinAnchor(GetRotPos(), pos))
-            return Anchor.Rotation;
-        return null;
+        return side switch
+        {
+            Anchor.Left => (Anchor.TopLeft, Anchor.BottomLeft),
+            Anchor.Right => (Anchor.TopRight, Anchor.BottomRight),
+            Anchor.Top => (Anchor.TopLeft, Anchor.TopRight),
+            Anchor.Bottom => (Anchor.BottomRight, Anchor.BottomLeft),
+            _ => throw new ArgumentException($"{side} is not a side anchor"),
+        };
     }
 }
+*/

+ 0 - 20
src/PixiEditorPrototype/CustomControls/TransformOverlay/AffineTransform.cs

@@ -1,20 +0,0 @@
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditorPrototype.CustomControls.TransformOverlay;
-internal record struct AffineTransform
-{
-    public AffineTransform(Vector2d center, Vector2d size, double angle)
-    {
-        Center = center;
-        Size = size;
-        Angle = angle;
-    }
-    public double Angle { get; }
-    public Vector2d Size { get; }
-    public Vector2d Center { get; }
-
-    public Vector2d TopLeft => -(Size / 2).Rotate(Angle) + Center;
-    public Vector2d TopRight => new Vector2d(Size.X / 2, -Size.Y / 2).Rotate(Angle) + Center;
-    public Vector2d BottomRight => (Size / 2).Rotate(Angle) + Center;
-    public Vector2d BottomLeft => new Vector2d(-Size.X / 2, Size.Y / 2).Rotate(Angle) + Center;
-}

+ 2 - 1
src/PixiEditorPrototype/CustomControls/TransformOverlay/Anchor.cs

@@ -2,6 +2,7 @@
 
 internal enum Anchor
 {
-    TopLeft, TopRight, BottomLeft, BottomRight, Rotation,
+    TopLeft, TopRight, BottomLeft, BottomRight,
     Top, Left, Right, Bottom,
+    Rotation, Origin
 }

+ 0 - 10
src/PixiEditorPrototype/CustomControls/TransformOverlay/ITransformMode.cs

@@ -1,10 +0,0 @@
-using System.Windows.Media;
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditorPrototype.CustomControls.TransformOverlay;
-internal interface ITransformMode
-{
-    void OnRender(DrawingContext context);
-    void OnAnchorDrag(Vector2d newPos, Anchor anchor);
-    Anchor? GetAnchorInPosition(Vector2d pos);
-}

+ 0 - 58
src/PixiEditorPrototype/CustomControls/TransformOverlay/PerpectiveMode.cs

@@ -1,58 +0,0 @@
-using System.Windows.Media;
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditorPrototype.CustomControls.TransformOverlay;
-internal class PerpectiveMode : ITransformMode
-{
-    private TransformOverlay owner;
-    private static Pen blackPen = new Pen(Brushes.Black, 1);
-    private static Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 6 }, 0) };
-
-    public PerpectiveMode(TransformOverlay owner)
-    {
-        this.owner = owner;
-    }
-
-    public Anchor? GetAnchorInPosition(Vector2d pos)
-    {
-        if (owner.IsWithinAnchor(owner.PerspectiveTransform.TopLeft, pos))
-            return Anchor.TopLeft;
-        if (owner.IsWithinAnchor(owner.PerspectiveTransform.TopRight, pos))
-            return Anchor.TopRight;
-        if (owner.IsWithinAnchor(owner.PerspectiveTransform.BottomLeft, pos))
-            return Anchor.BottomLeft;
-        if (owner.IsWithinAnchor(owner.PerspectiveTransform.BottomRight, pos))
-            return Anchor.BottomRight;
-        return null;
-    }
-
-    public void OnAnchorDrag(Vector2d pos, Anchor anchor)
-    {
-        var trans = owner.PerspectiveTransform;
-        if (anchor == Anchor.TopLeft)
-            trans = trans with { TopLeft = pos };
-        else if (anchor == Anchor.TopRight)
-            trans = trans with { TopRight = pos };
-        else if (anchor == Anchor.BottomLeft)
-            trans = trans with { BottomLeft = pos };
-        else if (anchor == Anchor.BottomRight)
-            trans = trans with { BottomRight = pos };
-        owner.PerspectiveTransform = trans;
-    }
-
-    public void OnRender(DrawingContext context)
-    {
-        blackPen.Thickness = 1 / owner.ZoomboxScale;
-        blackDashedPen.Thickness = 1 / owner.ZoomboxScale;
-
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(owner.PerspectiveTransform.TopLeft), TransformOverlay.ToPoint(owner.PerspectiveTransform.TopRight));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(owner.PerspectiveTransform.TopLeft), TransformOverlay.ToPoint(owner.PerspectiveTransform.BottomLeft));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(owner.PerspectiveTransform.BottomRight), TransformOverlay.ToPoint(owner.PerspectiveTransform.BottomLeft));
-        context.DrawLine(blackDashedPen, TransformOverlay.ToPoint(owner.PerspectiveTransform.BottomRight), TransformOverlay.ToPoint(owner.PerspectiveTransform.TopRight));
-
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(owner.PerspectiveTransform.TopLeft));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(owner.PerspectiveTransform.TopRight));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(owner.PerspectiveTransform.BottomLeft));
-        context.DrawRectangle(Brushes.White, blackPen, owner.ToRect(owner.PerspectiveTransform.BottomRight));
-    }
-}

+ 8 - 0
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformCornerFreedom.cs

@@ -0,0 +1,8 @@
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+internal enum TransformCornerFreedom
+{
+    Locked,
+    ScaleProportionally,
+    Scale,
+    Free
+}

+ 252 - 0
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformHelper.cs

@@ -0,0 +1,252 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+internal static class TransformHelper
+{
+    public const double SideLength = 10;
+
+    private static Pen blackPen = new Pen(Brushes.Black, 1);
+    private static Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 0) };
+    private static Pen whiteDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 2) };
+    private static Pen blackFreqDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 0) };
+    private static Pen whiteFreqDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 2) };
+
+    public static Rect ToRect(Vector2d pos, double zoomboxScale)
+    {
+        double scaled = SideLength / zoomboxScale;
+        return new Rect(pos.X - scaled / 2, pos.Y - scaled / 2, scaled, scaled);
+    }
+
+    public static Vector2d ToVector2d(Point pos) => new Vector2d(pos.X, pos.Y);
+    public static Point ToPoint(Vector2d vec) => new Point(vec.X, vec.Y);
+
+    public static Vector2d OriginFromCorners(ShapeCorners corners)
+    {
+        var maybeOrigin = TwoLineIntersection(
+            GetAnchorPosition(corners, Anchor.Top),
+            GetAnchorPosition(corners, Anchor.Bottom),
+            GetAnchorPosition(corners, Anchor.Left),
+            GetAnchorPosition(corners, Anchor.Right)
+            );
+        return maybeOrigin ?? corners.TopLeft.Lerp(corners.BottomRight, 0.5);
+    }
+
+    public static Vector2d? TwoLineIntersection(Vector2d line1Start, Vector2d line1End, Vector2d line2Start, Vector2d line2End)
+    {
+        const double epsilon = 0.0001;
+
+        Vector2d line1delta = line1End - line1Start;
+        Vector2d line2delta = line2End - line2Start;
+
+        // both lines are vertical, no intersections
+        if (Math.Abs(line1delta.X) < epsilon && Math.Abs(line2delta.X) < epsilon)
+            return null;
+
+        // y = mx + c
+        double m1 = line1delta.Y / line1delta.X;
+        double m2 = line2delta.Y / line2delta.X;
+
+        // line 1 is vertical (m1 is infinity)
+        if (Math.Abs(line1delta.X) < epsilon)
+        {
+            double c2 = line2Start.Y - line2Start.X * m2;
+            return new(line1Start.X, m2 * line1Start.X + c2);
+        }
+
+        // line 2 is vertical
+        if (Math.Abs(line2delta.X) < epsilon)
+        {
+            double c1 = line1Start.Y - line1Start.X * m1;
+            return new(line2Start.X, m1 * line2Start.X + c1);
+        }
+
+        // lines are parallel
+        if (Math.Abs(m1 - m2) < epsilon)
+            return null;
+
+        {
+            double c1 = line1Start.Y - line1Start.X * m1;
+            double c2 = line2Start.Y - line2Start.X * m2;
+            double x = (c1 - c2) / (m2 - m1);
+            return new(x, m1 * x + c1);
+        }
+    }
+
+    public static bool IsCorner(Anchor anchor)
+    {
+        return anchor is Anchor.TopLeft or Anchor.TopRight or Anchor.BottomRight or Anchor.BottomLeft;
+    }
+
+    public static bool IsSide(Anchor anchor)
+    {
+        return anchor is Anchor.Left or Anchor.Right or Anchor.Top or Anchor.Bottom;
+    }
+
+    public static Anchor GetOpposite(Anchor anchor)
+    {
+        return anchor switch
+        {
+            Anchor.TopLeft => Anchor.BottomRight,
+            Anchor.TopRight => Anchor.BottomLeft,
+            Anchor.BottomLeft => Anchor.TopRight,
+            Anchor.BottomRight => Anchor.TopLeft,
+            Anchor.Top => Anchor.Bottom,
+            Anchor.Left => Anchor.Right,
+            Anchor.Right => Anchor.Left,
+            Anchor.Bottom => Anchor.Top,
+            _ => throw new ArgumentException($"{anchor} is not a corner or a side"),
+        };
+    }
+
+    public static (Anchor, Anchor) GetCornersOnSide(Anchor side)
+    {
+        return side switch
+        {
+            Anchor.Left => (Anchor.TopLeft, Anchor.BottomLeft),
+            Anchor.Right => (Anchor.BottomRight, Anchor.TopRight),
+            Anchor.Top => (Anchor.TopRight, Anchor.TopLeft),
+            Anchor.Bottom => (Anchor.BottomLeft, Anchor.BottomRight),
+            _ => throw new ArgumentException($"{side} is not a side anchor"),
+        };
+    }
+
+    public static (Anchor, Anchor) GetNeighboringCorners(Anchor corner)
+    {
+        return corner switch
+        {
+            Anchor.TopLeft => (Anchor.TopRight, Anchor.BottomLeft),
+            Anchor.TopRight => (Anchor.TopLeft, Anchor.BottomRight),
+            Anchor.BottomLeft => (Anchor.TopLeft, Anchor.BottomRight),
+            Anchor.BottomRight => (Anchor.TopRight, Anchor.BottomLeft),
+            _ => throw new ArgumentException($"{corner} is not a corner anchor"),
+        };
+    }
+
+    public static ShapeCorners UpdateCorner(ShapeCorners original, Anchor corner, Vector2d newPos)
+    {
+        if (corner == Anchor.TopLeft)
+            original.TopLeft = newPos;
+        else if (corner == Anchor.BottomLeft)
+            original.BottomLeft = newPos;
+        else if (corner == Anchor.TopRight)
+            original.TopRight = newPos;
+        else if (corner == Anchor.BottomRight)
+            original.BottomRight = newPos;
+        else
+            throw new ArgumentException($"{corner} is not a corner");
+        return original;
+    }
+
+    public static Vector2d GetAnchorPosition(ShapeCorners corners, Anchor anchor)
+    {
+        return anchor switch
+        {
+            Anchor.TopLeft => corners.TopLeft,
+            Anchor.BottomRight => corners.BottomRight,
+            Anchor.TopRight => corners.TopRight,
+            Anchor.BottomLeft => corners.BottomLeft,
+            Anchor.Top => corners.TopLeft.Lerp(corners.TopRight, 0.5),
+            Anchor.Bottom => corners.BottomLeft.Lerp(corners.BottomRight, 0.5),
+            Anchor.Left => corners.TopLeft.Lerp(corners.BottomLeft, 0.5),
+            Anchor.Right => corners.BottomRight.Lerp(corners.TopRight, 0.5),
+            _ => throw new ArgumentException($"{anchor} is not a corner or a side"),
+        };
+    }
+
+    public static Anchor? GetAnchorInPosition(Vector2d pos, ShapeCorners corners, Vector2d origin, double zoomboxScale)
+    {
+        Vector2d topLeft = corners.TopLeft;
+        Vector2d topRight = corners.TopRight;
+        Vector2d bottomLeft = corners.BottomLeft;
+        Vector2d bottomRight = corners.BottomRight;
+
+        // corners
+        if (IsWithinAnchor(topLeft, pos, zoomboxScale))
+            return Anchor.TopLeft;
+        if (IsWithinAnchor(topRight, pos, zoomboxScale))
+            return Anchor.TopRight;
+        if (IsWithinAnchor(bottomLeft, pos, zoomboxScale))
+            return Anchor.BottomLeft;
+        if (IsWithinAnchor(bottomRight, pos, zoomboxScale))
+            return Anchor.BottomRight;
+
+        // sides
+        if (IsWithinAnchor((bottomLeft - topLeft) / 2 + topLeft, pos, zoomboxScale))
+            return Anchor.Left;
+        if (IsWithinAnchor((bottomRight - topRight) / 2 + topRight, pos, zoomboxScale))
+            return Anchor.Right;
+        if (IsWithinAnchor((topLeft - topRight) / 2 + topRight, pos, zoomboxScale))
+            return Anchor.Top;
+        if (IsWithinAnchor((bottomLeft - bottomRight) / 2 + bottomRight, pos, zoomboxScale))
+            return Anchor.Bottom;
+
+        // rotation
+        if (IsWithinAnchor(GetRotPos(corners, zoomboxScale), pos, zoomboxScale))
+            return Anchor.Rotation;
+
+        // origin
+        if (IsWithinAnchor(origin, pos, zoomboxScale))
+            return Anchor.Origin;
+        return null;
+    }
+
+    public static bool IsWithinAnchor(Vector2d anchorPos, Vector2d mousePos, double zoomboxScale)
+    {
+        return (anchorPos - mousePos).TaxicabLength <= (SideLength + 6) / zoomboxScale / 2;
+    }
+
+    public static void DrawOverlay
+        (DrawingContext context, ShapeCorners corners, Vector2d origin, double zoomboxScale)
+    {
+        blackPen.Thickness = 1 / zoomboxScale;
+        blackDashedPen.Thickness = 1 / zoomboxScale;
+        whiteDashedPen.Thickness = 1 / zoomboxScale;
+        blackFreqDashedPen.Thickness = 1 / zoomboxScale;
+        whiteFreqDashedPen.Thickness = 1 / zoomboxScale;
+
+        Vector2d topLeft = corners.TopLeft;
+        Vector2d topRight = corners.TopRight;
+        Vector2d bottomLeft = corners.BottomLeft;
+        Vector2d bottomRight = corners.BottomRight;
+
+        // lines
+        context.DrawLine(blackDashedPen, ToPoint(topLeft), ToPoint(topRight));
+        context.DrawLine(whiteDashedPen, ToPoint(topLeft), ToPoint(topRight));
+        context.DrawLine(blackDashedPen, ToPoint(topLeft), ToPoint(bottomLeft));
+        context.DrawLine(whiteDashedPen, ToPoint(topLeft), ToPoint(bottomLeft));
+        context.DrawLine(blackDashedPen, ToPoint(bottomRight), ToPoint(bottomLeft));
+        context.DrawLine(whiteDashedPen, ToPoint(bottomRight), ToPoint(bottomLeft));
+        context.DrawLine(blackDashedPen, ToPoint(bottomRight), ToPoint(topRight));
+        context.DrawLine(whiteDashedPen, ToPoint(bottomRight), ToPoint(topRight));
+
+        // corners
+        context.DrawRectangle(Brushes.White, blackPen, ToRect(topLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect(topRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect(bottomLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect(bottomRight, zoomboxScale));
+
+        // sides
+        context.DrawRectangle(Brushes.White, blackPen, ToRect((topLeft - topRight) / 2 + topRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect((topLeft - bottomLeft) / 2 + bottomLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect((bottomLeft - bottomRight) / 2 + bottomRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, ToRect((topRight - bottomRight) / 2 + bottomRight, zoomboxScale));
+
+        // rotation
+        Vector2d rotPos = GetRotPos(corners, zoomboxScale);
+        double radius = SideLength / zoomboxScale / 2;
+        context.DrawEllipse(Brushes.White, blackPen, ToPoint(rotPos), radius, radius);
+
+        // origin
+        context.DrawEllipse(Brushes.Transparent, blackFreqDashedPen, ToPoint(origin), radius, radius);
+        context.DrawEllipse(Brushes.Transparent, whiteFreqDashedPen, ToPoint(origin), radius, radius);
+    }
+
+    public static Vector2d GetRotPos(ShapeCorners corners, double zoomboxScale)
+    {
+        return (corners.TopLeft + corners.TopRight) / 2 +
+            (corners.TopLeft.Lerp(corners.TopRight, 0.5) - corners.BottomLeft.Lerp(corners.BottomRight, 0.5)).Normalize() * 15 / zoomboxScale;
+    }
+}

+ 92 - 69
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformOverlay.cs

@@ -8,39 +8,51 @@ namespace PixiEditorPrototype.CustomControls.TransformOverlay;
 
 internal class TransformOverlay : Control
 {
+    public static DependencyProperty RequestedCornersProperty =
+        DependencyProperty.Register(nameof(RequestedCorners), typeof(ShapeCorners), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(default(ShapeCorners), FrameworkPropertyMetadataOptions.AffectsRender, new(OnRequestedCorners)));
 
-    public static DependencyProperty AffineTransformProperty =
-        DependencyProperty.Register(nameof(AffineTransform), typeof(AffineTransform), typeof(TransformOverlay),
-            new FrameworkPropertyMetadata(default(AffineTransform), FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public static DependencyProperty PerspectiveTransformProperty =
-        DependencyProperty.Register(nameof(PerspectiveTransform), typeof(ShapeCorners), typeof(TransformOverlay),
+    public static DependencyProperty CornersProperty =
+        DependencyProperty.Register(nameof(Corners), typeof(ShapeCorners), typeof(TransformOverlay),
             new FrameworkPropertyMetadata(default(ShapeCorners), FrameworkPropertyMetadataOptions.AffectsRender));
 
+    public static DependencyProperty OriginProperty =
+        DependencyProperty.Register(nameof(Origin), typeof(Vector2d), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(default(Vector2d), FrameworkPropertyMetadataOptions.AffectsRender));
+
     public static DependencyProperty ZoomboxScaleProperty =
         DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(TransformOverlay),
             new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsRender));
 
-    public static readonly DependencyProperty ModeProperty =
-        DependencyProperty.Register(nameof(Mode), typeof(TransformOverlayMode), typeof(TransformOverlay),
-            new FrameworkPropertyMetadata(TransformOverlayMode.None, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnModeChange)));
+    public static readonly DependencyProperty SideFreedomProperty =
+        DependencyProperty.Register(nameof(SideFreedom), typeof(TransformSideFreedom), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(TransformSideFreedom.Locked));
+
+    public static readonly DependencyProperty CornerFreedomProperty =
+        DependencyProperty.Register(nameof(CornerFreedom), typeof(TransformCornerFreedom), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(TransformCornerFreedom.Locked));
 
-    public TransformOverlayMode Mode
+    public TransformCornerFreedom CornerFreedom
     {
-        get => (TransformOverlayMode)GetValue(ModeProperty);
-        set => SetValue(ModeProperty, value);
+        get { return (TransformCornerFreedom)GetValue(CornerFreedomProperty); }
+        set { SetValue(CornerFreedomProperty, value); }
     }
 
-    public AffineTransform AffineTransform
+    public TransformSideFreedom SideFreedom
+    {
+        get => (TransformSideFreedom)GetValue(SideFreedomProperty);
+        set => SetValue(SideFreedomProperty, value);
+    }
+    public ShapeCorners Corners
     {
-        get => (AffineTransform)GetValue(AffineTransformProperty);
-        set => SetValue(AffineTransformProperty, value);
+        get => (ShapeCorners)GetValue(CornersProperty);
+        set => SetValue(CornersProperty, value);
     }
 
-    public ShapeCorners PerspectiveTransform
+    public ShapeCorners RequestedCorners
     {
-        get => (ShapeCorners)GetValue(PerspectiveTransformProperty);
-        set => SetValue(PerspectiveTransformProperty, value);
+        get => (ShapeCorners)GetValue(RequestedCornersProperty);
+        set => SetValue(RequestedCornersProperty, value);
     }
 
     public double ZoomboxScale
@@ -48,67 +60,79 @@ internal class TransformOverlay : Control
         get => (double)GetValue(ZoomboxScaleProperty);
         set => SetValue(ZoomboxScaleProperty, value);
     }
-
-    private ITransformMode? currentMode;
-
-    public const double SideLength = 10;
-    public Anchor? CapturedAnchor { get; private set; } = null;
-
-    static TransformOverlay()
+    public Vector2d Origin
     {
-        DefaultStyleKeyProperty.OverrideMetadata(typeof(TransformOverlay), new FrameworkPropertyMetadata(typeof(TransformOverlay)));
+        get => (Vector2d)GetValue(OriginProperty);
+        set => SetValue(OriginProperty, value);
     }
 
+    private Anchor? capturedAnchor;
+
+    private bool originMoved = false;
+    private ShapeCorners mouseDownCorners;
+    private Vector2d mouseDownOriginPos;
+
     protected override void OnRender(DrawingContext drawingContext)
     {
         base.OnRender(drawingContext);
-        if (Mode == TransformOverlayMode.None)
-            return;
-        currentMode?.OnRender(drawingContext);
-    }
-
-    private static void OnModeChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
-    {
-        var overlay = (TransformOverlay)obj;
-        var value = (TransformOverlayMode)args.NewValue;
-        overlay.ReleaseAnchor();
-        overlay.currentMode = value switch
-        {
-            TransformOverlayMode.Affine => new AffineMode(overlay),
-            TransformOverlayMode.Perspective => new PerpectiveMode(overlay),
-            _ => null,
-        };
-        overlay.InvalidateVisual();
+        TransformHelper.DrawOverlay(drawingContext, Corners, Origin, ZoomboxScale);
     }
 
-
     protected override void OnMouseDown(MouseButtonEventArgs e)
     {
         base.OnMouseDown(e);
-        if (Mode == TransformOverlayMode.None || currentMode is null)
-            return;
 
-        var pos = ToVector2d(e.GetPosition(this));
-        Anchor? anchor = currentMode.GetAnchorInPosition(pos);
+        var pos = TransformHelper.ToVector2d(e.GetPosition(this));
+        var anchor = TransformHelper.GetAnchorInPosition(pos, Corners, Origin, ZoomboxScale);
         if (anchor is null)
             return;
-        CapturedAnchor = anchor;
-        CaptureMouse();
-        e.Handled = true;
-    }
+        capturedAnchor = anchor;
 
-    public bool IsWithinAnchor(Vector2d anchorPos, Vector2d mousePos)
-    {
-        return (anchorPos - mousePos).TaxicabLength <= (SideLength + 6) / ZoomboxScale / 2;
+        mouseDownCorners = Corners;
+        mouseDownOriginPos = Origin;
+
+        e.Handled = true;
+        CaptureMouse();
     }
 
     protected override void OnMouseMove(MouseEventArgs e)
     {
-        if (CapturedAnchor is null || Mode == TransformOverlayMode.None || currentMode is null)
+        if (capturedAnchor is null)
             return;
         e.Handled = true;
-        var pos = ToVector2d(e.GetPosition(this));
-        currentMode.OnAnchorDrag(pos, (Anchor)CapturedAnchor);
+        if (TransformHelper.IsCorner((Anchor)capturedAnchor) && CornerFreedom == TransformCornerFreedom.Locked ||
+            TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked)
+            return;
+
+        var pos = TransformHelper.ToVector2d(e.GetPosition(this));
+
+        if (TransformHelper.IsCorner((Anchor)capturedAnchor))
+        {
+            var newCorners = TransformUpdateHelper.UpdateShapeFromCorner((Anchor)capturedAnchor, CornerFreedom, mouseDownCorners, pos);
+            if (newCorners is not null)
+                Corners = (ShapeCorners)newCorners;
+            if (!originMoved)
+                Origin = TransformHelper.OriginFromCorners(Corners);
+        }
+        else if (TransformHelper.IsSide((Anchor)capturedAnchor))
+        {
+            var newCorners = TransformUpdateHelper.UpdateShapeFromSide((Anchor)capturedAnchor, SideFreedom, mouseDownCorners, pos);
+            if (newCorners is not null)
+                Corners = (ShapeCorners)newCorners;
+            if (!originMoved)
+                Origin = TransformHelper.OriginFromCorners(Corners);
+        }
+        else if (capturedAnchor == Anchor.Rotation)
+        {
+            var cur = TransformHelper.GetRotPos(mouseDownCorners, ZoomboxScale);
+            var angle = (cur - mouseDownOriginPos).CCWAngleTo(pos - mouseDownOriginPos);
+            Corners = TransformUpdateHelper.UpdateShapeFromRotation(mouseDownCorners, mouseDownOriginPos, angle);
+        }
+        else if (capturedAnchor == Anchor.Origin)
+        {
+            originMoved = true;
+            Origin = pos;
+        }
     }
 
     protected override void OnMouseUp(MouseButtonEventArgs e)
@@ -118,21 +142,20 @@ internal class TransformOverlay : Control
             e.Handled = true;
     }
 
+    private static void OnRequestedCorners(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+    {
+        TransformOverlay overlay = (TransformOverlay)obj;
+        overlay.originMoved = false;
+        overlay.Corners = (ShapeCorners)args.NewValue;
+        overlay.Origin = TransformHelper.OriginFromCorners(overlay.Corners);
+    }
+
     private bool ReleaseAnchor()
     {
-        if (CapturedAnchor is null)
+        if (capturedAnchor is null)
             return false;
         ReleaseMouseCapture();
-        CapturedAnchor = null;
+        capturedAnchor = null;
         return true;
     }
-
-    public Rect ToRect(Vector2d pos)
-    {
-        double scaled = SideLength / ZoomboxScale;
-        return new Rect(pos.X - scaled / 2, pos.Y - scaled / 2, scaled, scaled);
-    }
-
-    public static Vector2d ToVector2d(Point pos) => new Vector2d(pos.X, pos.Y);
-    public static Point ToPoint(Vector2d vec) => new Point(vec.X, vec.Y);
 }

+ 0 - 7
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformOverlayMode.cs

@@ -1,7 +0,0 @@
-namespace PixiEditorPrototype.CustomControls.TransformOverlay;
-internal enum TransformOverlayMode
-{
-    Perspective,
-    Affine,
-    None
-}

+ 9 - 0
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformSideFreedom.cs

@@ -0,0 +1,9 @@
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+internal enum TransformSideFreedom
+{
+    Locked,
+    ScaleProportionally,
+    Stretch,
+    Shear,
+    Free
+}

+ 125 - 0
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformUpdateHelper.cs

@@ -0,0 +1,125 @@
+using System;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+internal static class TransformUpdateHelper
+{
+    public static ShapeCorners? UpdateShapeFromCorner
+        (Anchor targetCorner, TransformCornerFreedom freedom, ShapeCorners corners, Vector2d desiredPos)
+    {
+        if (!TransformHelper.IsCorner(targetCorner))
+            throw new ArgumentException($"{targetCorner} is not a corner");
+
+        if (freedom == TransformCornerFreedom.Locked)
+            return corners;
+
+        if (freedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale)
+        {
+            var targetPos = TransformHelper.GetAnchorPosition(corners, targetCorner);
+            var opposite = TransformHelper.GetOpposite(targetCorner);
+            var oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
+
+            if (freedom == TransformCornerFreedom.ScaleProportionally)
+                desiredPos = desiredPos.ProjectOntoLine(targetPos, oppositePos);
+
+            var (neighbor1, neighbor2) = TransformHelper.GetNeighboringCorners(targetCorner);
+            var neighbor1Pos = TransformHelper.GetAnchorPosition(corners, neighbor1);
+            var neighbor2Pos = TransformHelper.GetAnchorPosition(corners, neighbor2);
+
+            double angle = corners.RectRotation;
+            var targetTrans = (targetPos - oppositePos).Rotate(-angle);
+            var neigh1Trans = (neighbor1Pos - oppositePos).Rotate(-angle);
+            var neigh2Trans = (neighbor2Pos - oppositePos).Rotate(-angle);
+
+            Vector2d delta = (desiredPos - targetPos).Rotate(-angle);
+
+            corners = TransformHelper.UpdateCorner(corners, targetCorner,
+                (targetTrans + delta).Rotate(angle) + oppositePos);
+            corners = TransformHelper.UpdateCorner(corners, neighbor1,
+                (neigh1Trans + delta.Multiply(neigh1Trans.Divide(targetTrans))).Rotate(angle) + oppositePos);
+            corners = TransformHelper.UpdateCorner(corners, neighbor2,
+                (neigh2Trans + delta.Multiply(neigh2Trans.Divide(targetTrans))).Rotate(angle) + oppositePos);
+
+            return corners;
+        }
+
+        if (freedom == TransformCornerFreedom.Free)
+        {
+            var newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
+            return newCorners.IsLegal ? newCorners : null;
+        }
+        throw new ArgumentException($"Freedom degree {freedom} is not supported");
+    }
+
+    public static ShapeCorners? UpdateShapeFromSide
+        (Anchor targetSide, TransformSideFreedom freedom, ShapeCorners corners, Vector2d desiredPos)
+    {
+        if (!TransformHelper.IsSide(targetSide))
+            throw new ArgumentException($"{targetSide} is not a side");
+
+        if (freedom == TransformSideFreedom.Locked)
+            return corners;
+
+        if (freedom is TransformSideFreedom.ScaleProportionally)
+        {
+            var targetPos = TransformHelper.GetAnchorPosition(corners, targetSide);
+            var opposite = TransformHelper.GetOpposite(targetSide);
+            var oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
+
+            desiredPos = desiredPos.ProjectOntoLine(targetPos, oppositePos);
+
+            Vector2d thing = targetPos - oppositePos;
+            thing = Vector2d.FromAngleAndLength(thing.Angle, 1 / thing.Length);
+            double scalingFactor = (desiredPos - oppositePos) * thing;
+            if (!double.IsNormal(scalingFactor))
+                return corners;
+
+            corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
+            corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
+            corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
+            corners.BottomLeft = (corners.BottomLeft - oppositePos) * scalingFactor + oppositePos;
+
+            if (scalingFactor < 0)
+            {
+                corners.TopLeft = corners.TopLeft.ReflectAcrossLine(targetPos, oppositePos);
+                corners.BottomRight = corners.BottomRight.ReflectAcrossLine(targetPos, oppositePos);
+                corners.TopRight = corners.TopRight.ReflectAcrossLine(targetPos, oppositePos);
+                corners.BottomLeft = corners.BottomLeft.ReflectAcrossLine(targetPos, oppositePos);
+            }
+
+            return corners;
+        }
+
+        if (freedom is TransformSideFreedom.Shear or TransformSideFreedom.Stretch or TransformSideFreedom.Free)
+        {
+            var (side1, side2) = TransformHelper.GetCornersOnSide(targetSide);
+            var side1Pos = TransformHelper.GetAnchorPosition(corners, side1);
+            var side2Pos = TransformHelper.GetAnchorPosition(corners, side2);
+            var targetPos = TransformHelper.GetAnchorPosition(corners, targetSide);
+
+            var opposite = TransformHelper.GetOpposite(targetSide);
+            var oppPos = TransformHelper.GetAnchorPosition(corners, opposite);
+
+            if (freedom == TransformSideFreedom.Shear)
+                desiredPos = desiredPos.ProjectOntoLine(side1Pos, side2Pos);
+            else if (freedom == TransformSideFreedom.Stretch)
+                desiredPos = desiredPos.ProjectOntoLine(targetPos, oppPos);
+
+            var delta = desiredPos - targetPos;
+            var newCorners = TransformHelper.UpdateCorner(corners, side1, side1Pos + delta);
+            newCorners = TransformHelper.UpdateCorner(newCorners, side2, side2Pos + delta);
+
+            return newCorners.IsLegal ? newCorners : null;
+        }
+        throw new ArgumentException($"Freedom degree {freedom} is not supported");
+    }
+
+    public static ShapeCorners UpdateShapeFromRotation(ShapeCorners corners, Vector2d origin, double angle)
+    {
+        corners.TopLeft = corners.TopLeft.Rotate(angle, origin);
+        corners.TopRight = corners.TopRight.Rotate(angle, origin);
+        corners.BottomLeft = corners.BottomLeft.Rotate(angle, origin);
+        corners.BottomRight = corners.BottomRight.Rotate(angle, origin);
+        return corners;
+    }
+}

+ 69 - 23
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -33,41 +33,73 @@ internal class DocumentViewModel : INotifyPropertyChanged
         }
     }
 
-    private TransformOverlayMode transformMode = TransformOverlayMode.None;
-    public TransformOverlayMode TransformMode
+    private TransformCornerFreedom transformCornerFreedom;
+    public TransformCornerFreedom TransformCornerFreedom
     {
-        get => transformMode;
-        private set
+        get => transformCornerFreedom;
+        set
         {
-            transformMode = value;
-            RaisePropertyChanged(nameof(TransformMode));
+            transformCornerFreedom = value;
+            PropertyChanged?.Invoke(this, new(nameof(TransformCornerFreedom)));
         }
     }
 
-    private ShapeCorners perspectiveTransform;
-    public ShapeCorners PerpectiveTransform
+    private TransformSideFreedom transformSideFreedom;
+    public TransformSideFreedom TransformSideFreedom
     {
-        get => perspectiveTransform;
+        get => transformSideFreedom;
         set
         {
-            perspectiveTransform = value;
-            RaisePropertyChanged(nameof(PerpectiveTransform));
-            OnTransformUpdate();
+            transformSideFreedom = value;
+            PropertyChanged?.Invoke(this, new(nameof(TransformSideFreedom)));
+        }
+    }
+
+    private bool transformActive;
+    public bool TransformActive
+    {
+        get => transformActive;
+        set
+        {
+            transformActive = value;
+            PropertyChanged?.Invoke(this, new(nameof(TransformActive)));
+        }
+    }
+
+    private ShapeCorners requestedTransformCorners;
+    public ShapeCorners RequestedTransformCorners
+    {
+        get => requestedTransformCorners;
+        set
+        {
+            requestedTransformCorners = value;
+            RaisePropertyChanged(nameof(RequestedTransformCorners));
         }
     }
 
-    private AffineTransform affineTransform;
-    public AffineTransform AffineTransform
+    private ShapeCorners transformCorners;
+    public ShapeCorners TransformCorners
     {
-        get => affineTransform;
+        get => transformCorners;
         set
         {
-            affineTransform = value;
-            RaisePropertyChanged(nameof(AffineTransform));
+            transformCorners = value;
+            RaisePropertyChanged(nameof(TransformCorners));
             OnTransformUpdate();
         }
     }
 
+    private Vector2d transformOrigin;
+    public Vector2d TransformOrigin
+    {
+        get => transformOrigin;
+        set
+        {
+            transformOrigin = value;
+            RaisePropertyChanged(nameof(TransformOrigin));
+        }
+    }
+
 
     public event PropertyChangedEventHandler? PropertyChanged;
 
@@ -148,12 +180,15 @@ internal class DocumentViewModel : INotifyPropertyChanged
                 bitmap.Value.BackBuffer, bitmap.Value.BackBufferStride);
             Surfaces[bitmap.Key] = surface;
         }
+
+        Helpers.ActionAccumulator.AddFinishedActions
+            (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
     }
 
     private bool drawingRectangle = false;
     private bool transformingRectangle = false;
 
-    private AffineTransform lastShape = new AffineTransform();
+    private ShapeCorners lastShape = new ShapeCorners();
     private ShapeData lastShapeData = new();
     public void StartUpdateRectangle(ShapeData data)
     {
@@ -164,7 +199,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
             return;
         drawingRectangle = true;
         Helpers.ActionAccumulator.AddActions(new DrawRectangle_Action(SelectedStructureMember.GuidValue, data, drawOnMask));
-        lastShape = new AffineTransform(data.Center, data.Size, data.Angle);
+        lastShape = new ShapeCorners(data.Center, data.Size, data.Angle);
         lastShapeData = data;
     }
 
@@ -173,8 +208,11 @@ internal class DocumentViewModel : INotifyPropertyChanged
         if (!drawingRectangle)
             return;
         drawingRectangle = false;
-        AffineTransform = lastShape;
-        TransformMode = TransformOverlayMode.Affine;
+
+        RequestedTransformCorners = lastShape;
+        TransformCornerFreedom = TransformCornerFreedom.Free;
+        TransformSideFreedom = TransformSideFreedom.Free;
+        TransformActive = true;
         transformingRectangle = true;
     }
 
@@ -182,8 +220,9 @@ internal class DocumentViewModel : INotifyPropertyChanged
     {
         if (!transformingRectangle)
             return;
-        TransformMode = TransformOverlayMode.None;
+
         transformingRectangle = false;
+        TransformActive = false;
         Helpers.ActionAccumulator.AddFinishedActions(new EndDrawRectangle_Action());
     }
 
@@ -208,7 +247,14 @@ internal class DocumentViewModel : INotifyPropertyChanged
     {
         if (!transformingRectangle)
             return;
-        StartUpdateRectangle(new ShapeData(AffineTransform.Center, AffineTransform.Size, AffineTransform.Angle, lastShapeData.StrokeWidth, lastShapeData.StrokeColor, lastShapeData.FillColor, lastShapeData.BlendMode));
+        StartUpdateRectangle(new ShapeData(
+            TransformCorners.RectCenter,
+            TransformCorners.RectSize,
+            TransformCorners.RectRotation,
+            lastShapeData.StrokeWidth,
+            lastShapeData.StrokeColor,
+            lastShapeData.FillColor,
+            lastShapeData.BlendMode));
     }
 
     public void ForceRefreshView()

+ 1 - 2
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -5,7 +5,6 @@ using System.Windows.Input;
 using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Zoombox;
-using PixiEditorPrototype.CustomControls.TransformOverlay;
 using PixiEditorPrototype.Models;
 using PixiEditorPrototype.Views;
 using SkiaSharp;
@@ -112,7 +111,7 @@ internal class ViewModelMain : INotifyPropertyChanged
 
     private void MouseDown(object? param)
     {
-        if (ActiveDocument is null || ZoomboxMode != ZoomboxMode.Normal || ActiveDocument.TransformMode != TransformOverlayMode.None)
+        if (ActiveDocument is null || ZoomboxMode != ZoomboxMode.Normal || ActiveDocument.TransformActive)
             return;
         mouseIsDown = true;
         var args = (MouseButtonEventArgs)(param!);

+ 8 - 5
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -198,11 +198,14 @@
                                 </i:EventTrigger>
                             </i:Interaction.Triggers>
                         </Image>
-                        <to:TransformOverlay HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
-                                                   AffineTransform="{Binding ActiveDocument.AffineTransform, Mode=TwoWay}"
-                                                   PerspectiveTransform="{Binding ActiveDocument.PerpectiveTransform, Mode=TwoWay}"
-                                                   Mode="{Binding ActiveDocument.TransformMode}"
-                                                   ZoomboxScale="{Binding RelativeSource={RelativeSource AncestorType=zoombox:Zoombox}, Path=Scale}"/>
+                        <to:TransformOverlay HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
+                                            Visibility="{Binding ActiveDocument.TransformActive, Converter={StaticResource BoolToVisibilityConverter}}"
+                                            Corners="{Binding ActiveDocument.TransformCorners, Mode=TwoWay}"
+                                            RequestedCorners="{Binding ActiveDocument.RequestedTransformCorners}"
+                                            CornerFreedom="{Binding ActiveDocument.TransformCornerFreedom}"
+                                            SideFreedom="{Binding ActiveDocument.TransformSideFreedom}"
+                                            Origin="{Binding ActiveDocument.TransformOrigin}"
+                                            ZoomboxScale="{Binding RelativeSource={RelativeSource AncestorType=zoombox:Zoombox}, Path=Scale}"/>
                     </Grid>
                 </Border>
             </zoombox:Zoombox>