Browse Source

Merge branch 'master' into development

flabbet 8 months ago
parent
commit
41fea06e8c
53 changed files with 839 additions and 238 deletions
  1. 22 0
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  2. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs
  3. 8 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  4. 9 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  5. 15 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  6. 7 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  7. 10 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  8. 5 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  9. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  10. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  11. 4 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  12. 7 0
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  13. 8 0
      src/PixiEditor.SVG/Enums/SvgStrokeLineCap.cs
  14. 10 0
      src/PixiEditor.SVG/Enums/SvgStrokeLineJoin.cs
  15. 4 1
      src/PixiEditor.SVG/Features/IStrokable.cs
  16. 20 1
      src/PixiEditor.SVG/StyleContext.cs
  17. 19 1
      src/PixiEditor.SVG/SvgDocument.cs
  18. 30 1
      src/PixiEditor/Helpers/GeometryHelper.cs
  19. 34 0
      src/PixiEditor/Helpers/Resources/VectorPathResource.cs
  20. 3 0
      src/PixiEditor/Images/Handles/shear.svg
  21. 1 1
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  22. 40 14
      src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs
  23. 27 14
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  24. 26 6
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  25. 14 6
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  26. 1 1
      src/PixiEditor/Models/Handlers/IPathOverlayHandler.cs
  27. 3 0
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  28. 2 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  29. 1 0
      src/PixiEditor/Models/Handlers/ITransformHandler.cs
  30. 1 0
      src/PixiEditor/Models/Handlers/Tools/ILineToolHandler.cs
  31. 1 0
      src/PixiEditor/Models/Handlers/Tools/IShapeToolHandler.cs
  32. 15 2
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  33. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  34. 71 60
      src/PixiEditor/Styles/PixiEditor.Handles.axaml
  35. 12 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  36. 55 17
      src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs
  37. 16 4
      src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayViewModel.cs
  38. 10 0
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  39. 8 0
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  40. 1 0
      src/PixiEditor/ViewModels/Tools/ShapeTool.cs
  41. 3 0
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  42. 2 0
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  43. 5 4
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  44. 5 2
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  45. 5 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  46. 7 5
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  47. 18 2
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  48. 7 2
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  49. 12 0
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  50. 10 3
      src/PixiEditor/Views/Overlays/Handles/Handle.cs
  51. 1 1
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  52. 134 26
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  53. 101 6
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

+ 22 - 0
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -196,6 +196,28 @@ public struct ShapeCorners
         };
         };
     }
     }
 
 
+    public ShapeCorners AsScaled(float scaleX, float scaleY)
+    {
+        VecD center = RectCenter;
+        VecD topLeftDelta = TopLeft - center;
+        VecD topRightDelta = TopRight - center;
+        VecD bottomLeftDelta = BottomLeft - center;
+        VecD bottomRightDelta = BottomRight - center;
+
+        topLeftDelta = new VecD(topLeftDelta.X * scaleX, topLeftDelta.Y * scaleY);
+        topRightDelta = new VecD(topRightDelta.X * scaleX, topRightDelta.Y * scaleY);
+        bottomLeftDelta = new VecD(bottomLeftDelta.X * scaleX, bottomLeftDelta.Y * scaleY);
+        bottomRightDelta = new VecD(bottomRightDelta.X * scaleX, bottomRightDelta.Y * scaleY);
+
+        return new ShapeCorners()
+        {
+            TopLeft = center + topLeftDelta,
+            TopRight = center + topRightDelta,
+            BottomLeft = center + bottomLeftDelta,
+            BottomRight = center + bottomRightDelta
+        };
+    }
+
     public static bool operator !=(ShapeCorners left, ShapeCorners right) => !(left == right);
     public static bool operator !=(ShapeCorners left, ShapeCorners right) => !(left == right);
 
 
     public static bool operator ==(ShapeCorners left, ShapeCorners right)
     public static bool operator ==(ShapeCorners left, ShapeCorners right)

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs

@@ -1,8 +1,11 @@
-using Drawie.Backend.Core.Vector;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 
 
 public interface IReadOnlyPathData : IReadOnlyShapeVectorData
 public interface IReadOnlyPathData : IReadOnlyShapeVectorData
 {
 {
     public VectorPath Path { get; }
     public VectorPath Path { get; }
+    public StrokeCap StrokeLineCap { get; }
+    public StrokeJoin StrokeLineJoin { get; }
 }
 }

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -30,23 +30,23 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         Radius = radius;
         Radius = radius;
     }
     }
 
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
     {
         Rasterize(drawingSurface, false);
         Rasterize(drawingSurface, false);
     }
     }
 
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
     {
         Rasterize(drawingSurface, true);
         Rasterize(drawingSurface, true);
     }
     }
 
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
         int saved = 0;
         int saved = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
         }
 
 
         using Paint shapePaint = new Paint();
         using Paint shapePaint = new Paint();
@@ -56,7 +56,7 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         {
         {
             shapePaint.Color = FillColor;
             shapePaint.Color = FillColor;
             shapePaint.Style = PaintStyle.Fill;
             shapePaint.Style = PaintStyle.Fill;
-            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+            canvas.DrawOval(Center, Radius, shapePaint);
         }
         }
 
 
         if (StrokeWidth > 0)
         if (StrokeWidth > 0)
@@ -64,12 +64,12 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             shapePaint.Color = StrokeColor;
             shapePaint.Color = StrokeColor;
             shapePaint.Style = PaintStyle.Stroke;
             shapePaint.Style = PaintStyle.Stroke;
             shapePaint.StrokeWidth = StrokeWidth;
             shapePaint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+            canvas.DrawOval(Center, Radius, shapePaint);
         }
         }
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
-            drawingSurface.Canvas.RestoreToCount(saved);
+            canvas.RestoreToCount(saved);
         }
         }
     }
     }
 
 

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -53,23 +53,23 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
     public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
     public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
         .WithMatrix(TransformationMatrix);
         .WithMatrix(TransformationMatrix);
 
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
     }
 
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
     }
 
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
         int num = 0;
         int num = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
         }
 
 
         using Paint paint = new Paint() { IsAntiAliased = true };
         using Paint paint = new Paint() { IsAntiAliased = true };
@@ -78,11 +78,11 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
         paint.Style = PaintStyle.Stroke;
         paint.Style = PaintStyle.Stroke;
         paint.StrokeWidth = StrokeWidth;
         paint.StrokeWidth = StrokeWidth;
 
 
-        drawingSurface.Canvas.DrawLine(Start, End, paint);
+        canvas.DrawLine(Start, End, paint);
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
         }
     }
     }
 
 

+ 15 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -17,33 +17,37 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public override ShapeCorners TransformationCorners =>
     public override ShapeCorners TransformationCorners =>
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
 
+    public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
+    
+    public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
+
     public PathVectorData(VectorPath path)
     public PathVectorData(VectorPath path)
     {
     {
         Path = path;
         Path = path;
     }
     }
 
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
     }
 
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
     }
 
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
         int num = 0;
         int num = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
         }
 
 
         using Paint paint = new Paint()
         using Paint paint = new Paint()
         {
         {
-            IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
+            IsAntiAliased = true, StrokeJoin = StrokeLineJoin, StrokeCap = StrokeLineCap
         };
         };
 
 
         if (Fill && FillColor.A > 0)
         if (Fill && FillColor.A > 0)
@@ -51,7 +55,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             paint.Color = FillColor;
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
 
 
-            drawingSurface.Canvas.DrawPath(Path, paint);
+            canvas.DrawPath(Path, paint);
         }
         }
 
 
         if (StrokeWidth > 0)
         if (StrokeWidth > 0)
@@ -60,12 +64,12 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             paint.Style = PaintStyle.Stroke;
             paint.Style = PaintStyle.Stroke;
             paint.StrokeWidth = StrokeWidth;
             paint.StrokeWidth = StrokeWidth;
             
             
-            drawingSurface.Canvas.DrawPath(Path, paint);
+            canvas.DrawPath(Path, paint);
         }
         }
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
         }
     }
     }
 
 

+ 7 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -23,17 +23,17 @@ public class PointsVectorData : ShapeVectorData
     public override ShapeCorners TransformationCorners => new ShapeCorners(
     public override ShapeCorners TransformationCorners => new ShapeCorners(
         GeometryAABB).WithMatrix(TransformationMatrix);
         GeometryAABB).WithMatrix(TransformationMatrix);
 
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
     {
         Rasterize(drawingSurface, false);
         Rasterize(drawingSurface, false);
     }
     }
 
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
     {
         Rasterize(drawingSurface, true);
         Rasterize(drawingSurface, true);
     }
     }
 
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
         using Paint paint = new Paint();
         using Paint paint = new Paint();
         paint.Color = FillColor;
         paint.Color = FillColor;
@@ -42,17 +42,17 @@ public class PointsVectorData : ShapeVectorData
         int num = 0;
         int num = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {
-            num = drawingSurface.Canvas.Save();
+            num = canvas.Save();
             Matrix3X3 final = TransformationMatrix;
             Matrix3X3 final = TransformationMatrix;
-            drawingSurface.Canvas.SetMatrix(final);
+            canvas.SetMatrix(final);
         }
         }
 
 
-        drawingSurface.Canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((int)p.X, (int)p.Y)).ToArray(),
+        canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((int)p.X, (int)p.Y)).ToArray(),
             paint);
             paint);
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
         }
     }
     }
 
 

+ 10 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -41,23 +41,23 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         Size = new VecD(width, height);
         Size = new VecD(width, height);
     }
     }
 
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
     }
 
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
     }
 
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
         int saved = 0;
         int saved = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
         }
 
 
         using Paint paint = new Paint();
         using Paint paint = new Paint();
@@ -67,7 +67,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         {
         {
             paint.Color = FillColor;
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
-            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
         }
         }
 
 
         if (StrokeWidth > 0)
         if (StrokeWidth > 0)
@@ -76,12 +76,12 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             paint.Style = PaintStyle.Stroke;
             paint.Style = PaintStyle.Stroke;
 
 
             paint.StrokeWidth = StrokeWidth;
             paint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
         }
         }
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
-            drawingSurface.Canvas.RestoreToCount(saved);
+            canvas.RestoreToCount(saved);
         }
         }
     }
     }
 
 

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -23,17 +23,17 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public abstract ShapeCorners TransformationCorners { get; } 
     public abstract ShapeCorners TransformationCorners { get; } 
     
     
-    protected void ApplyTransformTo(DrawingSurface drawingSurface)
+    protected void ApplyTransformTo(Canvas canvas)
     {
     {
-        Matrix3X3 canvasMatrix = drawingSurface.Canvas.TotalMatrix;
+        Matrix3X3 canvasMatrix = canvas.TotalMatrix;
 
 
         Matrix3X3 final = canvasMatrix.Concat(TransformationMatrix);
         Matrix3X3 final = canvasMatrix.Concat(TransformationMatrix);
 
 
-        drawingSurface.Canvas.SetMatrix(final);
+        canvas.SetMatrix(final);
     }
     }
 
 
-    public abstract void RasterizeGeometry(DrawingSurface drawingSurface);
-    public abstract void RasterizeTransformed(DrawingSurface drawingSurface);
+    public abstract void RasterizeGeometry(Canvas canvas);
+    public abstract void RasterizeTransformed(Canvas canvas);
     public abstract bool IsValid();
     public abstract bool IsValid();
     public abstract int GetCacheHash();
     public abstract int GetCacheHash();
     public abstract int CalculateHash();
     public abstract int CalculateHash();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -26,7 +26,7 @@ public class RasterizeShapeNode : RenderNode
         if (shape == null || !shape.IsValid())
         if (shape == null || !shape.IsValid())
             return;
             return;
         
         
-        shape.RasterizeTransformed(surface);
+        shape.RasterizeTransformed(surface.Canvas);
     }
     }
 
 
     public override Node CreateCopy() => new RasterizeShapeNode();
     public override Node CreateCopy() => new RasterizeShapeNode();
@@ -42,7 +42,7 @@ public class RasterizeShapeNode : RenderNode
         if (shape == null || !shape.IsValid())
         if (shape == null || !shape.IsValid())
             return false;
             return false;
 
 
