Browse Source

Skewing changes, clipboard fix, vector resource loading, line caps, joins

flabbet 8 months ago
parent
commit
d39f319a97
25 changed files with 383 additions and 141 deletions
  1. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs
  2. 8 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  3. 9 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  4. 15 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  5. 7 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  6. 10 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  7. 5 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  8. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  10. 4 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  11. 7 0
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  12. 8 0
      src/PixiEditor.SVG/Enums/SvgStrokeLineCap.cs
  13. 10 0
      src/PixiEditor.SVG/Enums/SvgStrokeLineJoin.cs
  14. 4 1
      src/PixiEditor.SVG/Features/IStrokable.cs
  15. 20 1
      src/PixiEditor.SVG/StyleContext.cs
  16. 19 1
      src/PixiEditor.SVG/SvgDocument.cs
  17. 34 0
      src/PixiEditor/Helpers/Resources/VectorPathResource.cs
  18. 3 0
      src/PixiEditor/Images/Handles/shear.svg
  19. 1 1
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  20. 15 2
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  21. 71 60
      src/PixiEditor/Styles/PixiEditor.Handles.axaml
  22. 12 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  23. 10 3
      src/PixiEditor/Views/Overlays/Handles/Handle.cs
  24. 1 1
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  25. 103 16
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

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

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

+ 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)

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

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

+ 103 - 16
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -18,6 +18,7 @@ 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;
@@ -160,8 +161,9 @@ internal class TransformOverlay : Overlay
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
 
-    public static readonly StyledProperty<bool> ScaleFromCenterProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
-        nameof(ScaleFromCenter));
+    public static readonly StyledProperty<bool> ScaleFromCenterProperty =
+        AvaloniaProperty.Register<TransformOverlay, bool>(
+            nameof(ScaleFromCenter));
 
     public bool ScaleFromCenter
     {
@@ -190,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;
@@ -263,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()
     {
@@ -337,7 +344,7 @@ internal class TransformOverlay : Overlay
         DrawOverlay(drawingContext, canvasBounds.Size, Corners, InternalState.Origin, (float)ZoomScale);
 
         if (capturedAnchor is null)
-            UpdateRotationCursor(lastPointerPos);
+            UpdateSpecialCursors(lastPointerPos);
     }
 
     private void DrawOverlay
@@ -431,6 +438,26 @@ internal class TransformOverlay : Overlay
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
         }
 
+        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);
 
         if (IsSizeBoxEnabled)
@@ -465,17 +492,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)
@@ -486,6 +519,7 @@ internal class TransformOverlay : Overlay
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     {
         rotationCursorActive = false;
+        shearCursorActive = false;
         Refresh();
     }
 
@@ -496,7 +530,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);
         }
@@ -549,10 +587,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);
         }
@@ -601,11 +640,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)
@@ -631,6 +681,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;
@@ -652,6 +720,15 @@ 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;
@@ -733,15 +810,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;
     }
 
@@ -1104,7 +1185,7 @@ internal class TransformOverlay : Overlay
             ActionCompleted.Execute(null);
 
         IsSizeBoxEnabled = false;
-        
+
         SnappingController.HighlightedXAxis = string.Empty;
         SnappingController.HighlightedYAxis = string.Empty;
     }
@@ -1129,6 +1210,12 @@ internal class TransformOverlay : Overlay
     {
         isMoving = false;
         isRotating = false;
+        if (isShearing)
+        {
+            SideFreedom = beforeShearSideFreedom;
+        }
+
+        isShearing = false;
         Corners = corners;
         InternalState = new()
         {