Browse Source

Merge branch 'master' into development

flabbet 8 tháng trước cách đây
mục cha
commit
41fea06e8c
53 tập tin đã thay đổi với 839 bổ sung238 xóa
  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)

+ 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;
 
 public interface IReadOnlyPathData : IReadOnlyShapeVectorData
 {
     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;
     }
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         int saved = 0;
         if (applyTransform)
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         using Paint shapePaint = new Paint();
@@ -56,7 +56,7 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         {
             shapePaint.Color = FillColor;
             shapePaint.Style = PaintStyle.Fill;
-            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+            canvas.DrawOval(Center, Radius, shapePaint);
         }
 
         if (StrokeWidth > 0)
@@ -64,12 +64,12 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             shapePaint.Color = StrokeColor;
             shapePaint.Style = PaintStyle.Stroke;
             shapePaint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+            canvas.DrawOval(Center, Radius, shapePaint);
         }
 
         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)
         .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;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         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.StrokeWidth = StrokeWidth;
 
-        drawingSurface.Canvas.DrawLine(Start, End, paint);
+        canvas.DrawLine(Start, End, paint);
 
         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 =>
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
+    public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
+    
+    public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
+
     public PathVectorData(VectorPath 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;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         using Paint paint = new Paint()
         {
-            IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
+            IsAntiAliased = true, StrokeJoin = StrokeLineJoin, StrokeCap = StrokeLineCap
         };
 
         if (Fill && FillColor.A > 0)
@@ -51,7 +55,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
 
-            drawingSurface.Canvas.DrawPath(Path, paint);
+            canvas.DrawPath(Path, paint);
         }
 
         if (StrokeWidth > 0)
@@ -60,12 +64,12 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             paint.Style = PaintStyle.Stroke;
             paint.StrokeWidth = StrokeWidth;
             
-            drawingSurface.Canvas.DrawPath(Path, paint);
+            canvas.DrawPath(Path, paint);
         }
 
         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(
         GeometryAABB).WithMatrix(TransformationMatrix);
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         using Paint paint = new Paint();
         paint.Color = FillColor;
@@ -42,17 +42,17 @@ public class PointsVectorData : ShapeVectorData
         int num = 0;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
+            num = canvas.Save();
             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);
 
         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);
     }
 
-    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;
         if (applyTransform)
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         using Paint paint = new Paint();
@@ -67,7 +67,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         {
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
-            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
         }
 
         if (StrokeWidth > 0)
@@ -76,12 +76,12 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             paint.Style = PaintStyle.Stroke;
 
             paint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
         }
 
         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 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);
 
-        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 int GetCacheHash();
     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())
             return;
         
-        shape.RasterizeTransformed(surface);
+        shape.RasterizeTransformed(surface.Canvas);
     }
 
     public override Node CreateCopy() => new RasterizeShapeNode();
@@ -42,7 +42,7 @@ public class RasterizeShapeNode : RenderNode
         if (shape == null || !shape.IsValid())
             return false;
 
-        shape.RasterizeTransformed(renderOn);
+        shape.RasterizeTransformed(renderOn.Canvas);
 
         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)
     {
         int layer = surface.Canvas.SaveLayer(paint);
-        ShapeData?.RasterizeTransformed(surface);
+        ShapeData?.RasterizeTransformed(surface.Canvas);
         
         surface.Canvas.RestoreToCount(layer);
     }

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

@@ -1,4 +1,5 @@
 using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 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> Stroke { get; } = new("stroke");
     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)
     {
-        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);
     }
 }

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