-        shape.RasterizeTransformed(renderOn);
+        shape.RasterizeTransformed(renderOn.Canvas);
 
 
         return true;
         return true;
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -160,7 +160,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     public void Rasterize(DrawingSurface surface, Paint paint)
     public void Rasterize(DrawingSurface surface, Paint paint)
     {
     {
         int layer = surface.Canvas.SaveLayer(paint);
         int layer = surface.Canvas.SaveLayer(paint);
-        ShapeData?.RasterizeTransformed(surface);
+        ShapeData?.RasterizeTransformed(surface.Canvas);
         
         
         surface.Canvas.RestoreToCount(layer);
         surface.Canvas.RestoreToCount(layer);
     }
     }

+ 4 - 1
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -1,4 +1,5 @@
 using System.Xml;
 using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
@@ -11,10 +12,12 @@ public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
 
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
-        List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth };
+        List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);
     }
     }
 }
 }

+ 7 - 0
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -1,4 +1,5 @@
 using System.Xml;
 using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
@@ -10,6 +11,10 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
 
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
@@ -19,6 +24,8 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
         properties.Add(Fill);
         properties.Add(Fill);
         properties.Add(Stroke);
         properties.Add(Stroke);
         properties.Add(StrokeWidth);
         properties.Add(StrokeWidth);
+        properties.Add(StrokeLineCap);
+        properties.Add(StrokeLineJoin);
 
 
         do
         do
         {
         {

+ 8 - 0
src/PixiEditor.SVG/Enums/SvgStrokeLineCap.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgStrokeLineCap
+{
+    Butt,
+    Round,
+    Square
+}

+ 10 - 0
src/PixiEditor.SVG/Enums/SvgStrokeLineJoin.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgStrokeLineJoin
+{
+    Arcs,
+    Bevel,
+    Miter,
+    MiterClip,
+    Round
+}

+ 4 - 1
src/PixiEditor.SVG/Features/IStrokable.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Units;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Features;
 namespace PixiEditor.SVG.Features;
 
 
@@ -6,4 +7,6 @@ public interface IStrokable
 {
 {
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
 }
 }

+ 20 - 1
src/PixiEditor.SVG/StyleContext.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG;
 namespace PixiEditor.SVG;
@@ -9,6 +10,10 @@ public struct StyleContext
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
 
 
     public StyleContext()
     public StyleContext()
     {
     {
@@ -16,6 +21,8 @@ public struct StyleContext
         Stroke = new("stroke");
         Stroke = new("stroke");
         Fill = new("fill");
         Fill = new("fill");
         Transform = new("transform");
         Transform = new("transform");
+        StrokeLineCap = new("stroke-linecap");
+        StrokeLineJoin = new("stroke-linejoin");
     }
     }
     
     
     public StyleContext(SvgDocument document)
     public StyleContext(SvgDocument document)
@@ -24,6 +31,8 @@ public struct StyleContext
         Stroke = document.Stroke;
         Stroke = document.Stroke;
         Fill = document.Fill;
         Fill = document.Fill;
         Transform = document.Transform;
         Transform = document.Transform;
+        StrokeLineCap = document.StrokeLineCap;
+        StrokeLineJoin = document.StrokeLineJoin;
     }
     }
 
 
     public StyleContext WithElement(SvgElement element)
     public StyleContext WithElement(SvgElement element)
@@ -51,6 +60,16 @@ public struct StyleContext
             {
             {
                 styleContext.StrokeWidth.Unit = strokableElement.StrokeWidth.Unit;
                 styleContext.StrokeWidth.Unit = strokableElement.StrokeWidth.Unit;
             }
             }
+            
+            if (strokableElement.StrokeLineCap.Unit != null)
+            {
+                styleContext.StrokeLineCap.Unit = strokableElement.StrokeLineCap.Unit;
+            }
+            
+            if (strokableElement.StrokeLineJoin.Unit != null)
+            {
+                styleContext.StrokeLineJoin.Unit = strokableElement.StrokeLineJoin.Unit;
+            }
         }
         }
 
 
         return styleContext;
         return styleContext;

+ 19 - 1
src/PixiEditor.SVG/SvgDocument.cs

@@ -1,6 +1,7 @@
 using System.Xml;
 using System.Xml;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
@@ -15,6 +16,10 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public List<SvgElement> Children { get; } = new();
     public List<SvgElement> Children { get; } = new();
 
 
@@ -35,7 +40,10 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
             Fill,
             Fill,
             Stroke,
             Stroke,
             StrokeWidth,
             StrokeWidth,
-            Transform
+            Transform,
+            ViewBox,
+            StrokeLineCap,
+            StrokeLineJoin
         };
         };
         
         
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);
@@ -118,5 +126,15 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         {
         {
             root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
             root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
         }
         }
+        
+        if (StrokeLineCap.Unit != null)
+        {
+            root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml()));
+        }
+        
+        if (StrokeLineJoin.Unit != null)
+        {
+            root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml()));
+        }
     }
     }
 }
 }

+ 30 - 1
src/PixiEditor/Helpers/GeometryHelper.cs

@@ -4,7 +4,36 @@ namespace PixiEditor.Helpers;
 
 
 public static class GeometryHelper
 public static class GeometryHelper
 {
 {
-    public static VecI Get45IncrementedPosition(VecD startPos, VecD curPos)
+    public static VecD Get45IncrementedPosition(VecD startPos, VecD curPos)
+    {
+        Span<VecD> positions =
+        [
+            (curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())),
+            (curPos.ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())),
+            (curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())),
+            (curPos.ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs()))
+        ];
+
+        VecD max = positions[0];
+        double maxLength = double.MaxValue;
+        foreach (var pos in positions)
+        {
+            double length = (pos - curPos).LengthSquared;
+            if (length < maxLength)
+            {
+                maxLength = length;
+                max = pos;
+            }
+        }
+
+        return max;
+    }
+
+    public static VecI Get45IncrementedPositionAligned(VecD startPos, VecD curPos)
     {
     {
         Span<VecI> positions =
         Span<VecI> positions =
         [
         [

+ 34 - 0
src/PixiEditor/Helpers/Resources/VectorPathResource.cs

@@ -0,0 +1,34 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Helpers.Extensions;
+using Color = Avalonia.Media.Color;
+
+namespace PixiEditor.Helpers.Resources;
+
+public class VectorPathResource
+{
+    public string SvgPath { get; set; }
+    public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
+    public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
+    public Color FillColor { get; set; } = Avalonia.Media.Colors.Transparent;
+    public Color StrokeColor { get; set; } = Avalonia.Media.Colors.Black;
+    public float StrokeWidth { get; set; } = 1;
+    public PathFillType FillType { get; set; } = PathFillType.Winding;
+    
+    public PathVectorData ToVectorPathData()
+    {
+        VectorPath path = VectorPath.FromSvgPath(SvgPath);
+        path.FillType = FillType;
+        
+        return new PathVectorData(path)
+        {
+            StrokeLineCap = StrokeLineCap,
+            StrokeLineJoin = StrokeLineJoin,
+            FillColor = FillColor.ToColor(),
+            StrokeColor = StrokeColor.ToColor(),
+            StrokeWidth = StrokeWidth
+        };
+    }
+}

+ 3 - 0
src/PixiEditor/Images/Handles/shear.svg

@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <path d="M16 3L20 7L16 11M20 7L4 7M8 21L4 17L8 13M4 17L20 17" fill-rule="nonzero" transform="matrix(1, 0, 0, 1, 0, 0)" fill="rgba(255,255,255,0)" stroke="rgba(0,0,0,255)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+</svg>

+ 1 - 1
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -223,7 +223,7 @@ internal static class ClipboardController
             var layerId = layerIds[i];
             var layerId = layerIds[i];
 
 
             var layer = doc.StructureHelper.Find(layerId);
             var layer = doc.StructureHelper.Find(layerId);
-            if (layer is not { TightBounds: not null } || layer.TightBounds.Value.Pos.AlmostEquals(pos))
+            if (layer is not { TightBounds: not null } || !layer.TightBounds.Value.Pos.AlmostEquals(pos))
                 return false;
                 return false;
         }
         }
         
         

+ 40 - 14
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -264,10 +264,10 @@ public class SnappingController
     ///     Gets the intersection of closest snap axis along projected axis.
     ///     Gets the intersection of closest snap axis along projected axis.
     /// </summary>
     /// </summary>
     /// <param name="pos">Position to snap</param>
     /// <param name="pos">Position to snap</param>
-    /// <param name="direction">Direction to project from <paramref name="pos">/></param>
+    /// <param name="direction">Direction to project from position</param>
     /// <param name="xAxis">Intersected horizontal axis</param>
     /// <param name="xAxis">Intersected horizontal axis</param>
     /// <param name="yAxis">Intersected vertical axis</param>
     /// <param name="yAxis">Intersected vertical axis</param>
-    /// <returns>Snapped position to the closest snap point along projected axis from <paramref name="pos">/></returns> 
+    /// <returns>Snapped position to the closest snap point along projected axis from position.</returns>
     public VecD GetSnapPoint(VecD pos, VecD direction, out string xAxis, out string yAxis)
     public VecD GetSnapPoint(VecD pos, VecD direction, out string xAxis, out string yAxis)
     {
     {
         if (!SnappingEnabled)
         if (!SnappingEnabled)
@@ -277,7 +277,7 @@ public class SnappingController
             return pos;
             return pos;
         }
         }
 
 
-        if (direction.X == 0 || direction.Y == 0)
+        if (direction is { X: 0, Y: 0 })
         {
         {
             return GetSnapPoint(pos, out xAxis, out yAxis);
             return GetSnapPoint(pos, out xAxis, out yAxis);
         }
         }
@@ -286,14 +286,17 @@ public class SnappingController
 
 
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
-
-
+        
         VecD? xIntersect = null;
         VecD? xIntersect = null;
         if (closestX != null)
         if (closestX != null)
         {
         {
             double x = closestX.Value;
             double x = closestX.Value;
             double y = pos.Y + direction.Y * (x - pos.X) / direction.X;
             double y = pos.Y + direction.Y * (x - pos.X) / direction.X;
             xIntersect = new VecD(x, y);
             xIntersect = new VecD(x, y);
+            if (xIntersect.Value.IsNaNOrInfinity())
+            {
+                xIntersect = null;
+            }
         }
         }
 
 
         VecD? yIntersect = null;
         VecD? yIntersect = null;
@@ -302,6 +305,10 @@ public class SnappingController
             double y = closestY.Value;
             double y = closestY.Value;
             double x = pos.X + direction.X * (y - pos.Y) / direction.Y;
             double x = pos.X + direction.X * (y - pos.Y) / direction.Y;
             yIntersect = new VecD(x, y);
             yIntersect = new VecD(x, y);
+            if (yIntersect.Value.IsNaNOrInfinity())
+            {
+                yIntersect = null;
+            }
         }
         }
 
 
         if (xIntersect.HasValue && yIntersect.HasValue)
         if (xIntersect.HasValue && yIntersect.HasValue)
@@ -309,27 +316,41 @@ public class SnappingController
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
                 && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
                 && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
             {
             {
-                xAxis = closestXAxis;
-                yAxis = closestYAxis;
-                return xIntersect.Value;
+                if(IsWithinSnapDistance(xIntersect.Value, pos))
+                {
+                    xAxis = closestXAxis;
+                    yAxis = closestYAxis;
+                    return xIntersect.Value;
+                }
+                
+                xAxis = string.Empty;
+                yAxis = string.Empty;
+                return pos;
             }
             }
 
 
             double xDist = (xIntersect.Value - pos).LengthSquared;
             double xDist = (xIntersect.Value - pos).LengthSquared;
             double yDist = (yIntersect.Value - pos).LengthSquared;
             double yDist = (yIntersect.Value - pos).LengthSquared;
 
 
-            if (xDist < yDist)
+            if (xDist < yDist && IsWithinSnapDistance(xIntersect.Value, pos))
             {
             {
                 xAxis = closestXAxis;
                 xAxis = closestXAxis;
                 yAxis = null;
                 yAxis = null;
                 return xIntersect.Value;
                 return xIntersect.Value;
             }
             }
 
 
-            xAxis = null;
-            yAxis = closestYAxis;
-            return yIntersect.Value;
+            if (IsWithinSnapDistance(yIntersect.Value, pos))
+            {
+                xAxis = null;
+                yAxis = closestYAxis;
+                return yIntersect.Value;
+            }
+            
+            xAxis = string.Empty;
+            yAxis = string.Empty;
+            return pos;
         }
         }
 
 