@@ -1,4 +1,5 @@
 using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 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> Stroke { get; } = new("stroke");
     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)
     {
@@ -19,6 +24,8 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
         properties.Add(Fill);
         properties.Add(Stroke);
         properties.Add(StrokeWidth);
+        properties.Add(StrokeLineCap);
+        properties.Add(StrokeLineJoin);
 
         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;
 
@@ -6,4 +7,6 @@ public interface IStrokable
 {
     public SvgProperty<SvgColorUnit> Stroke { 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;
 
 namespace PixiEditor.SVG;
@@ -9,6 +10,10 @@ public struct StyleContext
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
+    
+    public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
 
     public StyleContext()
     {
@@ -16,6 +21,8 @@ public struct StyleContext
         Stroke = new("stroke");
         Fill = new("fill");
         Transform = new("transform");
+        StrokeLineCap = new("stroke-linecap");
+        StrokeLineJoin = new("stroke-linejoin");
     }
     
     public StyleContext(SvgDocument document)
@@ -24,6 +31,8 @@ public struct StyleContext
         Stroke = document.Stroke;
         Fill = document.Fill;
         Transform = document.Transform;
+        StrokeLineCap = document.StrokeLineCap;
+        StrokeLineJoin = document.StrokeLineJoin;
     }
 
     public StyleContext WithElement(SvgElement element)
@@ -51,6 +60,16 @@ public struct StyleContext
             {
                 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;

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

@@ -1,6 +1,7 @@
 using System.Xml;
 using System.Xml.Linq;
 using Drawie.Numerics;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 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> Stroke { get; } = new("stroke");
     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 List<SvgElement> Children { get; } = new();
 
@@ -35,7 +40,10 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
             Fill,
             Stroke,
             StrokeWidth,
-            Transform
+            Transform,
+            ViewBox,
+            StrokeLineCap,
+            StrokeLineJoin
         };
         
         ParseAttributes(properties, reader);
@@ -118,5 +126,15 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         {
             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 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 =
         [

+ 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 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;
         }
         

+ 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.
     /// </summary>
     /// <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="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)
     {
         if (!SnappingEnabled)
@@ -277,7 +277,7 @@ public class SnappingController
             return pos;
         }
 
-        if (direction.X == 0 || direction.Y == 0)
+        if (direction is { X: 0, Y: 0 })
         {
             return GetSnapPoint(pos, out xAxis, out yAxis);
         }
@@ -286,14 +286,17 @@ public class SnappingController
 
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
-
-
+        
         VecD? xIntersect = null;
         if (closestX != null)
         {
             double x = closestX.Value;
             double y = pos.Y + direction.Y * (x - pos.X) / direction.X;
             xIntersect = new VecD(x, y);
+            if (xIntersect.Value.IsNaNOrInfinity())
+            {
+                xIntersect = null;
+            }
         }
 
         VecD? yIntersect = null;
@@ -302,6 +305,10 @@ public class SnappingController
             double y = closestY.Value;
             double x = pos.X + direction.X * (y - pos.Y) / direction.Y;
             yIntersect = new VecD(x, y);
+            if (yIntersect.Value.IsNaNOrInfinity())
+            {
+                yIntersect = null;
+            }
         }
 
         if (xIntersect.HasValue && yIntersect.HasValue)
@@ -309,27 +316,41 @@ public class SnappingController
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < 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 yDist = (yIntersect.Value - pos).LengthSquared;
 
-            if (xDist < yDist)
+            if (xDist < yDist && IsWithinSnapDistance(xIntersect.Value, pos))
             {
                 xAxis = closestXAxis;
                 yAxis = null;
                 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;
             yAxis = null;
@@ -337,7 +358,7 @@ public class SnappingController
             return xIntersect.Value;
         }
 
-        if (yIntersect != null)
+        if (yIntersect != null && IsWithinSnapDistance(yIntersect.Value, pos))
         {
             xAxis = null;
             yAxis = closestYAxis;
@@ -355,4 +376,9 @@ public class SnappingController
         HorizontalSnapPoints[identifier] = () => pointFunc().X;
         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 RectD lastRect;
     protected double lastRadians;
-    
+
     private ShapeCorners initialCorners;
     private bool noMovement = true;
     protected IFillableShapeToolbar toolbar;
@@ -64,13 +64,15 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
                 ignoreNextColorChange = colorsVM.ColorsTempSwapped;
             }
-            
+
             lastRect = new RectD(startDrawingPos, VecD.Zero);
 
             document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)),
                 false);
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.IsSizeBoxEnabled = true;
+            document.TransformHandler.CanAlignToPixels = AlignToPixels;
+            
             return ExecutionState.Success;
         }
 
@@ -89,7 +91,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             toolbar.ToolSize = shapeData.StrokeWidth;
             toolbar.Fill = shapeData.FillColor != Colors.Transparent;
             initialCorners = shapeData.TransformationCorners;
-            
+
             ActiveMode = ShapeToolMode.Transform;
         }
         else
@@ -131,19 +133,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         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)
     {
         if (ActiveMode != ShapeToolMode.Transform)
             return;
 
         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 };
         IAction drawAction = TransformMovedAction(shapeData, corners);
 
@@ -172,9 +169,10 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             {
                 ignoreNextColorChange = false;
             }
+
             return;
         }
-        
+
         ignoreNextColorChange = ActiveMode == ShapeToolMode.Drawing;
 
         toolbar.StrokeColor = color.ToColor();
@@ -206,10 +204,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     {
         VecD adjustedPos = AlignToPixels ? (VecI)pos.Floor() : pos;
 
-        VecD snapped = adjustedPos;
+        VecD startPos = startDrawingPos;
+
+        VecD snapped;
         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();
             snapped = Snap(adjustedPos, startDrawingPos, dir, true);
         }
@@ -220,6 +222,13 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         noMovement = false;
 
+        if (toolViewModel.DrawFromCenter)
+        {
+            VecD center = startDrawingPos;
+
+            startDrawingPos = center + (center - snapped);
+        }
+
         if (AlignToPixels)
         {
             DrawShape((VecI)snapped.Floor(), lastRadians, false);
@@ -229,7 +238,10 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             DrawShape(snapped, lastRadians, false);
         }
 
+        startDrawingPos = startPos;
+
         document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false);
+        document.TransformHandler.CanAlignToPixels = AlignToPixels;
         document!.TransformHandler.Corners = new ShapeCorners((RectD)lastRect);
     }
 
@@ -302,7 +314,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         var layer = document.StructureHelper.Find(memberId);
         if (layer is null)
             return;
-        
+
         if (CanEditShape(layer))
         {
             internals!.ActionAccumulator.AddActions(SettingsChangedAction());
@@ -343,6 +355,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         {
             document.TransformHandler.HideTransform();
             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 double StrokeWidth => toolViewModel!.ToolSize;
-    
+
     protected bool drawOnMask;
 
     protected VecD curPos;
@@ -29,6 +29,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private IColorsHandler? colorsVM;
     protected IShapeToolbar? toolbar;
     private bool ignoreNextColorChange = false;
+    private VecD lastStartPos;
 
     public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
     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.ShowHandles = false;
             document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
-            
+
             return ExecutionState.Success;
         }
 
@@ -107,10 +108,20 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         VecD snapped = endPos;
         string snapX = "";
         string snapY = "";
-        
+
+        VecD startPos = startDrawingPos;
+
         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;
             snapped =
                 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);
         }
-        
+
+        if (toolViewModel.DrawFromCenter)
+        {
+            VecD center = startDrawingPos;
+            startDrawingPos = center + (center - snapped);
+        }
 
         HighlightSnapping(snapX, snapY);
         document!.LineToolOverlayHandler.LineEnd = snapped;
@@ -129,6 +145,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
         var drawLineAction = DrawLine(curPos);
         internals!.ActionAccumulator.AddActions(drawLineAction);
+
+        lastStartPos = startDrawingPos;
+        startDrawingPos = startPos;
     }
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
@@ -141,7 +160,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         }
 
         document!.LineToolOverlayHandler.Hide();
-        document!.LineToolOverlayHandler.Show(startDrawingPos, curPos, true);
+        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, true);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
 
@@ -165,6 +184,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             {
                 ignoreNextColorChange = false;
             }
+
             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.Helpers.Extensions;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
@@ -37,6 +38,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
     public bool CanUndo => document.PathOverlayHandler.HasUndo;
     public bool CanRedo => document.PathOverlayHandler.HasRedo;
+    public bool StopExecutionOnNormalUndo => false;
 
     public override bool BlocksOtherActions => false;
 
@@ -78,7 +80,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
                 return ExecutionState.Success;
             }
 