-        if (xIntersect != null)
+        if (xIntersect != null && IsWithinSnapDistance(xIntersect.Value, pos))
         {
         {
             xAxis = closestXAxis;
             xAxis = closestXAxis;
             yAxis = null;
             yAxis = null;
@@ -337,7 +358,7 @@ public class SnappingController
             return xIntersect.Value;
             return xIntersect.Value;
         }
         }
 
 
-        if (yIntersect != null)
+        if (yIntersect != null && IsWithinSnapDistance(yIntersect.Value, pos))
         {
         {
             xAxis = null;
             xAxis = null;
             yAxis = closestYAxis;
             yAxis = closestYAxis;
@@ -355,4 +376,9 @@ public class SnappingController
         HorizontalSnapPoints[identifier] = () => pointFunc().X;
         HorizontalSnapPoints[identifier] = () => pointFunc().X;
         VerticalSnapPoints[identifier] = () => pointFunc().Y;
         VerticalSnapPoints[identifier] = () => pointFunc().Y;
     }
     }
+    
+    private bool IsWithinSnapDistance(VecD snapPoint, VecD pos)
+    {
+        return (snapPoint - pos).LengthSquared < SnapDistance * SnapDistance;
+    }
 }
 }

+ 27 - 14
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -29,7 +29,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     protected T? toolViewModel;
     protected T? toolViewModel;
     protected RectD lastRect;
     protected RectD lastRect;
     protected double lastRadians;
     protected double lastRadians;
-    
+
     private ShapeCorners initialCorners;
     private ShapeCorners initialCorners;
     private bool noMovement = true;
     private bool noMovement = true;
     protected IFillableShapeToolbar toolbar;
     protected IFillableShapeToolbar toolbar;
@@ -64,13 +64,15 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
                 ignoreNextColorChange = colorsVM.ColorsTempSwapped;
                 ignoreNextColorChange = colorsVM.ColorsTempSwapped;
             }
             }
-            
+
             lastRect = new RectD(startDrawingPos, VecD.Zero);
             lastRect = new RectD(startDrawingPos, VecD.Zero);
 
 
             document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)),
             document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)),
                 false);
                 false);
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.IsSizeBoxEnabled = true;
             document.TransformHandler.IsSizeBoxEnabled = true;
+            document.TransformHandler.CanAlignToPixels = AlignToPixels;
+            
             return ExecutionState.Success;
             return ExecutionState.Success;
         }
         }
 
 
@@ -89,7 +91,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             toolbar.ToolSize = shapeData.StrokeWidth;
             toolbar.ToolSize = shapeData.StrokeWidth;
             toolbar.Fill = shapeData.FillColor != Colors.Transparent;
             toolbar.Fill = shapeData.FillColor != Colors.Transparent;
             initialCorners = shapeData.TransformationCorners;
             initialCorners = shapeData.TransformationCorners;
-            
+
             ActiveMode = ShapeToolMode.Transform;
             ActiveMode = ShapeToolMode.Transform;
         }
         }
         else
         else
@@ -131,19 +133,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         return pos1;
         return pos1;
     }
     }
 
 
-    public static RectI GetSquaredCoordinates(VecI startPos, VecI curPos)
-    {
-        VecI pos = GetSquaredPosition(startPos, curPos);
-        return RectI.FromTwoPixels(startPos, pos);
-    }
-
     public override void OnTransformMoved(ShapeCorners corners)
     public override void OnTransformMoved(ShapeCorners corners)
     {
     {
         if (ActiveMode != ShapeToolMode.Transform)
         if (ActiveMode != ShapeToolMode.Transform)
             return;
             return;
 
 
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
-        ShapeData shapeData = new ShapeData(rect.Center, rect.Size, corners.RectRotation, (float)StrokeWidth, StrokeColor,
+        ShapeData shapeData = new ShapeData(rect.Center, rect.Size, corners.RectRotation, (float)StrokeWidth,
+            StrokeColor,
             FillColor) { AntiAliasing = toolbar.AntiAliasing };
             FillColor) { AntiAliasing = toolbar.AntiAliasing };
         IAction drawAction = TransformMovedAction(shapeData, corners);
         IAction drawAction = TransformMovedAction(shapeData, corners);
 
 
@@ -172,9 +169,10 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             {
             {
                 ignoreNextColorChange = false;
                 ignoreNextColorChange = false;
             }
             }
+
             return;
             return;
         }
         }
-        
+
         ignoreNextColorChange = ActiveMode == ShapeToolMode.Drawing;
         ignoreNextColorChange = ActiveMode == ShapeToolMode.Drawing;
 
 
         toolbar.StrokeColor = color.ToColor();
         toolbar.StrokeColor = color.ToColor();
@@ -206,10 +204,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     {
     {
         VecD adjustedPos = AlignToPixels ? (VecI)pos.Floor() : pos;
         VecD adjustedPos = AlignToPixels ? (VecI)pos.Floor() : pos;
 
 
-        VecD snapped = adjustedPos;
+        VecD startPos = startDrawingPos;
+
+        VecD snapped;
         if (toolViewModel.DrawEven)
         if (toolViewModel.DrawEven)
         {
         {
-            adjustedPos = AlignToPixels ? GetSquaredPosition((VecI)startDrawingPos, (VecI)adjustedPos) : GetSquaredPosition(startDrawingPos, adjustedPos);
+            adjustedPos = AlignToPixels
+                ? GetSquaredPosition((VecI)startDrawingPos, (VecI)adjustedPos)
+                : GetSquaredPosition(startPos, adjustedPos);
             VecD dir = (adjustedPos - startDrawingPos).Normalize();
             VecD dir = (adjustedPos - startDrawingPos).Normalize();
             snapped = Snap(adjustedPos, startDrawingPos, dir, true);
             snapped = Snap(adjustedPos, startDrawingPos, dir, true);
         }
         }
@@ -220,6 +222,13 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
 
         noMovement = false;
         noMovement = false;
 
 
+        if (toolViewModel.DrawFromCenter)
+        {
+            VecD center = startDrawingPos;
+
+            startDrawingPos = center + (center - snapped);
+        }
+
         if (AlignToPixels)
         if (AlignToPixels)
         {
         {
             DrawShape((VecI)snapped.Floor(), lastRadians, false);
             DrawShape((VecI)snapped.Floor(), lastRadians, false);
@@ -229,7 +238,10 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             DrawShape(snapped, lastRadians, false);
             DrawShape(snapped, lastRadians, false);
         }
         }
 
 
+        startDrawingPos = startPos;
+
         document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false);
         document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false);
+        document.TransformHandler.CanAlignToPixels = AlignToPixels;
         document!.TransformHandler.Corners = new ShapeCorners((RectD)lastRect);
         document!.TransformHandler.Corners = new ShapeCorners((RectD)lastRect);
     }
     }
 
 
@@ -302,7 +314,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         var layer = document.StructureHelper.Find(memberId);
         var layer = document.StructureHelper.Find(memberId);
         if (layer is null)
         if (layer is null)
             return;
             return;
-        
+
         if (CanEditShape(layer))
         if (CanEditShape(layer))
         {
         {
             internals!.ActionAccumulator.AddActions(SettingsChangedAction());
             internals!.ActionAccumulator.AddActions(SettingsChangedAction());
@@ -343,6 +355,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         {
         {
             document.TransformHandler.HideTransform();
             document.TransformHandler.HideTransform();
             document!.TransformHandler.ShowTransform(TransformMode, false, initialCorners, true);
             document!.TransformHandler.ShowTransform(TransformMode, false, initialCorners, true);
+            document.TransformHandler.CanAlignToPixels = AlignToPixels;
         }
         }
     }
     }
 
 

+ 26 - 6
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -20,7 +20,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
 
     protected Color StrokeColor => toolbar!.StrokeColor.ToColor();
     protected Color StrokeColor => toolbar!.StrokeColor.ToColor();
     protected double StrokeWidth => toolViewModel!.ToolSize;
     protected double StrokeWidth => toolViewModel!.ToolSize;
-    
+
     protected bool drawOnMask;
     protected bool drawOnMask;
 
 
     protected VecD curPos;
     protected VecD curPos;
@@ -29,6 +29,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private IColorsHandler? colorsVM;
     private IColorsHandler? colorsVM;
     protected IShapeToolbar? toolbar;
     protected IShapeToolbar? toolbar;
     private bool ignoreNextColorChange = false;
     private bool ignoreNextColorChange = false;
+    private VecD lastStartPos;
 
 
     public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
     public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
     public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
     public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
@@ -63,7 +64,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false);
             document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false);
             document.LineToolOverlayHandler.ShowHandles = false;
             document.LineToolOverlayHandler.ShowHandles = false;
             document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
             document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
-            
+
             return ExecutionState.Success;
             return ExecutionState.Success;
         }
         }
 
 
@@ -107,10 +108,20 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         VecD snapped = endPos;
         VecD snapped = endPos;
         string snapX = "";
         string snapX = "";
         string snapY = "";
         string snapY = "";
-        
+
+        VecD startPos = startDrawingPos;
+
         if (toolViewModel!.Snap)
         if (toolViewModel!.Snap)
         {
         {
-            endPos = GeometryHelper.Get45IncrementedPosition(startDrawingPos, pos);
+            if (AlignToPixels)
+            {
+                endPos = GeometryHelper.Get45IncrementedPositionAligned(startDrawingPos, pos);
+            }
+            else
+            {
+                endPos = GeometryHelper.Get45IncrementedPosition(startDrawingPos, pos);
+            }
+
             VecD directionConstraint = endPos - startDrawingPos;
             VecD directionConstraint = endPos - startDrawingPos;
             snapped =
             snapped =
                 document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, directionConstraint, out snapX,
                 document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, directionConstraint, out snapX,
@@ -120,7 +131,12 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         {
         {
             snapped = document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, out snapX, out snapY);
             snapped = document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, out snapX, out snapY);
         }
         }
-        
+
+        if (toolViewModel.DrawFromCenter)
+        {
+            VecD center = startDrawingPos;
+            startDrawingPos = center + (center - snapped);
+        }
 
 
         HighlightSnapping(snapX, snapY);
         HighlightSnapping(snapX, snapY);
         document!.LineToolOverlayHandler.LineEnd = snapped;
         document!.LineToolOverlayHandler.LineEnd = snapped;
@@ -129,6 +145,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
 
         var drawLineAction = DrawLine(curPos);
         var drawLineAction = DrawLine(curPos);
         internals!.ActionAccumulator.AddActions(drawLineAction);
         internals!.ActionAccumulator.AddActions(drawLineAction);
+
+        lastStartPos = startDrawingPos;
+        startDrawingPos = startPos;
     }
     }
 
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
@@ -141,7 +160,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         }
         }
 
 
         document!.LineToolOverlayHandler.Hide();
         document!.LineToolOverlayHandler.Hide();
-        document!.LineToolOverlayHandler.Show(startDrawingPos, curPos, true);
+        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, true);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
     }
 
 
@@ -165,6 +184,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             {
             {
                 ignoreNextColorChange = false;
                 ignoreNextColorChange = false;
             }
             }
+
             return;
             return;
         }
         }
 
 

+ 14 - 6
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -13,6 +13,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
@@ -37,6 +38,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
 
     public bool CanUndo => document.PathOverlayHandler.HasUndo;
     public bool CanUndo => document.PathOverlayHandler.HasUndo;
     public bool CanRedo => document.PathOverlayHandler.HasRedo;
     public bool CanRedo => document.PathOverlayHandler.HasRedo;
+    public bool StopExecutionOnNormalUndo => false;
 
 
     public override bool BlocksOtherActions => false;
     public override bool BlocksOtherActions => false;
 
 
@@ -78,7 +80,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
                 return ExecutionState.Success;
                 return ExecutionState.Success;
             }
             }
 
 
-            document.PathOverlayHandler.Show(startingPath, false);
+            document.PathOverlayHandler.Show(startingPath, false, AddToUndo);
             if (controller.LeftMousePressed)
             if (controller.LeftMousePressed)
             {
             {
                 var snapped =
                 var snapped =
@@ -178,7 +180,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
     {
         if (document.PathOverlayHandler.IsActive)
         if (document.PathOverlayHandler.IsActive)
         {
         {
-            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData(startingPath)));
         }
         }
     }
     }
 
 
@@ -190,9 +192,15 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
         internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
         internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
     }
     }
 
 