-            document.PathOverlayHandler.Show(startingPath, false);
+            document.PathOverlayHandler.Show(startingPath, false, AddToUndo);
             if (controller.LeftMousePressed)
             {
                 var snapped =
@@ -178,7 +180,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
         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());
     }
 
-    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 })
             {
@@ -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,
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -215,7 +223,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
         if (document.PathOverlayHandler.IsActive)
         {
             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 void Show(VectorPath path, bool showApplyButton);
+    public void Show(VectorPath path, bool showApplyButton, Action<VectorPath>? customAddToUndo = null);
     public void Hide();
     public event Action<VectorPath> PathChanged;
     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 ApplyToolSetSettings(IToolSetHandler toolset);
     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 T GetTool<T>() where T : IToolHandler;
     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 bool ShowHandles { 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 bool Snap { get; }
+    public bool DrawFromCenter { get; }
 }

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

@@ -3,4 +3,5 @@
 internal interface IShapeToolHandler : IToolHandler
 {
     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 Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -174,8 +175,17 @@ internal class SvgDocumentBuilder : IDocumentBuilder
                 _ => 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)
@@ -208,7 +218,10 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             var targetWidth = styleContext.StrokeWidth.Unit;
             
             shapeData.StrokeColor = targetColor.Value.Color;
-            shapeData.StrokeWidth = (float)targetWidth.Value.Value;
+            if (targetWidth != null)
+            {
+                shapeData.StrokeWidth = (float)targetWidth.Value.Value;
+            }
         }
 
         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
 // by using the '*' as shown below:
 // [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"
-                    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>
         <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="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="AnchorHandleSize">10</system:Double>
@@ -70,4 +81,4 @@
         </ResourceDictionary>
     </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.StrokeLineJoin.Unit = new SvgEnumUnit<SvgStrokeLineJoin>(ToSvgLineJoin(data.StrokeLineJoin));
+            path.StrokeLineCap.Unit = new SvgEnumUnit<SvgStrokeLineCap>((SvgStrokeLineCap)data.StrokeLineCap);
         }
 
         return path;