-    private PathVectorData ConstructShapeData()
+    private void AddToUndo(VectorPath path)
     {
     {
-        if(startingPath == null)
+        internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action(),
+            new SetShapeGeometry_Action(member.Id, ConstructShapeData(path)), new EndSetShapeGeometry_Action());
+    }
+
+    private PathVectorData ConstructShapeData(VectorPath? path)
+    {
+        if(path is null)
         {
         {
             return new PathVectorData(new VectorPath() { FillType = (PathFillType)vectorPathToolHandler.FillMode })
             return new PathVectorData(new VectorPath() { FillType = (PathFillType)vectorPathToolHandler.FillMode })
             {
             {
@@ -202,7 +210,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             };
             };
         }
         }
         
         
-        return new PathVectorData(new VectorPath(startingPath) { FillType = (PathFillType)vectorPathToolHandler.FillMode })
+        return new PathVectorData(new VectorPath(path) { FillType = (PathFillType)vectorPathToolHandler.FillMode })
         {
         {
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeColor = toolbar.StrokeColor.ToColor(),
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -215,7 +223,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
         if (document.PathOverlayHandler.IsActive)
         if (document.PathOverlayHandler.IsActive)
         {
         {
             startingPath = path;
             startingPath = path;
-            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData(startingPath)));
         }
         }
     }
     }
 
 

+ 1 - 1
src/PixiEditor/Models/Handlers/IPathOverlayHandler.cs

@@ -4,7 +4,7 @@ namespace PixiEditor.Models.Handlers;
 
 
 public interface IPathOverlayHandler : IHandler
 public interface IPathOverlayHandler : IHandler
 {
 {
-    public void Show(VectorPath path, bool showApplyButton);
+    public void Show(VectorPath path, bool showApplyButton, Action<VectorPath>? customAddToUndo = null);
     public void Hide();
     public void Hide();
     public event Action<VectorPath> PathChanged;
     public event Action<VectorPath> PathChanged;
     public bool IsActive { get; }
     public bool IsActive { get; }

+ 3 - 0
src/PixiEditor/Models/Handlers/IToolHandler.cs

@@ -63,4 +63,7 @@ internal interface IToolHandler : IHandler
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
     public void ApplyToolSetSettings(IToolSetHandler toolset);
     public void ApplyToolSetSettings(IToolSetHandler toolset);
     public void OnDeselecting(bool transient);
     public void OnDeselecting(bool transient);
+    
+    public void OnPostUndo();
+    public void OnPostRedo();
 }
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/IToolsHandler.cs

@@ -31,4 +31,6 @@ internal interface IToolsHandler : IHandler
     public void UseToolEventInlet(VecD argsPositionOnCanvas, MouseButton argsButton);
     public void UseToolEventInlet(VecD argsPositionOnCanvas, MouseButton argsButton);
     public T GetTool<T>() where T : IToolHandler;
     public T GetTool<T>() where T : IToolHandler;
     public void AddPropertyChangedCallback(string propertyName, Action callback);
     public void AddPropertyChangedCallback(string propertyName, Action callback);
+    public void OnPostUndoInlet();
+    public void OnPostRedoInlet();
 }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/ITransformHandler.cs

@@ -21,4 +21,5 @@ internal interface ITransformHandler : IHandler
     public ShapeCorners Corners { get; set; }
     public ShapeCorners Corners { get; set; }
     public bool ShowHandles { get; set; }
     public bool ShowHandles { get; set; }
     public bool IsSizeBoxEnabled { get; set; }
     public bool IsSizeBoxEnabled { get; set; }
+    public bool CanAlignToPixels { get; set; }
 }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/ILineToolHandler.cs

@@ -4,4 +4,5 @@ internal interface ILineToolHandler : IToolHandler
 {
 {
     public double ToolSize { get; }
     public double ToolSize { get; }
     public bool Snap { get; }
     public bool Snap { get; }
+    public bool DrawFromCenter { get; }
 }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IShapeToolHandler.cs

@@ -3,4 +3,5 @@
 internal interface IShapeToolHandler : IToolHandler
 internal interface IShapeToolHandler : IToolHandler
 {
 {
     public bool DrawEven { get; }
     public bool DrawEven { get; }
+    public bool DrawFromCenter { get; }
 }
 }

+ 15 - 2
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -174,8 +175,17 @@ internal class SvgDocumentBuilder : IDocumentBuilder
                 _ => PathFillType.Winding
                 _ => PathFillType.Winding
             };
             };
         }
         }
+        
+        StrokeCap strokeLineCap = StrokeCap.Round;
+        StrokeJoin strokeLineJoin = StrokeJoin.Round;
+        
+        if(element.StrokeLineCap.Unit != null)
+        {
+            strokeLineCap = (StrokeCap)element.StrokeLineCap.Unit.Value.Value;
+            strokeLineJoin = (StrokeJoin)element.StrokeLineJoin.Unit.Value.Value;
+        }
 
 
-        return new PathVectorData(path);
+        return new PathVectorData(path) { StrokeLineCap = strokeLineCap, StrokeLineJoin = strokeLineJoin };
     }
     }
 
 
     private RectangleVectorData AddRect(SvgRectangle element)
     private RectangleVectorData AddRect(SvgRectangle element)
@@ -208,7 +218,10 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             var targetWidth = styleContext.StrokeWidth.Unit;
             var targetWidth = styleContext.StrokeWidth.Unit;
             
             
             shapeData.StrokeColor = targetColor.Value.Color;
             shapeData.StrokeColor = targetColor.Value.Color;
-            shapeData.StrokeWidth = (float)targetWidth.Value.Value;
+            if (targetWidth != null)
+            {
+                shapeData.StrokeWidth = (float)targetWidth.Value.Value;
+            }
         }
         }
 
 
         if (hasTransform)
         if (hasTransform)

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.35")]
-[assembly: AssemblyFileVersion("2.0.0.35")]
+[assembly: AssemblyVersion("2.0.0.36")]
+[assembly: AssemblyFileVersion("2.0.0.36")]

+ 71 - 60
src/PixiEditor/Styles/PixiEditor.Handles.axaml

@@ -1,68 +1,79 @@
 <Styles xmlns="https://github.com/avaloniaui"
 <Styles xmlns="https://github.com/avaloniaui"
-                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-                    xmlns:system="clr-namespace:System;assembly=System.Runtime">
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime"
+        xmlns:data="clr-namespace:PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;assembly=PixiEditor.ChangeableDocument"
+        xmlns:resources="clr-namespace:PixiEditor.Helpers.Resources">
     <Styles.Resources>
     <Styles.Resources>
         <ResourceDictionary>
         <ResourceDictionary>
-             <DrawingGroup x:Key="MoveHandle">
-        <DrawingGroup.ClipGeometry>
-            <RectangleGeometry Rect="0.0,0.0,24.0,24.0"/>
-        </DrawingGroup.ClipGeometry>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <PathGeometry Figures="M 5 9 L 2 12 L 5 15"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <PathGeometry Figures="M 9 5 L 12 2 L 15 5"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <PathGeometry Figures="M 15 19 L 12 22 L 9 19"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <PathGeometry Figures="M 19 9 L 22 12 L 19 15"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <LineGeometry StartPoint="2.0,12.0" EndPoint="22.0,12.0"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-        <GeometryDrawing>
-            <GeometryDrawing.Pen>
-                <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round"/>
-            </GeometryDrawing.Pen>
-            <GeometryDrawing.Geometry>
-                <LineGeometry StartPoint="12.0,2.0" EndPoint="12.0,22.0"/>
-            </GeometryDrawing.Geometry>
-        </GeometryDrawing>
-    </DrawingGroup>
+            <DrawingGroup x:Key="MoveHandle">
+                <DrawingGroup.ClipGeometry>
+                    <RectangleGeometry Rect="0.0,0.0,24.0,24.0" />
+                </DrawingGroup.ClipGeometry>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <PathGeometry Figures="M 5 9 L 2 12 L 5 15" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <PathGeometry Figures="M 9 5 L 12 2 L 15 5" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <PathGeometry Figures="M 15 19 L 12 22 L 9 19" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <PathGeometry Figures="M 19 9 L 22 12 L 19 15" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <LineGeometry StartPoint="2.0,12.0" EndPoint="22.0,12.0" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+                <GeometryDrawing>
+                    <GeometryDrawing.Pen>
+                        <Pen Brush="{DynamicResource HandleBrush}" LineJoin="Round" Thickness="2.0" LineCap="Round" />
+                    </GeometryDrawing.Pen>
+                    <GeometryDrawing.Geometry>
+                        <LineGeometry StartPoint="12.0,2.0" EndPoint="12.0,22.0" />
+                    </GeometryDrawing.Geometry>
+                </GeometryDrawing>
+            </DrawingGroup>
 
 
             <system:String x:Key="MarkerHandle">M -1.1146 -0.6603 c -0.1215 -0.1215 -0.3187 -0.1215 -0.4401 0 l -0.4401 0.4401 c -0.1215 0.1215 -0.1215 0.3187 0 0.4401 l 0.4401 0.4401 c 0.1215 0.1215 0.3187 0.1215 0.4401 0 l 0.4401 -0.4401 c 0.1215 -0.1215 0.1215 -0.3187 0 -0.4401 l -0.4401 -0.4401 Z M -0.5834 0.0012 l 0.5833 -0.0013</system:String>
             <system:String x:Key="MarkerHandle">M -1.1146 -0.6603 c -0.1215 -0.1215 -0.3187 -0.1215 -0.4401 0 l -0.4401 0.4401 c -0.1215 0.1215 -0.1215 0.3187 0 0.4401 l 0.4401 0.4401 c 0.1215 0.1215 0.3187 0.1215 0.4401 0 l 0.4401 -0.4401 c 0.1215 -0.1215 0.1215 -0.3187 0 -0.4401 l -0.4401 -0.4401 Z M -0.5834 0.0012 l 0.5833 -0.0013</system:String>
-
             <system:String x:Key="RotateHandle">M -1.26 -0.455 Q 0 0.175 1.26 -0.455 L 1.12 -0.735 L 2.1 -0.7 L 1.54 0.105 L 1.4 -0.175 Q 0 0.525 -1.4 -0.175 L -1.54 0.105 L -2.1 -0.7 L -1.12 -0.735 Z</system:String>
             <system:String x:Key="RotateHandle">M -1.26 -0.455 Q 0 0.175 1.26 -0.455 L 1.12 -0.735 L 2.1 -0.7 L 1.54 0.105 L 1.4 -0.175 Q 0 0.525 -1.4 -0.175 L -1.54 0.105 L -2.1 -0.7 L -1.12 -0.735 Z</system:String>
-            <SolidColorBrush x:Key="HandleBrush" Color="{DynamicResource GlyphColor}"/>
-            <SolidColorBrush x:Key="SelectedHandleBrush" Color="{DynamicResource ThemeAccent2Color}"/>
-            <SolidColorBrush x:Key="HandleBackgroundBrush" Color="{DynamicResource GlyphBackground}"/>
+            <resources:VectorPathResource x:Key="ShearHandle"
+                                          StrokeWidth="0.5"
+                                          StrokeColor="Black"
+                                          FillColor="White"
+                                          StrokeLineCap="Butt"
+                                          StrokeLineJoin="Bevel">
+                <resources:VectorPathResource.SvgPath>
+                    M17.586,6L15.293,3.707L16.707,2.293L21.414,7L16.707,11.707L15.293,10.293L17.586,8L4,8L4,6L17.586,6ZM6.414,16L20,16L20,18L6.414,18L8.707,20.293L7.293,21.707L2.586,17L7.293,12.293L8.707,13.707L6.414,16Z
+                </resources:VectorPathResource.SvgPath>
+            </resources:VectorPathResource>
+            <SolidColorBrush x:Key="HandleBrush" Color="{DynamicResource GlyphColor}" />
+            <SolidColorBrush x:Key="SelectedHandleBrush" Color="{DynamicResource ThemeAccent2Color}" />
+            <SolidColorBrush x:Key="HandleBackgroundBrush" Color="{DynamicResource GlyphBackground}" />
 
 
             <system:Double x:Key="HandleSize">20</system:Double>
             <system:Double x:Key="HandleSize">20</system:Double>
             <system:Double x:Key="AnchorHandleSize">10</system:Double>
             <system:Double x:Key="AnchorHandleSize">10</system:Double>
@@ -70,4 +81,4 @@
         </ResourceDictionary>
         </ResourceDictionary>
     </Styles.Resources>
     </Styles.Resources>
 
 
-</Styles>
+</Styles>

+ 12 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -233,6 +233,8 @@ internal partial class DocumentViewModel
             };
             };
             
             
             path.FillRule.Unit = new SvgEnumUnit<SvgFillRule>(fillRule);
             path.FillRule.Unit = new SvgEnumUnit<SvgFillRule>(fillRule);
+            path.StrokeLineJoin.Unit = new SvgEnumUnit<SvgStrokeLineJoin>(ToSvgLineJoin(data.StrokeLineJoin));
+            path.StrokeLineCap.Unit = new SvgEnumUnit<SvgStrokeLineCap>((SvgStrokeLineCap)data.StrokeLineCap);
         }
         }
 
 
         return path;
         return path;
@@ -513,4 +515,14 @@ internal partial class DocumentViewModel
             NodeId = idMap[rasterKeyFrame.NodeId], KeyFrameId = keyFrameIds[rasterKeyFrame.Id],
             NodeId = idMap[rasterKeyFrame.NodeId], KeyFrameId = keyFrameIds[rasterKeyFrame.Id],
         });
         });
     }
     }
+    
+    private static SvgStrokeLineJoin ToSvgLineJoin(StrokeJoin strokeLineJoin)
+    {
+        return strokeLineJoin switch
+        {
+            StrokeJoin.Bevel => SvgStrokeLineJoin.Bevel,
+            StrokeJoin.Round => SvgStrokeLineJoin.Round,
+            _ => SvgStrokeLineJoin.Miter
+        };
+    }
 }
 }

+ 55 - 17
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -19,10 +19,11 @@ namespace PixiEditor.ViewModels.Document.TransformOverlays;
 internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 {
 {
     private DocumentViewModel document;
     private DocumentViewModel document;
-    
+
     private TransformOverlayUndoStack<(ShapeCorners, TransformState)>? undoStack = null;
     private TransformOverlayUndoStack<(ShapeCorners, TransformState)>? undoStack = null;
 
 
     private TransformState internalState;
     private TransformState internalState;
+
     public TransformState InternalState
     public TransformState InternalState
     {
     {
         get => internalState;
         get => internalState;
@@ -30,6 +31,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private TransformCornerFreedom cornerFreedom;
     private TransformCornerFreedom cornerFreedom;
+
     public TransformCornerFreedom CornerFreedom
     public TransformCornerFreedom CornerFreedom
     {
     {
         get => cornerFreedom;
         get => cornerFreedom;
@@ -37,13 +39,23 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private TransformSideFreedom sideFreedom;
     private TransformSideFreedom sideFreedom;
+
     public TransformSideFreedom SideFreedom
     public TransformSideFreedom SideFreedom
     {
     {
         get => sideFreedom;
         get => sideFreedom;
         set => SetProperty(ref sideFreedom, value);
         set => SetProperty(ref sideFreedom, value);
     }
     }
 
 
+    private bool scaleFromCenter;
+
+    public bool ScaleFromCenter
+    {
+        get => scaleFromCenter;
+        set => SetProperty(ref scaleFromCenter, value);
+    }
+
     private bool lockRotation;
     private bool lockRotation;
+
     public bool LockRotation
     public bool LockRotation
     {
     {
         get => lockRotation;
         get => lockRotation;
@@ -51,6 +63,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private bool snapToAngles;
     private bool snapToAngles;
+
     public bool SnapToAngles
     public bool SnapToAngles
     {
     {
         get => snapToAngles;
         get => snapToAngles;
@@ -58,6 +71,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private bool transformActive;
     private bool transformActive;
+
     public bool TransformActive
     public bool TransformActive
     {
     {
         get => transformActive;
         get => transformActive;
@@ -70,7 +84,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
 
             if (value)
             if (value)
             {
             {
-                document.ActionDisplays[nameof(DocumentTransformViewModel)] = new LocalizedString($"TRANSFORM_ACTION_DISPLAY_{activeTransformMode.GetDescription()}");
+                document.ActionDisplays[nameof(DocumentTransformViewModel)] =
+                    new LocalizedString($"TRANSFORM_ACTION_DISPLAY_{activeTransformMode.GetDescription()}");
             }
             }
             else
             else
             {
             {
@@ -80,15 +95,24 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private bool showTransformControls;
     private bool showTransformControls;
+
     public bool ShowTransformControls
     public bool ShowTransformControls
     {
     {
         get => showTransformControls;
         get => showTransformControls;
         set => SetProperty(ref showTransformControls, value);
         set => SetProperty(ref showTransformControls, value);
     }
     }
+    
+    private bool canAlignToPixels = true;
+    public bool CanAlignToPixels
+    {
+        get => canAlignToPixels;
+        set => SetProperty(ref canAlignToPixels, value);
+    }
 
 
     public event Action<MouseOnCanvasEventArgs>? PassthroughPointerPressed;
     public event Action<MouseOnCanvasEventArgs>? PassthroughPointerPressed;
 
 
     private bool coverWholeScreen;
     private bool coverWholeScreen;
+
     public bool CoverWholeScreen
     public bool CoverWholeScreen
     {
     {
         get => coverWholeScreen;
         get => coverWholeScreen;
@@ -96,6 +120,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private ShapeCorners corners;
     private ShapeCorners corners;
+
     public ShapeCorners Corners
     public ShapeCorners Corners
     {
     {
         get => corners;
         get => corners;
@@ -112,25 +137,27 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     {
     {
         get => showHandles;
         get => showHandles;
         set => SetProperty(ref showHandles, value);
         set => SetProperty(ref showHandles, value);
-    } 
-    
+    }
+
     private bool isSizeBoxEnabled;
     private bool isSizeBoxEnabled;
 
 
     public bool IsSizeBoxEnabled
     public bool IsSizeBoxEnabled
     {
     {
         get => isSizeBoxEnabled;
         get => isSizeBoxEnabled;
         set => SetProperty(ref isSizeBoxEnabled, value);
         set => SetProperty(ref isSizeBoxEnabled, value);
-    } 
+    }
 
 
     private bool enableSnapping = true;
     private bool enableSnapping = true;
+
     public bool EnableSnapping
     public bool EnableSnapping
     {
     {
         get => enableSnapping;
         get => enableSnapping;
         set => SetProperty(ref enableSnapping, value);
         set => SetProperty(ref enableSnapping, value);
     }
     }
-    
-    
+
+
     private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
     private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
+
     public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
     public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
     {
     {
         get => requestedCornersExecutor;
         get => requestedCornersExecutor;
@@ -138,18 +165,21 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
     }
 
 
     private ICommand? actionCompletedCommand = null;
     private ICommand? actionCompletedCommand = null;
+
     public ICommand? ActionCompletedCommand
     public ICommand? ActionCompletedCommand
     {
     {
         get => actionCompletedCommand;
         get => actionCompletedCommand;
         set => SetProperty(ref actionCompletedCommand, value);
         set => SetProperty(ref actionCompletedCommand, value);
     }
     }
 
 
-    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand; 
+    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand;
+
     public RelayCommand<MouseOnCanvasEventArgs> PassThroughPointerPressedCommand
     public RelayCommand<MouseOnCanvasEventArgs> PassThroughPointerPressedCommand
     {
     {
         get
         get
         {
         {
-            return passThroughPointerPressedCommand ??= new RelayCommand<MouseOnCanvasEventArgs>(x => PassthroughPointerPressed?.Invoke(x));
+            return passThroughPointerPressedCommand ??=
+                new RelayCommand<MouseOnCanvasEventArgs>(x => PassthroughPointerPressed?.Invoke(x));
         }
         }
     }
     }
 
 
@@ -166,7 +196,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 return;
                 return;
 
 
             var lastState = undoStack.PeekCurrent();
             var lastState = undoStack.PeekCurrent();
-            if (lastState is not null && lastState.Value.Item1.AlmostEquals(Corners) && lastState.Value.Item2.AlmostEquals(InternalState))
+            if (lastState is not null && lastState.Value.Item1.AlmostEquals(Corners) &&
+                lastState.Value.Item2.AlmostEquals(InternalState))
                 return;
                 return;
 
 
             undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
             undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
@@ -208,7 +239,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         return true;
         return true;
     }
     }
 
 
-    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0; 
+    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;
     public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0;
     public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0;
 
 
     public void HideTransform()
     public void HideTransform()
@@ -221,7 +252,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         ShowTransformControls = false;
         ShowTransformControls = false;
     }
     }
 
 
-    public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos, bool showApplyButton)
+    public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos,
+        bool showApplyButton)
     {
     {
         if (undoStack is not null || initPos.IsPartiallyDegenerate)
         if (undoStack is not null || initPos.IsPartiallyDegenerate)
             return;
             return;
@@ -234,6 +266,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         CoverWholeScreen = coverWholeScreen;
         CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         TransformActive = true;
         ShowTransformControls = showApplyButton;
         ShowTransformControls = showApplyButton;
+        CanAlignToPixels = true;
 
 
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
         ShowHandles = true;
         ShowHandles = true;
@@ -253,21 +286,24 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             requestedCornerFreedom = TransformCornerFreedom.ScaleProportionally;
             requestedCornerFreedom = TransformCornerFreedom.ScaleProportionally;
             requestedSideFreedom = TransformSideFreedom.ScaleProportionally;
             requestedSideFreedom = TransformSideFreedom.ScaleProportionally;
         }
         }
-        else if (isCtrlDown)
+        else if (isAltDown)
         {
         {
             requestedCornerFreedom = TransformCornerFreedom.Free;
             requestedCornerFreedom = TransformCornerFreedom.Free;
             requestedSideFreedom = TransformSideFreedom.Free;
             requestedSideFreedom = TransformSideFreedom.Free;
         }
         }
-        else if (isAltDown)
+        /*else if (isAltDown)
         {
         {
+        TODO: Add shear to the transform overlay
             requestedSideFreedom = TransformSideFreedom.Shear;
             requestedSideFreedom = TransformSideFreedom.Shear;
-        }
+        }*/
         else
         else
         {
         {
             requestedCornerFreedom = TransformCornerFreedom.Scale;
             requestedCornerFreedom = TransformCornerFreedom.Scale;
             requestedSideFreedom = TransformSideFreedom.Stretch;
             requestedSideFreedom = TransformSideFreedom.Stretch;
         }
         }
 
 
+        ScaleFromCenter = isCtrlDown;
+
         switch (activeTransformMode)
         switch (activeTransformMode)
         {
         {
             case DocumentTransformMode.Scale_Rotate_Shear_Perspective:
             case DocumentTransformMode.Scale_Rotate_Shear_Perspective:
@@ -290,6 +326,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 break;
                 break;
         }
         }
     }
     }
-    
-    public override string ToString() => !TransformActive ? "Not active" : $"Transform Mode: {activeTransformMode}; Corner Freedom: {CornerFreedom}; Side Freedom: {SideFreedom}";
+
+    public override string ToString() => !TransformActive
+        ? "Not active"
+        : $"Transform Mode: {activeTransformMode}; Corner Freedom: {CornerFreedom}; Side Freedom: {SideFreedom}";
 }
 }

+ 16 - 4
src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayViewModel.cs

@@ -2,6 +2,8 @@
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using CommunityToolkit.Mvvm.Input;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 
 
@@ -44,7 +46,13 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
     public bool HasUndo => undoStack.UndoCount > 0;
     public bool HasUndo => undoStack.UndoCount > 0;
     public bool HasRedo => undoStack.RedoCount > 0;
     public bool HasRedo => undoStack.RedoCount > 0;
 
 
-    public RelayCommand<VectorPath> AddToUndoCommand { get; }
+    private RelayCommand<VectorPath> addToUndoCommand;
+
+    public RelayCommand<VectorPath> AddToUndoCommand
+    {
+        get => addToUndoCommand;
+        set => SetProperty(ref addToUndoCommand, value);
+    }
     
     
     private bool showApplyButton;
     private bool showApplyButton;
 
 
@@ -55,17 +63,20 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
     }
     }
 
 
     private bool suppressUndo = false;
     private bool suppressUndo = false;
+    private RelayCommand<VectorPath> embeddedAddToUndo;
 
 
     public PathOverlayViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
     public PathOverlayViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
     {
     {
         this.documentViewModel = documentViewModel;
         this.documentViewModel = documentViewModel;
         this.internals = internals;
         this.internals = internals;
 
 
-        AddToUndoCommand = new RelayCommand<VectorPath>(AddToUndo);
+        embeddedAddToUndo = new RelayCommand<VectorPath>(AddToUndo);
+
+        AddToUndoCommand = embeddedAddToUndo;
         undoStack = new PathOverlayUndoStack<VectorPath>();
         undoStack = new PathOverlayUndoStack<VectorPath>();
     }
     }
 
 
-    public void Show(VectorPath newPath, bool showApplyButton)
+    public void Show(VectorPath newPath, bool showApplyButton, Action<VectorPath>? customAddToUndo = null)
     {
     {
         if (IsActive)
         if (IsActive)
         {
         {
@@ -78,6 +89,7 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
         Path = newPath;
         Path = newPath;
         IsActive = true;
         IsActive = true;
         ShowApplyButton = showApplyButton;
         ShowApplyButton = showApplyButton;
+        AddToUndoCommand = customAddToUndo != null ? new RelayCommand<VectorPath>(customAddToUndo) : embeddedAddToUndo;
     }
     }
 
 
     public void Hide()
     public void Hide()
@@ -113,6 +125,6 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
 
 
     private void PathDataChanged(VectorPath path)
     private void PathDataChanged(VectorPath path)
     {
     {
-        AddToUndo(path);
+        AddToUndoCommand.Execute(path);
     }
     }
 }
 }

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

@@ -417,6 +417,16 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
     {
         ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
         ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
     }
     }
+    
+    public void OnPostUndoInlet()
+    {
+        ActiveTool?.OnPostUndo();
+    }
+    
+    public void OnPostRedoInlet()
+    {
+        ActiveTool?.OnPostRedo();
+    }
 
 
     private void ToolbarSettingChanged(string settingName, object value)
     private void ToolbarSettingChanged(string settingName, object value)
     {
     {

+ 8 - 0
src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs

@@ -25,6 +25,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedRedo))
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedRedo))
             return;
             return;
         doc.Operations.Redo();
         doc.Operations.Redo();
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            Owner.ToolsSubViewModel.OnPostRedoInlet();
+        });
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -38,6 +42,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
             return;
             return;
         doc.Operations.Undo();
         doc.Operations.Undo();
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            Owner.ToolsSubViewModel.OnPostUndoInlet();
+        });
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 1 - 0
src/PixiEditor/ViewModels/Tools/ShapeTool.cs