@@ -513,4 +515,14 @@ internal partial class DocumentViewModel
             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
 {
     private DocumentViewModel document;
-    
+
     private TransformOverlayUndoStack<(ShapeCorners, TransformState)>? undoStack = null;
 
     private TransformState internalState;
+
     public TransformState InternalState
     {
         get => internalState;
@@ -30,6 +31,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private TransformCornerFreedom cornerFreedom;
+
     public TransformCornerFreedom CornerFreedom
     {
         get => cornerFreedom;
@@ -37,13 +39,23 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private TransformSideFreedom sideFreedom;
+
     public TransformSideFreedom SideFreedom
     {
         get => sideFreedom;
         set => SetProperty(ref sideFreedom, value);
     }
 
+    private bool scaleFromCenter;
+
+    public bool ScaleFromCenter
+    {
+        get => scaleFromCenter;
+        set => SetProperty(ref scaleFromCenter, value);
+    }
+
     private bool lockRotation;
+
     public bool LockRotation
     {
         get => lockRotation;
@@ -51,6 +63,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool snapToAngles;
+
     public bool SnapToAngles
     {
         get => snapToAngles;
@@ -58,6 +71,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool transformActive;
+
     public bool TransformActive
     {
         get => transformActive;
@@ -70,7 +84,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
             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
             {
@@ -80,15 +95,24 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool showTransformControls;
+
     public bool ShowTransformControls
     {
         get => showTransformControls;
         set => SetProperty(ref showTransformControls, value);
     }
+    
+    private bool canAlignToPixels = true;
+    public bool CanAlignToPixels
+    {
+        get => canAlignToPixels;
+        set => SetProperty(ref canAlignToPixels, value);
+    }
 
     public event Action<MouseOnCanvasEventArgs>? PassthroughPointerPressed;
 
     private bool coverWholeScreen;
+
     public bool CoverWholeScreen
     {
         get => coverWholeScreen;
@@ -96,6 +120,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private ShapeCorners corners;
+
     public ShapeCorners Corners
     {
         get => corners;
@@ -112,25 +137,27 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     {
         get => showHandles;
         set => SetProperty(ref showHandles, value);
-    } 
-    
+    }
+
     private bool isSizeBoxEnabled;
 
     public bool IsSizeBoxEnabled
     {
         get => isSizeBoxEnabled;
         set => SetProperty(ref isSizeBoxEnabled, value);
-    } 
+    }
 
     private bool enableSnapping = true;
+
     public bool EnableSnapping
     {
         get => enableSnapping;
         set => SetProperty(ref enableSnapping, value);
     }
-    
-    
+
+
     private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
+
     public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
     {
         get => requestedCornersExecutor;
@@ -138,18 +165,21 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private ICommand? actionCompletedCommand = null;
+
     public ICommand? ActionCompletedCommand
     {
         get => actionCompletedCommand;
         set => SetProperty(ref actionCompletedCommand, value);
     }
 
-    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand; 
+    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand;
+
     public RelayCommand<MouseOnCanvasEventArgs> PassThroughPointerPressedCommand
     {
         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;
 
             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;
 
             undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
@@ -208,7 +239,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         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 void HideTransform()
@@ -221,7 +252,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         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)
             return;
@@ -234,6 +266,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         ShowTransformControls = showApplyButton;
+        CanAlignToPixels = true;
 
         IsSizeBoxEnabled = false;
         ShowHandles = true;
@@ -253,21 +286,24 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             requestedCornerFreedom = TransformCornerFreedom.ScaleProportionally;
             requestedSideFreedom = TransformSideFreedom.ScaleProportionally;
         }
-        else if (isCtrlDown)
+        else if (isAltDown)
         {
             requestedCornerFreedom = TransformCornerFreedom.Free;
             requestedSideFreedom = TransformSideFreedom.Free;
         }
-        else if (isAltDown)
+        /*else if (isAltDown)
         {
+        TODO: Add shear to the transform overlay
             requestedSideFreedom = TransformSideFreedom.Shear;
-        }
+        }*/
         else
         {
             requestedCornerFreedom = TransformCornerFreedom.Scale;
             requestedSideFreedom = TransformSideFreedom.Stretch;
         }
 
+        ScaleFromCenter = isCtrlDown;
+
         switch (activeTransformMode)
         {
             case DocumentTransformMode.Scale_Rotate_Shear_Perspective:
@@ -290,6 +326,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 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.Input;
 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.Handlers;
 
@@ -44,7 +46,13 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
     public bool HasUndo => undoStack.UndoCount > 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;
 
@@ -55,17 +63,20 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
     }
 
     private bool suppressUndo = false;
+    private RelayCommand<VectorPath> embeddedAddToUndo;
 
     public PathOverlayViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
     {
         this.documentViewModel = documentViewModel;
         this.internals = internals;
 
-        AddToUndoCommand = new RelayCommand<VectorPath>(AddToUndo);
+        embeddedAddToUndo = new RelayCommand<VectorPath>(AddToUndo);
+
+        AddToUndoCommand = embeddedAddToUndo;
         undoStack = new PathOverlayUndoStack<VectorPath>();
     }
 
-    public void Show(VectorPath newPath, bool showApplyButton)
+    public void Show(VectorPath newPath, bool showApplyButton, Action<VectorPath>? customAddToUndo = null)
     {
         if (IsActive)
         {
@@ -78,6 +89,7 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
         Path = newPath;
         IsActive = true;
         ShowApplyButton = showApplyButton;
+        AddToUndoCommand = customAddToUndo != null ? new RelayCommand<VectorPath>(customAddToUndo) : embeddedAddToUndo;
     }
 
     public void Hide()
@@ -113,6 +125,6 @@ internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
 
     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);
     }
+    
+    public void OnPostUndoInlet()
+    {
+        ActiveTool?.OnPostUndo();
+    }
+    
+    public void OnPostRedoInlet()
+    {
+        ActiveTool?.OnPostRedo();
+    }
 
     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))
             return;
         doc.Operations.Redo();
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            Owner.ToolsSubViewModel.OnPostRedoInlet();
+        });
     }
 
     /// <summary>
@@ -38,6 +42,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
             return;
         doc.Operations.Undo();
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            Owner.ToolsSubViewModel.OnPostUndoInlet();
+        });
     }
 
     /// <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 bool DrawEven { get; protected set; }