@@ -14,6 +14,7 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
 
 
     public override bool IsErasable => true;
     public override bool IsErasable => true;
     public bool DrawEven { get; protected set; }
     public bool DrawEven { get; protected set; }
+    public bool DrawFromCenter { get; protected set; }
 
 
     public ShapeTool()
     public ShapeTool()
     {
     {

+ 3 - 0
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -158,6 +158,9 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public virtual void OnDeselecting(bool transient)
     public virtual void OnDeselecting(bool transient)
     {
     {
     }
     }
+    
+    public virtual void OnPostUndo() { }
+    public virtual void OnPostRedo() { }
 
 
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings)
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings)
     {
     {

+ 2 - 0
src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs

@@ -30,6 +30,8 @@ internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+        
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             ActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT";

+ 5 - 4
src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs

@@ -29,8 +29,7 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override string DefaultIcon => PixiPerfectIcons.LowResLine;
     public override string DefaultIcon => PixiPerfectIcons.LowResLine;
 
 
-    [Settings.Inherited] 
-    public double ToolSize => GetValue<double>();
+    [Settings.Inherited] public double ToolSize => GetValue<double>();
 
 
     public bool Snap { get; private set; }
     public bool Snap { get; private set; }
 
 
@@ -38,6 +37,8 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
@@ -58,8 +59,8 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
     public override void OnSelected(bool restoring)
     public override void OnSelected(bool restoring)
     {
     {
         if (restoring) return;
         if (restoring) return;
-        
+
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         document.Tools.UseRasterLineTool();
         document.Tools.UseRasterLineTool();
-    } 
+    }
 }
 }

+ 5 - 2
src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs

@@ -14,6 +14,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHandler
 internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHandler
 {
 {
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
+
     public RasterRectangleToolViewModel()
     public RasterRectangleToolViewModel()
     {
     {
         ActionDisplay = defaultActionDisplay;
         ActionDisplay = defaultActionDisplay;
@@ -31,6 +32,8 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             DrawEven = true;
             DrawEven = true;
@@ -50,8 +53,8 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
 
     public override void OnSelected(bool restoring)
     public override void OnSelected(bool restoring)
     {
     {
-        if(restoring) return;
-        
+        if (restoring) return;
+
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterRectangleTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterRectangleTool();
     }
     }
 }
 }

+ 5 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -15,7 +15,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.C)]
 [Command.Tool(Key = Key.C)]
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 {
 {
-    public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME"; 
+    public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME";
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
     public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
 
 
@@ -43,6 +43,8 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             DrawEven = true;
             DrawEven = true;
@@ -67,6 +69,8 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
             ShapeCorners corners = vectorLayer.TransformationCorners;
             ShapeCorners corners = vectorLayer.TransformationCorners;
             document.TransformViewModel.ShowTransform(
             document.TransformViewModel.ShowTransform(
                 DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
                 DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
+
+            document.TransformViewModel.CanAlignToPixels = false;
         }
         }
 
 
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();

+ 7 - 5
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -23,7 +23,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
 
     public override bool IsErasable => false;
     public override bool IsErasable => false;
-    
+
     public VectorLineToolViewModel()
     public VectorLineToolViewModel()
     {
     {
         ActionDisplay = defaultActionDisplay;
         ActionDisplay = defaultActionDisplay;
@@ -37,8 +37,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override Type[]? SupportedLayerTypes { get; } = [];
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
 
-    [Settings.Inherited] 
-    public double ToolSize => GetValue<double>();
+    [Settings.Inherited] public double ToolSize => GetValue<double>();
 
 
     public bool Snap { get; private set; }
     public bool Snap { get; private set; }
 
 
@@ -46,6 +45,8 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
@@ -66,12 +67,13 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     public override void OnSelected(bool restoring)
     public override void OnSelected(bool restoring)
     {
     {
         if (restoring) return;
         if (restoring) return;
-        
+
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var layer = document.SelectedStructureMember;
         var layer = document.SelectedStructureMember;
         if (layer is IVectorLayerHandler vectorLayer)
         if (layer is IVectorLayerHandler vectorLayer)
         {
         {
-            if (vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyLineData lineVectorData)
+            if (vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyLineData
+                lineVectorData)
             {
             {
                 document.LineToolOverlayViewModel.Show(lineVectorData.TransformedStart, lineVectorData.TransformedEnd,
                 document.LineToolOverlayViewModel.Show(lineVectorData.TransformedStart, lineVectorData.TransformedEnd,
                     false);
                     false);

+ 18 - 2
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -97,7 +97,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
             ActionDisplay = actionDisplayDefault;
             ActionDisplay = actionDisplayDefault;
         }
         }
     }
     }
-
+    
     public override void OnSelected(bool restoring)
     public override void OnSelected(bool restoring)
     {
     {
         if (restoring) return;
         if (restoring) return;
@@ -105,7 +105,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         isActivated = true;
         isActivated = true;
     }
     }
-
+    
     public override void OnDeselecting(bool transient)
     public override void OnDeselecting(bool transient)
     {
     {
         if (!transient)
         if (!transient)
@@ -115,6 +115,22 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         }
         }
     }
     }
 
 
+    public override void OnPostUndo()
+    {
+        if (isActivated)
+        {
+            OnSelected(false);
+        }
+    }
+
+    public override void OnPostRedo()
+    {
+        if (isActivated)
+        {
+            OnSelected(false);
+        }
+    }
+
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     {
     {
         OnDeselecting(false);
         OnDeselecting(false);

+ 7 - 2
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -16,7 +16,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHandler
 internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHandler
 {
 {
     public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";
     public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";
-    
+
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override bool IsErasable => false;
     public override bool IsErasable => false;
@@ -36,6 +36,8 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
 
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             DrawEven = true;
             DrawEven = true;
@@ -63,8 +65,11 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
             vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyRectangleData)
             vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyRectangleData)
         {
         {
             ShapeCorners corners = vectorLayer.TransformationCorners;
             ShapeCorners corners = vectorLayer.TransformationCorners;
-            ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel.ShowTransform(
+            var transformVm = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel;
+            transformVm.ShowTransform(
                 DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
                 DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
+            
+            transformVm.CanAlignToPixels = false;
         }
         }
 
 
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();

+ 12 - 0
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -326,6 +326,16 @@ internal class ViewportOverlays
         };
         };
 
 
         Binding zoomboxAngleBinding = new() { Source = Viewport, Path = "Zoombox.Angle", Mode = BindingMode.OneWay };
         Binding zoomboxAngleBinding = new() { Source = Viewport, Path = "Zoombox.Angle", Mode = BindingMode.OneWay };
+        
+        Binding scaleFromCenterBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.ScaleFromCenter", Mode = BindingMode.OneWay
+        };
+        
+        Binding canAlignToPixelsBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.CanAlignToPixels", Mode = BindingMode.OneWay
+        };
 
 
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -342,6 +352,8 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.ZoomboxAngleProperty, zoomboxAngleBinding);
         transformOverlay.Bind(TransformOverlay.ZoomboxAngleProperty, zoomboxAngleBinding);
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
+        transformOverlay.Bind(TransformOverlay.ScaleFromCenterProperty, scaleFromCenterBinding);
+        transformOverlay.Bind(TransformOverlay.CanAlignToPixelsProperty, canAlignToPixelsBinding);
     }
     }
     
     
     private void BindVectorPathOverlay()
     private void BindVectorPathOverlay()

+ 10 - 3
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -9,7 +9,9 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Resources;
 using PixiEditor.Views.Overlays.TransformOverlay;
 using PixiEditor.Views.Overlays.TransformOverlay;
 using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 using Path = Avalonia.Controls.Shapes.Path;
 using Path = Avalonia.Controls.Shapes.Path;
@@ -65,17 +67,22 @@ public abstract class Handle : IHandle
         return ResourceLoader.GetResource<T>(key);
         return ResourceLoader.GetResource<T>(key);
     }
     }
 
 
-    public static VectorPath GetHandleGeometry(string handleName)
+    public static PathVectorData GetHandleGeometry(string handleName)
     {
     {
         if (Application.Current.Styles.TryGetResource(handleName, null, out object shape))
         if (Application.Current.Styles.TryGetResource(handleName, null, out object shape))
         {
         {
             if (shape is string path)
             if (shape is string path)
             {
             {
-                return VectorPath.FromSvgPath(path);
+                return new PathVectorData(VectorPath.FromSvgPath(path));
+            }
+
+            if (shape is VectorPathResource resource)
+            {
+                return resource.ToVectorPathData();
             }
             }
         }
         }
 
 
-        return VectorPath.FromSvgPath("M 0 0 L 1 0 M 0 0 L 0 1");
+        return new PathVectorData(VectorPath.FromSvgPath("M 0 0 L 1 0 M 0 0 L 0 1"));
     }
     }
 
 
     protected static Paint? GetPaint(string key, PaintStyle style = PaintStyle.Fill)
     protected static Paint? GetPaint(string key, PaintStyle style = PaintStyle.Fill)

+ 1 - 1
src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -86,7 +86,7 @@ internal class SymmetryOverlay : Overlay
     public static readonly StyledProperty<VecI> SizeProperty = AvaloniaProperty.Register<SymmetryOverlay, VecI>(nameof(Size));
     public static readonly StyledProperty<VecI> SizeProperty = AvaloniaProperty.Register<SymmetryOverlay, VecI>(nameof(Size));
 
 
     private const double HandleSize = 12;
     private const double HandleSize = 12;
-    private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle");
+    private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle").Path;
 
 
     private const float DashWidth = 10.0f;
     private const float DashWidth = 10.0f;
     const int RulerOffset = -35;
     const int RulerOffset = -35;

+ 134 - 26
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -1,27 +1,19 @@
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.Linq;
-using System.Windows.Input;
+using System.Windows.Input;
 using Avalonia;
 using Avalonia;
 using Avalonia.Input;
 using Avalonia.Input;
-using Avalonia.Media;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
-using PixiEditor.Helpers;
-using PixiEditor.Helpers.Extensions;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Views.Overlays.Drawables;
 using PixiEditor.Views.Overlays.Drawables;
 using PixiEditor.Views.Overlays.Handles;
 using PixiEditor.Views.Overlays.Handles;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
-using Point = Avalonia.Point;
 
 
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 #nullable enable
 #nullable enable
@@ -160,6 +152,25 @@ internal class TransformOverlay : Overlay
         set => SetValue(IsSizeBoxEnabledProperty, value);
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
     }
 
 
+    public static readonly StyledProperty<bool> ScaleFromCenterProperty =
+        AvaloniaProperty.Register<TransformOverlay, bool>(
+            nameof(ScaleFromCenter));
+
+    public bool ScaleFromCenter
+    {
+        get => GetValue(ScaleFromCenterProperty);
+        set => SetValue(ScaleFromCenterProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> CanAlignToPixelsProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
+        nameof(CanAlignToPixels), defaultValue: true);
+
+    public bool CanAlignToPixels
+    {
+        get => GetValue(CanAlignToPixelsProperty);
+        set => SetValue(CanAlignToPixelsProperty, value);
+    }
+
     static TransformOverlay()
     static TransformOverlay()
     {
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -181,6 +192,7 @@ internal class TransformOverlay : Overlay
     private double propAngle1OnStartRotate = 0;
     private double propAngle1OnStartRotate = 0;
     private double propAngle2OnStartRotate = 0;
     private double propAngle2OnStartRotate = 0;
 
 
+    private TransformSideFreedom beforeShearSideFreedom;
     private Anchor? capturedAnchor;
     private Anchor? capturedAnchor;
     private ShapeCorners cornersOnStartAnchorDrag;
     private ShapeCorners cornersOnStartAnchorDrag;
     private VecD mousePosOnStartAnchorDrag;
     private VecD mousePosOnStartAnchorDrag;
@@ -254,13 +266,17 @@ internal class TransformOverlay : Overlay
     private List<Handle> snapPoints = new();
     private List<Handle> snapPoints = new();
     private Handle? snapHandleOfOrigin;
     private Handle? snapHandleOfOrigin;
 
 
-    private VectorPath rotateCursorGeometry = Handle.GetHandleGeometry("RotateHandle");
+    private VectorPath rotateCursorGeometry = Handle.GetHandleGeometry("RotateHandle").Path;
+    private PathVectorData shearCursorGeometry = Handle.GetHandleGeometry("ShearHandle");
     private bool rotationCursorActive = false;
     private bool rotationCursorActive = false;
+    private bool shearCursorActive = false;
+    private Anchor? hoveredAnchor;
 
 
     private VecD lastPointerPos;
     private VecD lastPointerPos;
     private InfoBox infoBox;
     private InfoBox infoBox;
     private VecD lastSize;
     private VecD lastSize;
     private bool actuallyMoved = false;
     private bool actuallyMoved = false;
+    private bool isShearing = false;
 
 
     public TransformOverlay()
     public TransformOverlay()
     {
     {
@@ -328,7 +344,9 @@ internal class TransformOverlay : Overlay
         DrawOverlay(drawingContext, canvasBounds.Size, Corners, InternalState.Origin, (float)ZoomScale);
         DrawOverlay(drawingContext, canvasBounds.Size, Corners, InternalState.Origin, (float)ZoomScale);
 
 
         if (capturedAnchor is null)
         if (capturedAnchor is null)
-            UpdateRotationCursor(lastPointerPos);
+        {
+            UpdateSpecialCursors(lastPointerPos);
+        }
     }
     }
 
 
     private void DrawOverlay
     private void DrawOverlay
@@ -414,13 +432,37 @@ internal class TransformOverlay : Overlay
             double angle = (lastPointerPos - InternalState.Origin).Angle * 180 / Math.PI - 90;
             double angle = (lastPointerPos - InternalState.Origin).Angle * 180 / Math.PI - 90;
             matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees((float)angle, (float)lastPointerPos.X,
             matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees((float)angle, (float)lastPointerPos.X,
                 (float)lastPointerPos.Y));
                 (float)lastPointerPos.Y));
-            matrix = matrix.PostConcat(Matrix3X3.CreateScale(8f / (float)ZoomScale, 8 / (float)ZoomScale,
+            matrix = matrix.PostConcat(Matrix3X3.CreateScale(7f / (float)ZoomScale, 7f / (float)ZoomScale,
                 (float)lastPointerPos.X, (float)lastPointerPos.Y));
                 (float)lastPointerPos.X, (float)lastPointerPos.Y));
             context.SetMatrix(context.TotalMatrix.Concat(matrix));
             context.SetMatrix(context.TotalMatrix.Concat(matrix));
 
 
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
         }
         }
+        
+        context.RestoreToCount(saved);
+        
+        saved = context.Save();
+
+        if (ShowHandles && shearCursorActive)
+        {
+            var matrix = Matrix3X3.CreateTranslation((float)lastPointerPos.X, (float)lastPointerPos.Y);
+            
+            matrix = matrix.PostConcat(Matrix3X3.CreateTranslation(
+                (float)-shearCursorGeometry.VisualAABB.Center.X,
+                (float)-shearCursorGeometry.VisualAABB.Center.Y));
+            
+            matrix = matrix.PostConcat(Matrix3X3.CreateScale(
+                20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.X, 20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.Y,
+                (float)lastPointerPos.X, (float)lastPointerPos.Y));
+
+            if(hoveredAnchor is Anchor.Right or Anchor.Left)
+                matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees(90, (float)lastPointerPos.X, (float)lastPointerPos.Y));
+
+            context.SetMatrix(context.TotalMatrix.Concat(matrix));
+
+            shearCursorGeometry.RasterizeTransformed(context);
+        }
 
 
         context.RestoreToCount(saved);
         context.RestoreToCount(saved);
 
 
@@ -456,17 +498,23 @@ internal class TransformOverlay : Overlay
 
 
     private void OnAnchorHandlePressed(Handle source, OverlayPointerArgs args)
     private void OnAnchorHandlePressed(Handle source, OverlayPointerArgs args)
     {
     {
-        capturedAnchor = anchorMap[source];
-        cornersOnStartAnchorDrag = Corners;
-        originOnStartAnchorDrag = InternalState.Origin;
-        mousePosOnStartAnchorDrag = lastPointerPos;
-        IsSizeBoxEnabled = true;
+        CaptureAnchor(anchorMap[source]);
 
 
         if (source == originHandle)
         if (source == originHandle)
         {
         {
             IsSizeBoxEnabled = false;
             IsSizeBoxEnabled = false;
             snapHandleOfOrigin = null;
             snapHandleOfOrigin = null;
         }
         }
+
+        IsSizeBoxEnabled = true;
+    }
+
+    private void CaptureAnchor(Anchor anchor)
+    {
+        capturedAnchor = anchor;
+        cornersOnStartAnchorDrag = Corners;
+        originOnStartAnchorDrag = InternalState.Origin;
+        mousePosOnStartAnchorDrag = lastPointerPos;
     }
     }
 
 
     private void OnMoveHandlePressed(Handle source, OverlayPointerArgs args)
     private void OnMoveHandlePressed(Handle source, OverlayPointerArgs args)
@@ -477,6 +525,7 @@ internal class TransformOverlay : Overlay
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     {
     {
         rotationCursorActive = false;
         rotationCursorActive = false;
+        shearCursorActive = false;
         Refresh();
         Refresh();
     }
     }
 
 
@@ -487,7 +536,11 @@ internal class TransformOverlay : Overlay
 
 
         if (Handles.Any(x => x.IsWithinHandle(x.Position, args.Point, ZoomScale))) return;
         if (Handles.Any(x => x.IsWithinHandle(x.Position, args.Point, ZoomScale))) return;
 
 
-        if (!CanRotate(args.Point))
+        if (CanShear(args.Point, out var side))
+        {
+            StartShearing(args, side);
+        }
+        else if (!CanRotate(args.Point))
         {
         {
             StartMoving(args.Point);
             StartMoving(args.Point);
         }
         }
@@ -540,10 +593,11 @@ internal class TransformOverlay : Overlay
         if (capturedAnchor is not null)
         if (capturedAnchor is not null)
         {
         {
             HandleCapturedAnchorMovement(e.Point);
             HandleCapturedAnchorMovement(e.Point);
+            lastPointerPos = e.Point;
             return;
             return;
         }
         }
 
 
-        if (UpdateRotationCursor(e.Point))
+        if (UpdateSpecialCursors(e.Point))
         {
         {
             finalCursor = new Cursor(StandardCursorType.None);
             finalCursor = new Cursor(StandardCursorType.None);
         }
         }
@@ -592,11 +646,22 @@ internal class TransformOverlay : Overlay
             e.Pointer.Capture(null);
             e.Pointer.Capture(null);
             Cursor = new Cursor(StandardCursorType.Arrow);
             Cursor = new Cursor(StandardCursorType.Arrow);
             var pos = e.Point;
             var pos = e.Point;
-            UpdateRotationCursor(pos);
+            UpdateSpecialCursors(pos);
+        }
+
+        if (isShearing)
+        {
+            isShearing = false;
+            SideFreedom = beforeShearSideFreedom;
+            e.Pointer.Capture(null);
+            Cursor = new Cursor(StandardCursorType.Arrow);
+            var pos = e.Point;
+            UpdateSpecialCursors(pos);
         }
         }
 
 
         StopMoving();
         StopMoving();
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
+        capturedAnchor = null;
     }
     }
 
 
     public override bool TestHit(VecD point)
     public override bool TestHit(VecD point)
@@ -622,6 +687,24 @@ internal class TransformOverlay : Overlay
         StopMoving();
         StopMoving();
     }
     }
 
 
+    private bool CanShear(VecD mousePos, out Anchor side)
+    {
+        double distance = 20 / ZoomScale;
+        var sides = new[] { Anchor.Top, Anchor.Bottom, Anchor.Left, Anchor.Right };
+
+        bool isOverHandle = Handles.Any(x => x.IsWithinHandle(x.Position, mousePos, ZoomScale));
+        if (isOverHandle)
+        {
+            side = default;
+            return false;
+        }
+
+        side = sides.FirstOrDefault(side => VecD.Distance(TransformHelper.GetAnchorPosition(Corners, side), mousePos)
+                                            < distance);
+
+        return side != default;
+    }
+
     private void StopMoving()
     private void StopMoving()
     {
     {
         isMoving = false;
         isMoving = false;
@@ -643,11 +726,20 @@ internal class TransformOverlay : Overlay
         actuallyMoved = false;
         actuallyMoved = false;
     }
     }
 
 
+    private void StartShearing(OverlayPointerArgs args, Anchor side)
+    {
+        isShearing = true;
+        beforeShearSideFreedom = SideFreedom;
+        SideFreedom = TransformSideFreedom.Shear;
+        CaptureAnchor(side);
+        lastPointerPos = args.Point;
+    }
+
     private void HandleTransform(VecD pos)
     private void HandleTransform(VecD pos)
     {
     {
         VecD delta = pos - mousePosOnStartMove;
         VecD delta = pos - mousePosOnStartMove;
 
 
-        if (Corners.IsAlignedToPixels)
+        if (Corners.IsAlignedToPixels && CanAlignToPixels)
             delta = delta.Round();
             delta = delta.Round();
 
 
         ShapeCorners rawCorners = new ShapeCorners()
         ShapeCorners rawCorners = new ShapeCorners()
@@ -724,15 +816,19 @@ internal class TransformOverlay : Overlay
                Handles.All(x => !x.IsWithinHandle(x.Position, mousePos, ZoomScale)) && TestHit(mousePos);
                Handles.All(x => !x.IsWithinHandle(x.Position, mousePos, ZoomScale)) && TestHit(mousePos);
     }
     }
 
 
-    private bool UpdateRotationCursor(VecD mousePos)
+    private bool UpdateSpecialCursors(VecD mousePos)
     {
     {
-        if ((!CanRotate(mousePos) && !isRotating) || LockRotation)
+        bool canShear = CanShear(mousePos, out Anchor anchor);
+        if ((!canShear && !CanRotate(mousePos) && !isRotating) || LockRotation)
         {
         {
             rotationCursorActive = false;
             rotationCursorActive = false;
+            shearCursorActive = false;
             return false;
             return false;
         }
         }
 
 
-        rotationCursorActive = true;
+        rotationCursorActive = !canShear;
+        shearCursorActive = canShear;
+        hoveredAnchor = anchor;
         return true;
         return true;
     }
     }
 
 
@@ -754,7 +850,9 @@ internal class TransformOverlay : Overlay
 
 
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController,
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
+                ScaleFromCenter,
+                SnappingController,
                 out string snapX, out string snapY);
                 out string snapX, out string snapY);
 
 
             HighlightSnappedAxis(snapX, snapY);
             HighlightSnappedAxis(snapX, snapY);