+    public bool DrawFromCenter { get; protected set; }
 
     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 OnPostUndo() { }
+    public virtual void OnPostRedo() { }
 
     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)
     {
+        DrawFromCenter = ctrlIsDown;
+        
         if (shiftIsDown)
         {
             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 string DefaultIcon => PixiPerfectIcons.LowResLine;
 
-    [Settings.Inherited] 
-    public double ToolSize => GetValue<double>();
+    [Settings.Inherited] public double ToolSize => GetValue<double>();
 
     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)
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         {
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
@@ -58,8 +59,8 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
     public override void OnSelected(bool restoring)
     {
         if (restoring) return;
-        
+
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         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
 {
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
+
     public RasterRectangleToolViewModel()
     {
         ActionDisplay = defaultActionDisplay;
@@ -31,6 +32,8 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         {
             DrawEven = true;
@@ -50,8 +53,8 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
     public override void OnSelected(bool restoring)
     {
-        if(restoring) return;
-        
+        if (restoring) return;
+
         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)]
 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";
     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)
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         {
             DrawEven = true;
@@ -67,6 +69,8 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
             ShapeCorners corners = vectorLayer.TransformationCorners;
             document.TransformViewModel.ShowTransform(
                 DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
+
+            document.TransformViewModel.CanAlignToPixels = false;
         }
 
         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";
 
     public override bool IsErasable => false;
-    
+
     public VectorLineToolViewModel()
     {
         ActionDisplay = defaultActionDisplay;
@@ -37,8 +37,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     public override Type[]? SupportedLayerTypes { get; } = [];
     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; }
 
@@ -46,6 +45,8 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         {
             ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
@@ -66,12 +67,13 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     public override void OnSelected(bool restoring)
     {
         if (restoring) return;
-        
+
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var layer = document.SelectedStructureMember;
         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,
                     false);

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

@@ -97,7 +97,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
             ActionDisplay = actionDisplayDefault;
         }
     }
-
+    
     public override void OnSelected(bool restoring)
     {
         if (restoring) return;
@@ -105,7 +105,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         isActivated = true;
     }
-
+    
     public override void OnDeselecting(bool 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)
     {
         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
 {
     public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";
-    
+
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override bool IsErasable => false;
@@ -36,6 +36,8 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
+        DrawFromCenter = ctrlIsDown;
+
         if (shiftIsDown)
         {
             DrawEven = true;
@@ -63,8 +65,11 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
             vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyRectangleData)
         {
             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);
+            
+            transformVm.CanAlignToPixels = false;
         }
 
         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 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(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -342,6 +352,8 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.ZoomboxAngleProperty, zoomboxAngleBinding);
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
+        transformOverlay.Bind(TransformOverlay.ScaleFromCenterProperty, scaleFromCenterBinding);
+        transformOverlay.Bind(TransformOverlay.CanAlignToPixelsProperty, canAlignToPixelsBinding);
     }
     
     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 PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Resources;
 using PixiEditor.Views.Overlays.TransformOverlay;
 using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 using Path = Avalonia.Controls.Shapes.Path;
@@ -65,17 +67,22 @@ public abstract class Handle : IHandle
         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 (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)

+ 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));
 
     private const double HandleSize = 12;
-    private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle");
+    private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle").Path;
 
     private const float DashWidth = 10.0f;
     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.Input;
-using Avalonia.Media;
 using ChunkyImageLib.DataHolders;
-using PixiEditor.Helpers;
-using PixiEditor.Helpers.Extensions;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers.InputDevice;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Views.Overlays.Drawables;
 using PixiEditor.Views.Overlays.Handles;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
-using Point = Avalonia.Point;
 
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 #nullable enable
@@ -160,6 +152,25 @@ internal class TransformOverlay : Overlay
         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()
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -181,6 +192,7 @@ internal class TransformOverlay : Overlay
     private double propAngle1OnStartRotate = 0;
     private double propAngle2OnStartRotate = 0;
 
+    private TransformSideFreedom beforeShearSideFreedom;
     private Anchor? capturedAnchor;
     private ShapeCorners cornersOnStartAnchorDrag;
     private VecD mousePosOnStartAnchorDrag;
@@ -254,13 +266,17 @@ internal class TransformOverlay : Overlay
     private List<Handle> snapPoints = new();
     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 shearCursorActive = false;
+    private Anchor? hoveredAnchor;
 
     private VecD lastPointerPos;
     private InfoBox infoBox;
     private VecD lastSize;
     private bool actuallyMoved = false;
+    private bool isShearing = false;
 
     public TransformOverlay()
     {
@@ -328,7 +344,9 @@ internal class TransformOverlay : Overlay
         DrawOverlay(drawingContext, canvasBounds.Size, Corners, InternalState.Origin, (float)ZoomScale);
 
         if (capturedAnchor is null)
-            UpdateRotationCursor(lastPointerPos);
+        {
+            UpdateSpecialCursors(lastPointerPos);
+        }
     }
 
     private void DrawOverlay
@@ -414,13 +432,37 @@ internal class TransformOverlay : Overlay
             double angle = (lastPointerPos - InternalState.Origin).Angle * 180 / Math.PI - 90;
             matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees((float)angle, (float)lastPointerPos.X,
                 (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));
             context.SetMatrix(context.TotalMatrix.Concat(matrix));
 
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             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);
 
@@ -456,17 +498,23 @@ internal class TransformOverlay : Overlay
 
     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)
         {
             IsSizeBoxEnabled = false;
             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)