@@ -836,6 +934,7 @@ internal class TransformOverlay : Overlay
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta,
+                ScaleFromCenter,
                 SnappingController, out string snapX, out string snapY);
                 SnappingController, out string snapX, out string snapY);
 
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
@@ -1092,6 +1191,9 @@ internal class TransformOverlay : Overlay
             ActionCompleted.Execute(null);
             ActionCompleted.Execute(null);
 
 
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
+
+        SnappingController.HighlightedXAxis = string.Empty;
+        SnappingController.HighlightedYAxis = string.Empty;
     }
     }
 
 
     private Handle? GetSnapHandleOfOrigin()
     private Handle? GetSnapHandleOfOrigin()
@@ -1114,6 +1216,12 @@ internal class TransformOverlay : Overlay
     {
     {
         isMoving = false;
         isMoving = false;
         isRotating = false;
         isRotating = false;
+        if (isShearing)
+        {
+            SideFreedom = beforeShearSideFreedom;
+        }
+
+        isShearing = false;
         Corners = corners;
         Corners = corners;
         InternalState = new()
         InternalState = new()
         {
         {

+ 101 - 6
src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using Avalonia;
+using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
@@ -11,7 +12,7 @@ internal static class TransformUpdateHelper
 
 
     public static ShapeCorners? UpdateShapeFromCorner
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
-        VecD desiredPos,
+        VecD desiredPos, bool scaleFromCenter,
         SnappingController? snappingController, out string snapX, out string snapY)
         SnappingController? snappingController, out string snapX, out string snapY)
     {
     {
         if (!TransformHelper.IsCorner(targetCorner))
         if (!TransformHelper.IsCorner(targetCorner))
@@ -71,6 +72,11 @@ internal static class TransformUpdateHelper
             if (double.IsNaN(angle))
             if (double.IsNaN(angle))
                 angle = 0;
                 angle = 0;
 
 
+            if (scaleFromCenter)
+            {
+                return ScaleCornersFromCenter(corners, targetCorner, desiredPos, angle);
+            }
+
             // find positions of neighboring corners relative to the opposite corner, while also undoing the transform rotation
             // find positions of neighboring corners relative to the opposite corner, while also undoing the transform rotation
             VecD targetTrans = (targetPos - oppositePos).Rotate(-angle);
             VecD targetTrans = (targetPos - oppositePos).Rotate(-angle);
             VecD leftNeighTrans = (leftNeighborPos - oppositePos).Rotate(-angle);
             VecD leftNeighTrans = (leftNeighborPos - oppositePos).Rotate(-angle);
@@ -79,6 +85,7 @@ internal static class TransformUpdateHelper
             // find by how much move each corner
             // find by how much move each corner
             VecD delta = (desiredPos - targetPos).Rotate(-angle);
             VecD delta = (desiredPos - targetPos).Rotate(-angle);
             VecD leftNeighDelta, rightNeighDelta;
             VecD leftNeighDelta, rightNeighDelta;
+
             if (corners.IsPartiallyDegenerate)
             if (corners.IsPartiallyDegenerate)
             {
             {
                 // handle cases where we'd need to scale by infinity
                 // handle cases where we'd need to scale by infinity
@@ -138,6 +145,36 @@ internal static class TransformUpdateHelper
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
     }
     }
 
 
+    private static ShapeCorners? ScaleCornersFromCenter(ShapeCorners corners, Anchor targetCorner, VecD desiredPos, 
+        double angle)
+    {
+        // un rotate to properly calculate the scaling
+        // here is a skewing issue, since angle for skewed rects is already non 0
+        // (this is an issue in itself, since when user skews a non-rotated rect, the angle should be 0,
+        // so maybe if we find a way to get "un skewed" angle
+        // we can use it here and there. Idk if it's possible, It's hard to say what should be a "proper" angle for skewed rect,
+        // when you didn't see it getting skewed, so perhaps some tracking for overlay session would be the only solution)
+        desiredPos = desiredPos.Rotate(-angle, corners.RectCenter);
+        corners = corners.AsRotated(-angle, corners.RectCenter);
+
+        VecD targetPos = TransformHelper.GetAnchorPosition(corners, targetCorner);
+
+        VecD currentCenter = corners.RectCenter;
+        VecD targetPosToCenter = (targetPos - currentCenter);
+
+        if (targetPosToCenter.Length < epsilon)
+            return corners;
+
+        VecD desiredPosToCenter = (desiredPos - currentCenter);
+
+        VecD scaling = new(desiredPosToCenter.X / targetPosToCenter.X, desiredPosToCenter.Y / targetPosToCenter.Y);
+        
+        // when rect is skewed and falsely un rotated, this applies scaling in wrong directions
+        corners = corners.AsScaled((float)scaling.X, (float)scaling.Y);
+
+        return corners.AsRotated(angle, corners.RectCenter);
+    }
+
     private static VecD SwapAxes(VecD vec) => new VecD(vec.Y, vec.X);
     private static VecD SwapAxes(VecD vec) => new VecD(vec.Y, vec.X);
 
 
     private static VecD TransferZeros(VecD from, VecD to)
     private static VecD TransferZeros(VecD from, VecD to)
@@ -151,7 +188,8 @@ internal static class TransformUpdateHelper
 
 
     public static ShapeCorners? UpdateShapeFromSide
     public static ShapeCorners? UpdateShapeFromSide
     (Anchor targetSide, TransformSideFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
     (Anchor targetSide, TransformSideFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
-        VecD desiredPos, SnappingController? snappingController, out string snapX, out string snapY)
+        VecD desiredPos, bool scaleFromCenter, SnappingController? snappingController, out string snapX,
+        out string snapY)
     {
     {
         if (!TransformHelper.IsSide(targetSide))
         if (!TransformHelper.IsSide(targetSide))
             throw new ArgumentException($"{targetSide} is not a side");
             throw new ArgumentException($"{targetSide} is not a side");
@@ -171,10 +209,11 @@ internal static class TransformUpdateHelper
 
 
             VecD direction = targetPos - oppositePos;
             VecD direction = targetPos - oppositePos;
             direction = VecD.FromAngleAndLength(direction.Angle, 1 / direction.Length);
             direction = VecD.FromAngleAndLength(direction.Angle, 1 / direction.Length);
-            
+
             if (snappingController is not null)
             if (snappingController is not null)
             {
             {
-                desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                VecD scaleDirection = corners.RectCenter - oppositePos;
+                desiredPos = snappingController.GetSnapPoint(desiredPos, scaleDirection, out snapX, out snapY);
             }
             }
 
 
             double scalingFactor = (desiredPos - oppositePos) * direction;
             double scalingFactor = (desiredPos - oppositePos) * direction;
@@ -183,9 +222,14 @@ internal static class TransformUpdateHelper
 
 
             if (corners.IsRect)
             if (corners.IsRect)
             {
             {
-                var delta = desiredPos - targetPos;
+                var delta = (desiredPos - targetPos);
                 var center = oppositePos.Lerp(desiredPos, 0.5);
                 var center = oppositePos.Lerp(desiredPos, 0.5);
 
 
+                if (scaleFromCenter)
+                {
+                    return ScaleEvenlyToPos(corners, desiredPos, targetPos);
+                }
+
                 var (leftCorn, rightCorn) = TransformHelper.GetCornersOnSide(targetSide);
                 var (leftCorn, rightCorn) = TransformHelper.GetCornersOnSide(targetSide);
                 var (leftOppCorn, _) = TransformHelper.GetNeighboringCorners(leftCorn);
                 var (leftOppCorn, _) = TransformHelper.GetNeighboringCorners(leftCorn);
                 var (_, rightOppCorn) = TransformHelper.GetNeighboringCorners(rightCorn);
                 var (_, rightOppCorn) = TransformHelper.GetNeighboringCorners(rightCorn);
@@ -203,6 +247,7 @@ internal static class TransformUpdateHelper
                     center + VecD.FromAngleAndLength(leftAngle, 1));
                     center + VecD.FromAngleAndLength(leftAngle, 1));
                 var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta,
                 var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta,
                     center, center + VecD.FromAngleAndLength(rightAngle, 1));
                     center, center + VecD.FromAngleAndLength(rightAngle, 1));
+
                 var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
                 var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
                     center + VecD.FromAngleAndLength(rightAngle, 1));
                     center + VecD.FromAngleAndLength(rightAngle, 1));
                 var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
                 var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
@@ -220,6 +265,12 @@ internal static class TransformUpdateHelper
             }
             }
 
 
             fallback:
             fallback:
+
+            if (scaleFromCenter)
+            {
+                return ScaleEvenlyToPos(corners, desiredPos, targetPos);
+            }
+
             corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
             corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
             corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
             corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
             corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
             corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
@@ -249,6 +300,11 @@ internal static class TransformUpdateHelper
             if (freedom == TransformSideFreedom.Shear)
             if (freedom == TransformSideFreedom.Shear)
             {
             {
                 desiredPos = desiredPos.ProjectOntoLine(leftCornerPos, rightCornerPos);
                 desiredPos = desiredPos.ProjectOntoLine(leftCornerPos, rightCornerPos);
+                if (snappingController is not null)
+                {
+                    VecD direction = corners.RectCenter - desiredPos;
+                    desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                }
             }
             }
             else if (freedom == TransformSideFreedom.Stretch)
             else if (freedom == TransformSideFreedom.Stretch)
             {
             {
@@ -257,18 +313,57 @@ internal static class TransformUpdateHelper
                 else
                 else
                     desiredPos = desiredPos.ProjectOntoLine(targetPos,
                     desiredPos = desiredPos.ProjectOntoLine(targetPos,
                         (leftCornerPos - targetPos).Rotate(Math.PI / 2) + targetPos);
                         (leftCornerPos - targetPos).Rotate(Math.PI / 2) + targetPos);
+
+                if (snappingController is not null)
+                {
+                    VecD direction = desiredPos - targetPos;
+                    desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                }
             }
             }
 
 
             var delta = desiredPos - targetPos;
             var delta = desiredPos - targetPos;
+
             var newCorners = TransformHelper.UpdateCorner(corners, leftCorner, leftCornerPos + delta);
             var newCorners = TransformHelper.UpdateCorner(corners, leftCorner, leftCornerPos + delta);
             newCorners = TransformHelper.UpdateCorner(newCorners, rightCorner, rightCornerPos + delta);
             newCorners = TransformHelper.UpdateCorner(newCorners, rightCorner, rightCornerPos + delta);
 
 
+            if (scaleFromCenter)
+            {
+                VecD oppositeDelta = -delta;
+                Anchor leftCornerOpp = TransformHelper.GetOpposite(leftCorner);
+                Anchor rightCornerOpp = TransformHelper.GetOpposite(rightCorner);
+
+                var leftCornerOppPos = TransformHelper.GetAnchorPosition(corners, leftCornerOpp);
+                var rightCornerOppPos = TransformHelper.GetAnchorPosition(corners, rightCornerOpp);
+
+                newCorners = TransformHelper.UpdateCorner(newCorners, leftCornerOpp, leftCornerOppPos + oppositeDelta);
+                newCorners =
+                    TransformHelper.UpdateCorner(newCorners, rightCornerOpp, rightCornerOppPos + oppositeDelta);
+            }
+
             return newCorners.IsLegal ? newCorners : null;
             return newCorners.IsLegal ? newCorners : null;
         }
         }
 
 
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
     }
     }
 
 
+    private static ShapeCorners ScaleEvenlyToPos(ShapeCorners corners, VecD desiredPos, VecD targetPos)
+    {
+        VecD currentCenter = corners.RectCenter;
+        float targetPosToCenter = (float)(targetPos - currentCenter).Length;
+
+        if (targetPosToCenter < epsilon)
+            return corners;
+
+        VecD reflectedDesiredPos = desiredPos.ReflectAcrossLine(currentCenter, targetPos);
+
+        float desiredPosToCenter = (float)(reflectedDesiredPos - currentCenter).Length;
+
+        float scaling = desiredPosToCenter / targetPosToCenter;
+
+        corners = corners.AsScaled(scaling);
+        return corners;
+    }
+
     public static ShapeCorners UpdateShapeFromRotation(ShapeCorners corners, VecD origin, double angle)
     public static ShapeCorners UpdateShapeFromRotation(ShapeCorners corners, VecD origin, double angle)
     {
     {
         corners.TopLeft = corners.TopLeft.Rotate(angle, origin);
         corners.TopLeft = corners.TopLeft.Rotate(angle, origin);