@@ -477,6 +525,7 @@ internal class TransformOverlay : Overlay
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     {
         rotationCursorActive = false;
+        shearCursorActive = false;
         Refresh();
     }
 
@@ -487,7 +536,11 @@ internal class TransformOverlay : Overlay
 
         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);
         }
@@ -540,10 +593,11 @@ internal class TransformOverlay : Overlay
         if (capturedAnchor is not null)
         {
             HandleCapturedAnchorMovement(e.Point);
+            lastPointerPos = e.Point;
             return;
         }
 
-        if (UpdateRotationCursor(e.Point))
+        if (UpdateSpecialCursors(e.Point))
         {
             finalCursor = new Cursor(StandardCursorType.None);
         }
@@ -592,11 +646,22 @@ internal class TransformOverlay : Overlay
             e.Pointer.Capture(null);
             Cursor = new Cursor(StandardCursorType.Arrow);
             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();
         IsSizeBoxEnabled = false;
+        capturedAnchor = null;
     }
 
     public override bool TestHit(VecD point)
@@ -622,6 +687,24 @@ internal class TransformOverlay : Overlay
         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()
     {
         isMoving = false;
@@ -643,11 +726,20 @@ internal class TransformOverlay : Overlay
         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)
     {
         VecD delta = pos - mousePosOnStartMove;
 
-        if (Corners.IsAlignedToPixels)
+        if (Corners.IsAlignedToPixels && CanAlignToPixels)
             delta = delta.Round();
 
         ShapeCorners rawCorners = new ShapeCorners()
@@ -724,15 +816,19 @@ internal class TransformOverlay : Overlay
                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;
+            shearCursorActive = false;
             return false;
         }
 
-        rotationCursorActive = true;
+        rotationCursorActive = !canShear;
+        shearCursorActive = canShear;
+        hoveredAnchor = anchor;
         return true;
     }
 
@@ -754,7 +850,9 @@ internal class TransformOverlay : Overlay
 
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController,
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
+                ScaleFromCenter,
+                SnappingController,
                 out string snapX, out string snapY);
 
             HighlightSnappedAxis(snapX, snapY);
@@ -836,6 +934,7 @@ internal class TransformOverlay : Overlay
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta,
+                ScaleFromCenter,
                 SnappingController, out string snapX, out string snapY);
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
@@ -1092,6 +1191,9 @@ internal class TransformOverlay : Overlay
             ActionCompleted.Execute(null);
 
         IsSizeBoxEnabled = false;
+
+        SnappingController.HighlightedXAxis = string.Empty;
+        SnappingController.HighlightedYAxis = string.Empty;
     }
 
     private Handle? GetSnapHandleOfOrigin()
@@ -1114,6 +1216,12 @@ internal class TransformOverlay : Overlay
     {
         isMoving = false;
         isRotating = false;
+        if (isShearing)
+        {
+            SideFreedom = beforeShearSideFreedom;
+        }
+
+        isShearing = false;
         Corners = corners;
         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.Numerics;
 using PixiEditor.Models.Controllers.InputDevice;
@@ -11,7 +12,7 @@ internal static class TransformUpdateHelper
 
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
-        VecD desiredPos,
+        VecD desiredPos, bool scaleFromCenter,
         SnappingController? snappingController, out string snapX, out string snapY)
     {
         if (!TransformHelper.IsCorner(targetCorner))
@@ -71,6 +72,11 @@ internal static class TransformUpdateHelper
             if (double.IsNaN(angle))
                 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
             VecD targetTrans = (targetPos - oppositePos).Rotate(-angle);
             VecD leftNeighTrans = (leftNeighborPos - oppositePos).Rotate(-angle);
@@ -79,6 +85,7 @@ internal static class TransformUpdateHelper
             // find by how much move each corner
             VecD delta = (desiredPos - targetPos).Rotate(-angle);
             VecD leftNeighDelta, rightNeighDelta;
+
             if (corners.IsPartiallyDegenerate)
             {
                 // 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");
     }
 
+    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 TransferZeros(VecD from, VecD to)
@@ -151,7 +188,8 @@ internal static class TransformUpdateHelper
 
     public static ShapeCorners? UpdateShapeFromSide
     (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))
             throw new ArgumentException($"{targetSide} is not a side");
@@ -171,10 +209,11 @@ internal static class TransformUpdateHelper
 
             VecD direction = targetPos - oppositePos;
             direction = VecD.FromAngleAndLength(direction.Angle, 1 / direction.Length);
-            
+
             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;
@@ -183,9 +222,14 @@ internal static class TransformUpdateHelper
 
             if (corners.IsRect)
             {
-                var delta = desiredPos - targetPos;
+                var delta = (desiredPos - targetPos);
                 var center = oppositePos.Lerp(desiredPos, 0.5);
 
+                if (scaleFromCenter)
+                {
+                    return ScaleEvenlyToPos(corners, desiredPos, targetPos);
+                }
+
                 var (leftCorn, rightCorn) = TransformHelper.GetCornersOnSide(targetSide);
                 var (leftOppCorn, _) = TransformHelper.GetNeighboringCorners(leftCorn);
                 var (_, rightOppCorn) = TransformHelper.GetNeighboringCorners(rightCorn);
@@ -203,6 +247,7 @@ internal static class TransformUpdateHelper
                     center + VecD.FromAngleAndLength(leftAngle, 1));
                 var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta,
                     center, center + VecD.FromAngleAndLength(rightAngle, 1));
+
                 var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
                     center + VecD.FromAngleAndLength(rightAngle, 1));
                 var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
@@ -220,6 +265,12 @@ internal static class TransformUpdateHelper
             }
 
             fallback:
+
+            if (scaleFromCenter)
+            {
+                return ScaleEvenlyToPos(corners, desiredPos, targetPos);
+            }
+
             corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
             corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
             corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
@@ -249,6 +300,11 @@ internal static class TransformUpdateHelper
             if (freedom == TransformSideFreedom.Shear)
             {
                 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)
             {
@@ -257,18 +313,57 @@ internal static class TransformUpdateHelper
                 else
                     desiredPos = desiredPos.ProjectOntoLine(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 newCorners = TransformHelper.UpdateCorner(corners, leftCorner, leftCornerPos + 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;
         }
 
         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)
     {
         corners.TopLeft = corners.TopLeft.Rotate(angle, origin);